const common = require('./common'); // const { Prisma, PrismaClient, Publisher, Shift, DayOfWeek } = require("@prisma/client"); // or const DayOfWeek = require("@prisma/client").DayOfWeek; async function findPublisher(names, email, select, getAll = false) { // Normalize and split the name if provided let nameParts = names ? names.normalize('NFC').trim().split(/\s+/) : []; // Construct the select clause let selectClause = select ? select.split(",").reduce((acc, curr) => ({ ...acc, [curr]: true }), {}) : { id: true, firstName: true, lastName: true }; // Construct the where clause based on the provided data let whereClause = []; if (nameParts.length > 0) { if (nameParts.length === 1) { // If only one name part is provided, check it against both firstName and lastName whereClause.push({ OR: [{ firstName: nameParts[0] }, { lastName: nameParts[0] }] }); } else if (nameParts.length === 2) { // If two name parts are provided, check combinations of firstName and lastName whereClause.push({ firstName: nameParts[0], lastName: nameParts[1] }); whereClause.push({ firstName: nameParts[1], lastName: nameParts[0] }); } else if (nameParts.length === 3) { // If three name parts are provided, consider the middle name part of first or last name whereClause.push({ firstName: `${nameParts[0]} ${nameParts[1]}`, lastName: nameParts[2] }); whereClause.push({ firstName: nameParts[0], lastName: `${nameParts[1]} ${nameParts[2]}` }); } else if (nameParts.length > 3) { // Join all parts except the last as the first name, and consider the last part as the last name const firstName = nameParts.slice(0, -1).join(' '); const lastName = nameParts.slice(-1).join(' '); whereClause.push({ firstName: firstName, lastName: lastName }); // Alternative case: First part as first name, and join the rest as the last name const altFirstName = nameParts.slice(0, 1).join(' '); const altLastName = nameParts.slice(1).join(' '); whereClause.push({ firstName: altFirstName, lastName: altLastName }); } } if (email) { whereClause.push({ email: email }); } if (whereClause.length === 0) { return null; // No search criteria provided } const prisma = common.getPrismaClient(); // let publisher = await prisma.publisher.findFirst({ // select: selectClause, // where: { OR: whereClause } // }); // Retrieve all matching records try { let result = await prisma.publisher.findMany({ select: selectClause, where: { OR: whereClause } }); // If getAll is false, take only the first record if (!getAll && result.length > 0) { result = result.length > 0 ? [result[0]] : []; } if (result.length === 0 && names) { console.log("No publisher found, trying fuzzy search for '" + names + "'- email: '" + email + "'"); const publishers = await prisma.publisher.findMany(); result = [await common.fuzzySearch(publishers, names, 0.90)]; console.log("Fuzzy search result: " + result[0]?.firstName + " " + result[0]?.lastName + " - " + result[0]?.email); } //always return an array result = result || []; return result; } catch (error) { console.error("Error finding publisher: ", error); throw error; // Rethrow the error or handle it as needed } } //# new - to verify // should be equivalent to the following prisma filer // whereClause["availabilities"] = { // some: { // OR: [ // // Check if dayOfMonth is set and filterDate is between start and end dates (Assignments on specific days AND time) // { // dayOfMonth: filterDate.getDate(), // startTime: { lte: filterDate }, // endTime: { gte: filterDate } // }, // // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) // { // dayOfMonth: null, // dayofweek: dayOfWeekEnum, // startTime: { gte: filterDate }, // } // ] // } // }; async function getAvailabilities(userId) { const prismaClient = common.getPrismaClient(); const items = await prismaClient.availability.findMany({ where: { publisherId: userId, }, select: { id: true, name: true, isActive: true, isFromPreviousAssignment: true, dayofweek: true, dayOfMonth: true, startTime: true, endTime: true, repeatWeekly: true, endDate: true, publisher: { select: { firstName: true, lastName: true, id: true, }, }, }, }); // Convert Date objects to ISO strings const serializableItems = items.map(item => ({ ...item, startTime: item.startTime.toISOString(), endTime: item.endTime.toISOString(), name: common.getTimeFormatted(item.startTime) + "-" + common.getTimeFormatted(item.endTime), //endDate can be null endDate: item.endDate ? item.endDate.toISOString() : null, type: 'availability', // Convert other Date fields similarly if they exist })); /*model Assignment { id Int @id @default(autoincrement()) shift Shift @relation(fields: [shiftId], references: [id], onDelete: Cascade) shiftId Int publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) publisherId String isActive Boolean @default(true) isConfirmed Boolean @default(false) isWithTransport Boolean @default(false) Report Report[] }*/ //get assignments for this user const assignments = await prismaClient.assignment.findMany({ where: { publisherId: userId, }, select: { id: true, isBySystem: true, isConfirmed: true, isWithTransport: true, shift: { select: { id: true, name: true, startTime: true, endTime: true, //select all assigned publishers names as name - comma separated assignments: { select: { publisher: { select: { firstName: true, lastName: true, } } } } } } } }); const serializableAssignments = assignments.map(item => ({ ...item, startTime: item.shift.startTime.toISOString(), endTime: item.shift.endTime.toISOString(), // name: item.shift.publishers.map(p => p.firstName + " " + p.lastName).join(", "), //name: item.shift.assignments.map(a => a.publisher.firstName[0] + " " + a.publisher.lastName).join(", "), name: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)), type: 'assignment', //delete shift object shift: null, publisher: { id: userId } })); serializableItems.push(...serializableAssignments); return serializableItems; } /** * Filters publishers based on various criteria including exact times, monthly duration, * and whether or not to include statistics about publishers' availabilities and assignments. * This function heavily relies on the `prisma` client to query and manipulate data related to publishers. * (ToDo: implement separate and simple fns if it does not work) * * @param {Array|string} selectFields - Fields to select from the publishers data. Can be an array of field names or a comma-separated string of field names. * @param {string|Date|null} filterDate - The reference date for filtering. Can be a date string, a Date object, or null. Used to determine relevant time frames like current month, previous month, etc. * @param {boolean} [isExactTime=false] - If true, filters publishers who are available at the exact time of `filterDate` plus/minus a specific duration (e.g., 90 minutes). * @param {boolean} [isForTheMonth=false] - If true, adjusts the filtering to encompass the entire month based on `filterDate`. * @param {boolean} [noEndDateFilter=false] - If true, removes any filtering based on the end date of publishers' availabilities. * @param {boolean} [isWithStats=true] - If true, includes statistical data about publishers' availabilities and assignments in the output. * @param {boolean} [includeOldAvailabilities=false] - If true, includes publishers' previous availabilities in the calculations and output. * * @returns {Promise} Returns a promise that resolves to an array of publishers with filtered data according to the specified criteria. */ // (ToDo: implement separate and simple fns if it does not work) async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false, id = null, filterAvailabilitiesByDate = true) { const prisma = common.getPrismaClient(); let filterTimeFrom, filterTimeTo; if (filterDate === null || filterDate === "" || filterDate === undefined) { noEndDateFilter = true; isForTheMonth = false; isExactTime = false; } else { filterDate = new Date(filterDate); // Convert to date object if not already //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 isExactTime = false; } else { filterTimeFrom = new Date(filterDate) filterTimeTo = new Date(filterDate); if (isExactTime) { //add +- 90 minutes to the filterDate ToDo: should be "shift duration" // filterTimeFrom.setMinutes(filterTimeFrom.getMinutes() - 90); filterTimeTo.setMinutes(filterTimeTo.getMinutes() + 90); } else { filterTimeFrom.setHours(0, 0, 0, 0); filterTimeTo.setHours(23, 59, 59, 999); } } } const dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterTimeFrom); const monthInfo = common.getMonthDatesInfo(filterDate); let prevMnt = new Date(filterDate) prevMnt.setMonth(prevMnt.getMonth() - 1); const prevMonthInfo = common.getMonthDatesInfo(prevMnt); // Only attempt to split if selectFields is a string; otherwise, use it as it is. selectFields = typeof selectFields === 'string' ? selectFields.split(",") : selectFields; let selectBase = selectFields.reduce((acc, curr) => { acc[curr] = true; return acc; }, {}); selectBase.assignments = { select: { id: true, shift: { select: { id: true, startTime: true, endTime: true } } }, // where: { // shift: { // startTime: { // gte: prevMnt, // } // } // } }; let isDayFilter = true; let whereClause = {}; if (id) { whereClause.id = String(id) } if (isForTheMonth) { var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber filterTimeFrom = monthInfo.firstMonday; filterTimeTo = monthInfo.lastSunday; isDayFilter = false; } if (filterTimeFrom) { // if no date is provided, we don't filter by date selectBase.assignments.where = { shift: { startTime: { gte: prevMnt }, } }; whereClause["availabilities"] = { some: { OR: [ // ONE TIME AVAILABILITIES // Check if dayOfMonth is not null and startTime is after monthInfo.firstMonday (Assignments on specific days AND time) { //dayOfMonth: { not: null }, startTime: { gte: filterTimeFrom }, // endTime: { lte: monthInfo.lastSunday } }, // REPEATING WEEKLY AVAILABILITIES // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) { // dayOfMonth: null, // startTime: { gte: filterTimeFrom }, AND: [ { dayOfMonth: null }, //{ dayofweek: dayOfWeekEnum }, // we want all days of the week for now // moved down to conditional filters { startTime: { lte: filterTimeTo } }, // we ignore startTime as it will be filtered later only by the time and not by date. ! but we need it to filter future repeating availabilities that are not yet started on the request date { 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) { isDayFilter = false; } else { whereClause["availabilities"].some.OR[0].endTime = { lte: filterTimeTo }; if (isForTheMonth) { // no dayofweek or time filters here } else { //moved to upper qery as it is now also dependant on date filter if ((isDayFilter || filterAvailabilitiesByDate) && !isForTheMonth) { 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 { } } } } console.log(`getting publishers for date: ${filterDate}, isExactTime: ${isExactTime}, isForTheMonth: ${isForTheMonth}`); //console.log`whereClause: ${JSON.stringify(whereClause)}` //include availabilities if flag is true let publishers = await prisma.publisher.findMany({ where: whereClause, select: { ...selectBase, availabilities: true // we select all availabilities here and should filter them later } }); ///console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`); // include repeating weekly availabilities. generate occurrences for the month // convert matching weekly availabilities to availabilities for the day to make further processing easier on the client. publishers.forEach(pub => { pub.availabilities = pub.availabilities.map(avail => { if (avail.dayOfMonth == null) { if (filterAvailabilitiesByDate && !isForTheMonth) { // filter out repeating availabilities when on other day of week if (filterTimeFrom) { if (avail.dayofweek != dayOfWeekEnum) { return null; } } } let newStart = new Date(filterDate); newStart.setHours(avail.startTime.getHours(), avail.startTime.getMinutes(), 0, 0); let newEnd = new Date(filterDate); newEnd.setHours(avail.endTime.getHours(), avail.endTime.getMinutes(), 0, 0); return { ...avail, startTime: newStart, endTime: newEnd } } else { if (filterAvailabilitiesByDate && !isForTheMonth) { if (avail.startTime >= filterTimeFrom && avail.startTime <= filterTimeTo) { return avail; } return null; } return avail; } }) .filter(avail => avail !== null); }); let currentWeekStart, currentWeekEnd; if (isWithStats) { currentWeekStart = common.getStartOfWeek(filterDate); currentWeekEnd = common.getEndOfWeek(filterDate); //get if publisher has assignments for current weekday, week, current month, previous month publishers.forEach(pub => { // Filter assignments for current day if (isDayFilter) { pub.currentDayAssignments = pub.assignments?.filter(assignment => { return assignment.shift.startTime >= filterDate && assignment.shift.startTime <= filterTimeTo; }).length; } // Filter assignments for current week pub.currentWeekAssignments = pub.assignments?.filter(assignment => { return assignment.shift.startTime >= currentWeekStart && assignment.shift.startTime <= currentWeekEnd; }).length; // Filter assignments for current month pub.currentMonthAssignments = pub.assignments?.filter(assignment => { return assignment.shift.startTime >= monthInfo.firstMonday && (noEndDateFilter || assignment.shift.startTime <= monthInfo.lastSunday); }).length; // Filter assignments for previous month pub.previousMonthAssignments = pub.assignments?.filter(assignment => { return assignment.shift.startTime >= prevMonthInfo.firstMonday && assignment.shift.startTime <= prevMonthInfo.lastSunday; }).length; }); } //get the availabilities for the day. Calculate: //1. how many days the publisher is available for the current month - only with dayOfMonth //2. how many days the publisher is available without dayOfMonth (previous months count) //3. how many hours in total the publisher is available for the current month publishers.forEach(pub => { if (isWithStats) { pub.currentMonthAvailability = pub.availabilities?.filter(avail => { // return avail.dayOfMonth != null && avail.startTime >= currentMonthStart && avail.startTime <= monthInfo.lastSunday; return (avail.startTime >= monthInfo.firstMonday && (noEndDateFilter || avail.startTime <= monthInfo.lastSunday)) || (avail.dayOfMonth == null && avail.startTime <= monthInfo.firstMonday); // include repeating availabilities }) pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length; // pub.currentMonthAvailabilityDaysCount += pub.availabilities.filter(avail => { // return avail.dayOfMonth == null; // }).length; pub.currentMonthAvailabilityHoursCount = pub.currentMonthAvailability.reduce((acc, curr) => { return acc + (curr.endTime.getTime() - curr.startTime.getTime()) / (1000 * 60 * 60); }, 0); //if pub has up-to-date availabilities (with dayOfMonth) for the current month pub.hasUpToDateAvailabilities = pub.availabilities?.some(avail => { return avail.dayOfMonth != null && avail.startTime >= monthInfo.firstMonday; // && avail.startTime <= monthInfo.lastSunday; }); } //if pub has ever filled the form - if has availabilities which are not from previous assignments pub.hasEverFilledForm = pub.availabilities?.some(avail => { return avail.isFromPreviousAssignments == false; }); if (isDayFilter) { //if pub has availabilities for the current day pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => { return avail.startTime >= filterDate && avail.startTime <= filterTimeTo; }); } }); // ToDo: test case/unit test // ToDo: check and validate the filtering and calculations if (isExactTime) { //HERE WE FILTER by time for repeating availabilities. We can't do that if we don't have // whereClause["availabilities"].some.OR[1].startTime = { gte: filterTimeFrom }; // whereClause["availabilities"].some.OR[1].endTime = { gte: filterTimeTo } publishers.forEach(pub => { pub.availabilities = pub.availabilities.filter(a => a.startTime <= filterTimeFrom && a.endTime >= filterTimeTo); }); publishers = publishers.filter(pub => pub.availabilities.length > 0); } // if (isExactTime) { // // Post filter for time if dayOfMonth is null as we can't only by time for multiple dates in SQL // // Modify the availabilities array of the filtered publishers // publishers.forEach(pub => { // pub.availabilities = pub.availabilities?.filter(avail => matchesAvailability(avail, filterDate)); // }); // } return publishers; } //ToDo: refactor this function async function getAllPublishersWithStatisticsMonth(filterDateDuringMonth, noEndDateFilter = false, includeOldAvailabilities = true) { const prisma = common.getPrismaClient(); const monthInfo = common.getMonthDatesInfo(new Date(filterDateDuringMonth)); const dateStr = new Date(monthInfo.firstMonday).toISOString().split('T')[0]; let publishers = await filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', dateStr, false, true, noEndDateFilter, true, true, null, false); // const axios = await axiosServer(context); // const { data: publishers } = await axios.get(`api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); // api/index?action=filterPublishers&assignments=true&availabilities=true&date=2024-03-14&select=id%2CfirstName%2ClastName%2CisActive%2CdesiredShiftsPerMonth publishers.forEach(publisher => { publisher.desiredShiftsPerMonth = publisher.desiredShiftsPerMonth || 0; publisher.assignments = publisher.assignments || []; publisher.availabilities = publisher.availabilities || []; publisher.lastUpdate = publisher.availabilities.reduce((acc, curr) => curr.dateOfEntry > acc ? curr.dateOfEntry : acc, null); if (publisher.lastUpdate) { publisher.lastUpdate = common.getDateFormated(publisher.lastUpdate); } else { publisher.lastUpdate = "Няма данни"; } //serialize dates in publisher.assignments and publisher.availabilities publisher.assignments.forEach(assignment => { if (assignment.shift && assignment.shift.startTime) { assignment.shift.startTime = assignment.shift.startTime.toISOString(); assignment.shift.endTime = assignment.shift.endTime.toISOString(); } }); publisher.availabilities.forEach(availability => { if (availability.startTime) { availability.startTime = availability.startTime.toISOString(); availability.endTime = availability.endTime.toISOString(); if (availability.dateOfEntry) { availability.dateOfEntry = availability.dateOfEntry.toISOString(); } } }); publisher.lastLogin = publisher.lastLogin ? publisher.lastLogin.toISOString() : null; //remove availabilities that isFromPreviousAssignment publisher.availabilities = publisher.availabilities.filter(availability => !availability.isFromPreviousAssignment); }); //remove publishers without availabilities publishers = publishers.filter(publisher => publisher.availabilities.length > 0); let allPublishers = await prisma.publisher.findMany({ select: { id: true, firstName: true, lastName: true, email: true, phone: true, isActive: true, desiredShiftsPerMonth: true, lastLogin: true, type: true, pushSubscription: true, assignments: { select: { id: true, shift: { select: { startTime: true, endTime: true, }, }, }, }, availabilities: { select: { startTime: true, endTime: true, dateOfEntry: true, isFromPreviousAssignment: true, }, // where: { // startTime: { // gte: new Date(monthInfo.firstMonday), // }, // }, }, }, orderBy: [ { firstName: 'asc', // or 'desc' if you want descending order }, { lastName: 'asc', // or 'desc' if you want descending order } ], }); let prevMnt = new Date(); prevMnt.setMonth(prevMnt.getMonth() - 1); let prevMonthInfo = common.getMonthDatesInfo(prevMnt); allPublishers.forEach(publisher => { publisher.isPushActive = publisher.pushSubscription ? true : false; delete publisher.pushSubscription // Use helper functions to calculate and assign assignment counts publisher.currentMonthAssignments = countAssignments(publisher.assignments, monthInfo.firstMonday, monthInfo.lastSunday); publisher.previousMonthAssignments = countAssignments(publisher.assignments, prevMonthInfo.firstMonday, prevMonthInfo.lastSunday); publisher.lastLogin = publisher.lastLogin ? publisher.lastLogin.toISOString() : null; // Convert date formats within the same iteration convertShiftDates(publisher.assignments); convertAvailabilityDates(publisher.availabilities); // common.convertDatesToISOStrings(publisher.availabilities); //ToDo fix the function to work with this sctucture and use it }); //debug const pubsWithAvailabilities = allPublishers.filter(publisher => publisher.availabilities.length > 0); const pubsWithAvailabilities2 = publishers.filter(publisher => publisher.currentMonthAvailabilityHoursCount > 0); console.log(`publishers: ${allPublishers.length}, publishers with availabilities: ${pubsWithAvailabilities.length}, publishers with availabilities2: ${pubsWithAvailabilities2.length}`); //merge allPublishers with publishers allPublishers = allPublishers.map(pub => { const found = publishers.find(publisher => publisher.id === pub.id); if (found) { return { ...pub, ...found }; } return pub; }); return allPublishers; } // Helper functions ToDo: move them to common and replace all implementations with the common ones function countAssignments(assignments, startTime, endTime) { return assignments.filter(assignment => assignment.shift.startTime >= startTime && assignment.shift.startTime <= endTime ).length; } function convertAvailabilityDates(availabilities) { availabilities.forEach(av => { av.startTime = new Date(av.startTime).toISOString(); av.endTime = new Date(av.endTime).toISOString(); av.dateOfEntry = new Date(av.dateOfEntry).toISOString() }); } function convertShiftDates(assignments) { assignments.forEach(assignment => { if (assignment.shift && assignment.shift.startTime) { assignment.shift.startTime = new Date(assignment.shift.startTime).toISOString(); assignment.shift.endTime = new Date(assignment.shift.endTime).toISOString(); } }); } async function getCalendarEvents(publisherId, availabilities = true, assignments = true, includeUnpublished = false) { const result = []; // let pubs = await filterPublishers("id,firstName,lastName,email".split(","), "", date, assignments, availabilities, date ? true : false, publisherId); const prisma = common.getPrismaClient(); let publisher = await prisma.publisher.findUnique({ where: { id: publisherId }, select: { id: true, firstName: true, lastName: true, email: true, availabilities: { select: { id: true, dayOfMonth: true, dayofweek: true, weekOfMonth: true, startTime: true, endTime: true, name: true, isFromPreviousAssignment: true, isFromPreviousMonth: true, repeatWeekly: true, isWithTransportIn: true, isWithTransportOut: true, } }, assignments: { select: { id: true, // publisherId: true, shift: { select: { id: true, startTime: true, endTime: true, isPublished: true } } } } } }); if (publisher) { if (availabilities) { publisher.availabilities?.forEach(item => { result.push({ ...item, title: common.getTimeFormatted(new Date(item.startTime)) + "-" + common.getTimeFormatted(new Date(item.endTime)), //item.name, date: new Date(item.startTime), startTime: new Date(item.startTime), endTime: new Date(item.endTime), publisherId: publisher.id, type: "availability", isFromPreviousAssignment: item.isFromPreviousAssignment, isWithTransportIn: item.isWithTransportIn, isWithTransportOut: item.isWithTransportOut, }); }); } if (assignments) { //only published shifts publisher.assignments?.filter( assignment => assignment.shift.isPublished || includeUnpublished ).forEach(item => { result.push({ ...item, title: common.getTimeFormatted(new Date(item.shift.startTime)) + "-" + common.getTimeFormatted(new Date(item.shift.endTime)), date: new Date(item.shift.startTime), startTime: new Date(item.shift.startTime), endTime: new Date(item.shift.endTime), // publisherId: item.publisherId, publisherId: publisher.id, type: "assignment", }); }); } } return result; } // exports.getCoverMePublisherEmails = async function (shiftId) { async function getCoverMePublisherEmails(shiftId) { const prisma = common.getPrismaClient(); let subscribedPublishers = await prisma.publisher.findMany({ where: { isSubscribedToCoverMe: true }, select: { id: true, firstName: true, lastName: true, email: true } }).then(pubs => { return pubs.map(pub => { return { id: pub.id, firstName: pub.firstName, lastName: pub.lastName, name: pub.firstName + " " + pub.lastName, email: pub.email } }); }); let shift = await prisma.shift.findUnique({ where: { id: shiftId }, include: { assignments: { include: { publisher: { select: { id: true, firstName: true, lastName: true, email: true } } } } } }); let availableIn = new Date(shift.startTime) let availablePublishers = await filterPublishersNew("id,firstName,lastName,email", availableIn, true, false, false, false, false); //filter out publishers that are already assigned to the shift availablePublishers = availablePublishers.filter(pub => { return shift.assignments.findIndex(assignment => assignment.publisher.id == pub.id) == -1; }); //subscribed list includes only publishers that are not already assigned to the shift subscribedPublishers = subscribedPublishers.filter(pub => { return availablePublishers.findIndex(availablePub => availablePub.id == pub.id) == -1 && shift.assignments.findIndex(assignment => assignment.publisher.id == pub.id) == -1; }); //return names and email info only availablePublishers = availablePublishers.map(pub => { return { id: pub.id, firstName: pub.firstName, lastName: pub.lastName, name: pub.firstName + " " + pub.lastName, email: pub.email } }); return { shift, availablePublishers: availablePublishers, subscribedPublishers }; } // ### COPIED TO shift api (++) ### /** JSDoc * Generates a schedule. * 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. * * @param {Axios} axios Axios instance for making requests. * @param {string} date The date for the schedule. * @param {boolean} [copyFromPreviousMonth=false] Whether to copy from the previous month. * @param {boolean} [autoFill=false] Whether to autofill data. * @param {boolean} forDay Specific day flag. */ async function GenerateSchedule(axios, date, copyFromPreviousMonth = false, autoFill = false, forDay) { let missingPublishers = []; let publishersWithChangedPref = []; const prisma = common.getPrismaClient(); try { const monthInfo = common.getMonthDatesInfo(new Date(date)); const lastMonthInfo = common.getMonthDatesInfo(new Date(monthInfo.date.getFullYear(), monthInfo.date.getMonth() - 1, 1)); if (forDay) { await DeleteShiftsForDay(monthInfo.date); } else { await DeleteShiftsForMonth(monthInfo); } const events = await prisma.cartEvent.findMany({ where: { isActive: true } }); let shiftsLastMonth = await getShiftsFromLastMonth(lastMonthInfo); let publishers = await getAllPublishersWithStatisticsMonth('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', date, false, true, false, true, true); 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 filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true); console.log("passing schedule generation for " + day.toLocaleDateString()); const dayOfM = day.getDate(); let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(day); let dayName = common.DaysOfWeekArray[day.getDayEuropean()]; const event = events.find((event) => event.dayofweek == dayName && (event.dayOfMonth == null || event.dayOfMonth == dayOfM)); if (!event) { day.setDate(day.getDate() + 1); continue; } event.startTime = new Date(event.startTime); event.endTime = new Date(event.endTime); let startTime = new Date(day); startTime.setHours(event.startTime.getHours()); startTime.setMinutes(event.startTime.getMinutes()); let endTime = new Date(day); endTime.setHours(event.endTime.getHours()); endTime.setMinutes(event.endTime.getMinutes()); let shiftStart = new Date(startTime); let shiftEnd = new Date(startTime); shiftEnd.setMinutes(shiftStart.getMinutes() + event.shiftDuration); let shiftNr = 0; while (shiftEnd <= endTime) { shiftNr++; const __shiftName = String(shiftStart.getHours()).padStart(2, "0") + ":" + String(shiftStart.getMinutes()).padStart(2, "0") + " - " + String(shiftEnd.getHours()).padStart(2, "0") + ":" + String(shiftEnd.getMinutes()).padStart(2, "0"); shiftAssignments = []; let isTransportRequired = shiftNr == 1 || shiftEnd.getTime() == endTime.getTime(); const shiftLastMonthSameDay = findTheSameShiftFromLastMonth(shiftsLastMonth, day, weekNr, shiftNr); if (shiftLastMonthSameDay) { for (let assignment of shiftLastMonthSameDay.assignments) { let publisher = assignment.publisher; console.log("found publisher from last month: " + publisher.firstName + " " + publisher.lastName); let availability = await FindPublisherAvailability(publisher.id, shiftStart, shiftEnd, dayOfWeekEnum, weekNr); console.log("availability " + availability?.id + ": " + common.getDateFormattedShort(availability?.startTime) + " " + common.getTimeFormatted(availability?.startTime) + " - " + common.getTimeFormatted(availability?.endTime)); if (availability && copyFromPreviousMonth && !publishersThisWeek.includes(publisher.id)) { shiftAssignments.push({ publisherId: publisher.id, isConfirmed: true, isWithTransportIn: availability.isWithTransportIn, isWithTransportOut: availability.isWithTransportOut }); publishersThisWeek.push(publisher.id); } } } let publishersNeeded = event.numberOfPublishers - shiftAssignments.length; //ToDo: check if getAvailablePublishersForShift is working correctly let availablePublishers = await getAvailablePublishersForShift(shiftStart, shiftEnd, publishers, publishersThisWeek); console.log("shift " + __shiftName + " needs " + publishersNeeded + " publishers, available: " + availablePublishers.length + " for the day: " + availablePubsForTheDay.length); // Prioritize publishers with minimal availability // SKIP ADDING PUBLISHERS FOR NOW // availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); // for (let i = 0; i < publishersNeeded; i++) { // if (availablePublishers[i]) { // shiftAssignments.push({ publisherId: availablePublishers[i].id }); // publishersThisWeek.push(availablePublishers[i].id); // } // } const createdShift = await prisma.shift.create({ data: { startTime: shiftStart, endTime: shiftEnd, name: event.dayofweek + " " + shiftStart.toLocaleTimeString() + " - " + shiftEnd.toLocaleTimeString(), requiresTransport: isTransportRequired, cartEvent: { connect: { id: event.id, }, }, assignments: { create: shiftAssignments.map((a) => { return { publisher: { connect: { id: a.publisherId } }, 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, }, }, }, }); console.log(" second pass " + monthInfo.monthName + " " + monthInfo.year); // 2. First pass - prioritize shifts with transport where it is needed day = 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 filterPublishersNew('id,firstName,lastName,email,isActive,desiredShiftsPerMonth,lastLogin,type', day, false, false, false, true, true); let shifts = allShifts.filter(s => common.getISODateOnly(s.startTime) === common.getISODateOnly(day)); let transportShifts = shifts.filter(s => s.requiresTransport); transportShifts.forEach(shift => { let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id)); availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); let publishersNeeded = event.numberOfPublishers - shift.assignments.length; if (publishersNeeded > 0) {//get the beset match if (availablePublishers[0]) { shift.assignments.push({ publisherId: availablePublishers[i].id }); } } }); // 3. Second pass - fill the rest of the shifts let shiftsToFill = shifts.filter(s => !s.requiresTransport); shiftsToFill.forEach(shift => { let availablePublishers = availablePubsForTheDay.filter(p => !shift.assignments.some(a => a.publisher.id === p.id)); availablePublishers = availablePublishers.sort((a, b) => a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount); let publishersNeeded = event.numberOfPublishers - shift.assignments.length; if (publishersNeeded > 0) {//get the beset match if (availablePublishers[0]) { shift.assignments.push({ publisherId: availablePublishers[i].id }); } } }); } day.setDate(day.getDate() + 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 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 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 matchesAvailability(avail, filterDate) { // // Setting the start and end time of the filterDate // filterDate.setHours(0, 0, 0, 0); // const filterDateEnd = new Date(filterDate); // filterDateEnd.setHours(23, 59, 59, 999); // // Return true if avail.startTime is between filterDate and filterDateEnd // return avail.startTime >= filterDate && avail.startTime <= filterDateEnd; // } const fs = require('fs'); const { filter } = require('jszip'); const path = require('path'); async function runSqlFile(filePath) { const prisma = common.getPrismaClient(); // Seed the database with some sample data try { // Read the SQL file const sql = fs.readFileSync(filePath, { encoding: 'utf-8' }); // Split the file content by semicolon and filter out empty statements const statements = sql.split(';').map(s => s.trim()).filter(s => s.length); // Execute each statement for (const statement of statements) { await prisma.$executeRawUnsafe(statement); } console.log('SQL script executed successfully.'); } catch (error) { console.error('Error executing SQL script:', error); } } module.exports = { findPublisher, FindPublisherAvailability, runSqlFile, getAvailabilities, filterPublishersNew, getCoverMePublisherEmails, getAllPublishersWithStatisticsMonth, getCalendarEvents, GenerateSchedule, DeleteShiftsForMonth, DeleteShiftsForDay, };