Files
mwitnessing/components/availability/AvailabilityForm.js
Dobromir Popov ffcfb6b15c UI changes;
availability form prettify
2024-03-02 22:49:11 +02:00

530 lines
20 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 } 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 { ToastContainer } from 'react-toastify';
import axios from 'axios';
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
@@map("Availability")
}
*/
//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,
name: "Нов",
dayofweek: "Monday",
dayOfMonth: null,
startTime: "08:00",
endTime: "20:00",
isactive: true,
repeatWeekly: false,
endDate: null,
});
const [items, setItems] = useState(itemsForDay || []); // [existingItem, ...items]
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();
minTime.setHours(8, 0, 0, 0); // 8:00 AM
const maxTime = new Date();
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
}
}, [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);
}
};
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 {
if (!availability.name) {
// availability.name = "От календара";
availability.name = common.getTimeFomatted(availability.startTime) + "-" + common.getTimeFomatted(availability.endTime);
}
availability.dayofweek = common.getDayOfWeekNameEnEnum(availability.startTime);
if (availability.repeatWeekly) {
availability.dayOfMonth = null;
} else {
availability.endDate = null;
availability.dayOfMonth = availability.startTime.getDate();
}
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);
} 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);
}
};
}
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
}
} catch (error) {
alert("Нещо се обърка при изтриването. Моля, опитайте отново или се свържете с нас");
console.log(JSON.stringify(error));
toast.error(error.response?.data?.message || "An error occurred");
}
};
const handleCompletion = async (result) => {
console.log("AvailabilityForm: handleCompletion");
if (isInline) {
if (onDone) {
onDone(result);
}
} else {
router.push(urls.indexUrl);
}
}
console.log("AvailabilityForm: publisherId: " + availability.publisherId + ", id: " + availability.id, ", inline: " + isInline);
const generateTimeSlots = (start, end, increment, item) => {
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
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
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
// Check if the slot overlaps with the availability's time range
const isChecked = slotStart < itemEndDate && slotEnd > itemStartDate;
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;
return slots;
};
const TimeSlotCheckboxes = ({ slots, setSlots, item }) => {
const [allDay, setAllDay] = useState(false);
const handleAllDayChange = (e) => {
const updatedSlots = slots.map(slot => ({
...slot,
isChecked: e.target.checked,
}));
setSlots(updatedSlots);
setAllDay(e.target.checked)
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;
}
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 = `${slot.startTime.getHours()}:${slot.startTime.getMinutes() === 0 ? '00' : slot.startTime.getMinutes()} до ${slot.endTime.getHours()}:${slot.endTime.getMinutes() === 0 ? '00' : slot.endTime.getMinutes()}`;
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 ${allDay ? 'opacity-50' : ''}`}>
<input type="checkbox" checked={slot.isChecked || allDay} onChange={() => handleSlotCheckedChange(slot)}
disabled={allDay}
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 || allDay) ? 'opacity-50' : ''}`}>
<input type="checkbox"
className="form-checkbox h-5 w-5 text-gray-600 mx-2"
checked={slot.isWithTransport}
disabled={!slot.isChecked || allDay}
onChange={() => handleTransportChange(slot)} />
{slot.isFirst ? 'Вземане' : 'Връщане'}
<span className="checkmark"></span>
</label>
)}
</div>
);
})}
</>
);
};
useEffect(() => {
setTimeSlots(generateTimeSlots(9, 18, 1.5, availability));
}, []);
return (
// <div style={{ width: isMobile ? '90%' : 'max-w-xs', margin: '0 auto' }} >
<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">
{availability.id ? "Редактирай" : "Нова"} възможност
</h3>
<LocalizationProvider dateAdapter={AdapterDateFns} localeText={bgBG} adapterLocale={bg}>
<div className="mb-2">
<DatePicker label="Изберете дата" value={availability.startTime} onChange={(value) => setAvailability({ ...availability, endTime: value })} />
</div>
<div>
<div className="mb-1">
{/* Time slot checkboxes */}
<TimeSlotCheckboxes slots={timeSlots} setSlots={setTimeSlots} item={availability} />
</div>
</div>
<div className="mb-2">
<label className="checkbox-container">
<input type="checkbox" checked={availability.repeatWeekly} className="form-checkbox h-5 w-5 text-gray-600 mx-2"
onChange={() => setAvailability({ ...availability, repeatWeekly: !availability.repeatWeekly })} />
Повтаряй всяка {' '}
{/* {availability.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={availability.repeatFrequency || 1}
onChange={(e) => setAvailability({ ...availability, repeatFrequency: parseInt(e.target.value, 10) })}
>
<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 && availability.repeatWeekly && (
<div className="mb-2">
<DatePicker label="До" value={availability.endDate} onChange={(value) => setAvailability({ ...availability, endDate: value })} />
</div>
)}
</LocalizationProvider>
<div className="mb-2 hidden">
<div className="form-check">
<input className="checkbox form-input" type="checkbox" id="isactive" name="isactive" onChange={handleChange} checked={availability.isactive} autoComplete="off" />
<label className="label" htmlFor="isactive">активно</label>
</div>
</div>
{/* <input type="hidden" name="isactive" value={availability.isactive} /> */}
<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>
{availability.id && (
<><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"
> {availability.id ? "Обнови" : "Запиши"}
</button>
</div>
</form>
</div>
);
}