diff --git a/README.md b/README.md index f4d6153..aa4c467 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,4 @@ - - -

📅 React Material Scheduler

developed with @mui

@@ -17,80 +14,124 @@ React mui scheduler is a react component based on @mui v5 that allows you to manage data in a calendar. -## 🗣️ Installation +## 🚀 Installation ```nodejs npm install react-mui-scheduler ``` ## 💻 Usage ```javascript - import React from 'react' - import ReactDOM from 'react-dom' - import Scheduler from "react-mui-scheduler" - - function App() { - const events = [ - { - id: "event-1", - label: "Consultation médicale", - title: "Dr Shaun Murphy", - color: "#f28f6a", - startHour: "9 AM", - endHour: "10 AM", - date: "2021-09-09", - createdAt: new Date(), - createdBy: "Kristina Mayer" - }, - { - id: "event-2", - label: "Consultation médicale", - title: "Dr Claire Brown", - color: "#099ce5", - startHour: "9 AM", - endHour: "10 AM", - date: "2021-09-09", - createdAt: new Date(), - createdBy: "Kristina Mayer" - }, - { - id: "event-3", - label: "Consultation médicale", - title: "Dr Menlendez Hary", - color: "#263686", - startHour: "13 AM", - endHour: "14 AM", - date: "2021-09-12", - createdAt: new Date(), - createdBy: "Kristina Mayer" - }, - ] - - const onCellClick = (event, row, day) => { - // Do something... - } - - const onEventClick = (event, task) => { - // Do something... +import React, {useState} from 'react' +import ReactDOM from 'react-dom' +import Scheduler from "react-mui-scheduler" + +function App() { + const [state, setState] = useState({ + options: { + transitionMode: "zoom", + startWeekOn: "Mon", + defaultMode: "week" + }, + alertProps: { + open: true, + color: "info", + severity: "info", + message: "🚀 Let's start with awesome react-mui-scheduler 🔥 🔥 🔥" , + showActionButton: true, + }, + toolbarProps: { + showSearchBar: true, + showSwitchModeButtons: true, + showDatePicker: true } - - const onEventsChange = (item) => { - // Do something... + }) + + const events = [ + { + id: "event-1", + label: "Consultation médicale", + groupLabel: "Dr Shaun Murphy", + user: "Dr Shaun Murphy", + color: "#f28f6a", + startHour: "04:00 AM", + endHour: "05:00 AM", + date: "2021-09-28", + createdAt: new Date(), + createdBy: "Kristina Mayer" + }, + { + id: "event-2", + label: "Consultation médicale", + groupLabel: "Dr Claire Brown", + user: "Dr Claire Brown", + color: "#099ce5", + startHour: "09:00 AM", + endHour: "10:00 AM", + date: "2021-09-29", + createdAt: new Date(), + createdBy: "Kristina Mayer" + }, + { + id: "event-3", + label: "Consultation médicale", + groupLabel: "Dr Menlendez Hary", + user: "Dr Menlendez Hary", + color: "#263686", + startHour: "13 AM", + endHour: "14 AM", + date: "2021-09-30", + createdAt: new Date(), + createdBy: "Kristina Mayer" + }, + { + id: "event-4", + label: "Consultation prénatale", + groupLabel: "Dr Shaun Murphy", + user: "Dr Shaun Murphy", + color: "#f28f6a", + startHour: "08:00 AM", + endHour: "09:00 AM", + date: "2021-10-01", + createdAt: new Date(), + createdBy: "Kristina Mayer" } + ] - return ( - - ) + const handleCellClick = (event, row, day) => { + // Do something... } - - ReactDOM.render(, document.querySelector('#app')) + + const handleEventClick = (event, task) => { + // Do something... + } + + const handleEventsChange = (item) => { + // Do something... + } + + const handleAlertCloseButtonClicked = (item) => { + // Do something... + setState({ + ...state, + alertProps: {...state.alertProps, open: false} + }) + } + + return ( + + ) +} + +ReactDOM.render(, document.querySelector('#yourComponentRootId')) ``` @@ -109,16 +150,18 @@ React mui scheduler is a react component based on @mui v5 that allows you to man ## 🙇‍♂️ Extra - 😊 Do you like this library ? Buy me a coffee + Do you like this library ? Buy me a coffee -* Btc address: `1A2VNHSLGDyYsKWniJBe8cCqYWC52NvNZx` +* Btc address: `bc1qettgagenn9nc8ks7ghntjfme96yvvkfhntk774` -* Eth address: `0xFe444a01D9494Ec04f61797e15193C8016D666A5` +* Eth address: `0xB0413d8D0336E263e289A915c383e152155881E0` ## 🔥 Some features to add in next releases -- 👉 Week and day mode switch view +- ✅ Week mode switch view + +- 👉 Day mode switch view - 👉 Option menu diff --git a/package.json b/package.json index c02a340..2517d8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-mui-scheduler", - "version": "1.0.4", + "version": "1.1.0", "description": "\uD83D\uDCC5 React mui scheduler is a react component based on @mui v5 that allows you to manage data in a calendar", "main": "dist/index.esm.js", "directories": { @@ -18,6 +18,10 @@ ], "author": "rouftom", "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/rouftom/react-mui-scheduler.git" + }, "peerDependencies": { "react": "^17.0.2", "react-dom": "^17.0.2" diff --git a/src/MonthModeView.jsx b/src/MonthModeView.jsx index e923932..d82ffca 100644 --- a/src/MonthModeView.jsx +++ b/src/MonthModeView.jsx @@ -15,7 +15,7 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ borderTop: `1px solid #ccc !important`, borderBottom: `1px solid #ccc !important`, borderLeft: `1px solid #ccc !important`, - '&:nth-child(1)': { + '&:nth-of-type(1)': { borderLeft: `0px !important` } }, @@ -23,12 +23,13 @@ const StyledTableCell = styled(TableCell)(({ theme }) => ({ fontSize: 14, height: 96, width: 64, + maxWidth: 64, cursor: 'pointer', borderLeft: `1px solid #ccc`, - '&:nth-child(7n+1)': { + '&:nth-of-type(7n+1)': { borderLeft: 0 }, - '&:nth-child(even)': { + '&:nth-of-type(even)': { backgroundColor: theme.palette.action.hover }, }, @@ -73,7 +74,19 @@ function MonthModeView (props) { */ const onCellDragStart = (e, item, rowIndex) => { setState({...state, itemTransfert: {item, rowIndex}}) - //e.dataTransfer.setData('text/plain', `${item.id}_${rowIndex}`) + } + + /** + * @name onCellDragEnter + * @description + * @param e + * @param elementId + * @param rowIndex + * @return void + */ + const onCellDragEnter = (e, elementId, rowIndex) => { + e.preventDefault() + setState({...state, transfertTarget: {elementId, rowIndex}}) } /** @@ -98,20 +111,21 @@ function MonthModeView (props) { let splittedDate = transfert?.item?.date?.split('-') if (!transfert?.item?.day) { - // Jour de la date du début du mois en chiffre + // Get day of the date (DD) transfert.item.day = parseInt(splittedDate[2]) } if (transfert.item.day !== day?.day) { let itemCheck = day.data.findIndex(item => ( - item.day === transfert.item.day && item.title === transfert.item.title + item.day === transfert.item.day && item.label === transfert.item.label )) if (itemCheck === -1) { let prevDayEvents = rowsCopy[transfert.rowIndex].days.find(d => d.day === transfert.item.day) let itemIndexToRemove = prevDayEvents?.data?.findIndex(i => i.id === transfert.item.id) - + if (itemIndexToRemove === undefined || itemIndexToRemove === -1) { + console.log(prevDayEvents) return console.log("item to remove is not found") } @@ -119,26 +133,13 @@ function MonthModeView (props) { transfert.item.day = day?.day transfert.item.date = format(day?.date, 'yyyy-MM-dd') day.data.push(transfert.item) - setState({...state, rows: rowsCopy}) + setState({...state, rows: rowsCopy, itemTransfert: null, transfertTarget: null}) } } } } } - /** - * @name onCellDragEnter - * @description - * @param e - * @param elementId - * @param rowIndex - * @return void - */ - const onCellDragEnter = (e, elementId, rowIndex) => { - e.preventDefault() - setState({...state, transfertTarget: {elementId, rowIndex}}) - } - /** * @name handleCellClick * @description @@ -164,7 +165,13 @@ function MonthModeView (props) { */ const renderTask = (tasks = [], rowId) => { return tasks?.map((task, index) => ( - ((searchResult && task?.title === searchResult?.title) || !searchResult) && ( + ( + ( + searchResult && + (task?.groupLabel === searchResult?.groupLabel || task?.user === searchResult?.user) + ) || !searchResult + ) && + ( handleTaskClick(e, task)} @@ -181,7 +188,7 @@ function MonthModeView (props) { onDragStart={e => onCellDragStart(e, task, rowId)} > - {task?.title} + {task?.label} ) @@ -205,6 +212,7 @@ function MonthModeView (props) { if (state?.rows) { onEventsChange(Object.assign({}, state?.itemTransfert?.item)) } + // eslint-disable-next-line }, [state?.rows, state?.itemTransfert]) return ( diff --git a/src/Scheduler.jsx b/src/Scheduler.jsx index e7a7738..1dcfb09 100644 --- a/src/Scheduler.jsx +++ b/src/Scheduler.jsx @@ -1,13 +1,15 @@ import React, {useState, useEffect} from 'react' import PropTypes from "prop-types" -import { Grid, Paper } from "@mui/material" +import { Grid, Paper, Fade, Zoom } from "@mui/material" import {ThemeProvider} from "@mui/system" import { useTheme } from '@mui/material/styles' import { - format, getDaysInMonth, getDay, sub, startOfMonth, isSameDay, parse + format, getDaysInMonth, getDay, sub, startOfMonth, isSameDay, parse, + add, startOfDay, startOfWeek, getWeeksInMonth } from 'date-fns' import SchedulerToolbar from "./Toolbar.jsx" import MonthModeView from "./MonthModeView.jsx" +import WeekModeView from "./WeekModeView.jsx" /** @@ -19,64 +21,37 @@ import MonthModeView from "./MonthModeView.jsx" function Scheduler(props) { const { events, + options, onCellClick, onTaskClick, onEventsChange, - openAlert, alertMessage, - alertProps + alertProps, + onAlertCloseButtonClicked, + toolbarProps } = props const today = new Date() const theme = useTheme() - const weekDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] + const TransitionMode = options?.transitionMode === 'zoom' ? Zoom : Fade const [state, setState] = useState({}) const [searchResult, setSearchResult] = useState() - const [mode, setMode] = useState('month') + const [mode, setMode] = useState(options?.defaultMode || 'month') const [selectedDay, setSelectedDay] = useState(today) const [daysInMonth, setDaysInMonth] = useState(getDaysInMonth(today)) const [selectedDate, setSelectedDate] = useState(format(today, 'MMMM-yyyy')) /** - * @name handleDateChange - * @description - * @param day - * @param date - * @return void - */ - const handleDateChange = (day, date) => { - setDaysInMonth(day) - setSelectedDay(date) - setSelectedDate(format(date, 'MMMM-yyyy')) - setState({rows: getRows(), columns: getHeader()}) - } - - /** - * @name handleModeChange - * @description - * @param newMode - * @return void - */ - const handleModeChange = (newMode) => { - setMode(newMode) - } - - /** - * @name onSearchResult - * @description - * @param item - * @return void - */ - const onSearchResult = (item) => { - setSearchResult(item) - } - - /** - * @name getHeader + * @name getMonthHeader * @description * @return {{headerClassName: string, headerAlign: string, headerName: string, field: string, flex: number, editable: boolean, id: string, sortable: boolean, align: string}[]} */ - const getHeader = () => { + const getMonthHeader = () => { + let weekDays = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + if (options?.startWeekOn?.toUpperCase() === 'SUN') { + weekDays[0] = 'Sun' + weekDays[6] = 'Mon' + } return weekDays?.map((day, i) => ({ id: `row-day-header-${i+1}`, flex: 1, @@ -91,43 +66,56 @@ function Scheduler(props) { } /** - * @name getRows + * @name getMonthRows * @description * @return {[id: string, day: number, date: date, data: array]} */ - const getRows = () => { + const getMonthRows = () => { let rows = [], daysBefore = [] - let iteration = Math.ceil(daysInMonth / 7) - - // TODO Rester dans le même mois même si on selectionne - // une date du mois précédent mais visible sur le calendrier - let monthStartDate = startOfMonth(selectedDay) // Premier jour du mois - let monthStartDay = getDay(monthStartDate) // Index du jour de la semaine en chiffre - let dateDay = parseInt(format(monthStartDate, 'dd')) // Jour de la date du début du mois en chiffre - - // If Mon is the first day of week, apply b < monthStartDay - // and days: (monthStartDay-b) - for (let b = 1; b <= monthStartDay; b++) { - let subDate = sub(monthStartDate, {days: (monthStartDay-b) + 1}) - let day = parseInt(format(subDate, 'dd')) - let data = events.filter((event) => ( - isSameDay(subDate, parse(event?.date, 'yyyy-MM-dd', new Date())) - )) + let iteration = getWeeksInMonth(selectedDay) //Math.ceil(daysInMonth / 7) + let startOnSunday = options?.startWeekOn?.toUpperCase() === 'SUN' + let monthStartDate = startOfMonth(selectedDay) // First day of month + let monthStartDay = getDay(monthStartDate) // Index of the day in week + let dateDay = parseInt(format(monthStartDate, 'dd')) // Month start day + // Condition check helper + const checkCondition = (v) => (startOnSunday ? v <= monthStartDay : v < monthStartDay) - daysBefore.push({ - id: `day_-${day}`, - day: day, - date: subDate, - data: data - }) + if (monthStartDay > 1) { + // Add days of precedent month + // If Sunday is the first day of week, apply b <= monthStartDay + // and days: (monthStartDay-b) + 1 + for (let i = 1; checkCondition(i); i++) { + let subDate = sub( + monthStartDate, + {days: monthStartDay - i + (startOnSunday ? 1 : 0)} + ) + let day = parseInt(format(subDate, 'dd')) + let data = events.filter((event) => ( + isSameDay(subDate, parse(event?.date, 'yyyy-MM-dd', new Date())) + )) + + daysBefore.push({ + id: `day_-${day}`, + day: day, + date: subDate, + data: data + }) + } } - rows.push({ id: 0, days: daysBefore }) + if (daysBefore?.length > 0) { + rows.push({ id: 0, days: daysBefore }) + } + + // Add days and events data for (let i = 0; i < iteration; i++) { let obj = [] - + for ( let j = 0; + // This condition ensure that days will not exceed 30 + // i === 0 ? 7 - daysBefore?.length means that we substract inserted days + // in the first line to 7 j < (i === 0 ? 7 - daysBefore?.length : 7) && (dateDay <= daysInMonth); j++ ) { @@ -135,32 +123,154 @@ function Scheduler(props) { let data = events.filter((event) => ( isSameDay(date, parse(event?.date, 'yyyy-MM-dd', new Date())) )) - - obj.push({ id: `day_${dateDay}`, date: date, day: dateDay, data: data }) + + obj.push({ id: `day_-${dateDay}`, date: date, day: dateDay, data: data }) dateDay++ } - + if (i === 0 && daysBefore?.length > 0) { rows[0].days = rows[0].days.concat(obj) continue } - rows.push({id: i, days: obj}) + if (obj.length > 0) { + rows.push({id: i, days: obj}) + } } - + + // Check if last row is not fully filled + let lastRow = rows[iteration - 1] + let lastRowDaysdiff = 7 - lastRow?.days?.length + let lastDaysData = [] + + if (lastRowDaysdiff > 0) { + let day = lastRow.days[lastRow?.days?.length-1] + let addDate = day.date + + for (let i = dateDay; i < (dateDay + lastRowDaysdiff); i++) { + addDate = add(addDate, {days: 1}) + let d = format(addDate, 'dd') + // eslint-disable-next-line + let data = events.filter((event) => ( + isSameDay(addDate, parse(event?.date, 'yyyy-MM-dd', new Date())) + )) + lastDaysData.push({ id: `day_-${d}`, date: addDate, day: d, data: data }) + } + rows[iteration-1].days = rows[iteration-1].days.concat(lastDaysData) + } + return rows } - useEffect(() => { - if (daysInMonth) { - setState({rows: getRows(), columns: getHeader()}) + /** + * @name getWeekHeader + * @description + * @return {{headerClassName: string, headerAlign: string, headerName: string, field: string, flex: number, editable: boolean, id: string, sortable: boolean, align: string}[]} + */ + const getWeekHeader = () => { + let data = [] + let weekStart = startOfWeek(selectedDay, { weekStartsOn: 1 }) + for (let i = 0; i < 7; i++) { + let date = add(weekStart, {days: i}) + data.push({ + date: date, + weekDay: format(date, 'iii'), + day: format(date, 'dd'), + month: format(date, 'MM'), + }) } - }, [daysInMonth, selectedDate]) + return data + } + + const getWeekRows = () => { + const HOURS = 24 //* 2 + let data = [] + let dayStartHour = startOfDay(selectedDay) + + for (let i = 0; i <= HOURS; i++) { + let id = `line_${i}` + let label = format(dayStartHour, 'HH:mm aaa') + + //TODO Add everyday event capability + //if (i === 0) { + //id = `line_everyday`; label = 'Everyday' + //} + //TODO Place the processing bloc here if everyday capability is available + // ... + + if (i > 0) { + //Start processing bloc + let obj = { id: id, label: label, days: [] } + let columns = getWeekHeader() + // eslint-disable-next-line + columns.map((column, index) => { + let data = events.filter((event) => { + let eventDate = parse(event?.date, 'yyyy-MM-dd', new Date()) + return ( + isSameDay(column?.date, eventDate) && + event?.startHour?.toUpperCase() === label?.toUpperCase() + ) + }) + obj.days.push({ + id: `column-${index}_m-${column.month}_d-${column.day}_${id}`, + date: column?.date, + data: data + }) + }) + // Label affectation + data.push(obj) // End processing bloc + dayStartHour = add(dayStartHour, {minutes: 60}) // 30 + } + //if (i > 0) { + // dayStartHour = add(dayStartHour, {minutes: 30}) + //} + } + return data + } + + /** + * @name handleDateChange + * @description + * @param day + * @param date + * @return void + */ + const handleDateChange = (day, date) => { + setDaysInMonth(day) + setSelectedDay(date) + setSelectedDate(format(date, 'MMMM-yyyy')) + } + + /** + * @name handleModeChange + * @description + * @param newMode + * @return void + */ + const handleModeChange = (newMode) => { + setMode(newMode) + } + + /** + * @name onSearchResult + * @description + * @param item + * @return void + */ + const onSearchResult = (item) => { + setSearchResult(item) + } useEffect(() => { - if (!state?.rows && !state?.columns) { - setState({rows: getRows(), columns: getHeader()}) + if (mode) { + if (mode === 'month') { + setState({columns: getMonthHeader(), rows: getMonthRows()}) + } + if (mode === 'week') { + setState({columns: getWeekHeader(), rows: getWeekRows()}) + } } - }, []) + // eslint-disable-next-line + }, [daysInMonth, selectedDay, selectedDate, mode]) return ( @@ -168,27 +278,47 @@ function Scheduler(props) { - - {mode === 'month' && - } - + {mode === 'month' && + + + + + } + {mode === 'week' && + + + + + } @@ -197,8 +327,13 @@ function Scheduler(props) { Scheduler.propTypes = { events: PropTypes.array, + options: PropTypes.object, + alertProps: PropTypes.object, + toolbarProps: PropTypes.object, + onEventsChange: PropTypes.func, onCellClick: PropTypes.func, - onTaskClick: PropTypes.func + onTaskClick: PropTypes.func, + onAlertCloseButtonClicked: PropTypes.func, } Scheduler.defaultProps = { diff --git a/src/Toolbar.jsx b/src/Toolbar.jsx index 1c69c92..2e35993 100644 --- a/src/Toolbar.jsx +++ b/src/Toolbar.jsx @@ -9,8 +9,10 @@ import { } from "@mui/material" import LocalizationProvider from '@mui/lab/LocalizationProvider' import StaticDatePicker from '@mui/lab/StaticDatePicker' +import CloseIcon from '@mui/icons-material/Close' import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' import ChevronRightIcon from '@mui/icons-material/ChevronRight' +// eslint-disable-next-line import MoreVertIcon from '@mui/icons-material/MoreVert' import TodayIcon from '@mui/icons-material/Today' import SettingsIcon from '@mui/icons-material/Settings' @@ -24,17 +26,17 @@ import ToolbarSearchbar from "./ToolbarSeachBar.jsx" function SchedulerToolbar (props) { const { // events data - events, today, toolbarProps, + events, switchMode, today, toolbarProps, // Mode onModeChange, onSearchResult, // Alert props - openAlert, alertMessage, alertProps, + alertProps, onAlertCloseButtonClicked, // Date onDateChange } = props const [searchResult, setSearchResult] = useState() - const [mode, setMode] = useState('month') + const [mode, setMode] = useState(switchMode) const [anchorMenuEl, setAnchorMenuEl] = useState(null) const [anchorDateEl, setAnchorDateEl] = useState(null) const [selectedDate, setSelectedDate] = useState(today || new Date()) @@ -75,6 +77,7 @@ function SchedulerToolbar (props) { * @param event * @return void */ + // eslint-disable-next-line const handleOpenMenu = (event) => { setAnchorMenuEl(event.currentTarget) } @@ -115,21 +118,25 @@ function SchedulerToolbar (props) { */ const handleChangeDate = (method) => { if (typeof method !== 'function') return - let newDate = method(selectedDate, {months: 1}) + let options = mode === 'month' ? {months: 1} : {weeks: 1} + let newDate = method(selectedDate, options) setDaysInMonth(getDaysInMonth(newDate)) setSelectedDate(newDate) } useEffect(() => { if (mode) { onModeChange(mode) } + // eslint-disable-next-line }, [mode]) useEffect(() => { onDateChange(daysInMonth, selectedDate) + // eslint-disable-next-line }, [daysInMonth, selectedDate]) useEffect(() => { onSearchResult(searchResult) + // eslint-disable-next-line }, [searchResult]) return ( @@ -145,7 +152,7 @@ function SchedulerToolbar (props) { handleChangeDate(sub)} > @@ -160,10 +167,10 @@ function SchedulerToolbar (props) { onClick={handleOpenDateSelector} aria-expanded={openDateSelector ? 'true' : undefined} > - {format(selectedDate, 'MMMM-yyyy')} + {format(selectedDate, mode === 'month' ? 'MMMM-yyyy' : 'PPP')} handleChangeDate(add)} > @@ -213,21 +220,20 @@ function SchedulerToolbar (props) { {toolbarProps?.showSwitchModeButtons && { setMode(newMode) }} > - {['month', 'Week', 'Day'].map(tb => ( + {['month', 'week'].map(tb => ( {tb} ))} } - {toolbarProps?.showOptions && + {/*toolbarProps?.showOptions && - } + */} Settings - {openAlert && + {alertProps?.open && - - {alertMessage} + + + : null + } + > + {alertProps?.message} } @@ -266,20 +287,28 @@ function SchedulerToolbar (props) { } SchedulerToolbar.propTypes = { - title: PropTypes.string, - openAlert: PropTypes.bool, - alertMessage: PropTypes.string, + today: PropTypes.object.isRequired, + events: PropTypes.array.isRequired, + switchMode: PropTypes.string.isRequired, alertProps: PropTypes.object, - onDateChange: PropTypes.func + toolbarProps: PropTypes.object, + onDateChange: PropTypes.func.isRequired, + onModeChange: PropTypes.func.isRequired, + onSearchResult: PropTypes.func.isRequired, + onAlertCloseButtonClicked: PropTypes.func.isRequired, } SchedulerToolbar.defaultProps = { - openAlert: false, - alertMessage: 'This is a scheduler alert', - alertProps: {color: 'info', severity: 'info'}, + alertProps: { + open: false, + message: '', + color: 'info', + severity: 'info', + showActionButton: true, + }, toolbarProps: { showSearchBar: true, - showSwitchModeButtons: false, + showSwitchModeButtons: true, showDatePicker: true, showOptions: false } diff --git a/src/ToolbarSeachBar.jsx b/src/ToolbarSeachBar.jsx index 9effb1d..e928dd3 100644 --- a/src/ToolbarSeachBar.jsx +++ b/src/ToolbarSeachBar.jsx @@ -3,7 +3,6 @@ import PropTypes from 'prop-types' import { format, parse } from 'date-fns' import { styled } from '@mui/material/styles' import { TextField, Autocomplete, Box } from "@mui/material" -import { useTheme } from '@mui/material/styles' const StyledAutoComplete = styled(Autocomplete)(({ theme }) => ({ color: 'inherit', @@ -17,7 +16,6 @@ const StyledAutoComplete = styled(Autocomplete)(({ theme }) => ({ })) function ToolbarSearchbar (props) { - const theme = useTheme() const {events, onInputChange} = props const [value, setValue] = useState('') @@ -31,26 +29,12 @@ function ToolbarSearchbar (props) { return ( ( - - - {option?.title} - - )} + options={events?.sort((a, b) => -b.groupLabel.localeCompare(a.groupLabel))} + groupBy={(option) => option?.groupLabel} /* ( @@ -64,13 +48,13 @@ function ToolbarSearchbar (props) { backgroundColor: option?.color || theme.palette.secondary.main }} /> - {option?.title} + {option?.groupLabel} ) */ getOptionLabel={(option) => ( option && - `${option?.title} | (${option?.startHour} - ${option?.endHour})` + `${option?.groupLabel} | (${option?.startHour ?? ''} - ${option?.endHour ?? ''})` )} onInputChange={(event, newInputValue) => { setInputValue(newInputValue) @@ -78,7 +62,8 @@ function ToolbarSearchbar (props) { }} renderOption={(props, option) => ( - {format(parse(option?.date, 'yyyy-MM-dd', new Date()), 'dd-MMMM-yyyy')} ({option?.startHour} - {option?.endHour}) + {format(parse(option?.date, 'yyyy-MM-dd', new Date()), 'dd-MMMM-yyyy')} + ({option?.startHour ?? ''} - {option?.endHour ?? ''}) )} renderInput={(params) => ( diff --git a/src/WeekModeView.jsx b/src/WeekModeView.jsx new file mode 100644 index 0000000..e89f84c --- /dev/null +++ b/src/WeekModeView.jsx @@ -0,0 +1,366 @@ +import React, {useState, useEffect} from 'react' +import PropTypes from 'prop-types' +import {styled} from "@mui/system" +import { useTheme } from '@mui/material/styles' +import { + Paper, Typography, Table, TableBody, TableCell, TableContainer, + TableHead, TableRow, tableCellClasses, Box, +} from "@mui/material" +import { format, parse, add, differenceInMinutes, isValid } from 'date-fns' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + [`&.${tableCellClasses.head}`]: { + paddingLeft: 4, + paddingRight: 4, + borderTop: `1px solid #ccc !important`, + borderBottom: `1px solid #ccc !important`, + borderLeft: `1px solid #ccc !important`, + "&:nth-of-type(1)": { borderLeft: `0px !important` } + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 12, + height: 16, + width: 128, + maxWidth: 128, + cursor: 'pointer', + borderLeft: `1px solid #ccc`, + "&:nth-of-type(1)": { + width: 80, + maxWidth: 80, + }, + "&:nth-of-type(8n+1)": { borderLeft: 0 }, + "&:nth-of-type(even)": { + //backgroundColor: theme.palette.action.hover + }, + }, + [`&.${tableCellClasses.body}:hover`]: { + backgroundColor: "#eee" + } +})) + +const StyledTableRow = styled(TableRow)(({ theme }) => ({ + '&:nth-of-type(odd)': { + //backgroundColor: theme.palette.action.hover, + }, + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +const StyledTableContainer = styled(TableContainer)(({ theme }) => ({ + "&::-webkit-scrollbar": { + width: 7, + height: 6 + }, + "&::-webkit-scrollbar-track": { + WebkitBoxShadow: "inset 0 0 6px rgb(125, 161, 196, 0.5)" + }, + "&::-webkit-scrollbar-thumb": { + WebkitBorderRadius: 4, + borderRadius: 4, + background: "rgba(0, 172, 193, .5)", + WebkitBoxShadow: "inset 0 0 6px rgba(25, 118, 210, .5)" + }, + "&::-webkit-scrollbar-thumb:window-inactive": { + background: "rgba(125, 161, 196, 0.5)" + } +})) + +function WeekModeView (props) { + const { + columns, rows, searchResult, onTaskClick, onCellClick, onEventsChange + } = props + const theme = useTheme() + const [state, setState] = useState({columns, rows}) + + /** + * @name onCellDragOver + * @param e + * @return void + */ + const onCellDragOver = (e) => { + e.preventDefault() + } + + /** + * @name onCellDragStart + * @description + * @param e + * @param item + * @param rowLabel + * @param rowIndex + * @param dayIndex + * @return void + */ + const onCellDragStart = (e, item, rowLabel, rowIndex, dayIndex) => { + setState({ + ...state, + itemTransfert: {item, rowLabel, rowIndex, dayIndex}} + ) + } + + /** + * @name onCellDragEnter + * @description + * @param e + * @param rowLabel + * @param rowIndex + * @param dayIndex + * @return void + */ + const onCellDragEnter = (e, rowLabel, rowIndex, dayIndex) => { + e.preventDefault() + setState({...state, transfertTarget: {rowLabel, rowIndex, dayIndex}}) + } + + /** + * @name onCellDragEnd + * @description + * @param e + * @return void + */ + const onCellDragEnd = (e) => { + e.preventDefault() + if (!state?.itemTransfert || !state?.transfertTarget) { + return //console.log('undefined source or target') + } + + let transfert = state.itemTransfert + let transfertTarget = state.transfertTarget + let rowsData = Array.from(rows) + let day = rowsData[transfertTarget?.rowIndex]?.days[transfertTarget?.dayIndex] + + if (day) { + let foundEventIndex = day.data.findIndex(e => + e.id === transfert.item.id && + e.startHour === transfert?.item?.startHour && + e.endHour === transfert?.item?.endHour + ) + // Task already exists in the data array of the chosen cell + if (foundEventIndex !== -1) { + return + } + + // Timeline label (00:00 am, 01:00 am, etc.) + let label = transfertTarget.rowLabel?.toUpperCase() + // Event cell item to transfert + let prevEventCell = rowsData[transfert?.rowIndex].days[transfert?.dayIndex] + // Event's end hour + let endHourDate = parse(transfert.item.endHour, 'p', day?.date) + // Event start hour + let startHourDate = parse(transfert.item.startHour, 'p', day?.date) + // Minutes difference between end and start event hours + let minutesDiff = differenceInMinutes(endHourDate, startHourDate) + // New event end hour according to it new cell + let newEndHour = add( + parse(label, 'p', day?.date), {minutes: minutesDiff} + ) + + // If event is moved from timeline 00:00 AM + if (label === '00:00 AM') { + minutesDiff = differenceInMinutes(endHourDate, startHourDate) + newEndHour = add(day?.date, {minutes: minutesDiff}) + } + + // If event is moved from timeline 01:00 AM + if (label === '01:00 AM') { + minutesDiff = differenceInMinutes(endHourDate, startHourDate) + newEndHour = add(parse(label, 'p', day?.date), {minutes: minutesDiff}) + + if (!isValid(startHourDate)){ + startHourDate = day?.date + minutesDiff = differenceInMinutes(endHourDate, startHourDate) + newEndHour = add( + parse(label, 'p', startHourDate), {minutes: minutesDiff} + ) + } + } + + // If the start date of event is invalid, it's probably cause by date-fns + // So we initialize it at 00:00 AM of the event day + if (!isValid(startHourDate)){ + startHourDate = day?.date + minutesDiff = differenceInMinutes(endHourDate, startHourDate) + newEndHour = add(day?.date, {minutes: minutesDiff}) + + if (label !== '00:00 AM') { + newEndHour = add( + parse(label, 'p', startHourDate), {minutes: minutesDiff} + ) + } + } + + prevEventCell?.data?.splice(transfert?.item?.itemIndex, 1) + transfert.item.startHour = label + transfert.item.endHour = format(newEndHour, 'HH:mm aaa') + transfert.item.date = format(day?.date, 'yyyy-MM-dd') + day.data.push(transfert.item) + setState({...state, rows: rowsData}) + } + } + + /** + * @name handleCellClick + * @description + * @param event + * @param row + * @param day + * @return void + */ + const handleCellClick = (event, row, day) => { + console.log(day) + event.preventDefault() + event.stopPropagation() + //setState({...state, activeItem: day}) + onCellClick(event, row, day) + } + + /** + * @name renderTask + * @description + * @param tasks + * @param rowLabel + * @param rowIndex + * @param dayIndex + * @return {unknown[] | undefined} + */ + const renderTask = (tasks, rowLabel, rowIndex, dayIndex) => { + return tasks?.map((task, itemIndex) => ( + ( + ( + searchResult && + (task?.groupLabel === searchResult?.groupLabel || task?.user === searchResult?.user) + ) || !searchResult + ) && + ( + handleTaskClick(e, task)} + key={`item_id-${itemIndex}_r-${rowIndex}_d-${dayIndex}`} + onDragStart={e => onCellDragStart( + e, {...task, itemIndex}, rowLabel, rowIndex, dayIndex + )} + sx={{ + py: 0, mb: .5, color: "#fff", + backgroundColor: task?.color || theme.palette.primary.light + }} + > + + {task?.label} + + + ) + )) + } + + /** + * @name handleTaskClick + * @description + * @param event + * @param task + * @return void + */ + const handleTaskClick = (event, task) => { + event.preventDefault() + event.stopPropagation() + onTaskClick(event, task) + } + + useEffect(() => { + if (state?.rows && state?.itemTransfert?.item) { + onEventsChange(state?.itemTransfert?.item) + } + // eslint-disable-next-line + }, [state?.rows, state?.itemTransfert?.item]) + + return ( + + + + + + { + columns?.map((column, index) => ( + + {column?.weekDay} {column?.month}/{column?.day} + + )) + } + + + + { + rows?.map((row, rowIndex) => ( + + handleCellClick(event, row)} + > + {row?.label} + {row?.data?.length > 0 && renderTask(row?.data, row.id)} + + {row?.days?.map((day, dayIndex) => { + return ( + onCellDragEnter(e, row?.label, rowIndex, dayIndex)} + onClick={(event) => handleCellClick( + event, {rowIndex, ...row}, {dayIndex, ...day} + )} + > + {day?.data?.length > 0 && renderTask(day?.data, row?.label, rowIndex, dayIndex)} + + ) + })} + + )) + } + +
+
+ ) +} + +WeekModeView.propTypes = { + events: PropTypes.array, + columns: PropTypes.array, + rows: PropTypes.array, + date: PropTypes.string, + searchResult: PropTypes.object, + onDateChange: PropTypes.func.isRequired, + onTaskClick: PropTypes.func.isRequired, + onCellClick: PropTypes.func.isRequired, + onEventsChange: PropTypes.func.isRequired +} + +WeekModeView.defaultProps = { + +} + +export default WeekModeView \ No newline at end of file