initial commit - code moved to separate repo

This commit is contained in:
Dobromir Popov
2024-02-22 04:19:38 +02:00
commit 560d503219
240 changed files with 105125 additions and 0 deletions

View File

@ -0,0 +1,908 @@
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 { 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 [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);
let { data: availablePubsForDate } = await axiosInstance.get(`/api/?action=filterPublishers&assignments=true&availabilities=true&date=${dateStr}&select=id,firstName,lastName,isactive,desiredShiftsPerMonth`);
//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 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, isAvailableForShift, 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); // Assuming availablePubs is a state managed by useState
};
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 [isMenuOpen, setIsMenuOpen] = useState(false);
const [isConfirmModalOpen, setConfirmModalOpen] = useState(false);
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="Това ще изпрати имейли до всички участници за смените им през избрания месец. Сигурни ли сте?"
/>
<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>
</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}
</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);
};
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-4 border-yellow-500' : ''} ${assignmentExists ? 'bg-blue-200' : shift.color} h-24 flex flex-col justify-center`}
>
{common.getTimeRange(shift.startTime, shift.endTime)}
{!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 NEXTAUTH_URL: ' + process.env.NEXTAUTH_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,
},
};
}