958 lines
50 KiB
TypeScript
958 lines
50 KiB
TypeScript
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)}©FromPreviousMonth=${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,
|
||
},
|
||
};
|
||
|
||
} |