Files
mwitnessing/components/availability/AvailabilityForm.js
2024-05-05 20:43:47 +02:00

493 lines
19 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 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>
);
}