Files
mwitnessing/pages/cart/publishers/import.tsx
2024-05-05 01:42:11 +03:00

674 lines
37 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { 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<ModeState>({
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 <any character> [Четвъртък]
// 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 (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.POWERUSER, UserRole.ADMIN]}>
<div className="p-6">
<h1 className="text-3xl mb-4 font-semibold">Import Page</h1>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700" htmlFor="fileInput">Choose a file to import:</label>
<input type="file" id="fileInput" onChange={handleFile} className="mt-1 p-2 border rounded" placeholder="Choose a file" />
</div>
{/* <DatePicker label="Дата" value={date} onChange={setDate} /> */}
<div className="mb-4 space-y-2"> {/* Adjust this value as needed */}
<label className="flex items-center">
<input
type="radio"
value="PUBLISHERS1"
checked={mode.mainMode === "PUBLISHERS1"}
onChange={() => setMode({ mainMode: "PUBLISHERS1", schedule: false, publishers2Import: false })}
className="text-indigo-600"
/>
<span className="ml-2 space-x-4">Импортирай от Списък с участниците</span>
</label>
<label className="flex items-center">
<input
type="radio"
value="PUBLISHERS2"
checked={mode.mainMode === "PUBLISHERS2"}
onChange={() => setMode(prev => ({ ...prev, mainMode: "PUBLISHERS2" }))}
className="text-indigo-600"
/>
<span className="ml-2">Импортирай от Предпочитания за колички</span>
</label>
<label className="flex items-center">
{/* <DatePicker
label="Дата на импорт"
value={new Date()}
onChange={(date) => {
common.logger.debug("date changed to " + date);
}}
/> */}
{/* simple HTML datepicker for import date */}
<input type="date" id="importDate" name="importDate" />
</label>
{mode.mainMode === "PUBLISHERS2" && (
<div className="mt-2 space-y-1 pl-6">
<label className="flex items-center">
<input
type="checkbox"
checked={mode.schedule}
onChange={(e) => setMode(prev => ({ ...prev, schedule: e.target.checked }))}
className="text-indigo-600"
/>
<span className="ml-2">Предпочитания</span>
</label>
<label className="flex items-center ">
<input
type="checkbox"
checked={mode.publishers2Import}
onChange={(e) => setMode(prev => ({ ...prev, publishers2Import: e.target.checked }))}
className="text-indigo-600"
/>
<span className="ml-2">Вестители</span>
</label>
</div>
)}
</div>
<div className="mb-4">
<button onClick={handleSave} className="bg-indigo-600 text-white py-2 px-4 rounded hover:bg-indigo-700">Запази</button>
</div>
<div className="mb-4 flex items-center space-x-4">
<span className="text-gray-600">{data.length} вестители прочетени</span>
<span className="text-gray-600">{status.info}</span>
</div>
<table className="min-w-full border-collapse border border-gray-500">
<thead>
<tr>
{data.length > 0 &&
Object.keys(data[0]).map((key) => <th className="px-4 py-2 border-b font-medium" key={key}>{Object.values(data[0])[key]}</th>)}
</tr>
</thead>
<tbody>
{data.slice(1).map((row, index) => (
<tr key={index} className="even:bg-gray-100">
{Object.values(row).map((value, index) => (
<td key={index} className="border px-4 py-2">{value}</td>
))}
<td id={row[1]}>
<i className={`fa fa-circle ${status[row[3]] || 'text-gray-400'}`}></i>
</td>
</tr>
))}
</tbody>
</table>
</div>
</ProtectedRoute>
</Layout>
);
};