Files
mwitnessing/pages/cart/publishers/index.tsx
2025-01-08 10:41:42 +02:00

510 lines
20 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.

// Next.js page to show all locations in the database with a link to the location page
import { useSession } from "next-auth/react";
import { useEffect, useState, useRef, use } from "react";
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DesktopDatePicker } from '@mui/x-date-pickers/DesktopDatePicker';
import dayjs from 'dayjs';
// import { getSession } from 'next-auth/client'
// import { NextAuth } from 'next-auth/client'
import { Publisher, UserRole } from "@prisma/client";
import Layout from "../../../components/layout";
import PublisherCard from "../../../components/publisher/PublisherCard";
import axiosInstance from "../../../src/axiosSecure";
import axiosServer from '../../../src/axiosServer';
const common = require("../../../src/helpers/common");
import toast from "react-hot-toast";
import { levenshteinEditDistance } from "levenshtein-edit-distance";
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
import { relative } from "path";
import { set } from "lodash";
import { utils, write } from 'xlsx';
import { saveAs } from 'file-saver';
interface IProps {
initialItems: Publisher[];
}
function PublishersPage({ publishers = [] }: IProps) {
const [shownPubs, setShownPubs] = useState(publishers);
const [filter, setFilter] = useState("");
const [showZeroShiftsOnly, setShowZeroShiftsOnly] = useState(false);
const [selectedDate, setSelectedDate] = useState<dayjs.Dayjs | null>(null);
const [filterIsImported, setFilterIsImported] = useState({
checked: false,
indeterminate: true,
});
const [flterNoTraining, setFilterNoTraining] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
const [isModalOpenDeleteAllVisible, setIsModalOpenDeleteAllVisible] = useState(false);
const [isModalOpenDeleteAllAvaillabilities, setIsModalOpenDeleteAllAvaillabilities] = useState(false);
const handleDeleteAllVisible = async () => {
setIsDeleting(true);
for (const publisher of shownPubs) {
try {
await axiosInstance.delete(`/api/data/publishers/${publisher.id}`);
setShownPubs(shownPubs.filter(p => p.id !== publisher.id));
} catch (error) {
console.log(JSON.stringify(error));
}
}
setIsDeleting(false);
setIsModalOpenDeleteAllVisible(false);
};
const handleDeleteAllAvailabilities = async () => {
setIsDeleting(true);
try {
const today = new Date();
const targetDate = new Date(today.setDate(today.getDate() - 35));
await axiosInstance.post('/api/?action=deleteAllAvailabilities', { date: targetDate });
} catch (error) {
console.error(JSON.stringify(error)); // Log the error
}
setIsDeleting(false);
setIsModalOpenDeleteAllAvaillabilities(false);
};
useEffect(() => {
// const filteredPublishers = publishers.filter((publisher) => {
// return publisher.firstName.toLowerCase().includes(filter.toLowerCase())
// || publisher.lastName.toLowerCase().includes(filter.toLowerCase());
// });
//name filter
let filteredPublishersByName = publishers
.filter((publisher) => {
const fullName = publisher.firstName.toLowerCase() + " " + publisher.lastName.toLowerCase();
const distance = levenshteinEditDistance(fullName, filter.toLowerCase());
const lenDiff = Math.max(fullName.length, filter.length) - Math.min(fullName.length, filter.length);
let similarity;
if (distance === 0) {
similarity = 1; // Exact match
} else {
similarity = 1 - (distance - lenDiff) / distance;
}
//console.log("distance: " + distance + "; lenDiff: " + lenDiff + " similarity: " + similarity + "; " + fullName + " =? " + filter + "")
return similarity >= 0.95;
});
// Email filter
let filteredPublishersByEmail = publishers.filter(publisher =>
publisher.email.toLowerCase().includes(filter.toLowerCase())
);
// Combine name and email filters, removing duplicates
let filteredPublishers = [...new Set([...filteredPublishersByName, ...filteredPublishersByEmail])];
// Zero shifts (inactive) and date filter
if (showZeroShiftsOnly && selectedDate) {
filteredPublishers = publishers.filter(publisher => {
// If no assignments at all, include in results
if (publisher.assignments.length === 0) return true;
// Only include publishers who don't have any assignments after the selected date
return !publisher.assignments.some(assignment => {
const shiftDate = dayjs(assignment.shift.startTime);
return shiftDate.isAfter(selectedDate);
});
});
} else if (showZeroShiftsOnly) {
// If checkbox is checked but no date selected, show publishers with no assignments
filteredPublishers = publishers.filter(publisher => publisher.assignments.length === 0);
}
// trained filter
if (flterNoTraining) {
filteredPublishers = filteredPublishers.filter(p => p.isTrained === false);
}
setShownPubs(filteredPublishers);
}, [filter, showZeroShiftsOnly, selectedDate, flterNoTraining]);
// Separate effect to handle date reset when checkbox is unchecked
useEffect(() => {
if (!showZeroShiftsOnly) {
setSelectedDate(null);
}
}, [showZeroShiftsOnly]);
const renderPublishers = () => {
if (shownPubs.length === 0) {
return (
<div className="flex justify-center">
<button
className="px-4 py-2 text-white bg-blue-500 rounded hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
onClick={() => {
setFilter(""); // Assuming setFilter directly updates the filter state
if (typeof handleFilterChange === 'function') {
handleFilterChange({ target: { value: "" } }); // If needed for additional logic
}
}}
>
Clear filters
</button>
</div>
);
} else {
return shownPubs.map((publisher) => (
<PublisherCard key={publisher.id} publisher={publisher} />
));
}
};
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { name, value, type, checked } = event.target;
// setFilter(event.target.value);
if (type === 'text') {
setFilter(value);
} else if (type === 'checkbox') {
if (name === 'filterIsImported') {
setFilterIsImported({ checked, indeterminate: false });
}
if (name === 'filterTrained') {
// const nextState = cbFilterTrainingState === false ? null : cbFilterTrainingState === null ? true : false;
// setcbFilterTrainingState(nextState);
setFilterNoTraining(checked);
}
}
};
const exportPublishers = async () => {
try {
const response = await axiosInstance.get('/api/?action=exportPublishersExcel', { responseType: 'arraybuffer' });
// Get filename from Content-Disposition header
const contentDisposition = response.headers['content-disposition'];
let filename = 'publishers.xlsx'; // default fallback
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename=(.*?)(;|$)/);
if (filenameMatch) {
filename = decodeURI(filenameMatch[1]);
}
}
const blob = new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url); // Clean up
} catch (error) {
console.error(JSON.stringify(error));
toast.error("Грешка при експорт на данни");
}
}
// Add this function to your component
const exportFilteredPublishers = () => {
try {
// Calculate total statistics
const formatDateBG = (date) => {
if (!date) return '';
return new Date(date).toLocaleDateString('bg-BG', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
const stats = {
totalPublishers: shownPubs.length,
activePublishers: shownPubs.filter(p => p.isActive).length,
trainedPublishers: shownPubs.filter(p => p.isTrained).length,
totalAssignments: shownPubs.reduce((sum, p) => sum + p.assignments.length, 0),
totalAvailabilities: shownPubs.reduce((sum, p) => sum + p.availabilities.length, 0),
subscribedToReminders: shownPubs.filter(p => p.isSubscribedToReminders).length,
subscribedToCoverMe: shownPubs.filter(p => p.isSubscribedToCoverMe).length,
familyHeads: shownPubs.filter(p => p.familyHeadId).length,
avgAssignments: (shownPubs.reduce((sum, p) => sum + p.assignments.length, 0) / shownPubs.length).toFixed(2),
avgAvailabilities: (shownPubs.reduce((sum, p) => sum + p.availabilities.length, 0) / shownPubs.length).toFixed(2)
};
// Format the publishers data for Excel
const publishersData = shownPubs.map(publisher => ({
'First Name': publisher.firstName,
'Last Name': publisher.lastName,
'Email': publisher.email,
'Phone': publisher.phone || '',
'Active': publisher.isActive ? 'Yes' : 'No',
'Trained': publisher.isTrained ? 'Yes' : 'No',
'Role': publisher.role ? 'Yes' : 'No',
'Last Login': publisher.lastLogin ? formatDateBG(publisher.lastLogin) : 'Never',
'Subscribed to Reminders': publisher.isSubscribedToReminders ? 'Yes' : 'No',
'Subscribed to CoverMe': publisher.isSubscribedToCoverMe ? 'Yes' : 'No',
'Family Head ID': publisher.familyHeadId || '',
'Assignments Count': publisher.assignments.length,
'Availabilities Count': publisher.availabilities.length,
'Last Assignment': publisher.assignments.length > 0
? formatDateBG(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))) : 'Never',
}));
// Create workbook
const wb = utils.book_new();
// Create publishers worksheet
const ws_data = utils.json_to_sheet(publishersData);
// Set column widths for publishers sheet
const colWidths = [
{ wch: 15 }, // First Name
{ wch: 15 }, // Last Name
{ wch: 30 }, // Email
{ wch: 15 }, // Phone
{ wch: 10 }, // Active
{ wch: 10 }, // Trained
{ wch: 10 }, // Role
{ wch: 15 }, // Last Login
{ wch: 20 }, // Subscribed to Reminders
{ wch: 20 }, // Subscribed to CoverMe
{ wch: 15 }, // Family Head ID
{ wch: 15 }, // Assignments Count
{ wch: 15 }, // Availabilities Count
{ wch: 15 }, // Last Assignment
];
ws_data['!cols'] = colWidths;
// Create summary worksheet
const summary_data = [
['Summary Statistics', ''],
['Total Publishers', stats.totalPublishers],
['Active Publishers', stats.activePublishers],
['Trained Publishers', stats.trainedPublishers],
['Total Assignments', stats.totalAssignments],
['Average Assignments per Publisher', stats.avgAssignments],
['Total Availabilities', stats.totalAvailabilities],
['Average Availabilities per Publisher', stats.avgAvailabilities],
['Subscribed to Reminders', stats.subscribedToReminders],
['Subscribed to CoverMe', stats.subscribedToCoverMe],
['Family Heads', stats.familyHeads],
['Export Date', formatDateBG(new Date())],
[''],
['Applied Filters:', ''],
['Name Filter', filter || 'None'],
['Zero Shifts Only', showZeroShiftsOnly ? 'Yes' : 'No'],
['No Training', flterNoTraining ? 'Yes' : 'No'],
];
const ws_summary = utils.aoa_to_sheet(summary_data);
// Set column widths for summary sheet
ws_summary['!cols'] = [
{ wch: 30 }, // Label
{ wch: 15 }, // Value
];
// Add sheets to workbook
utils.book_append_sheet(wb, ws_summary, 'Summary');
utils.book_append_sheet(wb, ws_data, 'Publishers');
// Generate buffer
const excelBuffer = write(wb, { bookType: 'xlsx', type: 'array' });
// Create blob and save file
const blob = new Blob([excelBuffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
// Generate filename with current date and filter indication
const date = formatDateBG(new Date()).replace(/\./g, '-');
const isFiltered = filter || showZeroShiftsOnly || flterNoTraining;
const filename = `publishers-${isFiltered ? 'filtered-' : ''}${date}.xlsx`;
saveAs(blob, filename);
toast.success("Експортът е успешен");
} catch (error) {
console.error('Export error:', error);
toast.error("Грешка при експорт на данни");
}
};
return (
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
<div className="mx-auto ">
<div className="flex items-center justify-center space-x-4 m-4">
<div className="flex justify-center m-4">
<a href="/cart/publishers/new" className="btn">Добави вестител</a>
</div>
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpenDeleteAllVisible(true)} disabled={isDeleting}>
{isDeleting ? "Изтриване..." : "Изтрий показаните вестители"}
</button>
<ConfirmationModal
isOpen={isModalOpenDeleteAllVisible}
onClose={() => setIsModalOpenDeleteAllVisible(false)}
onConfirm={handleDeleteAllVisible}
message="Сигурни ли сте, че искате да изтриете всички показани в момента вестители?"
/>
<button className="button m-2 btn btn-danger" onClick={() => setIsModalOpenDeleteAllAvaillabilities(true)} disabled={isDeleting}>
{isDeleting ? "Изтриване..." : "Изтрий ВСИЧКИ предпочитания"}
</button>
<ConfirmationModal
isOpen={isModalOpenDeleteAllAvaillabilities}
onClose={() => setIsModalOpenDeleteAllAvaillabilities(false)}
onConfirm={handleDeleteAllAvailabilities}
message="Сигурни ли сте, че искате да изтриете предпочитанията на ВСИЧКИ вестители?"
/>
<div className="flex justify-center m-4">
<a href="/cart/publishers/import" className="btn">Import publishers</a>
</div>
{/* export by calling excel helper .ExportPublishersToExcel() */}
{/* <div className="flex justify-center m-4">
<button className="button m-2 btn btn-primary" onClick={exportPublishers}>Export to Excel</button>
</div> */}
<div className="flex justify-center m-4">
<button className="button m-2 btn btn-primary" onClick={exportPublishers}>
Export All XLSX
</button>
<button className="button m-2 btn btn-primary" onClick={exportFilteredPublishers}>
Export Filtered XLSX
</button>
</div>
</div>
<div className="z-60 sticky top-0" style={{ zIndex: 60, position: relative }}>
<div name="filters" className="flex items-center justify-center space-x-4 m-4 bg-gray-100 p-2" >
<label htmlFor="filter">Filter:</label>
<input type="text" id="filter" name="filter" value={filter} onChange={handleFilterChange}
className="border border-gray-300 rounded-md px-2 py-1"
/>
<label htmlFor="zeroShiftsOnly" className="ml-4 inline-flex items-center">
<input type="checkbox" id="zeroShiftsOnly" checked={showZeroShiftsOnly}
onChange={e => setShowZeroShiftsOnly(e.target.checked)}
className="form-checkbox text-indigo-600"
/>
<span className="ml-2">само без смени</span>
</label>
{showZeroShiftsOnly && (
<div className="flex items-center space-x-2 ml-2">
<span className="whitespace-nowrap">след:</span>
<DatePicker
value={selectedDate}
onChange={(newDate) => setSelectedDate(newDate)}
slotProps={{
textField: {
size: "small",
sx: {
width: '140px',
'& .MuiInputBase-input': {
padding: '4px 8px',
fontSize: '0.875rem'
}
}
},
mobileWrapper: {
sx: {
'& .MuiPickersLayout-root': {
minWidth: 'unset'
}
}
}
}}
format="DD.MM.YYYY"
className="min-w-[120px]"
/>
</div>
)}
<label htmlFor="filterTrained" className="ml-4 inline-flex items-center">
<input type="checkbox" id="filterTrained" name="filterTrained"
// support intermediate state if checkboxState is null
checked={flterNoTraining}
onChange={handleFilterChange}
className="form-checkbox text-indigo-600"
/>
<span className="ml-2">без обучение</span>
</label>
<span id="filter-info" className="ml-4">{shownPubs.length} от {publishers.length} вестителя</span>
</div>
</div>
<div className="grid gap-4 grid-cols-1 md:grid-cols-4 z-0">
{renderPublishers()}
</div>
</div>
</ProtectedRoute>
</Layout>
);
}
export default PublishersPage;
//import {set} from "date-fns";
export const getServerSideProps = async (context) => {
// const axios = await axiosServer(context);
// //ToDo: refactor all axios calls to use axiosInstance and this URL
// const {data: publishers } = await axios.get('/api/data/publishers?select=id,firstName,lastName,email,isActive,isTrained,isImported,assignments.shift.startTime,availabilities.startTime&dev=fromuseefect');
//use prisma instead of axios
const prisma = common.getPrismaClient();
let publishers = await prisma.publisher.findMany({
select: {
id: true,
firstName: true,
lastName: true,
email: true,
isActive: true,
isTrained: true,
isImported: true,
lastLogin: true,
phone: true,
isSubscribedToReminders: true,
isSubscribedToCoverMe: true,
familyHeadId: true,
role: true,
assignments: {
select: {
shift: {
select: {
startTime: true,
},
},
},
},
availabilities: {
select: {
startTime: true,
},
},
}
});
publishers = JSON.parse(JSON.stringify(publishers));
return {
props: {
publishers,
},
};
};