const common = require('./common'); 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 } } async function findPublisherAvailability(publisherId, date) { const prisma = common.getPrismaClient(); date = new Date(date); // Convert to date object if not already const hours = date.getHours(); const minutes = date.getMinutes(); const potentialAvailabilities = await prisma.availability.findMany({ where: { publisherId: publisherId, AND: [ // Ensure both conditions must be met { startTime: { lte: new Date(date), // startTime is less than or equal to the date }, }, { endTime: { gte: new Date(date), // endTime is greater than or equal to the date }, }, ], } }); if (potentialAvailabilities.length === 0) { return null; // No availability found } // Filter the results based on time and other criteria when not exact date match const availability = potentialAvailabilities.find(avail => { const availStartHours = avail.startTime.getHours(); const availStartMinutes = avail.startTime.getMinutes(); const availEndHours = avail.endTime.getHours(); const availEndMinutes = avail.endTime.getMinutes(); const isAfterStartTime = hours > availStartHours || (hours === availStartHours && minutes >= availStartMinutes); const isBeforeEndTime = hours < availEndHours || (hours === availEndHours && minutes <= availEndMinutes); // check day of week if not null const isCorrectDayOfWeek = avail.repeatWeekly ? avail.startTime.getDay() === date.getDay() : true; const isExactDateMatch = avail.dayOfMonth ? avail.startTime.toDateString() === date.toDateString() : true; const isBeforeEndDate = avail.repeatWeekly ? true : avail.endTime > date; //const isCorrectWeekOfMonth = avail.repeatWeekly ? true : avail.weekOfMonth === weekOfMonth; return isAfterStartTime && isBeforeEndTime && isCorrectDayOfWeek && isExactDateMatch && isBeforeEndDate; }); return availability; } 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; } async function filterPublishersNew(selectFields, filterDate, isExactTime = false, isForTheMonth = false, noEndDateFilter = false, isWithStats = true, includeOldAvailabilities = false) { const prisma = common.getPrismaClient(); filterDate = new Date(filterDate); // Convert to date object if not already 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 filterTimeFrom = new Date(filterDate) let filterTimeTo = new Date(filterDate); let isDayFilter = true; let whereClause = {}; if (isForTheMonth) { var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber filterTimeFrom = monthInfo.firstMonday; filterTimeTo = monthInfo.lastSunday; isDayFilter = false; } 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); } whereClause["availabilities"] = { some: { OR: [ // 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 } }, // 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 }, { 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) { isDayFilter = false; } else { whereClause["availabilities"].some.OR[0].endTime = { lte: filterTimeTo }; if (isForTheMonth) { // no dayofweek or time filters here } 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 { } } } 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 } }); console.log(`publishers: ${publishers.length}, WhereClause: ${JSON.stringify(whereClause)}`); // convert matching weekly availabilities to availabilities for the day to make further processing easier on the client. // we trust that the filtering was OK, so we use the dateFilter as date. publishers.forEach(pub => { pub.availabilities = pub.availabilities.map(avail => { if (avail.dayOfMonth == 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 } } return avail; }); }); 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); }) 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; }); } }); 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.filter(a => a.startTime > filterTimeFrom && a.endTime < filterTimeTo) }); 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 getAllPublishersWithStatistics(filterDate) { const prisma = common.getPrismaClient(); const monthInfo = common.getMonthDatesInfo(new Date(filterDate)); 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, true, true, true); // 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, 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), // }, // }, }, }, }); let prevMnt = new Date(); prevMnt.setMonth(prevMnt.getMonth() - 1); let prevMonthInfo = common.getMonthDatesInfo(prevMnt); allPublishers.forEach(publisher => { // 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 }); //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, date, availabilities = true, assignments = true) { 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 ).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 }; } // 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 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, getAllPublishersWithStatistics, getCalendarEvents };