diff --git a/next.config.js b/next.config.js index 687075b..87288ce 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,11 @@ const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); const withPWA = require('next-pwa')({ - dest: 'public' + dest: 'public', + register: true, // ? + publicExcludes: ["!_error*.js"], //? + + disable: process.env.NODE_ENV === 'development', }) module.exports = withPWA({ diff --git a/pages/api/index.ts b/pages/api/index.ts index afef4b1..6ce6997 100644 --- a/pages/api/index.ts +++ b/pages/api/index.ts @@ -30,15 +30,20 @@ export default async function handler(req, res) { var action = req.query.action; var filter = req.query.filter; - let date: Date, monthInfo: any; + let day: Date, monthInfo: any; + let isExactTime; if (req.query.date) { - date = new Date(req.query.date); + day = new Date(req.query.date); //date.setDate(date.getDate()); // Subtract one day to get the correct date, as calendar sends wrong date (one day ahead) //date.setHours(0, 0, 0, 0); } if (req.query.filterDate) { - date = new Date(req.query.filterDate); + day = new Date(req.query.filterDate); + isExactTime = true; } + + const searchText = req.query.searchText?.normalize('NFC'); + try { switch (action) { case "initDb": @@ -69,7 +74,7 @@ export default async function handler(req, res) { where: filter ? { OR: [ // { name: { contains: filter } }, - { starTime: { lte: date } } + { starTime: { lte: day } } ] } : {} }); @@ -130,7 +135,7 @@ export default async function handler(req, res) { break; case "getCalendarEvents": - let events = await getCalendarEvents(req.query.publisherId, date); + let events = await getCalendarEvents(req.query.publisherId, day); res.status(200).json(events); case "getPublisherInfo": @@ -138,24 +143,28 @@ export default async function handler(req, res) { res.status(200).json(pubs[0]); break; case "getMonthlyStatistics": - let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", date); + let allpubs = await getMonthlyStatistics("id,firstName,lastName,email", day); res.status(200).json(allpubs); break; case "getUnassignedPublishers": //let monthInfo = common.getMonthDatesInfo(date); - let allPubs = await filterPublishers("id,firstName,lastName,email,isActive".split(","), "", date, true, true, false); + let allPubs = await filterPublishers("id,firstName,lastName,email,isActive".split(","), "", day, true, true, false); let unassignedPubs = allPubs.filter(pub => pub.currentMonthAssignments == 0 && pub.availabilities.length > 0); res.status(200).json(unassignedPubs); break; case "filterPublishers": - const searchText = req.query.searchText?.normalize('NFC'); const fetchAssignments = common.parseBool(req.query.assignments); const fetchAvailabilities = common.parseBool(req.query.availabilities); - let publishers = await filterPublishers(req.query.select, searchText, date, fetchAssignments, fetchAvailabilities); + let publishers = await filterPublishers(req.query.select, searchText, day, fetchAssignments, fetchAvailabilities); //!console.log("publishers: (" + publishers.length + ") " + JSON.stringify(publishers.map(pub => pub.firstName + " " + pub.lastName))); res.status(200).json(publishers); break; + case "filterPublishersNew": + let results = await filterPublishersNew_Available(req.query.select, day, + common.parseBool(req.query.isExactTime), common.parseBool(req.query.isForTheMonth)); + res.status(200).json(results); + break; // find publisher by full name or email case "findPublisher": @@ -167,8 +176,8 @@ export default async function handler(req, res) { case "getShiftsForDay": // Setting the range for a day: starting from the beginning of the date and ending just before the next date. - let startOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate()); - let endOfDay = new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999); + let startOfDay = new Date(day.getFullYear(), day.getMonth(), day.getDate()); + let endOfDay = new Date(day.getFullYear(), day.getMonth(), day.getDate(), 23, 59, 59, 999); const pubAvCount = await prisma.publisher.findMany({ select: { @@ -206,13 +215,13 @@ export default async function handler(req, res) { } ); - console.log("shiftsForDate(" + date + ") - " + shiftsForDate.length + " : " + JSON.stringify(shiftsForDate.map(shift => shift.id))); + console.log("shiftsForDate(" + day + ") - " + shiftsForDate.length + " : " + JSON.stringify(shiftsForDate.map(shift => shift.id))); res.status(200).json(shiftsForDate); break; case "copyOldAvailabilities": //get all publishers that don't have availabilities for the current month - monthInfo = common.getMonthDatesInfo(date); + monthInfo = common.getMonthDatesInfo(day); // await prisma.availability.deleteMany({ // where: { // startTime: { @@ -302,7 +311,7 @@ export default async function handler(req, res) { break; case "deleteCopiedAvailabilities": //delete all availabilities that are copied from previous months - monthInfo = common.getMonthDatesInfo(date); + monthInfo = common.getMonthDatesInfo(day); await prisma.availability.deleteMany({ where: { startTime: { @@ -321,7 +330,7 @@ export default async function handler(req, res) { case "updateShifts": //get all shifts for the month and publish them (we pass date ) - let monthInfo = common.getMonthDatesInfo(date); + let monthInfo = common.getMonthDatesInfo(day); let isPublished = common.parseBool(req.query.isPublished); let updated = await prisma.shift.updateMany({ where: { @@ -415,6 +424,257 @@ export async function getMonthlyStatistics(selectFields, filterDate) { } +export async function filterPublishersNew_Available(selectFields, filterDate, isExactTime = false, isForTheMonth = false, isWithStats = true) { + + // 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: filterDate, + } + } + } + }; + + var monthInfo = common.getMonthDatesInfo(filterDate); + var weekNr = common.getWeekOfMonth(filterDate); //getWeekNumber + let dayOfWeekEnum = common.getDayOfWeekNameEnEnumForDate(filterDate); + if (!isExactTime) { + filterDate.setHours(0, 0, 0, 0); // Set to midnight + } + const filterDateEnd = new Date(filterDate); + filterDateEnd.setHours(23, 59, 59, 999); + + + let whereClause = {}; + //if full day, match by date only + if (!isExactTime) { // Check only by date without considering time ( Assignments on specific days without time) + whereClause["availabilities"] = { + some: { + OR: [ + { + startTime: { gte: filterDate }, + endTime: { lte: filterDateEnd }, + } + , + // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) + // This includes availabilities from previous assignments but not with preference + { + dayOfMonth: null, // includes monthly and weekly repeats + dayofweek: dayOfWeekEnum, + // ToDo: and weekOfMonth + startTime: { lte: filterDate }, + AND: [ + { + OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever + { endDate: { gte: filterDate } }, + { endDate: null } + ] + } + ] + } + ] + } + }; + } + //if not full day, match by date and time + else { + //match exact time (should be same as data.findPublisherAvailability()) + 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 }, + AND: [ + { + OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever + { endDate: { gte: filterDate } }, + { endDate: null } + ] + } + ] + } + ] + } + }; + } + if (isForTheMonth) { + // If no filter date, return all publishers's availabilities for currentMonthStart + whereClause["availabilities"] = { + some: { + OR: [ + // Check if dayOfMonth is not null and startTime is after currentMonthStart (Assignments on specific days AND time) + { + dayOfMonth: { not: null }, + startTime: { gte: currentMonthStart }, + endTime: { lte: currentMonthEnd } + }, + // Check if dayOfMonth is null and match by day of week using the enum (Assigments every week) + { + dayOfMonth: null, + AND: [ + { + OR: [ // OR condition for repeatUntil to handle events that either end after filterDate or repeat forever + { endDate: { gte: filterDate } }, + { endDate: null } + ] + } + ] + } + ] + } + }; + } + + console.log(`getting publishers for date: ${filterDate}, isExactTime: ${isExactTime}, isForTheMonth: ${isForTheMonth}`); + //include availabilities if flag is true + const prisma = common.getPrismaClient(); //why we need to get it again? + 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 furter 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: Date, currentWeekEnd: Date, + currentMonthStart: Date, currentMonthEnd: Date, + previousMonthStart: Date, previousMonthEnd: Date; + + if (isWithStats) { + currentWeekStart = common.getStartOfWeek(filterDate); + currentWeekEnd = common.getEndOfWeek(filterDate); + currentMonthStart = monthInfo.firstMonday; + currentMonthEnd = monthInfo.lastSunday; + let prevMnt = new Date(filterDate) + prevMnt.setMonth(prevMnt.getMonth() - 1); + monthInfo = common.getMonthDatesInfo(prevMnt); + previousMonthStart = monthInfo.firstMonday; + previousMonthEnd = monthInfo.lastSunday; + + //get if publisher has assignments for current weekday, week, current month, previous month + publishers.forEach(pub => { + // Filter assignments for current day + pub.currentDayAssignments = pub.assignments?.filter(assignment => { + return assignment.shift.startTime >= filterDate && assignment.shift.startTime <= filterDateEnd; + }).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 >= currentMonthStart && assignment.shift.startTime <= currentMonthEnd; + }).length; + + // Filter assignments for previous month + pub.previousMonthAssignments = pub.assignments?.filter(assignment => { + return assignment.shift.startTime >= previousMonthStart && assignment.shift.startTime <= previousMonthEnd; + }).length; + }); + + } + + //get the availabilities for the day. Calcullate: + //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 <= currentMonthEnd; + return avail.startTime >= currentMonthStart && avail.startTime <= currentMonthEnd; + }) + pub.currentMonthAvailabilityDaysCount = pub.currentMonthAvailability.length || 0; + // 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 >= currentMonthStart; // && avail.startTime <= currentMonthEnd; + }); + + } + + //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 pub has availabilities for the current day + pub.hasAvailabilityForCurrentDay = pub.availabilities?.some(avail => { + return avail.startTime >= filterDate && avail.startTime <= filterDateEnd; + }); + + }); + + + + 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; +} + // availabilites filter: // 1. if dayOfMonth is null, match by day of week (enum) // 2. if dayOfMonth is not null, match by date diff --git a/pages/cart/calendar/index.tsx b/pages/cart/calendar/index.tsx index 5bb9269..a04a1f4 100644 --- a/pages/cart/calendar/index.tsx +++ b/pages/cart/calendar/index.tsx @@ -110,7 +110,7 @@ export default function CalendarPage({ initialEvents, initialShifts }) { const { data: shiftsForDate } = await axiosInstance.get(`/api/?action=getShiftsForDay&date=${dateStr}`); setShifts(shiftsForDate); setIsPublished(shiftsForDate.some(shift => shift.isPublished)); - let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); + let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishersNew&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`); availablePubsForDate.forEach(pub => { pub.canTransport = pub.availabilities.some(av => @@ -143,6 +143,8 @@ export default function CalendarPage({ initialEvents, initialShifts }) { const handleShiftSelection = (selectedShift) => { setSelectedShiftId(selectedShift.id); const updatedPubs = availablePubs.map(pub => { + pub.isAvailableForShift = false; + pub.canTransport = false; const av = pub.availabilities?.find(avail => avail.startTime <= selectedShift.startTime && avail.endTime >= selectedShift.endTime diff --git a/pages/cart/publishers/import.tsx b/pages/cart/publishers/import.tsx index 5acedd8..99cda46 100644 --- a/pages/cart/publishers/import.tsx +++ b/pages/cart/publishers/import.tsx @@ -147,7 +147,7 @@ export default function ImportPage() { let isOld = false; const row = rawData[i]; - let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = false, publisherType = PublisherType.Publisher; + let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = true, publisherType = PublisherType.Publisher; //const date = new Date(row[0]).toISOS{tring().slice(0, 10); if (mode.mainMode == MODE_PUBLISHERS1) { @@ -173,19 +173,25 @@ export default function ImportPage() { } else { dateOfInput = common.excelSerialDateToDate(row[0]); + //substract 1 day, because excel serial date is 1 day ahead + dateOfInput.setDate(dateOfInput.getDate() - 1); email = row[1]; names = row[2].normalize('NFC').split(/[, ]+/); } //remove special characters from name - const day = new Date(); + let day = new Date(); day.setDate(1); // Set to the first day of the month to avoid overflow + if (importDate && importDate.value) { + let monthOfIportInfo = common.getMonthInfo(importDate.value); + day = monthOfIportInfo.firstMonday; + } dateOfInput = new Date(dateOfInput); // Calculate the total month difference by considering the year difference let totalMonthDifference = (day.getFullYear() - dateOfInput.getFullYear()) * 12 + (day.getMonth() - dateOfInput.getMonth()); // If the total month difference is 2 or more, set isOld to true - if (totalMonthDifference >= 0) { + if (totalMonthDifference >= 2) { isOld = true; } @@ -195,7 +201,7 @@ export default function ImportPage() { let personNames = names.join(' '); try { try { - const select = "&select=id,firstName,lastName,phone,isTrained,desiredShiftsPerMonth,isActive,type,availabilities"; + const select = "&select=id,firstName,lastName,email,phone,isTrained,desiredShiftsPerMonth,isActive,type,availabilities"; const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`); let existingPublisher = responseByName.data[0]; if (!existingPublisher) { @@ -325,12 +331,13 @@ export default function ImportPage() { const availabilities: Availability[] = []; for (let j = 3; j < header.length; j++) { + + + const dayHeader = header[j]; const shifts = row[j]; - if (!shifts || shifts === 'Не мога') { - continue; - } + // specific date: Седмица (17-23 април) [Четвъртък ] // const regex = /^Седмица \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/; @@ -373,8 +380,8 @@ export default function ImportPage() { const dayOfWeek = common.getDayOfWeekIndex(dayOfWeekStr); common.logger.debug("processing availability for week " + weekNr + ": " + weekStart + "." + month + "." + dayOfWeekStr) // Create a new Date object for the start date of the range - const day = new Date(); - day.setDate(1); // Set to the first day of the month to avoid overflow + // day = new Date(); + // day.setDate(1); // Set to the first day of the month to avoid overflow //day.setMonth(day.getMonth() + 1); // Add one month to the date, because we assume we are p day.setMonth(common.getMonthIndex(month)); @@ -385,10 +392,9 @@ export default function ImportPage() { common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday) const dayOfWeekName = common.getDayOfWeekNameEnEnumForDate(day); - let dayOfMonth = day.getDate(); - const name = `${names[0]} ${names[1]}`; - const intervals = shifts.split(","); - + if (!shifts || shifts === 'Не мога') { + continue; + } if (!oldAvDeleted && personId) { if (mode.schedule && email) { common.logger.debug(`Deleting existing availabilities for publisher ${personId} for date ${day}`); @@ -402,6 +408,11 @@ export default function ImportPage() { } } } + + let dayOfMonth = day.getDate(); + const name = `${names[0]} ${names[1]}`; + const intervals = shifts.split(","); + let parsedIntervals: { end: number; }[] = []; intervals.forEach(interval => { // Summer format: (12:00-14:00) @@ -451,7 +462,7 @@ export default function ImportPage() { // Iterate over intervals for (let i = 1; i < parsedIntervals.length; i++) { if (parsedIntervals[i].start > maxEndTime) { - availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld)); + availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld, dateOfInput)); minStartTime = parsedIntervals[i].start; maxEndTime = parsedIntervals[i].end; } else { @@ -460,7 +471,7 @@ export default function ImportPage() { } // Add the last availability - availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld)); + availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld, dateOfInput)); } else { @@ -477,7 +488,7 @@ export default function ImportPage() { // Experimental: add availabilities to all publishers with the same email //check if more than one publisher has the same email, and add the availabilities to both //check existing publishers with the same email - var sameNamePubs = axiosInstance.get(`/api/?action=findPublisher&all=true&email=${email}&select=id,firstName,lastName`); + var sameNamePubs = axiosInstance.get(`/api/?action=findPublisher&all=true&email=${email}&select=id,firstName,lastName,email`); sameNamePubs.then(function (response) { common.logger.debug("same name pubs: " + response.data.length); if (response.data.length > 1) { @@ -513,7 +524,9 @@ export default function ImportPage() { }; // Function to create an availability object - function createAvailabilityObject(start: any, end: number, day: Date, dayOfWeekName: any, dayOfMonth: number, weekNr: number, personId: string, name: string, isFromPreviousMonth: boolean): Availability { + function createAvailabilityObject(start: any, end: number, + day: Date, dayOfWeekName: any, dayOfMonth: number, weekNr: number, personId: string, name: string, + isFromPreviousMonth: boolean, dateOfInput: Date): Availability { const formatTime = (time) => { const paddedTime = String(time).padStart(4, '0'); return new Date(day.getFullYear(), day.getMonth(), day.getDate(), parseInt(paddedTime.substr(0, 2)), parseInt(paddedTime.substr(2, 4))); @@ -533,6 +546,7 @@ export default function ImportPage() { endTime, isActive: true, type: AvailabilityType.OneTime, + dateOfEntry: dateOfInput, isWithTransportIn: false, // Add the missing 'isWithTransport' property isWithTransportOut: false, // Add the missing 'isWithTransport' property isFromPreviousAssignment: false, // Add the missing 'isFromPreviousAssignment' property diff --git a/src/helpers/common.js b/src/helpers/common.js index a9a8b24..94c5e5e 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -86,7 +86,7 @@ exports.getPrismaClient = function getPrismaClient() { logger.debug("getPrismaClient: process.env.DATABASE = ", process.env.DATABASE); prisma = new PrismaClient({ // Optional: Enable logging - //log: ['query', 'info', 'warn', 'error'], + log: ['query', 'info', 'warn', 'error'], datasources: { db: { url: process.env.DATABASE } }, }); } @@ -201,7 +201,7 @@ exports.getDayOfWeekDate = function (dayOfWeekName, date = new Date()) { return date; }; //common.getWeekOfMonth(date) -exports.getWeekOfMonth = function (inputDate) { +exports.getWeekOfMonth = function (inputDate) { //getWeekOfMonth? let date = new Date(inputDate); let firstDayOfMonth = new Date(date.getFullYear(), date.getMonth(), 1); let firstMonday = new Date(firstDayOfMonth);