Files
mwitnessing/pages/cart/calendar/index.tsx
2024-04-02 02:04:45 +03:00

954 lines
51 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 React, { useState, useEffect, use } from 'react';
import { useSession } from "next-auth/react"
import Link from 'next/link';
import Calendar from 'react-calendar';
import 'react-calendar/dist/Calendar.css';
import axiosInstance from '../../../src/axiosSecure';
import Layout from "../../../components/layout"
import Shift from '../../../components/calendar/ShiftComponent';
import { DayOfWeek, UserRole } from '@prisma/client';
import { env } from 'process'
import ShiftComponent from '../../../components/calendar/ShiftComponent';
//import { set } from 'date-fns';
const common = require('src/helpers/common');
import { toast } from 'react-toastify';
import ProtectedRoute from '../../../components/protectedRoute';
import ConfirmationModal from '../../../components/ConfirmationModal';
import LocalShippingIcon from '@mui/icons-material/LocalShipping';
// import { FaPlus, FaCogs, FaTrashAlt, FaSpinner } from 'react-icons/fa'; // Import FontAwesome icons
// import { useSession,} from 'next-auth/react';
// import { getToken } from "next-auth/jwt"
//define Shift type
interface Shift {
id: number;
startTime: Date;
endTime: Date;
cartEventId: number;
assignments: Assignment[];
}
interface Assignment {
id: number;
publisherId: number;
shiftId: number;
isConfirmed: boolean;
publisher: Publisher;
}
interface Publisher {
id: number;
firstName: string;
lastName: string;
isImported: boolean;
}
// https://www.npmjs.com/package/react-calendar
export default function CalendarPage({ initialEvents, initialShifts }) {
const { data: session } = useSession()
//if logged in, get the user's email
// var email = "";
// const [events, setEvents] = useState(initialEvents);
const events = initialEvents;
const [allShifts, setAllShifts] = useState(initialShifts);
const [isPublished, setIsPublished] = useState(() => initialShifts.some(shift => shift.isPublished));
const [value, onChange] = useState<Date>(new Date());
const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null);
const [availablePubs, setAvailablePubs] = React.useState([]);
const [selectedShiftId, setSelectedShiftId] = useState(null);
const [isOperationInProgress, setIsOperationInProgress] = useState(false);
const [progress, setProgress] = useState(0);
const [activeButton, setActiveButton] = useState(null);
const isLoading = (buttonId) => activeButton === buttonId;
// ------------------ MODAL ------------------
const [isModalOpen, setIsModalOpen] = useState(false);
const [modalPub, setModalPub] = useState(null);
// ------------------ no assignments checkbox ------------------
const [isCheckboxChecked, setIsCheckboxChecked] = useState(false);
const handleCheckboxChange = (event) => {
setIsCheckboxChecked(!isCheckboxChecked); // Toggle the checkbox state
};
useEffect(() => {
console.log("checkbox checked: " + isCheckboxChecked);
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
}, [isCheckboxChecked]); // Dependency array
const [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
useEffect(() => {
const newMonth = value.getMonth();
if (newMonth !== selectedMonth) {
setSelectedMonth(newMonth);
}
}, [value, selectedMonth]);
const handleCalDateChange = async (selectedDate) => {
var date = new Date(common.getDateFromDateTime(selectedDate));//ToDo: check if seting the timezone affects the selectedDate?!
var dateStr = common.getISODateOnly(date);
console.log("Setting date to '" + date.toLocaleDateString() + "' from '" + selectedDate.toLocaleDateString() + "'. ISO: " + date.toISOString(), "locale ISO:", common.getISODateOnly(date));
if (isCheckboxChecked) {
console.log(`getting unassigned publishers for ${common.getMonthName(date.getMonth())} ${date.getFullYear()}`);
const { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=getUnassignedPublishers&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth`);
setAvailablePubs(availablePubsForDate);
}
else {
console.log(`getting shifts for ${common.getISODateOnly(date)}`)
try {
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`);
availablePubsForDate.forEach(pub => {
pub.canTransport = pub.availabilities.some(av =>
av.isWithTransportIn || av.isWithTransportOut
);
});
//remove availabilities that are isFromPreviousAssignment or from previous month for each publisher
// availablePubsForDate = availablePubsForDate.map(pub => {
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
// return pub;
// });
//commented for now: remove unavailable publishers
// availablePubsForDate = availablePubsForDate.map(pub => {
// pub.availabilities = pub.availabilities.filter(avail => avail.isFromPreviousAssignment == false);
// return pub;
// });
setAvailablePubs(availablePubsForDate);
console.log(`found shifts for ${dateStr}: ${shiftsForDate.length}`);
} catch (err) {
console.error("Error fetching shifts:", err);
setError(err);
}
onChange(selectedDate);
}
}
const handleShiftSelection = (selectedShift) => {
setSelectedShiftId(selectedShift.id);
const updatedPubs = availablePubs.map(pub => {
const av = pub.availabilities?.find(avail =>
avail.startTime <= selectedShift.startTime
&& avail.endTime >= selectedShift.endTime
);
if (av) {
pub.isAvailableForShift = true;
pub.canTransport = av.isWithTransportIn || av.isWithTransportOut;
}
// const isAvailableForShift = pub.availabilities.some(avail =>
// avail.startTime <= selectedShift.startTime
// && avail.endTime >= selectedShift.endTime
// && avail.isFromPreviousAssignment == false
// );
const isAvailableForShiftWithPrevious = pub.availabilities.some(avail =>
avail.startTime <= selectedShift.startTime
&& avail.endTime >= selectedShift.endTime
);
// //! console.log(`Publisher ${pub.firstName} ${pub.lastName} is available for shift ${selectedShift.id}: ${isAvailableForShift}`);
// //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + pub.availabilities.map(avail => avail.startTime + " - " + avail.endTime));
// //// console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities :` + stringify.join(', 'pub.availabilities.map(avail => avail.id)));
// const availabilitiesIds = pub.availabilities.map(avail => avail.id).join(', ');
// //! console.log(`Publisher ${pub.firstName} ${pub.lastName} has ${pub.availabilities.length} availabilities with IDs: ${availabilitiesIds}`);
return { ...pub, isAvailableForShiftWithPrevious, isSelected: pub.id === selectedShift.selectedPublisher?.id };
});
// Sort publishers based on their availability state. use currentDayAssignments, currentWeekAssignments,
// currentMonthAssignments and previousMonthAssignments properties
// Sort publishers based on availability and then by assignment counts.
const sortedPubs = updatedPubs.sort((a, b) => {
if (a.isActive !== b.isActive) {
return a.isActive ? -1 : 1;
}
// First, sort by isselected.
if (a.isSelected !== b.isSelected) {
return a.isSelected ? -1 : 1;
}
// Them, sort by availability.
if (a.isAvailableForShift !== b.isAvailableForShift) {
return a.isAvailableForShift ? -1 : 1;
}
// If both are available (or unavailable) for the shift, continue with the additional sorting logic.
// Prioritize those without currentDayAssignments.
if (!!a.currentDayAssignments !== !!b.currentDayAssignments) {
return a.currentDayAssignments ? 1 : -1;
}
// Then prioritize those without currentWeekAssignments.
if (!!a.currentWeekAssignments !== !!b.currentWeekAssignments) {
return a.currentWeekAssignments ? 1 : -1;
}
// Prioritize those with fewer currentMonthAvailabilityHoursCount.
if (a.currentMonthAvailabilityHoursCount !== b.currentMonthAvailabilityHoursCount) {
return a.currentMonthAvailabilityHoursCount - b.currentMonthAvailabilityHoursCount;
}
// Finally, sort by (currentMonthAssignments - previousMonthAssignments).
return (a.currentMonthAssignments - a.previousMonthAssignments) - (b.currentMonthAssignments - b.previousMonthAssignments);
});
setAvailablePubs(sortedPubs);
};
const handleSelectedPublisher = (publisher) => {
// Do something with the selected publisher
console.log("handle pub clicked:", publisher);
}
const handlePublisherModalOpen = async (publisher) => {
// Do something with the selected publisher
console.log("handle pub modal opened:", publisher.firstName + " " + publisher.lastName);
let date = new Date(value);
const { data: publisherInfo } = await axiosInstance.get(`/api/?action=getPublisherInfo&id=${publisher.id}&date=${common.getISODateOnly(date)}`);
publisher.assignments = publisherInfo.assignments;
publisher.availabilities = publisherInfo.availabilities;
publisher.email = publisherInfo.email;
setModalPub(publisher);
setIsModalOpen(true);
}
// file uploads
const [fileActionUrl, setFileActionUrl] = useState('');
const [file, setFile] = useState(null);
const handleFileUpload = async (event) => {
setIsOperationInProgress(true);
console.log('handleFileUpload(): Selected file:', event.target.files[0], 'actionUrl:', fileActionUrl);
setFile(event.target.files[0]);
if (!event.target.files[0]) {
toast.error('Моля, изберете файл!');
return;
}
uploadToServer(fileActionUrl, event.target.files[0]);
};
const uploadToServer = async (actionUrl, file) => {
const formData = new FormData();
formData.append('file', file);
try {
const response = await fetch('/' + actionUrl, {
method: 'POST',
body: formData,
});
const result = await response.json();
if (result.fileId) {
pollProgress(result.fileId);
}
console.log('Result from server-side API:', result);
toast.info(result.message || "Файла е качен! Започна обработката на данните...");
} catch (error) {
toast.error(error.message || "Възникна грешка при обработката на данните.");
} finally {
}
};
const pollProgress = (fileId: any) => {
fetch(`/api/upload?fileId=${fileId}`)
.then(response => response.json())
.then(data => {
updateProgressBar(data.progress); // Update the progress bar
if (data.progress < 98 && data.progress > 0) {
// Poll every second if progress is between 0 and 100
setTimeout(() => pollProgress(fileId), 1000);
} else if (data.progress === 0) {
// Handle error case
toast.error("Възникна грешка при обработката на данните.");
setIsOperationInProgress(false);
} else {
// Handle completion case
toast.success("Файла беше обработен успешно!");
setIsOperationInProgress(false);
}
})
.catch(error => {
console.error('Error polling for progress:', error);
toast.error("Грешка при обновяването на напредъка");
setIsOperationInProgress(false)
})
.finally();
};
const updateProgressBar = (progress: string) => {
// Implement the logic to update your progress bar based on the 'progress' value
// For example, updating the width of a progress bar element
const progressBar = document.getElementById('progress-bar');
if (progressBar) {
progressBar.style.width = progress + '%';
}
};
function getEventClassname(event, allShifts, date) {
if (event && allShifts) {
const matchingShifts = allShifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return shift.cartEventId === event.id && shiftDate.getDate() === date.getDate() && shiftDate.getMonth() === date.getMonth();
});
//get matching shifts with assignments using nextcrud
//const { data: withAss } = await axiosInstance.get(`/shifts?include=assignments&where={"id":{"$in":[${matchingShifts.map(shift => shift.id)}]}}`);
const minCount = Math.min(...matchingShifts.map(shift => shift.assignedCount)) || 0;
//const minCount = 4;
//console.log("matchingShifts: " + matchingShifts) + " for date " + date;
if (matchingShifts.length < 3) { return "text-gray"; }
else {
if (minCount === 0) return "text-red-700 font-bold ";
if (minCount === 1) return "text-brown-900 font-bold ";
if (minCount === 2) return "text-orange-500";
if (minCount === 3) return "text-yellow-500";
if (minCount >= 4) return "text-blue-500";
}
}
return "text-default"; // A default color in case none of the conditions are met.
}
const onTileContent = ({ date, view }) => {
// Add your logic here
var dayName = common.DaysOfWeekArray[date.getDayEuropean()];
var classname = "";
if (events == null) {
return <div>{" "}</div>;
}
const event = events.find((event) => {
return event.dayofweek == dayName;
});
if (event != null) {
const classname = getEventClassname(event, allShifts, date);
return <div className={classname}>
{new Date(event.startTime).getHours() + "-" + new Date(event.endTime).getHours()}ч.
</div>
}
return <div>{" "}</div>;
};
const addAssignment = async (publisher, shiftId) => {
try {
console.log(`new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
const newAssignment = {
publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } },
isActive: true,
isConfirmed: true
};
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
// Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher;
} catch (error) {
console.error("Error adding assignment:", error);
}
};
const removeAssignment = async (publisher, shiftId) => {
try {
const assignment = publisher.assignments.find(ass => ass.shift.id === shiftId);
console.log(`remove assignment for shift ${shiftId}`);
const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`);
} catch (error) {
console.error("Error removing assignment:", error);
}
}
// ----------------------------------------------------------
// button handlers
// ----------------------------------------------------------
const importShifts = async () => {
try {
setActiveButton("importShifts");
setIsOperationInProgress(true);
let date = new Date(value);
date.setDate(date.getDate() + 1);
const dateString = common.getISODateOnly(date);
const fileInput = document.getElementById('fileInput');
// setFileActionUrl(`readword/${dateString.slice(0, 4)}/${dateString.slice(5, 7)}/${dateString.slice(8, 10)}?action=import`);
setFileActionUrl(`api/upload?action=readword&date=${dateString}`);
console.log('fileaction set to ' + fileActionUrl);
fileInput.click();
//handleFileUpload({ target: { files: [file] } });
fileInput.value = null;
} catch (error) {
toast.error(error);
} finally {
setIsOperationInProgress(false);
setActiveButton(null);
}
}
const fetchShifts = async () => {
try {
setActiveButton("fetchShifts");
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/api/data/shifts?include=assignments.publisher&where={"startTime":{"$and":[{"$gte":"2022-12-04T15:09:47.768Z","$lt":"2022-12-10T15:09:47.768Z"}]}}`);
setShifts(data);
toast.success('Готово!', { autoClose: 1000 });
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const generateShifts = async (buttonId, copyFromPrevious = false, autoFill = false, forDay?: Boolean | null) => {
try {
setActiveButton(buttonId);
const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}&copyFromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}`;
const { shifts } = await axiosInstance.get(endpoint);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const deleteShifts = async (buttonId, forDay: Boolean) => {
try {
setActiveButton(buttonId);
await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}&forDay=${forDay}`);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const sendMails = async () => {
try {
var month = new Date(value).getMonth() + 1;
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/sendmails/${new Date(value).getFullYear()}/${month}`);
} catch (error) {
console.log(error);
}
}
const generateXLS = async () => {
try {
var month = new Date(value).getMonth() + 1;
// where:{"startTime":"$and":{{ "$gte": "2022-12-04T15:09:47.768Z", "$lt": "2022-12-10T15:09:47.768Z" }}}
const { data } = await axiosInstance.get(`/generatexcel/${new Date(value).getFullYear()}/${month}/2`);
} catch (error) {
console.log(error);
}
}
const generateDOCX = async () => {
try {
setActiveButton("generateDOCX");
var month = new Date(value).getMonth() + 1;
const response = await axiosInstance.get(`/getDocxFile/${new Date(value).getFullYear()}/${month}`, { responseType: 'blob' });
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `График 2023.${month}.docx`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.log(error);
}
}
//get all publishers and create txt file with their names, current and previous month assignments count (getPublisherInfo)
//
const generateMonthlyStatistics = async () => {
try {
var month = new Date(value).getMonth() + 1;
let { data: allPublishersInfo } = await axiosInstance.get(`/api/?action=getMonthlyStatistics&date=${common.getISODateOnly(value)}`);
//order by name and generate the list
allPublishersInfo = allPublishersInfo.sort((a, b) => {
if (a.firstName !== b.firstName) {
return a.firstName < b.firstName ? -1 : 1;
} if (a.lastName !== b.lastName) {
return a.lastName < b.lastName ? -1 : 1;
}
return 0;
});
var list = "";
allPublishersInfo.forEach(pub => {
// list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments} / ${pub.previousMonthAssignments}\n`;
list += `${pub.firstName} ${pub.lastName}\t ${pub.currentMonthAssignments}\n`;
});
//write to google sheets file
//download the file
const url = window.URL.createObjectURL(new Blob([list]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `Статистика 2023.${month}.txt`);
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
console.log(error);
}
}
const togglePublished = async () => {
try {
const publishState = !isPublished; // Toggle the state
const isPublishedParam = publishState ? 'true' : 'fasle';
await axiosInstance.get(`/api/?action=updateShifts&isPublished=${isPublishedParam}&date=${common.getISODateOnly(value)}`);
setIsPublished(publishState); // Update state based on the action
} catch (error) {
console.log(error);
}
}
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
async function copyOldAvailabilities(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
}
return (
<>
<Layout>
<ProtectedRoute allowedRoles={[UserRole.ADMIN, UserRole.POWERUSER]}>
{/* Page Overlay */}
{isOperationInProgress && (
<div className="loading-overlay">
<div className="spinner"></div>
</div>
)}
<input id="fileInput" title="file input" type="file" onChange={handleFileUpload}
accept=".json, .doc, .docx, .xls, .xlsx" style={{ display: 'none' }}
/>
<div className="mb-4">
<button className="button m-2 bg-blue-800" onClick={importShifts}>
{isLoading('importShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-import"></i>)} Импорт от Word
</button>
<button className="button btn m-2 bg-blue-800" onClick={generateDOCX}>
{isLoading('generateDOCX') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fa fa-file-export"></i>)}Експорт в Word
</button>
<button className="button btn m-2 bg-yellow-500 hover:bg-yellow-600 text-white" onClick={() => { setActiveButton("sendEmails"); setConfirmModalOpen(true) }}>
{isLoading('sendEmails') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-envelope mr-2"></i>)} изпрати мейли!
</button>
<ConfirmationModal
isOpen={isConfirmModalOpen}
onClose={() => setConfirmModalOpen(false)}
onConfirm={() => {
toast.info("Потвърдено!", { autoClose: 2000 });
setConfirmModalOpen(false);
sendMails()
}}
message="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
/>
<button
className={`button btn m-2 ${isPublished ? 'hover:bg-gray-100 bg-yellow-500' : 'hover:bg-red-300 bg-blue-400'}`}
onClick={togglePublished}>
<i className={`fas ${isPublished ? 'fa-check' : 'fa-close'} mr-2`}></i>
{isPublished ? "Скрий" : "Публикувай"} графика (м.)
</button>
<div className="relative inline-block text-left">
<button
className={`button m-2 ${isMenuOpen ? 'bg-gray-400 border border-blue-500' : 'bg-gray-300'} hover:bg-gray-400`}
onClick={() => { setIsMenuOpen(!isMenuOpen) }}>
<i className="fa fa-ellipsis-h"></i> Още
</button>
{isMenuOpen && (
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5">
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
{/* Group 1: Daily actions */}
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmptyDay", false, false, true)}>
{isLoading('genEmptyDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
създай празни ({value.getDate()}-ти) </button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={() => generateShifts("genDay", false, true, true)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени ({value.getDate()}-ти) </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShiftsDay", true) }}>
{isLoading('deleteShiftsDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
изтрий смените ({value.getDate()}-ти)</button>
<hr className="my-1" />
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genEmpty", false, false)}>
{isLoading('genEmpty') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-plus mr-2"></i>)}
създай празни </button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 whitespace-nowrap" onClick={() => generateShifts("genCopy", true)}>
{isLoading('genCopy') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-copy mr-2"></i>)}
копирай от миналия месец</button>
<button className="block w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100" onClick={() => { deleteShifts("deleteShifts", false) }}>
{isLoading('deleteShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-trash-alt mr-2"></i>)}
изтрий смените</button>
<hr className="my-1" />
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateXLS}><i className="fas fa-file-excel mr-2"></i> Генерирай XLSX</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={fetchShifts}>
{isLoading('fetchShifts') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-sync-alt mr-2"></i>)} презареди</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={generateMonthlyStatistics}><i className="fas fa-chart-bar mr-2"></i> Генерирай статистика</button>
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={copyOldAvailabilities}><i className="fas fa-copy mr-2"></i> Прехвърли предпочитанията</button>
</div>
</div>
)}
{/* <button className={`button m-2 bg-blue-800 ${isOperationInProgress ? 'disabled' : ''}`} onClick={importShifts}>
{isOperationInProgress ? <div className="spinner"></div> : 'Import shifts (and missing Publishers) from WORD'}
</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts()}>Generate empty shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true)}>Copy last month shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(true, true)}>Generate Auto shifts</button>
// <button className="button m-2 bg-blue-800" onClick={() => generateShifts(false, true, value)}>Generate Auto shifts DAY</button>
// <button className="button m-2" onClick={fetchShifts}>Fetch shifts</button>
// <button className="button m-2" onClick={sendMails}>Send mails</button>
// <button className="button m-2" onClick={generateXLS}>Generate XLSX</button>
// <button className="button m-2" onClick={async () => {
// await axiosInstance.get(`/api/shiftgenerate?action=delete&date=${common.getISODateOnly(value)}`);
// }
// }>Delete shifts (selected date's month)</button>
// <button className="button m-2" onClick={generateMonthlyStatistics}>Generate statistics</button>
*/}
</div>
</div>
{/* progress bar holder */}
{isOperationInProgress && (
<div id="progress" className="w-full h-2 bg-gray-300">
<div id="progress-bar" className="h-full bg-green-500" style={{ width: `${progress}%` }}></div>
</div>
)}
<div className="flex">
{/* Calendar section */}
<div className="flex-3">
<Calendar
className={['customCalendar']}
onChange={handleCalDateChange}
value={value}
tileContent={onTileContent}
locale="bg-BG"
/>
{/* ------------------------------- PUBLISHERS LIST ----------------------------------
list of publishers for the selected date with availabilities
------------------AVAILABLE PUBLISHERS LIST FOR THE SELECTED DATE0 ------------------ */}
<div className="flex flex-col items-center my-8 sticky top-0">
<h2 className="text-lg font-semibold mb-4">Достъпни за този ден: <span className="text-blue-600">{availablePubs.length}</span></h2>
<label className="toggle pb-3">
<input type="checkbox" className="toggle-checkbox" onChange={handleCheckboxChange} />
<span className="toggle-slider m-1">без назначения за месеца</span>
</label>
<ul className="w-full max-w-md">
{Array.isArray(availablePubs) && availablePubs?.map((pub, index) => {
// Determine background and border classes based on conditions
let bgAndBorderColorClass;
if (pub.isAvailableForShift) {
if (pub.currentDayAssignments === 0) {
const comparisonResultClass = pub.currentMonthAvailabilityDaysCount < pub.previousMonthAssignments ? 'bg-green-100' : 'bg-green-50';
bgAndBorderColorClass = `${comparisonResultClass} border-l-4 border-green-400`;
} else if (!pub.isSelected) {
bgAndBorderColorClass = 'bg-orange-50 border-l-4 border-orange-400';
}
} else {
if (pub.isAvailableForShiftWithPrevious) // add left orange border
{
bgAndBorderColorClass = 'border-l-4 border-orange-400';
}
else {
bgAndBorderColorClass = 'bg-white';
}
}
//tOdO: CHECK WHY THIS IS NOT WORKING
if (!pub.hasEverFilledForm) {
//bgAndBorderColorClass = 'border-t-2 border-yellow-400';
}
// Determine border class if selected
const selectedBorderClass = pub.isSelected ? 'border-blue-400 border-b-4' : '';
// Determine opacity class
const activeOpacityClass = pub.isActive ? '' : 'opacity-25';
return (
<li key={index}
className={`flex justify-between items-center p-4 rounded-lg shadow-sm mb-2
${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass}`}
onDoubleClick={(handlePublisherModalOpen.bind(this, pub))}
>
<span className={`text-gray-700 ${pub.isAvailableForShift ? 'font-bold' : 'font-medium'} `}>
{pub.firstName} {pub.lastName}
{pub.canTransport && (<LocalShippingIcon className="mx-2 text-gray-500" />)}
</span>
<div className="flex space-x-1 overflow-hidden">
<span title="Възможност: часове | дни" className={`badge py-1 px-2 rounded-md text-xs ${pub.currentMonthAvailabilityHoursCount || pub.currentMonthAvailabilityDaysCount ? 'bg-teal-500 text-white' : 'bg-teal-200 text-gray-300'} hover:underline`} >
{pub.currentMonthAvailabilityDaysCount || 0} | {pub.currentMonthAvailabilityHoursCount || 0}
</span>
<span title="участия тази седмица" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentWeekAssignments ? 'bg-yellow-500 text-white' : 'bg-yellow-200 text-gray-400'}`}>{pub.currentWeekAssignments || 0}</span>
<span title="участия този месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.currentMonthAssignments ? 'bg-green-500 text-white' : 'bg-green-200 text-gray-400'}`}>{pub.currentMonthAssignments || 0}</span>
<span tooltip="участия миналия месец" title="участия миналия месец" className={`badge py-1 px-2 rounded-full text-xs ${pub.previousMonthAssignments ? 'bg-blue-500 text-white' : 'bg-blue-200 text-gray-400'}`}>{pub.previousMonthAssignments || 0}</span>
<button tooltip="желани участия този месец" title="желани участия" className={`badge py-1 px-2 rounded-md text-xs ${pub.desiredShiftsPerMonth ? 'bg-purple-500 text-white' : 'bg-purple-200 text-gray-400'}`}>{pub.desiredShiftsPerMonth || 0}</button>
</div>
</li>
);
})}
</ul>
</div>
</div>
{/* Shift list section */}
<div className="flex-grow mx-5">
<div className="flex-col" id="shiftlist">
{shifts.map((shift, index) => (
<ShiftComponent key={index} shift={shift}
onShiftSelect={handleShiftSelection} isSelected={shift.id == selectedShiftId}
onPublisherSelect={handleSelectedPublisher} showAllAuto={true}
allPublishersInfo={availablePubs} />
))}
</div>
</div>
</div>
<div>
{/* <CustomCalendar date={value} shifts={shifts} /> */}
</div>
{isModalOpen && <PublisherShiftsModal publisher={modalPub} shifts={allShifts} onClose={() => setIsModalOpen(false)} />}
</ProtectedRoute >
</Layout >
</>
);
function PublisherShiftsModal({ publisher, shifts, onClose }) {
const monthInfo = common.getMonthDatesInfo(new Date(value));
const monthShifts = shifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return shiftDate > monthInfo.firstDay && shiftDate < monthInfo.lastDay;
});
const weekShifts = monthShifts.filter(shift => {
const shiftDate = new Date(shift.startTime);
return common.getStartOfWeek(value) <= shiftDate && shiftDate <= common.getEndOfWeek(value);
});
const dayShifts = weekShifts.map(shift => {
const isAvailable = publisher.availabilities.some(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
);
let color = isAvailable ? getColorForShift(shift) : 'bg-gray-300';
if (shift.isFromPreviousMonth) {
color += ' border-l-4 border-orange-500 ';
}
if (shift.isFromPreviousAssignment) {
color += ' border-l-4 border-red-500 ';
}
return { ...shift, isAvailable, color };
}).reduce((acc, shift) => {
const dayIndex = new Date(shift.startTime).getDay();
acc[dayIndex] = acc[dayIndex] || [];
acc[dayIndex].push(shift);
return acc;
}, {});
console.log("dayShifts:", dayShifts);
const hasAssignment = (shiftId) => {
// return publisher.assignments.some(ass => ass.shift.id == shiftId);
return publisher.assignments.some(ass => {
console.log(`Comparing: ${ass.shift.id} to ${shiftId}: ${ass.shift.id === shiftId}`);
return ass.shift.id === shiftId;
});
};
useEffect(() => {
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
console.log('ESC: closing modal.');
onClose(); // Call the onClose function when ESC key is pressed
}
};
// Add event listener
window.addEventListener('keydown', handleKeyDown);
// Remove event listener on cleanup
return () => {
window.removeEventListener('keydown', handleKeyDown);
};
}, [onClose]); // Include onClose in the dependency array
return (
<div className="fixed top-0 left-0 w-full h-full flex items-center justify-center bg-black bg-opacity-50 z-50">
<div className="relative bg-white p-8 rounded-lg shadow-xl max-w-xl w-full h-auto overflow-y-auto">
<h2 className="text-xl font-semibold mb-4">График на <span title={publisher.email} className='publisher'>
<strong>{publisher.firstName} {publisher.lastName}</strong>
<span className="publisher-tooltip" onClick={common.copyToClipboard}>{publisher.email}</span>
</span> тази седмица:</h2>
{/* ... Display shifts in a calendar-like UI ... */}
<div className="grid grid-cols-6 gap-4 mb-4">
{Object.entries(dayShifts).map(([dayIndex, shiftsForDay]) => (
<div key={dayIndex} className="flex flex-col space-y-2 justify-end">
{/* Day header */}
<div className="text-center font-medium">{new Date(shiftsForDay[0].startTime).getDate()}-ти</div>
{shiftsForDay.map((shift, index) => {
const assignmentExists = hasAssignment(shift.id);
const availability = publisher.availabilities.find(avail =>
avail.startTime <= shift.startTime && avail.endTime >= shift.endTime
);
const isFromPrevMonth = availability && availability.isFromPreviousMonth;
return (
<div
key={index}
className={`text-sm text-white p-2 rounded-md ${isFromPrevMonth ? 'border-l-6 border-black-500' : ''} ${assignmentExists ? 'bg-blue-200' : shift.color} h-24 flex flex-col justify-center`}
>
{common.getTimeRange(shift.startTime, shift.endTime)} {shift.id}
{!assignmentExists && shift.isAvailable && (
<button onClick={() => { addAssignment(publisher, shift.id); onClose() }}
className="mt-2 bg-green-500 text-white p-1 rounded hover:bg-green-600 active:bg-green-700 focus:outline-none"
>
добави
</button>
)}
{assignmentExists && (
<button onClick={() => { removeAssignment(publisher, shift.id) }} // Implement the removeAssignment function
className="mt-2 bg-red-500 text-white p-1 rounded hover:bg-red-600 active:bg-red-700 focus:outline-none"
>
махни
</button>
)}
</div>
);
}
)}
</div>
))}
</div>
{/* Close button in the top right corner */}
<button
onClick={onClose}
className="absolute top-3 right-2 p-2 px-3 bg-red-500 text-white rounded-full hover:bg-red-600 active:bg-red-700 focus:outline-none"
>
&times;
</button>
{/* <Link href={`/cart/publishers/edit/${modalPub.id}`}
className="mt-2 bg-blue-500 text-white p-1 rounded hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
<i className="fas fa-edit" />
</Link> */}
{/* Edit button in the top right corner, next to the close button */}
<Link href={`/cart/publishers/edit/${modalPub.id}`} className="absolute top-3 right-12 p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 active:bg-blue-700 focus:outline-none">
<i className="fas fa-edit" />
</Link>
</div>
</div >
);
}
function getColorForShift(shift) {
const assignedCount = shift.assignedCount || 0; // Assuming each shift has an assignedCount property
switch (assignedCount) {
case 0: return 'bg-blue-300';
case 1: return 'bg-green-300';
case 2: return 'bg-yellow-300';
case 3: return 'bg-orange-300';
case 4: return 'bg-red-200';
default: return 'bg-gray-300';
}
}
}
import axiosServer from '../../../src/axiosServer';
import { start } from 'repl';
export const getServerSideProps = async (context) => {
const axios = await axiosServer(context);
// const baseUrl = common.getBaseUrl();
// console.log('runtime BaseUrl: ' + baseUrl);
console.log('runtime NEXT_PUBLIC_PUBLIC_URL: ' + process.env.NEXT_PUBLIC_PUBLIC_URL);
console.log('Runtime Axios Base URL:', axios.defaults.baseURL);
const currentDate = new Date();
const firstDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() - 3, 1);
const lastDayOfMonth = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0); // 0th day of the next month gives the last day of the current month
const url = `/api/data/shifts?where={"startTime":{"$and":[{"$gte":"${common.getISODateOnly(firstDayOfMonth)}","$lt":"${common.getISODateOnly(lastDayOfMonth)}"}]}}`;
const prismaClient = common.getPrismaClient();
// let events = await prismaClient.cartEvent.findMany({ where: { isActive: true } });
// events = events.map(event => ({
// ...event,
// // Convert Date objects to ISO strings
// startTime: event.startTime.toISOString(),
// endTime: event.endTime.toISOString(),
// }));
const { data: events } = await axios.get(`/api/data/cartevents?where={"isActive":true}`);
//const { data: shifts } = await axios.get(url);
// get all shifts for the month, including assigments
let shifts = await prismaClient.shift.findMany({
where: {
isActive: true,
startTime: {
gte: firstDayOfMonth,
//lt: lastDayOfMonth
}
},
include: {
assignments: {
include: {
publisher: {
select: {
id: true,
}
}
}
}
}
});
//calculate assCount for each shift
shifts = shifts.map(shift => ({
...shift,
assignedCount: shift.assignments.length,
startTime: shift.startTime.toISOString(),
endTime: shift.endTime.toISOString(),
}));
return {
props: {
initialEvents: events,
initialShifts: shifts,
},
};
}