493 lines
19 KiB
JavaScript
493 lines
19 KiB
JavaScript
import axiosInstance from '../../src/axiosSecure';
|
||
import { useEffect, useState, useCallback, use } from "react";
|
||
import toast from "react-hot-toast";
|
||
import { useRouter } from "next/router";
|
||
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
|
||
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
|
||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFnsV3';
|
||
import bg from 'date-fns/locale/bg';
|
||
import { bgBG } from '../x-date-pickers/locales/bgBG';
|
||
import { ToastContainer } from 'react-toastify';
|
||
const common = require('src/helpers/common');
|
||
//todo import Availability type from prisma schema
|
||
import { isBefore, addMinutes, isAfter, isEqual, set, getHours, getMinutes, getSeconds } from 'date-fns'; //ToDo obsolete
|
||
import { stat } from 'fs';
|
||
|
||
const { DateTime, FixedOffsetZone } = require('luxon');
|
||
|
||
|
||
|
||
const fetchConfig = async () => {
|
||
const config = await import('../../config.json');
|
||
return config.default;
|
||
};
|
||
|
||
export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date, cartEvent, datePicker = false }) {
|
||
|
||
const router = useRouter();
|
||
const urls = {
|
||
apiUrl: "/api/data/availabilities/",
|
||
indexUrl: "/cart/availabilities"
|
||
};
|
||
|
||
const id = parseInt(router.query.id);
|
||
//coalsce existingItems to empty array
|
||
existingItems = existingItems || [];
|
||
|
||
const [editMode, setEditMode] = useState(existingItems.length > 0);
|
||
const [publisher, setPublisher] = useState({ id: publisherId });
|
||
const [day, setDay] = useState(new Date(date));
|
||
const [canUpdate, setCanUpdate] = useState(true);
|
||
|
||
const [timeSlots, setTimeSlots] = useState([]);
|
||
const [availabilities, setAvailabilities] = useState(existingItems && existingItems.length > 0 ? existingItems : [{
|
||
publisherId: publisher.id,
|
||
name: "Нов",
|
||
dayofweek: "Monday",
|
||
dayOfMonth: null,
|
||
// startTime: "08:00",
|
||
// endTime: "20:00",
|
||
isActive: true,
|
||
repeatWeekly: false,
|
||
endDate: null,
|
||
isFirst: false,
|
||
isLast: false,
|
||
}]);
|
||
|
||
const [doRepeat, setDoRepeat] = useState(existingItems && existingItems.length > 0 ? existingItems[0].repeatWeekly : false);
|
||
const [repeatFrequency, setRepeatFrequency] = useState(1);
|
||
const [repeatUntil, setRepeatUntil] = useState(null);
|
||
|
||
const [isInline, setInline] = useState(inline || false);
|
||
const [config, setConfig] = useState(null);
|
||
useEffect(() => {
|
||
fetchConfig().then(config => {
|
||
console.log("UI config: ", config);
|
||
setConfig(config);
|
||
});
|
||
}, []);
|
||
|
||
|
||
// get cart event or set default time for Sofia timezone
|
||
const minTime = cartEvent?.startTime || DateTime.now().set({ hour: 8, minute: 0, second: 0, millisecond: 0, zone: 'Europe/Sofia' }).toJSDate();
|
||
const maxTime = cartEvent?.endTime || DateTime.now().set({ hour: 20, minute: 0, second: 0, millisecond: 0, zone: 'Europe/Sofia' }).toJSDate();
|
||
|
||
useEffect(() => {
|
||
setTimeSlots(generateTimeSlots(new Date(minTime), new Date(maxTime), cartEvent.shiftDuration, availabilities));
|
||
console.log("AvailabilityForm: minTime: " + common.getTimeFormatted(minTime) + ", maxTime: " + common.getTimeFormatted(maxTime), ", " + cartEvent.shiftDuration + " min. shifts");
|
||
}, []);
|
||
|
||
|
||
const fetchItemFromDB = async () => {
|
||
if (existingItems.length == 0 && id) {
|
||
try {
|
||
const response = await axiosInstance.get(`/api/data/availabilities/${id}`);
|
||
setAvailabilities([response.data]);
|
||
setEditMode(true);
|
||
setDoRepeat(response.data.repeatWeekly);
|
||
} catch (error) {
|
||
console.error(error);
|
||
toast.error("Error fetching availability data.");
|
||
}
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
fetchItemFromDB();
|
||
}, [router.query.id]);
|
||
|
||
|
||
const handleSubmit = async (e) => {
|
||
e.preventDefault();
|
||
try {
|
||
const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots);
|
||
let avs = availabilities.filter(av => av.type !== "assignment");
|
||
// Determine if we need to delete and recreate, or just update
|
||
let shouldRecreate = avs.length > 0 && avs.length !== groupedTimeSlots.length || avs.some(av => !av.id);
|
||
shouldRecreate = shouldRecreate || (avs.length == 0 && availabilities.length > 0);
|
||
//create availability if we open a form with assignment without availability
|
||
|
||
if (shouldRecreate) {
|
||
// Delete existing availabilities if they have an ID
|
||
console.log("Recreating availabilities");
|
||
await Promise.all(avs.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`)));
|
||
|
||
// Create new availabilities
|
||
avs = await Promise.all(groupedTimeSlots.map(async group => {
|
||
const newAvailability = createAvailabilityFromGroup(group, publisher.id);
|
||
const response = await axiosInstance.post(urls.apiUrl, newAvailability);
|
||
return response.data; // Assuming the new availability is returned
|
||
}));
|
||
|
||
setAvailabilities(avs);
|
||
} else {
|
||
// Update existing availabilities
|
||
console.log("Updating existing availabilities");
|
||
avs = await Promise.all(avs.map(async (availability, index) => {
|
||
const group = groupedTimeSlots[index];
|
||
const id = availability.id;
|
||
const updatedAvailability = updateAvailabilityFromGroup(availability, group);
|
||
delete updatedAvailability.id;
|
||
//delete updatedAvailability.type;
|
||
delete updatedAvailability.publisherId;
|
||
delete updatedAvailability.title;
|
||
delete updatedAvailability.date;
|
||
updatedAvailability.publisher = { connect: { id: publisher.id } };
|
||
await axiosInstance.put(`${urls.apiUrl}${id}`, updatedAvailability);
|
||
return updatedAvailability;
|
||
}));
|
||
|
||
setAvailabilities(avs);
|
||
}
|
||
|
||
handleCompletion({ updated: true });
|
||
} catch (error) {
|
||
alert("Нещо се обърка. Моля, опитайте отново по-късно.");
|
||
// toast.error("Нещо се обърка. Моля, опитайте отново по-късно.");
|
||
// console.error(error.message);
|
||
// try {
|
||
// const { data: session, status } = useSession();
|
||
// const userId = session.user.id;
|
||
// axiosInstance.post('/log', { message: error.message, userId: userId });
|
||
// }
|
||
// catch (err) {
|
||
// console.error("Error logging error: ", err);
|
||
// }
|
||
}
|
||
};
|
||
|
||
|
||
function mergeCheckedTimeSlots(timeSlots) {
|
||
const selectedSlots = timeSlots.filter(slot => slot.isChecked);
|
||
// Sort the selected intervals by start time
|
||
const sortedSlots = [...selectedSlots].sort((a, b) => a.startTime - b.startTime);
|
||
|
||
// Group continuous slots
|
||
const groupedIntervals = [];
|
||
let currentGroup = [sortedSlots[0]];
|
||
|
||
for (let i = 1; i < sortedSlots.length; i++) {
|
||
const previousSlot = currentGroup[currentGroup.length - 1];
|
||
const currentSlot = sortedSlots[i];
|
||
// Calculate the difference in hours between slots
|
||
const difference = (currentSlot.startTime - previousSlot.endTime) / (60 * 60 * 1000);
|
||
|
||
// Assuming each slot represents an exact match to the increment (1.5 hours), we group them
|
||
if (difference === 0) {
|
||
currentGroup.push(currentSlot);
|
||
} else {
|
||
groupedIntervals.push(currentGroup);
|
||
currentGroup = [currentSlot];
|
||
}
|
||
}
|
||
// Don't forget the last group
|
||
if (currentGroup.length > 0) {
|
||
groupedIntervals.push(currentGroup);
|
||
}
|
||
return groupedIntervals;
|
||
}
|
||
|
||
// Common function to set shared properties
|
||
function setSharedAvailabilityProperties(availability, group, timeSlots) {
|
||
let startTime = common.setTimeHHmm(new Date(availability.startTime || day), common.getTimeFormatted(group[0].startTime));
|
||
let endTime = common.setTimeHHmm(new Date(availability.endTime || day), common.getTimeFormatted(group[group.length - 1].endTime));
|
||
availability.startTime = startTime;
|
||
availability.endTime = endTime;
|
||
availability.name = common.getTimeFormatted(group[0].startTime) + "--" + common.getTimeFormatted(group[group.length - 1].endTime);
|
||
|
||
availability.isWithTransportIn = group[0].isFirst && timeSlots[0].isWithTransport;
|
||
availability.isWithTransportOut = group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport;
|
||
|
||
// Adjustments for repeating settings
|
||
if (doRepeat) {
|
||
availability.repeatWeekly = true;
|
||
availability.type = "Weekly"
|
||
availability.dayOfMonth = null;
|
||
availability.endDate = repeatUntil;
|
||
} else {
|
||
availability.type = "OneTime"
|
||
availability.repeatWeekly = false;
|
||
availability.dayOfMonth = availability.startTime.getDate();
|
||
availability.endDate = null;
|
||
}
|
||
availability.isFromPreviousMonth = false;
|
||
availability.dateOfEntry = new Date();
|
||
}
|
||
|
||
function createAvailabilityFromGroup(group) {
|
||
let availability = {
|
||
publisherId: publisher.id,
|
||
dayofweek: common.getDayOfWeekNameEnEnumForDate(day),
|
||
};
|
||
|
||
setSharedAvailabilityProperties(availability, group, timeSlots);
|
||
|
||
return availability;
|
||
}
|
||
|
||
function updateAvailabilityFromGroup(availability, group) {
|
||
setSharedAvailabilityProperties(availability, group, timeSlots);
|
||
|
||
delete availability.weekOfMonth;
|
||
if (doRepeat) {
|
||
availability.weekOfMonth = 0;
|
||
}
|
||
if (availability.parentAvailabilityId) {
|
||
availability.parentAvailability = { connect: { id: parentAvailabilityId } };
|
||
}
|
||
delete availability.parentAvailabilityId;
|
||
|
||
return availability;
|
||
}
|
||
|
||
|
||
|
||
const handleDelete = async (e) => {
|
||
e.preventDefault();
|
||
try {
|
||
let avs = availabilities.filter(av => av.type !== "assignment");
|
||
const deletePromises = avs.map(async (availability) => {
|
||
if (availability.id) {
|
||
// console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id);
|
||
await axiosInstance.delete(urls.apiUrl + availability.id);
|
||
}
|
||
});
|
||
await Promise.all(deletePromises);
|
||
toast.success("Записът изтрит", {
|
||
position: "bottom-center",
|
||
});
|
||
if (handleCompletion) {
|
||
handleCompletion({ deleted: true });
|
||
}
|
||
} catch (error) {
|
||
//alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас");
|
||
console.log(JSON.stringify(error));
|
||
toast.error(error.response?.data?.message || "An error occurred");
|
||
fetchItemFromDB();
|
||
}
|
||
};
|
||
|
||
const handleCompletion = async (result) => {
|
||
console.log("AvailabilityForm: handleCompletion");
|
||
if (isInline) {
|
||
if (onDone) {
|
||
onDone(result);
|
||
}
|
||
} else {
|
||
router.push(urls.indexUrl);
|
||
}
|
||
}
|
||
|
||
// console.log("AvailabilityForm: publisherId: " + publisher.id + ", id: " + availabilit .id, ", inline: " + isInline);
|
||
//ToDo: this is examplary function to be used in the future. replace all date/time related functions with this one
|
||
const generateTimeSlots = (start, end, increment, items) => {
|
||
const slots = [];
|
||
let currentTime = start;
|
||
|
||
//const baseDate = new Date(Date.UTC(2000, 0, 1, 0, 0, 0));
|
||
//const baseDate = new Date(start);
|
||
|
||
while (isBefore(currentTime, end)) {
|
||
let slotStart = currentTime;
|
||
let slotEnd = addMinutes(currentTime, increment);
|
||
|
||
const isChecked = items.some(item => {
|
||
return item.startTime && item.endTime &&
|
||
(slotStart.getTime() < item.endTime.getTime()) &&
|
||
(slotEnd.getTime() > item.startTime.getTime());
|
||
});
|
||
|
||
slots.push({ startTime: slotStart, endTime: slotEnd, isChecked: isChecked, });
|
||
currentTime = addMinutes(currentTime, increment);
|
||
}
|
||
|
||
if (slots.length > 0 && items?.length > 0) {
|
||
slots[0].isFirst = true;
|
||
slots[slots.length - 1].isLast = true;
|
||
slots[0].isWithTransport = items[0]?.isWithTransportIn;
|
||
slots[slots.length - 1].isWithTransport = items[items.length - 1]?.isWithTransportOut;
|
||
}
|
||
|
||
return slots;
|
||
};
|
||
|
||
const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => {
|
||
const [allDay, setAllDay] = useState(slots.every(slot => slot.isChecked));
|
||
const handleAllDayChange = (e) => {
|
||
const updatedSlots = slots.map(slot => ({
|
||
...slot,
|
||
isChecked: e.target.checked,
|
||
}));
|
||
setSlots(updatedSlots);
|
||
setAllDay(e.target.checked)
|
||
// setCanUpdate(slots.some(slot => slot.isChecked));
|
||
const anyChecked = updatedSlots.some(slot => slot.isChecked);
|
||
setCanUpdate(anyChecked);
|
||
console.log("handleAllDayChange: allDay: " + allDay + ", updatedSlots: " + JSON.stringify(updatedSlots));
|
||
};
|
||
useEffect(() => {
|
||
console.log("allDay updated to: ", allDay);
|
||
const updatedSlots = slots.map(slot => ({
|
||
...slot,
|
||
isChecked: allDay
|
||
}));
|
||
//setSlots(updatedSlots);
|
||
}, [allDay]);
|
||
|
||
const handleSlotCheckedChange = (changedSlot) => {
|
||
const updatedSlots = slots.map(slot => {
|
||
if (slot.startTime === changedSlot.startTime && slot.endTime === changedSlot.endTime) {
|
||
return { ...slot, isChecked: !slot.isChecked };
|
||
}
|
||
return slot;
|
||
});
|
||
// If slot is either first or last and it's being unchecked, also uncheck and disable transport
|
||
if ((changedSlot.isFirst || changedSlot.isLast) && !changedSlot.isChecked) {
|
||
changedSlot.isWithTransport = false;
|
||
}
|
||
//if no slots are checked, disable Update button
|
||
const anyChecked = updatedSlots.some(slot => slot.isChecked);
|
||
setCanUpdate(anyChecked);
|
||
|
||
setSlots(updatedSlots);
|
||
};
|
||
|
||
const handleTransportChange = (changedSlot) => {
|
||
const updatedSlots = slots.map(slot => {
|
||
if (slot.startTime === changedSlot.startTime && slot.endTime === changedSlot.endTime) {
|
||
return { ...slot, isWithTransport: !slot.isWithTransport };
|
||
}
|
||
return slot;
|
||
});
|
||
setSlots(updatedSlots);
|
||
};
|
||
|
||
|
||
return (
|
||
<>
|
||
<label className="checkbox-container flex items-center mb-2">
|
||
<input type="checkbox" checked={allDay} onChange={e => handleAllDayChange(e)} className="form-checkbox h-5 w-5 text-gray-600 mx-2" />
|
||
Цял ден
|
||
<span className="checkmark"></span>
|
||
</label>
|
||
{slots.map((slot, index) => {
|
||
const slotLabel = `${common.getTimeFormatted(slot.startTime)} до ${common.getTimeFormatted(slot.endTime)}`;
|
||
slot.transportNeeded = slot.isFirst || slot.isLast;
|
||
// Determine if the current slot is the first or the last
|
||
|
||
return (
|
||
<div key={index} className="mb-1 flex justify-between items-center">
|
||
<label className={`checkbox-container flex items-center mb-2 `}>
|
||
<input type="checkbox" checked={slot.isChecked || allDay} onChange={() => handleSlotCheckedChange(slot)}
|
||
|
||
className="form-checkbox h-5 w-5 text-gray-600 mx-2" />
|
||
{slotLabel}
|
||
<span className="checkmark"></span>
|
||
</label>
|
||
|
||
{/* Conditionally render transport checkbox based on slot being first or last */}
|
||
{slot.transportNeeded && (
|
||
<label className={`checkbox-container flex items-center ${(!slot.isChecked) ? 'opacity-50' : ''}`}>
|
||
<input type="checkbox"
|
||
className="form-checkbox h-5 w-5 text-gray-600 mx-2"
|
||
checked={slot.isWithTransport}
|
||
disabled={!slot.isChecked}
|
||
// disabled={!slot.isChecked && slot.isFirst}
|
||
onChange={() => handleTransportChange(slot)} />
|
||
{slot.isFirst ? 'Вземане' : 'Връщане'}
|
||
<span className="checkmark"></span>
|
||
</label>
|
||
)}
|
||
</div>
|
||
);
|
||
})}
|
||
</>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className="w-full">
|
||
<ToastContainer></ToastContainer>
|
||
<form id="formAv" className="form p-5 bg-white shadow-md rounded-lg" onSubmit={handleSubmit}>
|
||
<h3 className="text-xl font-semibold mb-5 text-gray-800 border-b pb-2">
|
||
{editMode ? "Редактирай" : "Нова"} възможност: {common.getDateFormatedShort(new Date(day))}
|
||
</h3>
|
||
|
||
<LocalizationProvider dateAdapter={AdapterDateFns} localeText={bgBG} adapterLocale={bg}>
|
||
{datePicker && (
|
||
<div className="mb-2">
|
||
<DatePicker label="Изберете дата" value={day} onChange={setDay} />
|
||
</div>
|
||
)}
|
||
<div className="mb-2">
|
||
<label className="checkbox-container">
|
||
<input type="checkbox" checked={doRepeat} className="form-checkbox h-5 w-5 text-gray-600 mx-2"
|
||
onChange={(e) => setDoRepeat(e.target.checked)} />
|
||
Повтаряй всяка {' '}
|
||
{/* {repeatWeekly && (
|
||
<select
|
||
style={{
|
||
appearance: 'none',
|
||
MozAppearance: 'none',
|
||
WebkitAppearance: 'none',
|
||
border: 'black solid 1px',
|
||
background: 'transparent',
|
||
padding: '0 4px',
|
||
margin: '0 2px',
|
||
height: 'auto',
|
||
fontSize: '16px', // Adjust to match surrounding text
|
||
textAlign: 'center',
|
||
color: 'inherit',
|
||
}}
|
||
// className="appearance-none border border-black bg-transparent px-1 py-0 mx-0 mr-1 h-auto text-base text-center text-current align-middle cursor-pointer"
|
||
|
||
//className="form-select mx-2 h-8 text-gray-600"
|
||
value={repeatFrequency || 1}
|
||
onChange={(e) => setRepeatFrequency(e.target.value)}
|
||
>
|
||
<option value="1">1</option>
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4">4</option>
|
||
</select>
|
||
)} */}
|
||
седмица
|
||
<span className="checkmark"></span>
|
||
</label>
|
||
</div>
|
||
|
||
{false && repeatWeekly && (
|
||
|
||
<div className="mb-2">
|
||
<DatePicker label="До" value={repeatUntil} onChange={(value) => setRepeatUntil({ value })} />
|
||
</div>
|
||
)}
|
||
<div>
|
||
<div className="mb-1">
|
||
{/* Time slot checkboxes */}
|
||
<TimeSlotCheckboxes slots={timeSlots} setSlots={setTimeSlots} items={availabilities} />
|
||
</div>
|
||
</div>
|
||
|
||
</LocalizationProvider>
|
||
|
||
<div className="flex justify-between items-center flex-nowrap w-full p-1">
|
||
<button className="button border border-blue-500 text-blue-500 bg-transparent hover:text-white focus:outline-none focus:shadow-outline" onClick={() => handleCompletion()}> Отмени </button>
|
||
|
||
{editMode && (
|
||
<><button className="button btn-outline bg-red-500 hover:bg-red-700 focus:outline-none focus:shadow-outline" type="button" onClick={handleDelete}>
|
||
Изтрий
|
||
</button></>
|
||
)}
|
||
<button
|
||
className={`button bg-blue-500 hover:bg-blue-700 focus:outline-none focus:shadow-outline ${!canUpdate ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||
disabled={!canUpdate}
|
||
> {editMode ? "Обнови" : "Запиши"}
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
);
|
||
}
|