From 193dd91605ffba20b0b0d6932497666aad46a42d Mon Sep 17 00:00:00 2001 From: Dobromir Popov Date: Sun, 3 Mar 2024 02:52:41 +0200 Subject: [PATCH] rewrite availability form --- components/availability/AvailabilityForm.js | 464 +++++++++----------- components/availability/AvailabilityList.js | 28 +- components/calendar/avcalendar.tsx | 49 +-- pages/cart/cartevents/new.tsx | 2 - 4 files changed, 229 insertions(+), 314 deletions(-) diff --git a/components/availability/AvailabilityForm.js b/components/availability/AvailabilityForm.js index 86e0207..f4feda3 100644 --- a/components/availability/AvailabilityForm.js +++ b/components/availability/AvailabilityForm.js @@ -1,70 +1,39 @@ import axiosInstance from '../../src/axiosSecure'; -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import toast from "react-hot-toast"; import { useRouter } from "next/router"; -import DayOfWeek from "../DayOfWeek"; -const common = require('src/helpers/common'); -import { MobileTimePicker } from '@mui/x-date-pickers/MobileTimePicker'; 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 { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns'; - -import TextField from '@mui/material/TextField'; -import bg from 'date-fns/locale/bg'; // Bulgarian locale - - -import { bgBG } from '../x-date-pickers/locales/bgBG'; // Your custom translation file +import bg from 'date-fns/locale/bg'; +import { bgBG } from '../x-date-pickers/locales/bgBG'; import { ToastContainer } from 'react-toastify'; -import axios from 'axios'; - - +const common = require('src/helpers/common'); const fetchConfig = async () => { const config = await import('../../config.json'); return config.default; }; -/* -// ------------------ data model ------------------ -model Availability { - id Int @id @default(autoincrement()) - publisher Publisher @relation(fields: [publisherId], references: [id], onDelete: Cascade) - publisherId String - name String - dayofweek DayOfWeek - dayOfMonth Int? - weekOfMonth Int? - startTime DateTime - endTime DateTime - isactive Boolean @default(true) - type AvailabilityType @default(Weekly) - isWithTransport Boolean @default(false) - isFromPreviousAssignment Boolean @default(false) - isFromPreviousMonth Boolean @default(false) - repeatWeekly Boolean? // New field to indicate weekly repetition - repeatFrequency Int? // New field to indicate repetition frequency - endDate DateTime? // New field for the end date of repetition +export default function AvailabilityForm({ publisherId, existingItems, inline, onDone, date }) { - @@map("Availability") -} + const router = useRouter(); + const urls = { + apiUrl: "/api/data/availabilities/", + indexUrl: "/cart/availabilities" + }; + const [editMode, setEditMode] = useState(existingItems.length > 0); + const [publisher, setPublisher] = useState({ id: publisherId }); + const [day, setDay] = useState(new Date(date || new Date())); + const [doRepeat, setDoRepeat] = useState(false); + const [repeatFrequency, setRepeatFrequency] = useState(1); + const [repeatUntil, setRepeatUntil] = useState(null); + const [canUpdate, setCanUpdate] = useState(true); -*/ - -//enum for abailability type - day of week or day of month; and array of values -const AvailabilityType = { - WeeklyRecurrance: 'WeeklyRecurrance', - ExactDate: 'ExactDate' -} -//const AvailabilityTypeValues = Object.values(AvailabilityType); - - - -export default function AvailabilityForm({ publisherId, existingItem, inline, onDone, itemsForDay }) { - - const [availability, setAvailability] = useState(existingItem || { - publisherId: publisherId || null, + const [timeSlots, setTimeSlots] = useState([]); + const [availabilities, setAvailabilities] = useState(existingItems && existingItems.length > 0 ? existingItems : [{ + publisherId: publisher.id, name: "Нов", dayofweek: "Monday", dayOfMonth: null, @@ -74,47 +43,20 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on repeatWeekly: false, endDate: null, isFirst: false, - }); - const [items, setItems] = useState(itemsForDay || []); // [existingItem, ...items] + isLast: false, + }]); - const [selectedType, setSelectedOption] = useState(AvailabilityType.WeeklyRecurrance); const [isInline, setInline] = useState(inline || false); - const [timeSlots, setTimeSlots] = useState([]); - - const [isMobile, setIsMobile] = useState(false); - // Check screen width to determine if the device is mobile - useEffect(() => { - const handleResize = () => { - setIsMobile(window.innerWidth < 768); // 768px is a common breakpoint for mobile devices - }; - // Call the function to setAvailability the initial state - handleResize(); - // Add event listener - window.addEventListener('resize', handleResize); - // Cleanup - return () => window.removeEventListener('resize', handleResize); - }, []); - // Inside your component - - const [config, setConfig] = useState(null); useEffect(() => { fetchConfig().then(config => { - // Use config here to adjust form fields console.log("UI config: ", config); setConfig(config); }); }, []); - const [dataFetched, setDataFetched] = useState(false); - const router = useRouter(); - const initialId = existingItem?.id || router.query.id; - const urls = { - apiUrl: "/api/data/availabilities/", - indexUrl: "/cart/availabilities" - }; // Define the minimum and maximum times const minTime = new Date(); @@ -123,169 +65,162 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on maxTime.setHours(20, 0, 0, 0); // 8:00 PM - //always setAvailability publisherId useEffect(() => { - availability.publisherId = publisherId; - console.log("availability.publisherId: ", availability.publisherId); - }, [availability]); - - - - if (typeof window !== 'undefined') { - useEffect(() => { - // If component is not in inline mode and there's no existing availability, fetch the availability based on the query ID - // Fetch availability from DB only if it's not fetched yet, and there's no existing availability - if (!isInline && !existingItem && !dataFetched && router.query.id) { - fetchItemFromDB(parseInt(router.query.id.toString())); - setDataFetched(true); // Set data as fetched + const fetchItemFromDB = async () => { + const id = parseInt(router.query.id); + if (existingItems.length == 0 && id) { + try { + const response = await axiosInstance.get(`/api/data/availabilities/${id}`); + setAvailabilities([response.data]); + setEditMode(true); + } catch (error) { + console.error(error); + toast.error("Error fetching availability data."); + } } - }, [router.query.id, isInline, existingItem, dataFetched]); - } + }; - // const [isEdit, setIsEdit] = useState(false); - const fetchItemFromDB = async (id) => { - try { - console.log("fetching availability " + id); - const { data } = await axiosInstance.get(urls.apiUrl + id); - data.startTime = formatTime(data.startTime); - data.endTime = formatTime(data.endTime); - setAvailability(data); - console.log(data); - } catch (error) { - console.error(error); - } - }; + fetchItemFromDB(); + }, [router.query.id]); - const handleChange = ({ target }) => { - // const { name, value } = e.target; - // setItem((prev) => ({ ...prev, [name]: value })); - console.log("AvailabilityForm: handleChange: " + target.name + " = " + target.value); - setAvailability({ ...availability, [target.name]: target.value }); - } const handleSubmit = async (e) => { e.preventDefault(); try { + const groupedTimeSlots = mergeCheckedTimeSlots(timeSlots); + // Determine if we need to delete and recreate, or just update + const shouldRecreate = availabilities.length !== groupedTimeSlots.length || availabilities.some(av => !av.id); - if (!availability.name) { - // availability.name = "От календара"; - availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime); - } + if (shouldRecreate) { + // Delete existing availabilities if they have an ID + console.log("Recreating availabilities"); + await Promise.all(availabilities.filter(av => av.id).map(av => axiosInstance.delete(`${urls.apiUrl}${av.id}`))); - availability.dayofweek = common.getDayOfWeekNameEnEnum(availability.startTime); - if (availability.repeatWeekly) { - availability.dayOfMonth = null; + // Create new availabilities + const createdAvailabilities = 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(createdAvailabilities); } else { - availability.endDate = null; - availability.dayOfMonth = availability.startTime.getDate(); + // Update existing availabilities + console.log("Updating existing availabilities"); + const updatedAvailabilities = await Promise.all(availabilities.map(async (availability, index) => { + const group = groupedTimeSlots[index]; + const updatedAvailability = updateAvailabilityFromGroup(availability, group); + await axiosInstance.put(`${urls.apiUrl}${availability.id}`, updatedAvailability); + return updatedAvailability; + })); + + setAvailabilities(updatedAvailabilities); } - delete availability.date; //remove date from availability as it is not part of the db model - // ---------------------- CB UI -------------- - 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); - } - - // Create availability objects from grouped slots - const availabilities = groupedIntervals.map(group => { - const startTime = group[0].startTime; - const endTime = group[group.length - 1].endTime; - return { - publisherId: availability.publisherId, - startTime: startTime, - endTime: endTime, - isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport, - isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport, - // Add other necessary fields, like isWithTransport if applicable - }; - }); - - //if more than one interval, we delete and recreate the availability, as it is not possble to map them - if (availability.id && availabilities.length > 1) { - await axiosInstance.delete(urls.apiUrl + availability.id); - delete availability.id; - } - - // const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport; - // const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport; - - availabilities.forEach(async av => { - // expand availability - const avToStore = { - ...availability, - ...av, - startTime: av.startTime, - endTime: av.endTime, - name: "От календара", - id: undefined, - - // isWithTransportIn: firstSlotWithTransport, - // isWithTransportOut: lastSlotWithTransport, - - }; - console.log("AvailabilityForm: handleSubmit: " + av); - if (availability.id) { - // UPDATE EXISTING ITEM - await axiosInstance.put(urls.apiUrl + availability.id, { - ...avToStore, - }); - } else { - // CREATE NEW ITEM - await axiosInstance.post(urls.apiUrl, avToStore); - } - handleCompletion(avToStore); // Assuming `handleCompletion` is defined to handle post-save logic - }); - - handleCompletion(availability); + 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); + // 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; + } + + // const firstSlotWithTransport = timeSlots[0].checked && timeSlots[0]?.isWithTransport; + // const lastSlotWithTransport = timeSlots[timeSlots.length - 1].checked && timeSlots[timeSlots.length - 1]?.isWithTransport; + function createAvailabilityFromGroup(group) { + const startTime = new Date(day); + startTime.setTime(group[0].startTime) + const endTime = new Date(day); + endTime.setTime(group[group.length - 1].endTime); + + return { + name: common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime), + publisherId: publisher.id, + startTime: startTime, + endTime: endTime, + isWithTransportIn: group[0].isFirst && timeSlots[0].isWithTransport, + isWithTransportOut: group[group.length - 1].isLast && timeSlots[timeSlots.length - 1].isWithTransport, + dayofweek: common.getDayOfWeekNameEnEnum(day.getDay()), + repeatWeekly: doRepeat, + dayOfMonth: doRepeat ? null : startTime.getDate(), + endDate: doRepeat ? repeatUntil : null, }; } + function updateAvailabilityFromGroup(availability, group) { + availability.name = common.getTimeFomatted(startTime) + "-" + common.getTimeFomatted(endTime); + availability.startTime.setTime(group[0].startTime); + availability.endTime.setTime(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; + + availability.repeatWeekly = doRepeat; + availability.dayOfMonth = doRepeat ? null : group.startTime.getDate(); + availability.endDate = doRepeat ? repeatUntil : null; + + return availability; + } + + + + + const handleDelete = async (e) => { e.preventDefault(); try { - if (availability.id) { - // console.log("deleting publisher id = ", router.query.id, "; url=" + urls.apiUrl + router.query.id); - await axiosInstance.delete(urls.apiUrl + availability.id); - toast.success("Записът изтрит", { - position: "bottom-center", - }); - handleCompletion({ deleted: true }); // Assuming handleCompletion is defined and properly handles post-deletion logic + const deletePromises = availabilities.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("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас"); @@ -305,48 +240,53 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on } } - console.log("AvailabilityForm: publisherId: " + availability.publisherId + ", id: " + availability.id, ", inline: " + isInline); + // console.log("AvailabilityForm: publisherId: " + publisher.id + ", id: " + availabilit .id, ", inline: " + isInline); - const generateTimeSlots = (start, end, increment, item) => { + const generateTimeSlots = (start, end, increment, items) => { const slots = []; - // Ensure we're working with the correct date base - const baseDate = new Date(item?.startTime || new Date()); - baseDate.setHours(start, 0, 0, 0); // Set start time on the base date + const baseDate = new Date(day || new Date()); + baseDate.setHours(start, 0, 0, 0); // Initialize base date with start hour let currentTime = baseDate.getTime(); - const endDate = new Date(item?.startTime || new Date()); - endDate.setHours(end, 0, 0, 0); // Set end time on the same date + // Assuming end time is the same for all items, otherwise, this logic needs adjustment + const endDate = new Date(day || new Date()); + endDate.setHours(end, 0, 0, 0); const endTime = endDate.getTime(); - // Parse availability's startTime and endTime into Date objects for comparison - const itemStartDate = new Date(item?.startTime); - const itemEndDate = new Date(item?.endTime); - while (currentTime < endTime) { let slotStart = new Date(currentTime); - let slotEnd = new Date(currentTime + increment * 60 * 60 * 1000); // Calculate slot end time + let slotEnd = new Date(currentTime + increment * 60 * 60 * 1000); - // Check if the slot overlaps with the availability's time range - const isChecked = slotStart < itemEndDate && slotEnd > itemStartDate; + // Determine if the slot is checked based on overlapping with any item time ranges + const isChecked = items.some(item => { + const itemStartTime = new Date(item.startTime); + const itemEndTime = new Date(item.endTime); + return slotStart < itemEndTime && slotEnd > itemStartTime; + }); slots.push({ startTime: slotStart, endTime: slotEnd, isChecked: isChecked, }); - currentTime += increment * 60 * 60 * 1000; // Move to the next slot - } - slots[0].isFirst = true; - slots[slots.length - 1].isLast = true; - slots[0].isWithTransport = item.isWithTransportIn; - slots[slots.length - 1].isWithTransport = item.isWithTransportOut; + currentTime += increment * 60 * 60 * 1000; + } + + // Assign 'isFirst' and 'isLast' based on the slot's position in the array + if (slots.length > 0) { + slots[0].isFirst = true; + slots[slots.length - 1].isLast = true; + // Assuming isWithTransport flags are global settings, not unique per slot + slots[0].isWithTransport = items[0]?.isWithTransportIn; + slots[slots.length - 1].isWithTransport = items[items.length - 1]?.isWithTransportOut; + } return slots; }; - const TimeSlotCheckboxes = ({ slots, setSlots, item }) => { + const TimeSlotCheckboxes = ({ slots, setSlots, items: [] }) => { const [allDay, setAllDay] = useState(false); const handleAllDayChange = (e) => { @@ -378,6 +318,10 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on 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); }; @@ -434,35 +378,34 @@ export default function AvailabilityForm({ publisherId, existingItem, inline, on }; useEffect(() => { - setTimeSlots(generateTimeSlots(9, 18, 1.5, availability)); + setTimeSlots(generateTimeSlots(9, 18, 1.5, availabilities)); }, []); return ( - //

- {availability.id ? "Редактирай" : "Нова"} възможност + {editMode ? "Редактирай" : "Нова"} възможност

- setAvailability({ ...availability, endTime: value })} /> + setDay({ value })} />
{/* Time slot checkboxes */} - +
-
- {/* */} -
- {availability.id && ( + {editMode && ( <> )}
diff --git a/components/availability/AvailabilityList.js b/components/availability/AvailabilityList.js index bc9a2d1..7f20e42 100644 --- a/components/availability/AvailabilityList.js +++ b/components/availability/AvailabilityList.js @@ -95,27 +95,19 @@ export default function AvailabilityList({ publisher, showNew }) { { toggleAv(); setSelectedItem(null); - if (!item) { - // remove selected item from state - const updatedItems = items.filter(i => i.id !== selectedItem.id); - setItems([...updatedItems]); - return; - }; - const itemIndex = items.findIndex(i => i.id === item.id); // assuming each item has a unique 'id' property - - if (itemIndex !== -1) { - // Replace the existing item with the updated item - const updatedItems = [...items]; - updatedItems[itemIndex] = item; - setItems(updatedItems); - } else { - // Append the new item to the end of the list - setItems([...items, item]); - } + //get the updated list of availabilities from the server + axiosInstance.get(common.getBaseUrl("/api/data/availabilities")) + .then(({ data: items }) => { + setItems(items); + }) + .catch(error => { + console.error("Error getting availabilities:", error); + }); }} /> )} diff --git a/components/calendar/avcalendar.tsx b/components/calendar/avcalendar.tsx index ebe43f7..b1e6dd9 100644 --- a/components/calendar/avcalendar.tsx +++ b/components/calendar/avcalendar.tsx @@ -16,6 +16,7 @@ import { MdToday } from 'react-icons/md'; import { useSwipeable } from 'react-swipeable'; import axiosInstance from '../../src/axiosSecure'; +import { set } from 'date-fns'; // Set moment to use the Bulgarian locale moment.locale('bg'); @@ -43,7 +44,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { const [evts, setEvents] = useState(events); // Existing events const [displayedEvents, setDisplayedEvents] = useState(evts); // Events to display in the calendar const [isModalOpen, setIsModalOpen] = useState(false); - const [selectedEvent, setSelectedEvent] = useState(null); + const [selectedEvents, setSelectedEvents] = useState([]); const [visibleRange, setVisibleRange] = useState(() => { const start = new Date(); start.setDate(1); // Set to the first day of the current month @@ -182,18 +183,21 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { // get exising events for the selected date const existingEvents = evts?.filter(event => event.publisherId === publisherId && event.startTime === start.toDateString()); - setSelectedEvent({ - date: start, - startTime: start, - endTime: end, - dayOfMonth: start.getDate(), - isactive: true, - publisherId: publisherId, - // Add any other initial values needed - //set dayOfMonth to null, so that we repeat the availability every week - dayOfMonth: null, + console.log("handleSelect: " + existingEvents); + setSelectedEvents(existingEvents); - }); + // setSelectedEvent({ + // date: start, + // startTime: start, + // endTime: end, + // dayOfMonth: start.getDate(), + // isactive: true, + // publisherId: publisherId, + // // Add any other initial values needed + // //set dayOfMonth to null, so that we repeat the availability every week + // dayOfMonth: null, + + // }); setIsModalOpen(true); }; @@ -214,7 +218,7 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { delete eventForEditing.type; delete eventForEditing.publisher console.log("handleEventClick: " + eventForEditing); - setSelectedEvent(eventForEditing); + setSelectedEvents([eventForEditing]); setIsModalOpen(true); }; @@ -223,22 +227,6 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => { setIsModalOpen(false); if (dialogEvent === null || dialogEvent === undefined) { } else { - - // if (dialogEvent.deleted) { - // // Remove the old event from the calendar - // setEvents(currentEvents => currentEvents.filter(e => e.id !== selectedEvent.id)); - // } - // else { - // // Update the event data - // dialogEvent.start = dialogEvent.startTime; - // dialogEvent.end = dialogEvent.endTime; - // // Update the events array by first removing the old event and then adding the updated one - // setEvents(currentEvents => { - // const filteredEvents = currentEvents?.filter(e => e.id !== selectedEvent.id) || []; - // return [...filteredEvents, dialogEvent]; - // }); - // } - //refresh the events from the server let e = await axiosInstance.get(`/api/?action=getCalendarEvents&publisherId=${publisherId}`); var newEvents = e.data; // set start and end to Date objects for all events. Fix for the calendar component @@ -468,7 +456,8 @@ const AvCalendar = ({ publisherId, events, selectedDate }) => {
- {/* - */}
);