import { toast } from 'react-toastify'; import Layout from "../../../components/layout"; import { Publisher, Availability, AvailabilityType, DayOfWeek, UserRole, PublisherType } from "@prisma/client"; import ProtectedRoute from '../../../components/protectedRoute'; import axiosInstance from '../../../src/axiosSecure'; import { useState, useRef } from "react"; import * as XLSX from "xlsx"; // import { Table } from "react-bootstrap"; import { staticGenerationAsyncStorage } from "next/dist/client/components/static-generation-async-storage.external"; import moment from 'moment'; // import { DatePicker } from '@mui/x-date-pickers'; !! CAUSERS ERROR ??? // import { DatePicker } from '@mui/x-date-pickers/DatePicker'; import { set } from 'date-fns'; // import * as common from "src/helpers/common"; const common = require('../../../src/helpers/common'); export default function ImportPage() { const [data, setData] = useState([]) const [rawData, setRawData] = useState([]); const [status, setStatus] = useState({ status: 'idle', info: '' }); const MODE_PUBLISHERS1 = "PUBLISHERS1"; const MODE_PUBLISHERS2 = "PUBLISHERS2"; type ModeState = { mainMode: typeof MODE_PUBLISHERS1 | typeof MODE_PUBLISHERS2; schedule: boolean; publishers2Import: boolean; headerRow: number; } const [mode, setMode] = useState({ mainMode: MODE_PUBLISHERS1, schedule: false, publishers2Import: false, headerRow: 0 }); const headerRef = useRef({ header: null, dateIndex: -1, emailIndex: -1, nameIndex: -1, phoneIndex: -1, isTrainedIndex: -1, desiredShiftsIndex: -1, dataStartIndex: -1, isActiveIndex: -1, pubTypeIndex: -1, gender: -1 }); const handleFile = (e) => { const [file] = e.target.files; const reader = new FileReader(); reader.onload = (evt) => { const bstr = evt.target.result; const wb = XLSX.read(bstr, { type: "binary" }); const wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; const sheetData = XLSX.utils.sheet_to_json(ws, { header: 1, range: 0, blankrows: false, defval: '', }); for (let i = 0; i < 5; i++) { if (sheetData[i].includes('Имейл')) { setMode({ mainMode: MODE_PUBLISHERS1, schedule: false, publishers2Import: false, headerRow: i }); headerRef.current.header = sheetData[i]; headerRef.current.dataStartIndex = i; common.logger.debug("header at row " + i); break; } // it seems we are processing availability sheet. import only publishers by default, and read only the first 3 columns as publisher data if (sheetData[i].includes('Email Address')) { setMode({ mainMode: MODE_PUBLISHERS2, schedule: true, publishers2Import: false, headerRow: i }); headerRef.current.header = sheetData[i]; headerRef.current.dataStartIndex = i; break; } } if (!headerRef.current.header) { console.error("header not found in the first 5 rows!"); return; } const header = headerRef.current.header headerRef.current.dateIndex = header.indexOf('Timestamp'); headerRef.current.emailIndex = header.indexOf('Имейл') !== -1 ? header.indexOf('Имейл') : header.indexOf('Email Address'); headerRef.current.nameIndex = header.indexOf('Име, Фамилия'); headerRef.current.phoneIndex = header.indexOf('Телефон'); headerRef.current.isTrainedIndex = header.indexOf('Обучен'); headerRef.current.desiredShiftsIndex = header.indexOf('Желан брой участия'); headerRef.current.isActiveIndex = header.indexOf("Неактивен"); headerRef.current.pubTypeIndex = header.indexOf("Назначение"); headerRef.current.gender = header.indexOf("Пол"); const filteredData = sheetData.slice(headerRef.current.dataStartIndex).map((row) => { let date; date = common.excelSerialDateToDate(row[headerRef.current.dateIndex]); //substract 1 day, because excel serial date is 1 day ahead date.setDate(date.getDate() - 1); date = common.getDateFormated(date); common.logger.debug(date); return [date, row[headerRef.current.emailIndex], row[headerRef.current.nameIndex]]; }); setRawData(sheetData); setData(filteredData); setStatus({ status: 'зареден', info: `Заредени ${filteredData.length} от ${rawData.length} записа` }); }; reader.readAsBinaryString(file); // Reset the file input value e.target.value = null; }; const handleSave = async () => { try { common.logger.debug("handleSave to: " + common.getBaseUrl()); console.log("handleSave to: " + common.getBaseUrl()); const header = rawData[mode.headerRow]; for (let i = mode.headerRow + 1; i < rawData.length; i++) { //fullData.length; each publisher //update status.info with current publisher setStatus({ status: 'running', info: `Processing row ${i} of ${rawData.length}` }); //sleep for 300ms to allow the database to process the previous request await new Promise(r => setTimeout(r, 100)); let isOld = false; const row = rawData[i]; let email, phone, names, dateOfInput, oldAvDeleted = false, isTrained = false, desiredShiftsPerMonth = 4, isActive = true, publisherType = PublisherType.Publisher, isMale = 0 ; //ToDo: structure all vars above as single object: if (mode.mainMode == MODE_PUBLISHERS1) { email = row[headerRef.current.emailIndex]; phone = row[headerRef.current.phoneIndex].toString().trim(); // Trim whitespace // Remove any non-digit characters, except for the leading + //phone = phone.replace(/(?!^\+)\D/g, ''); phone = phone.replace(/[^+\d]/g, ''); if (phone.startsWith('8') || phone.startsWith('9')) { phone = '+359' + phone } else if (!phone.startsWith('+')) { phone = '+' + phone; // Add + if it's missing, assuming the number is complete } names = row[headerRef.current.nameIndex].normalize('NFC').split(/[ ]+/); dateOfInput = importDate.value || new Date().toISOString(); // not empty == true isTrained = row[headerRef.current.isTrainedIndex] !== ''; isActive = row[headerRef.current.isActiveIndex] == ''; desiredShiftsPerMonth = row[headerRef.current.desiredShiftsIndex] !== '' ? row[headerRef.current.desiredShiftsIndex] : 4; publisherType = row[headerRef.current.pubTypeIndex]; isMale = row[headerRef.current.gender].trim().toLowerCase() === 'брат'; } 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 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(new Date(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 >= 2) { isOld = true; } // let names = common.removeAccentsAndSpecialCharacters(row[2]).split(/[, ]+/); let personId = ''; let personNames = names.join(' '); try { try { const select = "&select=id,firstName,lastName,email,phone,isTrained,desiredShiftsPerMonth,isActive,isMale,type,availabilities"; const responseByName = await axiosInstance.get(`/api/?action=findPublisher&filter=${names.join(' ')}${select}`); let existingPublisher = responseByName.data[0]; if (!existingPublisher) { // Check if email is empty and generate a system one if needed if (!email) { const fullName = names.join(' ').toLowerCase(); // Assuming names is an array of [firstName, lastName] email = fullName.replace(/\s+/g, '.'); // Replace spaces with dots email += "";//@gmail.com? // Append a domain to make it a valid email format } // If no match by name, check by email const responseByEmail = await axiosInstance.get(`/api/?action=findPublisher&email=${email}${select}`); if (responseByEmail.data.length > 0) { // Iterate over all matches by email to find one with a matching or similar name const fullName = names.join(' ').toLowerCase(); // Simplify comparison existingPublisher = responseByEmail.data.find(publisher => { const publisherFullName = (publisher.firstName + ' ' + publisher.lastName).toLowerCase(); return fullName === publisherFullName; // Consider expanding this comparison for typos }); } } if (existingPublisher?.id) { // UPDATE // Create a flag to check if update is needed const updatedData = {}; personId = existingPublisher?.id; personNames = existingPublisher.firstName + ' ' + existingPublisher.lastName; let fieldsToUpdateString = ''; // Check for name update const fullName = names.join(' '); const existingFullName = existingPublisher.firstName + ' ' + existingPublisher.lastName; if (fullName !== existingFullName) { common.logger.debug(`Existing publisher '${existingFullName}' found for ${email} (ID:${personId})`); updatedData.firstName = names[0]; updatedData.lastName = names.slice(1).join(' '); data[i - mode.headerRow][4] = "name updated!"; fieldsToUpdateString += `name, `; } else { data[i - mode.headerRow][4] = "existing"; } common.logger.debug(`Existing publisher '${[existingPublisher.firstName, existingPublisher.lastName].join(' ')}' found for ${email} (ID:${personId})`); // Check for other updates const fieldsToUpdate = [ { key: 'email', value: email }, { key: 'phone', value: phone }, { key: 'desiredShiftsPerMonth', value: desiredShiftsPerMonth, parse: parseInt }, { key: 'isTrained', value: isTrained }, { key: 'isActive', value: isActive }, { key: "isMale", value: isMale }, { key: 'type', value: publisherType, parse: common.getPubTypeEnum } ]; fieldsToUpdate.forEach(({ key, value, parse }) => { const newValue = parse ? parse(value) : value; // Check if an update is needed: if the existing value is different or not set, and the new value is not empty/undefined if ((existingPublisher[key] !== newValue && value !== '' && value !== undefined) || (!existingPublisher.hasOwnProperty(key) && value !== '' && value !== undefined)) { updatedData[key] = parse ? parse(value) : value; fieldsToUpdateString += `${key}, `; } }); // Update the record if needed and if MODE_PUBLISHERS1 (Import from List of Participants) if (fieldsToUpdateString.length > 0 && (mode.publishers2Import || mode.mainMode == MODE_PUBLISHERS1)) { try { await axiosInstance.put(`/api/data/publishers/${personId}`, updatedData); common.logger.debug(`Updated publisher ${personId} - Fields Updated: ${fieldsToUpdateString}`); data[i - mode.headerRow][4] = fieldsToUpdateString.substring(0, fieldsToUpdateString.length - 2) + " updated"; } catch (error) { data[i - mode.headerRow][4] = "error updating!"; console.error(`Failed to update publisher ${personId} - Fields Attempted: ${fieldsToUpdateString}`, error); } } } else { // CREATE // If no publisher with the email exists, create one // const names = email.split('@')[0].split('.');\ //Save new publisher if (mode.publishers2Import) { const personResponse = await axiosInstance.post('/api/data/publishers', { email, phone, firstName: names[0], lastName: names[1], isActive: isActive, isTrained, desiredShiftsPerMonth, }); personId = personResponse.data.id; data[i][4] = "new"; } else if (mode.mainMode == MODE_PUBLISHERS1) { const firstname = names.length > 2 ? names.slice(0, -1).join(' ') : names[0]; const personResponse = await axiosInstance.post('/api/data/publishers', { email, phone, firstName: firstname, lastName: names[names.length - 1], isActive: isActive, isMale: isMale, isTrained, desiredShiftsPerMonth }); data[i - mode.headerRow][4] = "new"; } else { data[i - mode.headerRow][4] = "import disabled"; } common.logger.debug(`NEW Publisher ${personId} created for email ${email} (${names})`); } } catch (error) { console.error(error); data[i - mode.headerRow][4] = "error"; } if (mode.schedule) { // Parse the availability data from the Excel cell //get days of the month and add up to the next full week // Save availability records const availabilities: Availability[] = []; for (let j = 3; j < header.length; j++) { const dayHeader = header[j]; const shifts = row[j]; // specific date: Седмица (17-23 април) [Четвъртък ] // const regex = /^Седмица \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/; // specific week: Седмица 1 [Четвъртък] // const regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\) \[(\S+)\]$/; //allow optional space before and after the brackets // match both 'Седмица 3 (20-25 ноември) [пон]' and 'Седмица 4 (27 ноември - 2 декември) [четв]' //const regex = /^\s*Седмица\s+(\d{1,2})\s+\((\d{1,2})\s+(\S+)(?:\s*-\s*(\d{1,2})\s+(\S+))?\)\s*\[\s*(\S+)\s*\]\s*$/; //the original, but missing the two month names let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/; regex = /^Седмица (\d{1,2}) \((\d{1,2})(?:\s*-\s*(\d{1,2}))? (\S+)(?:\s*-\s*(\d{1,2}) (\S+))?\)\s*\[(\S+)\s*\]\s*$/; //const regex = /^Седмица (\d{1,2}) \((\d{1,2}(\s*\S*?))-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/; //both Седмица 1 (6-11 ноември) [пон] // Седмица 4 (27 ноември-2 декември) [пет] //const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/; //const regex = /^Седмица (\d{1,2}) \((\d{1,2} \S+)?-? ?(\d{1,2} \S+)\)\s*\[(\S+)\s*\]\s*$/; // const regex = /^Седмица (\d{1,2}) \* \[(\S+)\]$/; // const regex = /^Седмица (\d{1,2}) \[(\S+)\]$/; // replace multiple spaces with single space const normalizedHeader = dayHeader.replace(/\s+/g, ' '); var match = normalizedHeader.match(regex); if (!match) { common.logger.debug("was not able to parse availability " + shifts + "trying again with different regex"); let regex = /^Седмица (\d{1,2}) \((\d{1,2})-(\d{1,2}) (\S+)\)\s*\[(\S+)\s*\]\s*$/; match = normalizedHeader.match(regex); } if (match) { //ToDo: can't we use date.getDayEuropean() instead of this logic down? const weekNr = parseInt(match[1]); const weekStart = match[2]; // const endDate = match[2]; const month = match[4]; const dayOfWeekStr = match[7]; 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 // 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)); day.setDate(parseInt(weekStart) + dayOfWeek); day.setHours(0, 0, 0, 0); common.logger.debug("processing availabilities for " + day.toLocaleDateString()); // Output: Sun Apr 17 2022 14:07:11 GMT+0300 (Eastern European Summer Time) common.logger.debug("parsing availability input: " + shifts); // Output: 0 (Sunday) const dayOfWeekName = common.getDayOfWeekNameEnEnumForDate(day); if (!shifts || shifts === 'Не мога') { continue; } if (!oldAvDeleted && personId) { if (mode.schedule && email) { common.logger.debug(`Deleting existing availabilities for publisher ${personId} for date ${day}`); try { await axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${personId}&date=${day}&deleteFromPreviousAssignments=true`); common.logger.debug(`Deleted all availabilities for publisher ${personId}`); oldAvDeleted = true; } catch (error) { console.error(`Failed to delete availabilities for publisher ${personId}`, error); } } } 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) //if (!/\(\d{1,2}:\d{2}-\d{1,2}:\d{2}\)/.test(interval)) { //winter regex: //\d{1,2}-\d{1,2}:\d{2}/ // Regular expression to match both Summer format (12:00-14:00) and winter format '09-10:30' const regex = /\d{1,2}(?::\d{2})?-\d{1,2}(?::\d{2})?/; if (!regex.test(interval)) { common.logger.debug(`Skipping invalid interval: ${interval}`); return; } // If the interval matches the format, remove any non-essential characters const cleanedInterval = interval.replace(/[()]/g, ""); // Extract start and end times from interval string const [start, end] = cleanedInterval.split("-"); // Convert times like "12" to "12:00" for consistent parsing const formattedStart = start.includes(":") ? start : start + ":00"; const formattedEnd = end.includes(":") ? end : end + ":00"; // Try to parse the times, and skip the interval if it can't be parsed try { const parsedStart = Number(formattedStart.split(":").join("")); const parsedEnd = Number(formattedEnd.split(":").join("")); // Store parsed interval parsedIntervals.push({ start: parsedStart, end: parsedEnd }); } catch (error) { common.logger.debug(`Error parsing interval: ${interval}`); return; } }); // Sort intervals by start time parsedIntervals.sort((a, b) => a.start - b.start); // Initialize start and end times with the first interval let minStartTime = parsedIntervals[0].start; let maxEndTime = parsedIntervals[0].end; // 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, dateOfInput)); minStartTime = parsedIntervals[i].start; maxEndTime = parsedIntervals[i].end; } else { maxEndTime = Math.max(maxEndTime, parsedIntervals[i].end); } } // Add the last availability availabilities.push(createAvailabilityObject(minStartTime, maxEndTime, day, dayOfWeekName, dayOfMonth, weekNr, personId, name, isOld, dateOfInput)); } else { common.logger.debug("availability not matched. header:" + dayHeader + " shifts:" + shifts); } } common.logger.debug("availabilities to save for " + personNames + ": " + availabilities.length); // Send a single request to create all availabilities axiosInstance.post('/api/?action=createAvailabilities', availabilities) .then(response => common.logger.debug('Availabilities created:', response.data)) .catch(error => console.error('Error creating availabilities:', error)); // 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,email`); sameNamePubs.then(function (response) { common.logger.debug("same name pubs: " + response.data.length); if (response.data.length > 1) { response.data.forEach(pub => { //check the publisher is not the same as the one we already added the availabilities to if (pub.id != personId) { //change the publisher id to the new one availabilities.forEach(availability => { availability.publisherId = pub.id; } ); //delete existing availabilities for the publisher axiosInstance.post(`/api/?action=deleteAvailabilityForPublisher&publisherId=${pub.id}&date=${dateOfInput}`); // Send a single request to create all availabilities axiosInstance.post('/api/?action=createAvailabilities', availabilities) .then(response => common.logger.debug('Availabilities created:', response.data)) .catch(error => console.error('Error creating availabilities:', error)); } }); } }); } // await axios.post("/api/data/availabilities", availabilities); } catch (error) { console.error(error); } } toast.info('Records saved successfully', { autoClose: 30000 }); } catch (error) { console.error(error); toast.error('An error occurred while saving records!'); } }; // 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, 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))); }; const startTime = formatTime(start); const endTime = formatTime(end); return { id: 0, // Add the missing 'id' property publisherId: personId, name, dayofweek: dayOfWeekName, dayOfMonth, weekOfMonth: weekNr, // Add the missing 'weekOfMonth' startTime, 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 isFromPreviousMonth: isFromPreviousMonth // Add the missing 'isFromPreviousMonth' property }; } return (

Import Page

{/* */}
{/* Adjust this value as needed */} {mode.mainMode === "PUBLISHERS2" && (
)}
{data.length} вестители прочетени {status.info}
{data.length > 0 && Object.keys(data[0]).map((key) => )} {data.slice(1).map((row, index) => ( {Object.values(row).map((value, index) => ( ))} ))}
{Object.values(data[0])[key]}
{value}
); };