diff --git a/apps/app/components/core/gantt-chart-view/index.tsx b/apps/app/components/core/gantt-chart-view/index.tsx new file mode 100644 index 000000000..ca07e3d7a --- /dev/null +++ b/apps/app/components/core/gantt-chart-view/index.tsx @@ -0,0 +1,26 @@ +import { useRouter } from "next/router"; + +// components +import { CycleIssuesGanttChartView } from "components/cycles"; +import { IssueGanttChartView } from "components/issues/gantt-chart"; +import { ModuleIssuesGanttChartView } from "components/modules"; +import { ViewIssuesGanttChartView } from "components/views"; + +export const GanttChartView = () => { + const router = useRouter(); + const { cycleId, moduleId, viewId } = router.query; + + return ( + <> + {cycleId ? ( + + ) : moduleId ? ( + + ) : viewId ? ( + + ) : ( + + )} + + ); +}; diff --git a/apps/app/components/core/index.ts b/apps/app/components/core/index.ts index 01d87495f..e3e187d60 100644 --- a/apps/app/components/core/index.ts +++ b/apps/app/components/core/index.ts @@ -1,5 +1,6 @@ export * from "./board-view"; export * from "./calendar-view"; +export * from "./gantt-chart-view"; export * from "./list-view"; export * from "./sidebar"; export * from "./bulk-delete-issues-modal"; diff --git a/apps/app/components/core/issues-view-filter.tsx b/apps/app/components/core/issues-view-filter.tsx index 5910d85fa..64c6f8cfe 100644 --- a/apps/app/components/core/issues-view-filter.tsx +++ b/apps/app/components/core/issues-view-filter.tsx @@ -17,6 +17,7 @@ import { ListBulletIcon, Squares2X2Icon, CalendarDaysIcon, + ChartBarIcon, } from "@heroicons/react/24/outline"; // helpers import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper"; @@ -82,6 +83,15 @@ export const IssuesFilterView: React.FC = () => { > + = ({ isCompleted={isCompleted} userAuth={memberRole} /> - ) : ( + ) : issueView === "calendar" ? ( = ({ isCompleted={isCompleted} userAuth={memberRole} /> + ) : ( + issueView === "gantt_chart" && )} ) : type === "issue" ? ( diff --git a/apps/app/components/cycles/cycles-list-gantt-chart.tsx b/apps/app/components/cycles/cycles-list-gantt-chart.tsx new file mode 100644 index 000000000..2b783f4e2 --- /dev/null +++ b/apps/app/components/cycles/cycles-list-gantt-chart.tsx @@ -0,0 +1,67 @@ +import { FC } from "react"; + +// components +import { GanttChartRoot } from "components/gantt-chart"; +// types +import { ICycle } from "types"; + +type Props = { + cycles: ICycle[]; +}; + +export const CyclesListGanttChartView: FC = ({ cycles }) => { + // rendering issues on gantt sidebar + const GanttSidebarBlockView = ({ data }: any) => ( +
+
+
{data?.name}
+
+ ); + + // rendering issues on gantt card + const GanttBlockView = ({ data }: { data: ICycle }) => ( +
+
+
+ {data?.name} +
+
+ ); + + // handle gantt issue start date and target date + const handleUpdateDates = async (data: any) => { + const payload = { + id: data?.id, + start_date: data?.start_date, + target_date: data?.target_date, + }; + }; + + const blockFormat = (blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => { + if (_block?.start_date && _block.target_date) console.log("_block", _block); + return { + start_date: new Date(_block.created_at), + target_date: new Date(_block.updated_at), + data: _block, + }; + }) + : []; + + return ( +
+ } + blockRender={(data: any) => } + /> +
+ ); +}; diff --git a/apps/app/components/cycles/cycles-view.tsx b/apps/app/components/cycles/cycles-view.tsx index da5ba6aee..9f7ad3922 100644 --- a/apps/app/components/cycles/cycles-view.tsx +++ b/apps/app/components/cycles/cycles-view.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect } from "react"; import dynamic from "next/dynamic"; // headless ui import { Tab } from "@headlessui/react"; @@ -10,7 +10,7 @@ import { CompletedCyclesListProps, AllCyclesBoard, AllCyclesList, - CompletedCycles, + CyclesListGanttChartView, } from "components/cycles"; // ui import { EmptyState, Loader } from "components/ui"; @@ -41,7 +41,7 @@ export const CyclesView: React.FC = ({ draftCycles, }) => { const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All"); - const { storedValue: cycleView, setValue: setCycleView } = useLocalStorage("cycleView", "list"); + const { storedValue: cyclesView, setValue: setCyclesView } = useLocalStorage("cycleView", "list"); const currentTabValue = (tab: string | null) => { switch (tab) { @@ -73,8 +73,41 @@ export const CyclesView: React.FC = ({ ); return ( -
+ <> +
+

Cycles

+
+ + + +
+
{ switch (i) { @@ -94,59 +127,32 @@ export const CyclesView: React.FC = ({ } }} > - {" "}
- {["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => ( - - `rounded-3xl border px-6 py-1 outline-none ${ - selected - ? "border-brand-accent bg-brand-accent text-white font-medium" - : "border-brand-base bg-brand-base hover:bg-brand-surface-2" - }` - } - > - {tab} - - ))} + {["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => { + if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts")) + return null; + + return ( + + `rounded-3xl border px-6 py-1 outline-none ${ + selected + ? "border-brand-accent bg-brand-accent text-white font-medium" + : "border-brand-base bg-brand-base hover:bg-brand-surface-2" + }` + } + > + {tab} + + ); + })} - {cycleTab !== "Active" && ( -
- - - -
- )}
- - - {cycleView === "list" && ( + + + {cyclesView === "list" && ( = ({ type="current" /> )} - {cycleView === "board" && ( + {cyclesView === "grid" && ( = ({ type="current" /> )} - {cycleView === "gantt" && ( - + {cyclesView === "gantt_chart" && ( + )} - - {currentAndUpcomingCycles?.current_cycle?.[0] ? ( - - ) : ( - - )} - - - {cycleView === "list" && ( + {cyclesView !== "gantt_chart" && ( + + {currentAndUpcomingCycles?.current_cycle?.[0] ? ( + + ) : ( + + )} + + )} + + {cyclesView === "list" && ( = ({ type="upcoming" /> )} - {cycleView === "board" && ( + {cyclesView === "board" && ( = ({ type="upcoming" /> )} - {cycleView === "gantt" && ( - + {cyclesView === "gantt_chart" && ( + )} - - {cycleView === "list" && ( - - )} - {cycleView === "board" && ( - - )} - {cycleView === "gantt" && ( - - )} - + {cyclesView !== "gantt_chart" && ( + + {cyclesView === "list" && ( + + )} + {cyclesView === "board" && ( + + )} + + )}
-
+ ); }; diff --git a/apps/app/components/cycles/gantt-chart.tsx b/apps/app/components/cycles/gantt-chart.tsx new file mode 100644 index 000000000..d38ad727b --- /dev/null +++ b/apps/app/components/cycles/gantt-chart.tsx @@ -0,0 +1,81 @@ +import { FC } from "react"; + +import { useRouter } from "next/router"; + +// components +import { GanttChartRoot } from "components/gantt-chart"; +// hooks +import useGanttChartCycleIssues from "hooks/gantt-chart/cycle-issues-view"; + +type Props = {}; + +export const CycleIssuesGanttChartView: FC = ({}) => { + const router = useRouter(); + const { workspaceSlug, projectId, cycleId } = router.query; + + const { ganttIssues, mutateGanttIssues } = useGanttChartCycleIssues( + workspaceSlug as string, + projectId as string, + cycleId as string + ); + + // rendering issues on gantt sidebar + const GanttSidebarBlockView = ({ data }: any) => ( +
+
+
{data?.name}
+
+ ); + + // rendering issues on gantt card + const GanttBlockView = ({ data }: any) => ( +
+
+
+ {data?.name} +
+
+ ); + + // handle gantt issue start date and target date + const handleUpdateDates = async (data: any) => { + const payload = { + id: data?.id, + start_date: data?.start_date, + target_date: data?.target_date, + }; + + console.log("payload", payload); + }; + + const blockFormat = (blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => { + if (_block?.start_date && _block.target_date) console.log("_block", _block); + return { + start_date: new Date(_block.created_at), + target_date: new Date(_block.updated_at), + data: _block, + }; + }) + : []; + + return ( +
+ } + blockRender={(data: any) => } + /> +
+ ); +}; diff --git a/apps/app/components/cycles/index.ts b/apps/app/components/cycles/index.ts index 73fa92d88..3151529c5 100644 --- a/apps/app/components/cycles/index.ts +++ b/apps/app/components/cycles/index.ts @@ -1,10 +1,12 @@ export * from "./active-cycle-details"; export * from "./cycles-view"; export * from "./completed-cycles"; +export * from "./cycles-list-gantt-chart"; export * from "./all-cycles-board"; export * from "./all-cycles-list"; export * from "./delete-cycle-modal"; export * from "./form"; +export * from "./gantt-chart"; export * from "./modal"; export * from "./select"; export * from "./sidebar"; diff --git a/apps/app/components/gantt-chart/blocks/index.tsx b/apps/app/components/gantt-chart/blocks/index.tsx new file mode 100644 index 000000000..24ee4d39e --- /dev/null +++ b/apps/app/components/gantt-chart/blocks/index.tsx @@ -0,0 +1,81 @@ +import { FC, useEffect, useState } from "react"; +// helpers +import { ChartDraggable } from "../helpers/draggable"; +// data +import { datePreview } from "../data"; + +export const GanttChartBlocks: FC<{ + itemsContainerWidth: number; + blocks: null | any[]; + sidebarBlockRender: FC; + blockRender: FC; +}> = ({ itemsContainerWidth, blocks, sidebarBlockRender, blockRender }) => { + const handleChartBlockPosition = (block: any) => { + // setChartBlocks((prevData: any) => + // prevData.map((_block: any) => (_block?.data?.id == block?.data?.id ? block : _block)) + // ); + }; + + return ( +
+
+ {blocks && + blocks.length > 0 && + blocks.map((block: any, _idx: number) => ( + <> + {block.start_date && block.target_date && ( + +
+
+
+ {block?.start_date ? datePreview(block?.start_date, true) : "-"} +
+
+ +
+
+ {blockRender({ ...block?.data })} +
+
+ +
+
+ {block?.target_date ? datePreview(block?.target_date, true) : "-"} +
+
+
+
+ )} + + ))} +
+ + {/* sidebar */} + {/*
+ {blocks && + blocks.length > 0 && + blocks.map((block: any, _idx: number) => ( +
+ {sidebarBlockRender(block?.data)} +
+ ))} +
*/} +
+ ); +}; diff --git a/apps/app/components/gantt-chart/chart/bi-week.tsx b/apps/app/components/gantt-chart/chart/bi-week.tsx new file mode 100644 index 000000000..489bc3eb6 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/bi-week.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const BiWeekChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/chart/day.tsx b/apps/app/components/gantt-chart/chart/day.tsx new file mode 100644 index 000000000..1ad1649b6 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/day.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const DayChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/chart/hours.tsx b/apps/app/components/gantt-chart/chart/hours.tsx new file mode 100644 index 000000000..02b2d15b1 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/hours.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const HourChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/chart/index.tsx b/apps/app/components/gantt-chart/chart/index.tsx new file mode 100644 index 000000000..a1b80ef84 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/index.tsx @@ -0,0 +1,322 @@ +import { FC, useEffect, useState } from "react"; +// icons +import { + Bars4Icon, + XMarkIcon, + ArrowsPointingInIcon, + ArrowsPointingOutIcon, +} from "@heroicons/react/20/solid"; +// components +import { GanttChartBlocks } from "../blocks"; +// import { HourChartView } from "./hours"; +// import { DayChartView } from "./day"; +// import { WeekChartView } from "./week"; +// import { BiWeekChartView } from "./bi-week"; +import { MonthChartView } from "./month"; +// import { QuarterChartView } from "./quarter"; +// import { YearChartView } from "./year"; +// views +import { + // generateHourChart, + // generateDayChart, + generateWeekChart, + generateBiWeekChart, + generateMonthChart, + generateQuarterChart, + generateYearChart, + getNumberOfDaysBetweenTwoDatesInMonth, + getNumberOfDaysBetweenTwoDatesInQuarter, + getNumberOfDaysBetweenTwoDatesInYear, + getMonthChartItemPositionWidthInMonth, +} from "../views"; +// types +import { ChartDataType } from "../types"; +// data +import { datePreview, currentViewDataWithView } from "../data"; +// context +import { useChart } from "../hooks"; + +type ChartViewRootProps = { + title: null | string; + loaderTitle: string; + blocks: any; + blockUpdateHandler: (data: any) => void; + sidebarBlockRender: FC; + blockRender: FC; +}; + +export const ChartViewRoot: FC = ({ + title, + blocks = null, + loaderTitle, + blockUpdateHandler, + sidebarBlockRender, + blockRender, +}) => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + const [itemsContainerWidth, setItemsContainerWidth] = useState(0); + const [fullScreenMode, setFullScreenMode] = useState(false); + const [blocksSidebarView, setBlocksSidebarView] = useState(false); + + // blocks state management starts + const [chartBlocks, setChartBlocks] = useState(null); + + const renderBlockStructure = (view: any, blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => ({ + ..._block, + position: getMonthChartItemPositionWidthInMonth(view, _block), + })) + : []; + + useEffect(() => { + if (currentViewData && blocks && blocks.length > 0) + setChartBlocks(() => renderBlockStructure(currentViewData, blocks)); + }, [currentViewData, blocks]); + + // blocks state management ends + + const handleChartView = (key: string) => updateCurrentViewRenderPayload(null, key); + + const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: string) => { + const selectedCurrentView = view; + const selectedCurrentViewData: ChartDataType | undefined = + selectedCurrentView && selectedCurrentView === currentViewData?.key + ? currentViewData + : currentViewDataWithView(view); + + if (selectedCurrentViewData === undefined) return; + + let currentRender: any; + + // if (view === "hours") currentRender = generateHourChart(selectedCurrentViewData, side); + // if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side); + // if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side); + // if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side); + if (selectedCurrentView === "month") + currentRender = generateMonthChart(selectedCurrentViewData, side); + // if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side); + // if (selectedCurrentView === "year") + // currentRender = generateYearChart(selectedCurrentViewData, side); + + // updating the prevData, currentData and nextData + if (currentRender.payload.length > 0) { + if (side === "left") { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + currentView: selectedCurrentView, + currentViewData: currentRender.state, + renderView: [...currentRender.payload, ...renderView], + }, + }); + updatingCurrentLeftScrollPosition(currentRender.scrollWidth); + setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); + } else if (side === "right") { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + currentView: view, + currentViewData: currentRender.state, + renderView: [...renderView, ...currentRender.payload], + }, + }); + setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); + } else { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + currentView: view, + currentViewData: currentRender.state, + renderView: [...currentRender.payload], + }, + }); + setItemsContainerWidth(currentRender.scrollWidth); + setTimeout(() => { + handleScrollToCurrentSelectedDate( + currentRender.state, + currentRender.state.data.currentDate + ); + }, 50); + } + } + }; + + const handleToday = () => updateCurrentViewRenderPayload(null, currentView); + + // handling the scroll positioning from left and right + useEffect(() => { + handleToday(); + }, []); + + const updatingCurrentLeftScrollPosition = (width: number) => { + const scrollContainer = document.getElementById("scroll-container") as HTMLElement; + scrollContainer.scrollLeft = width + scrollContainer.scrollLeft; + setItemsContainerWidth(width + scrollContainer.scrollLeft); + }; + + const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => { + const scrollContainer = document.getElementById("scroll-container") as HTMLElement; + const clientVisibleWidth: number = scrollContainer.clientWidth; + let scrollWidth: number = 0; + let daysDifference: number = 0; + + // if (currentView === "hours") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "day") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "week") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "bi_week") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + if (currentView === "month") + daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "quarter") + // daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date); + // if (currentView === "year") + // daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date); + + scrollWidth = + daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width); + + scrollContainer.scrollLeft = scrollWidth; + }; + + // handling scroll functionality + const onScroll = () => { + const scrollContainer = document.getElementById("scroll-container") as HTMLElement; + + const scrollWidth: number = scrollContainer.scrollWidth; + const clientVisibleWidth: number = scrollContainer.clientWidth; + const currentScrollPosition: number = scrollContainer.scrollLeft; + + const approxRangeLeft: number = + scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth; + const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth); + + if (currentScrollPosition >= approxRangeRight) + updateCurrentViewRenderPayload("right", currentView); + if (currentScrollPosition <= approxRangeLeft) + updateCurrentViewRenderPayload("left", currentView); + }; + + useEffect(() => { + const scrollContainer = document.getElementById("scroll-container") as HTMLElement; + + scrollContainer.addEventListener("scroll", onScroll); + return () => { + scrollContainer.removeEventListener("scroll", onScroll); + }; + }, [renderView]); + + return ( +
+ {/* chart title */} +
+ {title && ( +
+
{title}
+
+ Gantt View Beta +
+
+ )} + {blocks === null ? ( +
Loading...
+ ) : ( +
+ {blocks.length} {loaderTitle} +
+ )} +
+ + {/* chart header */} +
+ {/*
setBlocksSidebarView(() => !blocksSidebarView)} + > + {blocksSidebarView ? ( + + ) : ( + + )} +
*/} + +
+ {`${datePreview(currentViewData?.data?.startDate)} - ${datePreview( + currentViewData?.data?.endDate + )}`} +
+ +
+ {allViews && + allViews.length > 0 && + allViews.map((_chatView: any, _idx: any) => ( +
handleChartView(_chatView?.key)} + > + {_chatView?.title} +
+ ))} +
+ +
+
+ Today +
+
+ +
setFullScreenMode(() => !fullScreenMode)} + > + {fullScreenMode ? ( + + ) : ( + + )} +
+
+ + {/* content */} +
+
+ {/* blocks components */} + {currentView && currentViewData && ( + + )} + + {/* chart */} + {/* {currentView && currentView === "hours" && } */} + {/* {currentView && currentView === "day" && } */} + {/* {currentView && currentView === "week" && } */} + {/* {currentView && currentView === "bi_week" && } */} + {currentView && currentView === "month" && } + {/* {currentView && currentView === "quarter" && } */} + {/* {currentView && currentView === "year" && } */} +
+
+
+ ); +}; diff --git a/apps/app/components/gantt-chart/chart/month.tsx b/apps/app/components/gantt-chart/chart/month.tsx new file mode 100644 index 000000000..15d02c488 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/month.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const MonthChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/chart/quarter.tsx b/apps/app/components/gantt-chart/chart/quarter.tsx new file mode 100644 index 000000000..57f6f4f5e --- /dev/null +++ b/apps/app/components/gantt-chart/chart/quarter.tsx @@ -0,0 +1,50 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const QuarterChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/chart/week.tsx b/apps/app/components/gantt-chart/chart/week.tsx new file mode 100644 index 000000000..695fa2aa5 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/week.tsx @@ -0,0 +1,56 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const WeekChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/chart/year.tsx b/apps/app/components/gantt-chart/chart/year.tsx new file mode 100644 index 000000000..d06f0e025 --- /dev/null +++ b/apps/app/components/gantt-chart/chart/year.tsx @@ -0,0 +1,50 @@ +import { FC } from "react"; +// context +import { useChart } from "../hooks"; + +export const YearChartView: FC = () => { + const { currentView, currentViewData, renderView, dispatch, allViews } = useChart(); + + return ( + <> +
+ {renderView && + renderView.length > 0 && + renderView.map((_itemRoot: any, _idxRoot: any) => ( +
+
+
+ {_itemRoot?.title} +
+
+ +
+ {_itemRoot.children && + _itemRoot.children.length > 0 && + _itemRoot.children.map((_item: any, _idx: any) => ( +
+
+
{_item.title}
+
+
+ {_item?.today && ( +
+ )} +
+
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/apps/app/components/gantt-chart/contexts/index.tsx b/apps/app/components/gantt-chart/contexts/index.tsx new file mode 100644 index 000000000..4f858cf33 --- /dev/null +++ b/apps/app/components/gantt-chart/contexts/index.tsx @@ -0,0 +1,48 @@ +import React, { createContext, useState } from "react"; +// types +import { ChartContextData, ChartContextActionPayload, ChartContextReducer } from "../types"; +// data +import { allViewsWithData, currentViewDataWithView } from "../data"; + +export const ChartContext = createContext(undefined); + +const chartReducer = ( + state: ChartContextData, + action: ChartContextActionPayload +): ChartContextData => { + switch (action.type) { + case "CURRENT_VIEW": + return { ...state, currentView: action.payload }; + case "CURRENT_VIEW_DATA": + return { ...state, currentViewData: action.payload }; + case "RENDER_VIEW": + return { ...state, currentViewData: action.payload }; + case "PARTIAL_UPDATE": + return { ...state, ...action.payload }; + default: + return state; + } +}; + +const initialView = "month"; + +export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const [state, dispatch] = useState({ + currentView: initialView, + currentViewData: currentViewDataWithView(initialView), + renderView: [], + allViews: allViewsWithData, + }); + + const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { + const newState = chartReducer(state, action); + dispatch(() => newState); + return newState; + }; + + return ( + + {children} + + ); +}; diff --git a/apps/app/components/gantt-chart/data/index.ts b/apps/app/components/gantt-chart/data/index.ts new file mode 100644 index 000000000..4be799998 --- /dev/null +++ b/apps/app/components/gantt-chart/data/index.ts @@ -0,0 +1,144 @@ +// types +import { WeekMonthDataType, ChartDataType } from "../types"; + +// constants +export const weeks: WeekMonthDataType[] = [ + { key: 0, shortTitle: "sun", title: "sunday" }, + { key: 1, shortTitle: "mon", title: "monday" }, + { key: 2, shortTitle: "tue", title: "tuesday" }, + { key: 3, shortTitle: "wed", title: "wednesday" }, + { key: 4, shortTitle: "thurs", title: "thursday" }, + { key: 5, shortTitle: "fri", title: "friday" }, + { key: 6, shortTitle: "sat", title: "saturday" }, +]; + +export const months: WeekMonthDataType[] = [ + { key: 0, shortTitle: "jan", title: "january" }, + { key: 1, shortTitle: "feb", title: "february" }, + { key: 2, shortTitle: "mar", title: "march" }, + { key: 3, shortTitle: "apr", title: "april" }, + { key: 4, shortTitle: "may", title: "may" }, + { key: 5, shortTitle: "jun", title: "june" }, + { key: 6, shortTitle: "jul", title: "july" }, + { key: 7, shortTitle: "aug", title: "august" }, + { key: 8, shortTitle: "sept", title: "september" }, + { key: 9, shortTitle: "oct", title: "october" }, + { key: 10, shortTitle: "nov", title: "november" }, + { key: 11, shortTitle: "dec", title: "december" }, +]; + +export const charCapitalize = (word: string) => + `${word.charAt(0).toUpperCase()}${word.substring(1)}`; + +export const bindZero = (value: number) => (value > 9 ? `${value}` : `0${value}`); + +export const timePreview = (date: Date) => { + let hours = date.getHours(); + const amPm = hours >= 12 ? "PM" : "AM"; + hours = hours % 12; + hours = hours ? hours : 12; + + let minutes: number | string = date.getMinutes(); + minutes = bindZero(minutes); + + return `${bindZero(hours)}:${minutes} ${amPm}`; +}; + +export const datePreview = (date: Date, includeTime: boolean = false) => { + const day = date.getDate(); + let month: number | WeekMonthDataType = date.getMonth(); + month = months[month as number] as WeekMonthDataType; + const year = date.getFullYear(); + + return `${charCapitalize(month?.shortTitle)} ${day}, ${year}${ + includeTime ? `, ${timePreview(date)}` : `` + }`; +}; + +// context data +export const allViewsWithData: ChartDataType[] = [ + // { + // key: "hours", + // title: "Hours", + // data: { + // startDate: new Date(), + // currentDate: new Date(), + // endDate: new Date(), + // approxFilterRange: 4, + // width: 40, + // }, + // }, + // { + // key: "days", + // title: "Days", + // data: { + // startDate: new Date(), + // currentDate: new Date(), + // endDate: new Date(), + // approxFilterRange: 4, + // width: 40, + // }, + // }, + // { + // key: "week", + // title: "Week", + // data: { + // startDate: new Date(), + // currentDate: new Date(), + // endDate: new Date(), + // approxFilterRange: 4, + // width: 180, // it will preview week dates with weekends highlighted with 1 week limitations ex: title (Wed 1, Thu 2, Fri 3) + // }, + // }, + // { + // key: "bi_week", + // title: "Bi-Week", + // data: { + // startDate: new Date(), + // currentDate: new Date(), + // endDate: new Date(), + // approxFilterRange: 4, + // width: 100, // it will preview monthly all dates with weekends highlighted with 3 week limitations ex: title (Wed 1, Thu 2, Fri 3) + // }, + // }, + { + key: "month", + title: "Month", + data: { + startDate: new Date(), + currentDate: new Date(), + endDate: new Date(), + approxFilterRange: 8, + width: 80, // it will preview monthly all dates with weekends highlighted with no limitations ex: title (1, 2, 3) + }, + }, + // { + // key: "quarter", + // title: "Quarter", + // data: { + // startDate: new Date(), + // currentDate: new Date(), + // endDate: new Date(), + // approxFilterRange: 12, + // width: 100, // it will preview week starting dates all months data and there is 3 months limitation for preview ex: title (2, 9, 16, 23, 30) + // }, + // }, + // { + // key: "year", + // title: "Year", + // data: { + // startDate: new Date(), + // currentDate: new Date(), + // endDate: new Date(), + // approxFilterRange: 10, + // width: 80, // it will preview week starting dates all months data and there is no limitation for preview ex: title (2, 9, 16, 23, 30) + // }, + // }, +]; + +export const currentViewDataWithView = (view: string = "month") => { + const currentView: ChartDataType | undefined = allViewsWithData.find( + (_viewData) => _viewData.key === view + ); + return currentView; +}; diff --git a/apps/app/components/gantt-chart/helpers/draggable.tsx b/apps/app/components/gantt-chart/helpers/draggable.tsx new file mode 100644 index 000000000..a5404cc8e --- /dev/null +++ b/apps/app/components/gantt-chart/helpers/draggable.tsx @@ -0,0 +1,138 @@ +import { useState, useRef } from "react"; + +export const ChartDraggable = ({ children, block, handleBlock, className }: any) => { + const [dragging, setDragging] = useState(false); + + const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0); + const [blockPositionLeft, setBlockPositionLeft] = useState(0); + const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0); + + const handleMouseDown = (event: any) => { + const chartBlockPositionLeft: number = block.position.marginLeft; + const blockPositionLeft: number = event.target.getBoundingClientRect().left; + const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left; + + console.log("--------------------"); + console.log("chartBlockPositionLeft", chartBlockPositionLeft); + console.log("blockPositionLeft", blockPositionLeft); + console.log("dragBlockOffsetX", dragBlockOffsetX); + console.log("-->"); + + setDragging(true); + setChartBlockPositionLeft(chartBlockPositionLeft); + setBlockPositionLeft(blockPositionLeft); + setDragBlockOffsetX(dragBlockOffsetX); + }; + + const handleMouseMove = (event: any) => { + if (!dragging) return; + + const currentBlockPosition = event.clientX - dragBlockOffsetX; + console.log("currentBlockPosition", currentBlockPosition); + if (currentBlockPosition <= blockPositionLeft) { + const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition); + console.log("updatedPosition", updatedPosition); + handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } }); + } else { + const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition); + console.log("updatedPosition", updatedPosition); + handleBlock({ ...block, position: { ...block.position, marginLeft: updatedPosition } }); + } + console.log("--------------------"); + }; + + const handleMouseUp = () => { + setDragging(false); + setChartBlockPositionLeft(0); + setBlockPositionLeft(0); + setDragBlockOffsetX(0); + }; + + return ( +
+ {children} +
+ ); +}; + +// import { useState } from "react"; + +// export const ChartDraggable = ({ children, id, className = "", style }: any) => { +// const [dragging, setDragging] = useState(false); + +// const [chartBlockPositionLeft, setChartBlockPositionLeft] = useState(0); +// const [blockPositionLeft, setBlockPositionLeft] = useState(0); +// const [dragBlockOffsetX, setDragBlockOffsetX] = useState(0); + +// const handleDragStart = (event: any) => { +// // event.dataTransfer.setData("text/plain", event.target.id); + +// const chartBlockPositionLeft: number = parseInt(event.target.style.left.slice(0, -2)); +// const blockPositionLeft: number = event.target.getBoundingClientRect().left; +// const dragBlockOffsetX: number = event.clientX - event.target.getBoundingClientRect().left; + +// console.log("chartBlockPositionLeft", chartBlockPositionLeft); +// console.log("blockPositionLeft", blockPositionLeft); +// console.log("dragBlockOffsetX", dragBlockOffsetX); +// console.log("--------------------"); + +// setDragging(true); +// setChartBlockPositionLeft(chartBlockPositionLeft); +// setBlockPositionLeft(blockPositionLeft); +// setDragBlockOffsetX(dragBlockOffsetX); +// }; + +// const handleDragEnd = () => { +// setDragging(false); +// setChartBlockPositionLeft(0); +// setBlockPositionLeft(0); +// setDragBlockOffsetX(0); +// }; + +// const handleDragOver = (event: any) => { +// event.preventDefault(); +// if (dragging) { +// const scrollContainer = document.getElementById(`block-parent-${id}`) as HTMLElement; +// const currentBlockPosition = event.clientX - dragBlockOffsetX; +// console.log('currentBlockPosition') +// if (currentBlockPosition <= blockPositionLeft) { +// const updatedPosition = chartBlockPositionLeft - (blockPositionLeft - currentBlockPosition); +// console.log("updatedPosition", updatedPosition); +// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`; +// } else { +// const updatedPosition = chartBlockPositionLeft + (blockPositionLeft - currentBlockPosition); +// console.log("updatedPosition", updatedPosition); +// if (scrollContainer) scrollContainer.style.left = `${updatedPosition}px`; +// } +// console.log("--------------------"); +// } +// }; + +// const handleDrop = (event: any) => { +// event.preventDefault(); +// setDragging(false); +// setChartBlockPositionLeft(0); +// setBlockPositionLeft(0); +// setDragBlockOffsetX(0); +// }; + +// return ( +//
+// {children} +//
+// ); +// }; diff --git a/apps/app/components/gantt-chart/hooks/index.tsx b/apps/app/components/gantt-chart/hooks/index.tsx new file mode 100644 index 000000000..a26b56f84 --- /dev/null +++ b/apps/app/components/gantt-chart/hooks/index.tsx @@ -0,0 +1,15 @@ +import { useContext } from "react"; +// types +import { ChartContextReducer } from "../types"; +// context +import { ChartContext } from "../contexts"; + +export const useChart = (): ChartContextReducer => { + const context = useContext(ChartContext); + + if (!context) { + throw new Error("useChart must be used within a GanttChart"); + } + + return context; +}; diff --git a/apps/app/components/gantt-chart/index.ts b/apps/app/components/gantt-chart/index.ts new file mode 100644 index 000000000..1efe34c51 --- /dev/null +++ b/apps/app/components/gantt-chart/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/apps/app/components/gantt-chart/root.tsx b/apps/app/components/gantt-chart/root.tsx new file mode 100644 index 000000000..787df0e46 --- /dev/null +++ b/apps/app/components/gantt-chart/root.tsx @@ -0,0 +1,34 @@ +import { FC } from "react"; +// components +import { ChartViewRoot } from "./chart"; +// context +import { ChartContextProvider } from "./contexts"; + +type GanttChartRootProps = { + title: null | string; + loaderTitle: string; + blocks: any; + blockUpdateHandler: (data: any) => void; + sidebarBlockRender: FC; + blockRender: FC; +}; + +export const GanttChartRoot: FC = ({ + title = null, + blocks, + loaderTitle = "blocks", + blockUpdateHandler, + sidebarBlockRender, + blockRender, +}) => ( + + + +); diff --git a/apps/app/components/gantt-chart/types/index.ts b/apps/app/components/gantt-chart/types/index.ts new file mode 100644 index 000000000..aa6ebe9da --- /dev/null +++ b/apps/app/components/gantt-chart/types/index.ts @@ -0,0 +1,43 @@ +// context types +export type allViewsType = { + key: string; + title: string; + data: Object | null; +}; + +export interface ChartContextData { + allViews: allViewsType[]; + currentView: "hours" | "day" | "week" | "bi_week" | "month" | "quarter" | "year"; + currentViewData: any; + renderView: any; +} + +export type ChartContextActionPayload = { + type: "CURRENT_VIEW" | "CURRENT_VIEW_DATA" | "PARTIAL_UPDATE" | "RENDER_VIEW"; + payload: any; +}; + +export interface ChartContextReducer extends ChartContextData { + dispatch: (action: ChartContextActionPayload) => void; +} + +// chart render types +export interface WeekMonthDataType { + key: number; + shortTitle: string; + title: string; +} + +export interface ChartDataType { + key: string; + title: string; + data: ChartDataTypeData; +} + +export interface ChartDataTypeData { + startDate: Date; + currentDate: Date; + endDate: Date; + approxFilterRange: number; + width: number; +} diff --git a/apps/app/components/gantt-chart/views/bi-week-view.ts b/apps/app/components/gantt-chart/views/bi-week-view.ts new file mode 100644 index 000000000..b8d8a0bfa --- /dev/null +++ b/apps/app/components/gantt-chart/views/bi-week-view.ts @@ -0,0 +1,163 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; +// helpers +import { + generateDate, + getWeekNumberByDate, + getNumberOfDaysInMonth, + getDatesBetweenTwoDates, +} from "./helpers"; + +type GetAllDaysInMonthInMonthViewType = { + date: any; + day: any; + dayData: any; + weekNumber: number; + title: string; + active: boolean; + today: boolean; +}; +const getAllDaysInMonthInMonthView = (month: number, year: number) => { + const day: GetAllDaysInMonthInMonthViewType[] = []; + const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year); + const currentDate = new Date(); + + Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => { + const date: Date = generateDate(_day + 1, month, year); + day.push({ + date: date, + day: _day + 1, + dayData: weeks[date.getDay()], + weekNumber: getWeekNumberByDate(date), + title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, + active: false, + today: + currentDate.getFullYear() === year && + currentDate.getMonth() === month && + currentDate.getDate() === _day + 1 + ? true + : false, + }); + }); + + return day; +}; + +const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + + const monthPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: getAllDaysInMonthInMonthView(currentMonth, currentYear), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return monthPayload; +}; + +export const generateBiWeekChart = (monthPayload: ChartDataType, side: null | "left" | "right") => { + let renderState = monthPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - 1, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 1, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear)); + } + + const scrollWidth = + renderPayload + .map((monthData: any) => monthData.children.length) + .reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width; + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; + +export const getNumberOfDaysBetweenTwoDatesInBiWeek = (startDate: Date, endDate: Date) => { + let daysDifference: number = 0; + startDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + const timeDifference: number = startDate.getTime() - endDate.getTime(); + daysDifference = Math.abs(Math.floor(timeDifference / (1000 * 60 * 60 * 24))); + + return daysDifference; +}; diff --git a/apps/app/components/gantt-chart/views/day-view.ts b/apps/app/components/gantt-chart/views/day-view.ts new file mode 100644 index 000000000..67f369361 --- /dev/null +++ b/apps/app/components/gantt-chart/views/day-view.ts @@ -0,0 +1,186 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; + +export const getWeekNumberByDate = (date: Date) => { + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const daysOffset = firstDayOfYear.getDay(); + + const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; + const weekStart = new Date(firstWeekStart); + + const weekNumber = + Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + + return weekNumber; +}; + +export const getNumberOfDaysInMonth = (month: number, year: number) => { + const date = new Date(year, month, 1); + + date.setMonth(date.getMonth() + 1); + date.setDate(date.getDate() - 1); + + return date.getDate(); +}; + +export const generateDate = (day: number, month: number, year: number) => + new Date(year, month, day); + +export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { + const months = []; + + const startYear = startDate.getFullYear(); + const startMonth = startDate.getMonth(); + + const endYear = endDate.getFullYear(); + const endMonth = endDate.getMonth(); + + const currentDate = new Date(startYear, startMonth); + + while (currentDate <= endDate) { + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth(); + months.push(new Date(currentYear, currentMonth)); + currentDate.setMonth(currentDate.getMonth() + 1); + } + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) + months.push(endDate); + + return months; +}; + +export type GetAllDaysInMonthType = { + date: any; + day: any; + dayData: any; + weekNumber: number; + title: string; + today: boolean; +}; +export const getAllDaysInMonth = (month: number, year: number) => { + const day: GetAllDaysInMonthType[] = []; + const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year); + const currentDate = new Date(); + + Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => { + const date: Date = generateDate(_day + 1, month, year); + day.push({ + date: date, + day: _day + 1, + dayData: weeks[date.getDay()], + weekNumber: getWeekNumberByDate(date), + title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, + today: currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === (_day + 1) ? true : false, + }); + }); + + return day; +}; + +export const generateMonthDataByMonth = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + + const monthPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: getAllDaysInMonth(currentMonth, currentYear), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return monthPayload; +}; + +export const generateMonthDataByYear = ( + monthPayload: ChartDataType, + side: null | "left" | "right" +) => { + let renderState = monthPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - 1, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 1, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonth(currentMonth, currentYear)); + } + + const scrollWidth = ((renderPayload.map((monthData: any) => monthData.children.length)).reduce((partialSum: number, a: number) => partialSum + a, 0)) * monthPayload.data.width; + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; \ No newline at end of file diff --git a/apps/app/components/gantt-chart/views/helpers.ts b/apps/app/components/gantt-chart/views/helpers.ts new file mode 100644 index 000000000..bfea64297 --- /dev/null +++ b/apps/app/components/gantt-chart/views/helpers.ts @@ -0,0 +1,93 @@ +// Generating the date by using the year, month, and day +export const generateDate = (day: number, month: number, year: number) => + new Date(year, month, day); + +// Getting the number of days in a month +export const getNumberOfDaysInMonth = (month: number, year: number) => { + const date = new Date(year, month, 1); + + date.setMonth(date.getMonth() + 1); + date.setDate(date.getDate() - 1); + + return date.getDate(); +}; + +// Getting the week number by date +export const getWeekNumberByDate = (date: Date) => { + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const daysOffset = firstDayOfYear.getDay(); + + const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; + const weekStart = new Date(firstWeekStart); + + const weekNumber = + Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + + return weekNumber; +}; + +// Getting all weeks between two dates +export const getWeeksByMonthAndYear = (month: number, year: number) => { + const weeks = []; + const startDate = new Date(year, month, 1); + const endDate = new Date(year, month + 1, 0); + const currentDate = new Date(startDate.getTime()); + + currentDate.setDate(currentDate.getDate() + ((7 - currentDate.getDay()) % 7)); + + while (currentDate <= endDate) { + weeks.push({ + year: year, + month: month, + weekNumber: getWeekNumberByDate(currentDate), + startDate: new Date(currentDate.getTime()), + endDate: new Date(currentDate.getTime() + 6 * 24 * 60 * 60 * 1000), + }); + currentDate.setDate(currentDate.getDate() + 7); + } + + return weeks; +}; + +// Getting all dates in a week by week number and year +export const getAllDatesInWeekByWeekNumber = (weekNumber: number, year: number) => { + const januaryFirst = new Date(year, 0, 1); + const firstDayOfYear = + januaryFirst.getDay() === 0 ? januaryFirst : new Date(year, 0, 1 + (7 - januaryFirst.getDay())); + + const startDate = new Date(firstDayOfYear.getTime()); + startDate.setDate(startDate.getDate() + 7 * (weekNumber - 1)); + + var datesInWeek = []; + for (var i = 0; i < 7; i++) { + const currentDate = new Date(startDate.getTime()); + currentDate.setDate(currentDate.getDate() + i); + datesInWeek.push(currentDate); + } + + return datesInWeek; +}; + +// Getting the dates between two dates +export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { + const dates = []; + + const startYear = startDate.getFullYear(); + const startMonth = startDate.getMonth(); + + const endYear = endDate.getFullYear(); + const endMonth = endDate.getMonth(); + + const currentDate = new Date(startYear, startMonth); + + while (currentDate <= endDate) { + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth(); + dates.push(new Date(currentYear, currentMonth)); + currentDate.setMonth(currentDate.getMonth() + 1); + } + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) + dates.push(endDate); + + return dates; +}; diff --git a/apps/app/components/gantt-chart/views/hours-view.ts b/apps/app/components/gantt-chart/views/hours-view.ts new file mode 100644 index 000000000..67f369361 --- /dev/null +++ b/apps/app/components/gantt-chart/views/hours-view.ts @@ -0,0 +1,186 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; + +export const getWeekNumberByDate = (date: Date) => { + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const daysOffset = firstDayOfYear.getDay(); + + const firstWeekStart = firstDayOfYear.getTime() - daysOffset * 24 * 60 * 60 * 1000; + const weekStart = new Date(firstWeekStart); + + const weekNumber = + Math.floor((date.getTime() - weekStart.getTime()) / (7 * 24 * 60 * 60 * 1000)) + 1; + + return weekNumber; +}; + +export const getNumberOfDaysInMonth = (month: number, year: number) => { + const date = new Date(year, month, 1); + + date.setMonth(date.getMonth() + 1); + date.setDate(date.getDate() - 1); + + return date.getDate(); +}; + +export const generateDate = (day: number, month: number, year: number) => + new Date(year, month, day); + +export const getDatesBetweenTwoDates = (startDate: Date, endDate: Date) => { + const months = []; + + const startYear = startDate.getFullYear(); + const startMonth = startDate.getMonth(); + + const endYear = endDate.getFullYear(); + const endMonth = endDate.getMonth(); + + const currentDate = new Date(startYear, startMonth); + + while (currentDate <= endDate) { + const currentYear = currentDate.getFullYear(); + const currentMonth = currentDate.getMonth(); + months.push(new Date(currentYear, currentMonth)); + currentDate.setMonth(currentDate.getMonth() + 1); + } + if (endYear === currentDate.getFullYear() && endMonth === currentDate.getMonth()) + months.push(endDate); + + return months; +}; + +export type GetAllDaysInMonthType = { + date: any; + day: any; + dayData: any; + weekNumber: number; + title: string; + today: boolean; +}; +export const getAllDaysInMonth = (month: number, year: number) => { + const day: GetAllDaysInMonthType[] = []; + const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year); + const currentDate = new Date(); + + Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => { + const date: Date = generateDate(_day + 1, month, year); + day.push({ + date: date, + day: _day + 1, + dayData: weeks[date.getDay()], + weekNumber: getWeekNumberByDate(date), + title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, + today: currentDate.getFullYear() === year && currentDate.getMonth() === month && currentDate.getDate() === (_day + 1) ? true : false, + }); + }); + + return day; +}; + +export const generateMonthDataByMonth = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + + const monthPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: getAllDaysInMonth(currentMonth, currentYear), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return monthPayload; +}; + +export const generateMonthDataByYear = ( + monthPayload: ChartDataType, + side: null | "left" | "right" +) => { + let renderState = monthPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - 1, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 1, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonth(currentMonth, currentYear)); + } + + const scrollWidth = ((renderPayload.map((monthData: any) => monthData.children.length)).reduce((partialSum: number, a: number) => partialSum + a, 0)) * monthPayload.data.width; + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; \ No newline at end of file diff --git a/apps/app/components/gantt-chart/views/index.ts b/apps/app/components/gantt-chart/views/index.ts new file mode 100644 index 000000000..8d4cb9be6 --- /dev/null +++ b/apps/app/components/gantt-chart/views/index.ts @@ -0,0 +1,7 @@ +// export * from "./hours-view"; +// export * from "./day-view"; +export * from "./week-view"; +export * from "./bi-week-view"; +export * from "./month-view"; +export * from "./quater-view"; +export * from "./year-view"; diff --git a/apps/app/components/gantt-chart/views/month-view.ts b/apps/app/components/gantt-chart/views/month-view.ts new file mode 100644 index 000000000..df537ba5e --- /dev/null +++ b/apps/app/components/gantt-chart/views/month-view.ts @@ -0,0 +1,199 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; +// helpers +import { + generateDate, + getWeekNumberByDate, + getNumberOfDaysInMonth, + getDatesBetweenTwoDates, +} from "./helpers"; + +type GetAllDaysInMonthInMonthViewType = { + date: any; + day: any; + dayData: any; + weekNumber: number; + title: string; + active: boolean; + today: boolean; +}; +const getAllDaysInMonthInMonthView = (month: number, year: number) => { + const day: GetAllDaysInMonthInMonthViewType[] = []; + const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year); + const currentDate = new Date(); + + Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => { + const date: Date = generateDate(_day + 1, month, year); + day.push({ + date: date, + day: _day + 1, + dayData: weeks[date.getDay()], + weekNumber: getWeekNumberByDate(date), + title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, + active: false, + today: + currentDate.getFullYear() === year && + currentDate.getMonth() === month && + currentDate.getDate() === _day + 1 + ? true + : false, + }); + }); + + return day; +}; + +const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + + const monthPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: getAllDaysInMonthInMonthView(currentMonth, currentYear), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return monthPayload; +}; + +export const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right") => { + let renderState = monthPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - 1, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 1, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear)); + } + + const scrollWidth = + renderPayload + .map((monthData: any) => monthData.children.length) + .reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width; + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; + +export const getNumberOfDaysBetweenTwoDatesInMonth = (startDate: Date, endDate: Date) => { + let daysDifference: number = 0; + startDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + const timeDifference: number = startDate.getTime() - endDate.getTime(); + daysDifference = Math.abs(Math.floor(timeDifference / (1000 * 60 * 60 * 24))); + + return daysDifference; +}; + +export const getMonthChartItemPositionWidthInMonth = (chartData: ChartDataType, itemData: any) => { + let scrollPosition: number = 0; + let scrollWidth: number = 0; + + const { startDate } = chartData.data; + const { start_date: itemStartDate, target_date: itemTargetDate } = itemData; + + startDate.setHours(0, 0, 0, 0); + itemStartDate.setHours(0, 0, 0, 0); + itemTargetDate.setHours(0, 0, 0, 0); + + // position code starts + const positionTimeDifference: number = startDate.getTime() - itemStartDate.getTime(); + const positionDaysDifference: number = Math.abs( + Math.floor(positionTimeDifference / (1000 * 60 * 60 * 24)) + ); + scrollPosition = positionDaysDifference * chartData.data.width; + + var diffMonths = (itemStartDate.getFullYear() - startDate.getFullYear()) * 12; + diffMonths -= startDate.getMonth(); + diffMonths += itemStartDate.getMonth(); + + scrollPosition = scrollPosition + diffMonths - 1; + // position code ends + + // width code starts + const widthTimeDifference: number = itemStartDate.getTime() - itemTargetDate.getTime(); + const widthDaysDifference: number = Math.abs( + Math.floor(widthTimeDifference / (1000 * 60 * 60 * 24)) + ); + scrollWidth = (widthDaysDifference + 1) * chartData.data.width + 1; + // width code ends + + return { marginLeft: scrollPosition, width: scrollWidth }; +}; diff --git a/apps/app/components/gantt-chart/views/quater-view.ts b/apps/app/components/gantt-chart/views/quater-view.ts new file mode 100644 index 000000000..0714cb28a --- /dev/null +++ b/apps/app/components/gantt-chart/views/quater-view.ts @@ -0,0 +1,117 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; +// helpers +import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; + +const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + const today = new Date(); + + const weeksBetweenTwoDates = getWeeksByMonthAndYear(month, year); + + const weekPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: weeksBetweenTwoDates.map((weekData: any) => { + const date: Date = weekData.startDate; + return { + date: date, + startDate: weekData.startDate, + endDate: weekData.endDate, + day: date.getDay(), + dayData: weeks[date.getDay()], + weekNumber: weekData.weekNumber, + title: `W${weekData.weekNumber} (${date.getDate()})`, + active: false, + today: today >= weekData.startDate && today <= weekData.endDate ? true : false, + }; + }), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return weekPayload; +}; + +export const generateQuarterChart = ( + quarterPayload: ChartDataType, + side: null | "left" | "right" +) => { + let renderState = quarterPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 0); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear)); + } + + const scrollWidth = + renderPayload + .map((monthData: any) => monthData.children.length) + .reduce((partialSum: number, a: number) => partialSum + a, 0) * quarterPayload.data.width; + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; + +export const getNumberOfDaysBetweenTwoDatesInQuarter = (startDate: Date, endDate: Date) => { + let weeksDifference: number = 0; + + const timeDiff = Math.abs(endDate.getTime() - startDate.getTime()); + const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24)); + weeksDifference = Math.floor(diffDays / 7); + + return weeksDifference; +}; diff --git a/apps/app/components/gantt-chart/views/week-view.ts b/apps/app/components/gantt-chart/views/week-view.ts new file mode 100644 index 000000000..024b8d4e1 --- /dev/null +++ b/apps/app/components/gantt-chart/views/week-view.ts @@ -0,0 +1,163 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; +// helpers +import { + generateDate, + getWeekNumberByDate, + getNumberOfDaysInMonth, + getDatesBetweenTwoDates, +} from "./helpers"; + +type GetAllDaysInMonthInMonthViewType = { + date: any; + day: any; + dayData: any; + weekNumber: number; + title: string; + active: boolean; + today: boolean; +}; +const getAllDaysInMonthInMonthView = (month: number, year: number) => { + const day: GetAllDaysInMonthInMonthViewType[] = []; + const numberOfDaysInMonth = getNumberOfDaysInMonth(month, year); + const currentDate = new Date(); + + Array.from(Array(numberOfDaysInMonth).keys()).map((_day: number) => { + const date: Date = generateDate(_day + 1, month, year); + day.push({ + date: date, + day: _day + 1, + dayData: weeks[date.getDay()], + weekNumber: getWeekNumberByDate(date), + title: `${weeks[date.getDay()].shortTitle} ${_day + 1}`, + active: false, + today: + currentDate.getFullYear() === year && + currentDate.getMonth() === month && + currentDate.getDate() === _day + 1 + ? true + : false, + }); + }); + + return day; +}; + +const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + + const monthPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: getAllDaysInMonthInMonthView(currentMonth, currentYear), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return monthPayload; +}; + +export const generateWeekChart = (monthPayload: ChartDataType, side: null | "left" | "right") => { + let renderState = monthPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - range, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() - 1, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + 1, + currentDate.getDate() + ); + plusDate = new Date( + currentDate.getFullYear(), + currentDate.getMonth() + range, + currentDate.getDate() + ); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear)); + } + + const scrollWidth = + renderPayload + .map((monthData: any) => monthData.children.length) + .reduce((partialSum: number, a: number) => partialSum + a, 0) * monthPayload.data.width; + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; + +export const getNumberOfDaysBetweenTwoDatesInWeek = (startDate: Date, endDate: Date) => { + let daysDifference: number = 0; + startDate.setHours(0, 0, 0, 0); + endDate.setHours(0, 0, 0, 0); + + const timeDifference: number = startDate.getTime() - endDate.getTime(); + daysDifference = Math.abs(Math.floor(timeDifference / (1000 * 60 * 60 * 24))); + + return daysDifference; +}; diff --git a/apps/app/components/gantt-chart/views/year-view.ts b/apps/app/components/gantt-chart/views/year-view.ts new file mode 100644 index 000000000..76edb0d57 --- /dev/null +++ b/apps/app/components/gantt-chart/views/year-view.ts @@ -0,0 +1,116 @@ +// types +import { ChartDataType } from "../types"; +// data +import { weeks, months } from "../data"; +// helpers +import { getDatesBetweenTwoDates, getWeeksByMonthAndYear } from "./helpers"; + +const generateMonthDataByMonthAndYearInMonthView = (month: number, year: number) => { + const currentMonth: number = month; + const currentYear: number = year; + const today = new Date(); + + const weeksBetweenTwoDates = getWeeksByMonthAndYear(month, year); + + const weekPayload = { + year: currentYear, + month: currentMonth, + monthData: months[currentMonth], + children: weeksBetweenTwoDates.map((weekData: any) => { + const date: Date = weekData.startDate; + return { + date: date, + startDate: weekData.startDate, + endDate: weekData.endDate, + day: date.getDay(), + dayData: weeks[date.getDay()], + weekNumber: weekData.weekNumber, + title: `W${weekData.weekNumber} (${date.getDate()})`, + active: false, + today: today >= weekData.startDate && today <= weekData.endDate ? true : false, + }; + }), + title: `${months[currentMonth].title} ${currentYear}`, + }; + + return weekPayload; +}; + +export const generateYearChart = (yearPayload: ChartDataType, side: null | "left" | "right") => { + let renderState = yearPayload; + const renderPayload: any = []; + + const range: number = renderState.data.approxFilterRange || 6; + let filteredDates: Date[] = []; + let minusDate: Date = new Date(); + let plusDate: Date = new Date(); + + if (side === null) { + const currentDate = renderState.data.currentDate; + + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { + ...renderState.data, + startDate: filteredDates[0], + endDate: filteredDates[filteredDates.length - 1], + }, + }; + } else if (side === "left") { + const currentDate = renderState.data.startDate; + + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 0); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, startDate: filteredDates[0] }, + }; + } else if (side === "right") { + const currentDate = renderState.data.endDate; + + minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1); + plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 0); + + if (minusDate && plusDate) filteredDates = getDatesBetweenTwoDates(minusDate, plusDate); + + renderState = { + ...renderState, + data: { ...renderState.data, endDate: filteredDates[filteredDates.length - 1] }, + }; + } + + if (filteredDates && filteredDates.length > 0) + for (const currentDate in filteredDates) { + const date = filteredDates[parseInt(currentDate)]; + const currentYear = date.getFullYear(); + const currentMonth = date.getMonth(); + renderPayload.push(generateMonthDataByMonthAndYearInMonthView(currentMonth, currentYear)); + } + + const scrollWidth = + renderPayload + .map((monthData: any) => monthData.children.length) + .reduce((partialSum: number, a: number) => partialSum + a, 0) * yearPayload.data.width; + + console.log("scrollWidth", scrollWidth); + + return { state: renderState, payload: renderPayload, scrollWidth: scrollWidth }; +}; + +export const getNumberOfDaysBetweenTwoDatesInYear = (startDate: Date, endDate: Date) => { + let weeksDifference: number = 0; + + const timeDiff = Math.abs(endDate.getTime() - startDate.getTime()); + const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24)); + weeksDifference = Math.floor(diffDays / 7); + + return weeksDifference; +}; diff --git a/apps/app/components/issues/gantt-chart.tsx b/apps/app/components/issues/gantt-chart.tsx new file mode 100644 index 000000000..5e79822c5 --- /dev/null +++ b/apps/app/components/issues/gantt-chart.tsx @@ -0,0 +1,81 @@ +import { FC } from "react"; + +import Link from "next/link"; +import { useRouter } from "next/router"; + +// components +import { GanttChartRoot } from "components/gantt-chart"; +// hooks +import useGanttChartIssues from "hooks/gantt-chart/issue-view"; + +type Props = {}; + +export const IssueGanttChartView: FC = ({}) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { ganttIssues, mutateGanttIssues } = useGanttChartIssues( + workspaceSlug as string, + projectId as string + ); + + // rendering issues on gantt sidebar + const GanttSidebarBlockView = ({ data }: any) => ( +
+
+
{data?.name}
+
+ ); + + // rendering issues on gantt card + const GanttBlockView = ({ data }: any) => ( +
+
+
+ {data?.name} +
+
+ ); + + // handle gantt issue start date and target date + const handleUpdateDates = async (data: any) => { + const payload = { + id: data?.id, + start_date: data?.start_date, + target_date: data?.target_date, + }; + + console.log("payload", payload); + }; + + const blockFormat = (blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => { + if (_block?.start_date && _block.target_date) console.log("_block", _block); + return { + start_date: new Date(_block.created_at), + target_date: new Date(_block.updated_at), + data: _block, + }; + }) + : []; + + return ( +
+ } + blockRender={(data: any) => } + /> +
+ ); +}; diff --git a/apps/app/components/issues/modal.tsx b/apps/app/components/issues/modal.tsx index acda96a84..de54e65b3 100644 --- a/apps/app/components/issues/modal.tsx +++ b/apps/app/components/issues/modal.tsx @@ -58,6 +58,7 @@ export const CreateUpdateIssueModal: React.FC = ({ const { issueView, params } = useIssuesView(); const { params: calendarParams } = useCalendarIssuesView(); + const { order_by, group_by, ...viewGanttParams } = params; if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string }; if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId as string }; @@ -135,7 +136,17 @@ export const CreateUpdateIssueModal: React.FC = ({ ? VIEW_ISSUES(viewId.toString(), calendarParams) : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", calendarParams); + const ganttFetchKey = cycleId + ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) + : moduleId + ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString()) + : viewId + ? VIEW_ISSUES(viewId.toString(), viewGanttParams) + : PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? ""); + const createIssue = async (payload: Partial) => { + if (!workspaceSlug) return; + await issuesService .createIssues(workspaceSlug as string, activeProject ?? "", payload) .then(async (res) => { @@ -144,6 +155,7 @@ export const CreateUpdateIssueModal: React.FC = ({ if (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module); if (issueView === "calendar") mutate(calendarFetchKey); + if (issueView === "gantt_chart") mutate(ganttFetchKey); if (!createMore) handleClose(); diff --git a/apps/app/components/modules/gantt-chart.tsx b/apps/app/components/modules/gantt-chart.tsx new file mode 100644 index 000000000..edc24cfc9 --- /dev/null +++ b/apps/app/components/modules/gantt-chart.tsx @@ -0,0 +1,81 @@ +import { FC } from "react"; + +import { useRouter } from "next/router"; + +// components +import { GanttChartRoot } from "components/gantt-chart"; +// hooks +import useGanttChartModuleIssues from "hooks/gantt-chart/module-issues-view"; + +type Props = {}; + +export const ModuleIssuesGanttChartView: FC = ({}) => { + const router = useRouter(); + const { workspaceSlug, projectId, moduleId } = router.query; + + const { ganttIssues, mutateGanttIssues } = useGanttChartModuleIssues( + workspaceSlug as string, + projectId as string, + moduleId as string + ); + + // rendering issues on gantt sidebar + const GanttSidebarBlockView = ({ data }: any) => ( +
+
+
{data?.name}
+
+ ); + + // rendering issues on gantt card + const GanttBlockView = ({ data }: any) => ( +
+
+
+ {data?.name} +
+
+ ); + + // handle gantt issue start date and target date + const handleUpdateDates = async (data: any) => { + const payload = { + id: data?.id, + start_date: data?.start_date, + target_date: data?.target_date, + }; + + console.log("payload", payload); + }; + + const blockFormat = (blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => { + if (_block?.start_date && _block.target_date) console.log("_block", _block); + return { + start_date: new Date(_block.created_at), + target_date: new Date(_block.updated_at), + data: _block, + }; + }) + : []; + + return ( +
+ } + blockRender={(data: any) => } + /> +
+ ); +}; diff --git a/apps/app/components/modules/index.ts b/apps/app/components/modules/index.ts index 5ba7ea47e..5a5e2a4f1 100644 --- a/apps/app/components/modules/index.ts +++ b/apps/app/components/modules/index.ts @@ -2,6 +2,8 @@ export * from "./select"; export * from "./sidebar-select"; export * from "./delete-module-modal"; export * from "./form"; +export * from "./gantt-chart"; export * from "./modal"; +export * from "./modules-list-gantt-chart"; export * from "./sidebar"; export * from "./single-module-card"; diff --git a/apps/app/components/modules/modules-list-gantt-chart.tsx b/apps/app/components/modules/modules-list-gantt-chart.tsx new file mode 100644 index 000000000..c6fbf329f --- /dev/null +++ b/apps/app/components/modules/modules-list-gantt-chart.tsx @@ -0,0 +1,74 @@ +import { FC } from "react"; + +// components +import { GanttChartRoot } from "components/gantt-chart"; +// types +import { IModule } from "types"; +// constants +import { MODULE_STATUS } from "constants/module"; + +type Props = { + modules: IModule[]; +}; + +export const ModulesListGanttChartView: FC = ({ modules }) => { + // rendering issues on gantt sidebar + const GanttSidebarBlockView = ({ data }: any) => ( +
+
s.value === data.status)?.color, + }} + /> +
{data?.name}
+
+ ); + + // rendering issues on gantt card + const GanttBlockView = ({ data }: { data: IModule }) => ( +
+
s.value === data.status)?.color }} + /> +
+ {data?.name} +
+
+ ); + + // handle gantt issue start date and target date + const handleUpdateDates = async (data: any) => { + const payload = { + id: data?.id, + start_date: data?.start_date, + target_date: data?.target_date, + }; + }; + + const blockFormat = (blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => { + if (_block?.start_date && _block.target_date) console.log("_block", _block); + return { + start_date: new Date(_block.created_at), + target_date: new Date(_block.updated_at), + data: _block, + }; + }) + : []; + + return ( +
+ } + blockRender={(data: any) => } + /> +
+ ); +}; diff --git a/apps/app/components/views/gantt-chart.tsx b/apps/app/components/views/gantt-chart.tsx new file mode 100644 index 000000000..dbe71fae0 --- /dev/null +++ b/apps/app/components/views/gantt-chart.tsx @@ -0,0 +1,81 @@ +import { FC } from "react"; + +import { useRouter } from "next/router"; + +// components +import { GanttChartRoot } from "components/gantt-chart"; +// hooks +import useGanttChartViewIssues from "hooks/gantt-chart/view-issues-view"; + +type Props = {}; + +export const ViewIssuesGanttChartView: FC = ({}) => { + const router = useRouter(); + const { workspaceSlug, projectId, viewId } = router.query; + + const { ganttIssues, mutateGanttIssues } = useGanttChartViewIssues( + workspaceSlug as string, + projectId as string, + viewId as string + ); + + // rendering issues on gantt sidebar + const GanttSidebarBlockView = ({ data }: any) => ( +
+
+
{data?.name}
+
+ ); + + // rendering issues on gantt card + const GanttBlockView = ({ data }: any) => ( +
+
+
+ {data?.name} +
+
+ ); + + // handle gantt issue start date and target date + const handleUpdateDates = async (data: any) => { + const payload = { + id: data?.id, + start_date: data?.start_date, + target_date: data?.target_date, + }; + + console.log("payload", payload); + }; + + const blockFormat = (blocks: any) => + blocks && blocks.length > 0 + ? blocks.map((_block: any) => { + if (_block?.start_date && _block.target_date) console.log("_block", _block); + return { + start_date: new Date(_block.created_at), + target_date: new Date(_block.updated_at), + data: _block, + }; + }) + : []; + + return ( +
+ } + blockRender={(data: any) => } + /> +
+ ); +}; diff --git a/apps/app/components/views/index.ts b/apps/app/components/views/index.ts index 7a6307e56..a68740368 100644 --- a/apps/app/components/views/index.ts +++ b/apps/app/components/views/index.ts @@ -1,5 +1,6 @@ export * from "./delete-view-modal"; export * from "./form"; +export * from "./gantt-chart"; export * from "./modal"; export * from "./select-filters"; -export * from "./single-view-item" +export * from "./single-view-item"; diff --git a/apps/app/hooks/gantt-chart/cycle-issues-view.tsx b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx new file mode 100644 index 000000000..1782ca339 --- /dev/null +++ b/apps/app/hooks/gantt-chart/cycle-issues-view.tsx @@ -0,0 +1,32 @@ +import useSWR from "swr"; + +// services +import cyclesService from "services/cycles.service"; +// fetch-keys +import { CYCLE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; + +const useGanttChartCycleIssues = ( + workspaceSlug: string | undefined, + projectId: string | undefined, + cycleId: string | undefined +) => { + // all issues under the workspace and project + const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( + workspaceSlug && projectId && cycleId ? CYCLE_ISSUES_WITH_PARAMS(cycleId.toString()) : null, + workspaceSlug && projectId && cycleId + ? () => + cyclesService.getCycleIssuesWithParams( + workspaceSlug.toString(), + projectId.toString(), + cycleId.toString() + ) + : null + ); + + return { + ganttIssues, + mutateGanttIssues, + }; +}; + +export default useGanttChartCycleIssues; diff --git a/apps/app/hooks/gantt-chart/issue-view.tsx b/apps/app/hooks/gantt-chart/issue-view.tsx new file mode 100644 index 000000000..bcdb0fca4 --- /dev/null +++ b/apps/app/hooks/gantt-chart/issue-view.tsx @@ -0,0 +1,23 @@ +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; +// fetch-keys +import { PROJECT_ISSUES_LIST_WITH_PARAMS } from "constants/fetch-keys"; + +const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: string | undefined) => { + // all issues under the workspace and project + const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( + workspaceSlug && projectId ? PROJECT_ISSUES_LIST_WITH_PARAMS(projectId) : null, + workspaceSlug && projectId + ? () => issuesService.getIssuesWithParams(workspaceSlug.toString(), projectId.toString()) + : null + ); + + return { + ganttIssues, + mutateGanttIssues, + }; +}; + +export default useGanttChartIssues; diff --git a/apps/app/hooks/gantt-chart/module-issues-view.tsx b/apps/app/hooks/gantt-chart/module-issues-view.tsx new file mode 100644 index 000000000..baf995944 --- /dev/null +++ b/apps/app/hooks/gantt-chart/module-issues-view.tsx @@ -0,0 +1,32 @@ +import useSWR from "swr"; + +// services +import modulesService from "services/modules.service"; +// fetch-keys +import { MODULE_ISSUES_WITH_PARAMS } from "constants/fetch-keys"; + +const useGanttChartModuleIssues = ( + workspaceSlug: string | undefined, + projectId: string | undefined, + moduleId: string | undefined +) => { + // all issues under the workspace and project + const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( + workspaceSlug && projectId && moduleId ? MODULE_ISSUES_WITH_PARAMS(moduleId.toString()) : null, + workspaceSlug && projectId && moduleId + ? () => + modulesService.getModuleIssuesWithParams( + workspaceSlug.toString(), + projectId.toString(), + moduleId.toString() + ) + : null + ); + + return { + ganttIssues, + mutateGanttIssues, + }; +}; + +export default useGanttChartModuleIssues; diff --git a/apps/app/hooks/gantt-chart/view-issues-view.tsx b/apps/app/hooks/gantt-chart/view-issues-view.tsx new file mode 100644 index 000000000..7fc138570 --- /dev/null +++ b/apps/app/hooks/gantt-chart/view-issues-view.tsx @@ -0,0 +1,37 @@ +import useSWR from "swr"; + +// services +import issuesService from "services/issues.service"; +// hooks +import useIssuesView from "hooks/use-issues-view"; +// fetch-keys +import { VIEW_ISSUES } from "constants/fetch-keys"; + +const useGanttChartViewIssues = ( + workspaceSlug: string | undefined, + projectId: string | undefined, + viewId: string | undefined +) => { + const { params } = useIssuesView(); + const { order_by, group_by, ...viewGanttParams } = params; + + // all issues under the view + const { data: ganttIssues, mutate: mutateGanttIssues } = useSWR( + workspaceSlug && projectId && viewId ? VIEW_ISSUES(viewId.toString(), viewGanttParams) : null, + workspaceSlug && projectId && viewId + ? () => + issuesService.getIssuesWithParams( + workspaceSlug.toString(), + projectId.toString(), + viewGanttParams + ) + : null + ); + + return { + ganttIssues, + mutateGanttIssues, + }; +}; + +export default useGanttChartViewIssues; diff --git a/apps/app/layouts/app-layout/app-header.tsx b/apps/app/layouts/app-layout/app-header.tsx index fb533da93..95f998dec 100644 --- a/apps/app/layouts/app-layout/app-header.tsx +++ b/apps/app/layouts/app-layout/app-header.tsx @@ -9,7 +9,7 @@ type Props = { }; const Header: React.FC = ({ breadcrumbs, left, right, setToggleSidebar }) => ( -
+
+
+ {modulesView === "grid" && ( +
+
+ {modules.map((module) => ( + handleEditModule(module)} + /> + ))} +
+
+ )} + {modulesView === "gantt_chart" && }
) : (