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

445 lines
16 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 {
// Format the data for Excel
const excelData = 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',
'Imported': publisher.isImported ? 'Yes' : 'No',
'Last Assignment': publisher.assignments.length > 0
? new Date(Math.max(...publisher.assignments.map(a => new Date(a.shift.startTime)))).toLocaleDateString()
: 'Never',
'Number of Assignments': publisher.assignments.length,
'Number of Availabilities': publisher.availabilities.length
}));
// Create worksheet
const ws = utils.json_to_sheet(excelData);
// Set column widths
const colWidths = [
{ wch: 15 }, // First Name
{ wch: 15 }, // Last Name
{ wch: 30 }, // Email
{ wch: 10 }, // Active
{ wch: 10 }, // Trained
{ wch: 10 }, // Imported
{ wch: 15 }, // Last Assignment
{ wch: 20 }, // Number of Assignments
{ wch: 20 }, // Number of Availabilities
];
ws['!cols'] = colWidths;
// Create workbook and add worksheet
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, '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
const date = new Date().toISOString().split('T')[0];
const filename = `publishers-${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,
assignments: {
select: {
shift: {
select: {
startTime: true,
},
},
},
},
availabilities: {
select: {
startTime: true,
},
},
}
});
publishers = JSON.parse(JSON.stringify(publishers));
return {
props: {
publishers,
},
};
};