Files
mwitnessing/pages/cart/calendar/index.tsx
2024-07-06 19:18:22 +03:00

958 lines
50 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 PublisherShiftsModal from '../../../components/publisher/PublisherShiftsModal';
//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 notify api
import { sendPush, broadcastPush } from '../../api/notify';
const { DateTime } = require('luxon');
// 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 [selectedMonth, setSelectedMonth] = useState(new Date().getMonth());
const [shifts, setShifts] = React.useState([]);
const [error, setError] = React.useState(null);
const [availablePubs, setAvailablePubs] = React.useState([]);
const [selectedPublisher, setSelectedPublisher] = React.useState(null);
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 [filterShowWithoutAssignments, setFilterShowWithoutAssignments] = useState(false);
const handleCheckboxChange = (event) => {
setFilterShowWithoutAssignments(!filterShowWithoutAssignments); // Toggle the checkbox state
};
useEffect(() => {
console.log("checkbox checked: " + filterShowWithoutAssignments);
handleCalDateChange(value); // Call handleCalDateChange whenever isCheckboxChecked changes
}, [filterShowWithoutAssignments]); // Dependency array
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 (filterShowWithoutAssignments) {
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=filterPublishersNew&date=${dateStr}&select=id,firstName,lastName,isActive,desiredShiftsPerMonth&includeOldAvailabilities=${filterShowWithoutAssignments}`);
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 => {
pub.isAvailableForShift = false;
pub.canTransport = false;
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) => {
console.log("handle pub clicked:", publisher);
setSelectedPublisher(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>;
};
//ToDo: DRY - move to common
const addAssignment = async (publisher, shiftId) => {
try {
console.log(`calendar.idx: new assignment for publisher ${publisher.id} - ${publisher.firstName} ${publisher.lastName}`);
const newAssignment = {
publisher: { connect: { id: publisher.id } },
shift: { connect: { id: shiftId } },
isConfirmed: true
};
const { data } = await axiosInstance.post("/api/data/assignments", newAssignment);
if (selectedShiftId == shiftId) {
handleShiftSelection(shifts.find(shift => shift.id === shiftId));
}
// Update the 'publisher' property of the returned data with the full publisher object
data.publisher = publisher;
data.shift = shifts.find(shift => shift.id === shiftId);
publisher.assignments = [...publisher.assignments, data];
handleAssignmentChange(publisher.id, 'add');
} 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(`calendar.idx: remove assignment for shift ${shiftId}`);
const { data } = await axiosInstance.delete(`/api/data/assignments/${assignment.id}`);
//remove from local assignments:
publisher.assignments = publisher.assignments.filter(a => a.id !== assignment.id)
//
handleAssignmentChange(publisher.id, 'remove')
} catch (error) {
console.error("Error removing assignment:", error);
}
}
function handleAssignmentChange(id, type) {
// Handle assignment change logic here
let pub = availablePubs.find(pub => pub.id === id)
pub.currentMonthAssignments += type === 'add' ? 1 : -1;
pub.currentWeekAssignments += type === 'add' ? 1 : -1;
//store in state
setAvailablePubs([...availablePubs]);
}
// ----------------------------------------------------------
// 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, type = 0) => {
try {
setActiveButton(buttonId);
const endpoint = `/api/shiftgenerate?action=generate&date=${common.getISODateOnly(value)}&copyFromPreviousMonth=${copyFromPrevious}&autoFill=${autoFill}&forDay=${forDay}&type=${type}`;
const { shifts } = await axiosInstance.get(endpoint);
toast.success('Готово!', { autoClose: 1000 });
setIsMenuOpen(false);
} catch (error) {
console.log(error);
} finally {
setActiveButton(null);
}
}
const deleteShifts = async (forDay: Boolean) => {
try {
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 [confirmModalProps, setConfirmModalProps] = useState({
isOpen: false,
message: '',
onConfirm: () => { }
});
const openConfirmModal = (message, action, actionName) => {
if (actionName) {
setActiveButton(actionName);
}
setConfirmModalProps({
isOpen: true,
message: message,
onConfirm: () => {
toast.info('Потвърдено!', { autoClose: 2000 });
setConfirmModalProps((prevProps) => ({ ...prevProps, isOpen: false }));
action();
},
});
};
//const [isConfirmModalDeletOpen, setConfirmModalDeleteOpen] = useState(false);
async function copyOldAvailabilities(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
await axiosInstance.get(`/api/?action=copyOldAvailabilities&date=${common.getISODateOnly(value)}`);
}
async function handleCreateNewShift(event: MouseEvent<HTMLButtonElement, MouseEvent>): Promise<void> {
//get last shift end time
let lastShift = shifts.sort((a, b) => new Date(b.endTime).getTime() - new Date(a.endTime).getTime())[0];
//default to 9:00 if no shifts
if (!lastShift) {
//get cart event id
var dayName = common.DaysOfWeekArray[value.getDayEuropean()];
const cartEvent = events.find(event => event.dayofweek == dayName);
lastShift = {
endTime: DateTime.fromJSDate(value).setZone('Europe/Sofia', { keepLocalTime: true }).set({ hour: 9 }).toJSDate(),
cartEventId: cartEvent.id
};
}
const lastShiftEndTime = new Date(lastShift.endTime);
//add 90 minutes
const newShiftEndTime = new Date(lastShiftEndTime.getTime() + 90 * 60000);
await axiosInstance.post(`/api/data/shifts`, {
name: "Нова смяна",
startTime: lastShiftEndTime,
endTime: newShiftEndTime,
isPublished: false,
cartEvent: { connect: { id: lastShift.cartEventId } }
}).then((response) => {
console.log("New shift created:", response.data);
// setShifts([...shifts, response.data]);
handleCalDateChange(value);
}
).catch((error) => {
console.error("Error creating new shift:", error);
});
}
/*
model Settings {
id Int @id @default(autoincrement())
key String
value String
description String?
}
*/
async function setAvailabilityBlockDate(AvailabilityBlockDate: Date): Promise<void> {
// set AvailabilityBlockDate to the selected date
let monthInfo = common.getMonthInfo(value);
await axiosInstance.put(`/api/?action=settings&key=AvailabilityBlockDate&value=${common.getISODateOnly(monthInfo.lastSunday)}`)
.then((response) => {
console.log("AvailabilityBlockDate set to:", response.data);
// setShifts([...shifts, response.data]);
handleCalDateChange(value);
}
).catch((error) => {
console.error("Error setting AvailabilityBlockDate:", error);
});
}
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={() => openConfirmModal(
'Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?',
() => sendMails(),
"sendEmails"
)}
>
{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="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
/> */}
<ConfirmationModal
isOpen={confirmModalProps.isOpen}
onClose={() => setConfirmModalProps((prevProps) => ({ ...prevProps, isOpen: false }))}
onConfirm={confirmModalProps.onConfirm}
message={confirmModalProps.message}
/>
<button
className={`button btn m-2 ${isPublished ? 'hover:bg-gray-500 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={() => openConfirmModal(
'Сигурни ли сте че искате да изтриете смените и назначения на този ден?',
() => deleteShifts(true),
"deleteShiftsDay"
)}
>
{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, null, 0)}>
{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 w-full px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center" onClick={() => generateShifts("genDay", true, true, null, 1)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 2 </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, null, 2)}>
{isLoading('genDay') ? (<i className="fas fa-sync-alt fa-spin mr-2"></i>) : (<i className="fas fa-cogs mr-2"></i>)}
Генерирай смени 3 </button>
<button className="block px-4 py-2 text-sm text-red-500 hover:bg-gray-100"
onClick={() => openConfirmModal(
'Сигурни ли сте че искате да изтриете ВСИЧКИ смени и назначения за месеца?',
() => deleteShifts(false),
"deleteShifts"
)}
>
{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> */}
<button className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100" onClick={setAvailabilityBlockDate}><i className="fas fa-copy mr-2"></i> Блокирай предпочитанията до края на {selectedMonth + 1} м.</button>
</div>
</div>
)}
</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-4 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" id="filterShowWithoutAssignments" onChange={handleCheckboxChange} />
<span className="toggle-slider m-1">без назначения за месеца</span>
<input type="checkbox" className="toggle-checkbox" id="filterIncludeOldAvailabilities" onChange={handleCheckboxChange} />
<span className="toggle-slider m-1">със стари предпочитания</span>
</label>
<ul className="w-full max-w-md" id="availablePubsList" name="availablePubsList">
{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 sm:py-2 rounded-lg shadow-sm mb-2
${bgAndBorderColorClass} ${selectedBorderClass} ${activeOpacityClass}
${pub.currentMonthAssignments === pub.desiredShiftsPerMonth ? 'text-gray-400' : pub.currentMonthAssignments > pub.desiredShiftsPerMonth ? 'text-orange-300' : 'text-gray-800'}`}
onDoubleClick={() => handlePublisherModalOpen(pub)}
>
<span className={`${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>
<button tooltip="push" title="push" className={`badge py-1 px-2 rounded-md text-xs bg-red-100`}
onClick={async () => {
handleSelectedPublisher(pub);
addAssignment(pub, selectedShiftId);
}}
>+</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}
onAssignmentChange={handleAssignmentChange}
showAllAuto={true}
allPublishersInfo={availablePubs}
/>
))}
</div>
<button
className="mt-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
onClick={handleCreateNewShift}
>
Добави нова смяна
</button>
</div>
</div>
<div>
{/* <CustomCalendar date={value} shifts={shifts} /> */}
</div>
{isModalOpen && (
<PublisherShiftsModal
publisher={modalPub}
shifts={allShifts}
onClose={() => setIsModalOpen(false)}
date={value}
onAssignmentChange={handleAssignmentChange}
/>
)}
</ProtectedRoute >
</Layout >
</>
);
}
import axiosServer from '../../../src/axiosServer';
import { start } from 'repl';
import { filter } from 'jszip';
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();
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,
},
};
}