feat: Gantt chart (#1062)

* dev: Helpers

* dev: views

* dev: Chart views Month, Year and Day

* dev: Chart Workflow updates

* update: scroll functionality implementation

* update: data vaidation

* update: date renders and issue filter in the month view

* update: new date render month view

* update: scroll enabled left in chart

* update: Item render from the date it created.

* update: width implementation in chat view

* dev: chart render functionality in the gantt chart

* update: month view fix

* dev: chart render issues resolved

* update: fixed allchat views

* update: updated week view default values

* update: integrated chart view in issues

* update: grabble and sidebar logic impleemntation and integrated gantt in issues

* update: Preview gantt chart in month view

* fix: mutation in gantt chart after creating a new issue

* chore: cycles and modules list gantt chart

* update: Ui changes on gantt view

* fix: gantt chart height, chore: remove link from issue

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
This commit is contained in:
guru_sainath 2023-05-20 17:30:15 +05:30 committed by GitHub
parent 9ccc35d181
commit e1e9a5ed96
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 3251 additions and 140 deletions

View File

@ -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 ? (
<CycleIssuesGanttChartView />
) : moduleId ? (
<ModuleIssuesGanttChartView />
) : viewId ? (
<ViewIssuesGanttChartView />
) : (
<IssueGanttChartView />
)}
</>
);
};

View File

@ -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";

View File

@ -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 = () => {
>
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
issueView === "gantt_chart" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setIssueView("gantt_chart")}
>
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
</button>
</div>
<SelectFilters
filters={filters}

View File

@ -18,10 +18,11 @@ import { useProjectMyMembership } from "contexts/project-member.context";
import useToast from "hooks/use-toast";
import useIssuesView from "hooks/use-issues-view";
// components
import { AllLists, AllBoards, FilterList, CalendarView } from "components/core";
import { AllLists, AllBoards, FilterList, CalendarView, GanttChartView } from "components/core";
import { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
import { CreateUpdateViewModal } from "components/views";
import { TransferIssues, TransferIssuesModal } from "components/cycles";
import { CycleIssuesGanttChartView, TransferIssues, TransferIssuesModal } from "components/cycles";
import { IssueGanttChartView } from "components/issues/gantt-chart";
// ui
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
// icons
@ -47,6 +48,7 @@ import {
PROJECT_ISSUES_LIST_WITH_PARAMS,
STATES_LIST,
} from "constants/fetch-keys";
import { ModuleIssuesGanttChartView } from "components/modules";
type Props = {
type?: "issue" | "cycle" | "module";
@ -528,7 +530,7 @@ export const IssuesView: React.FC<Props> = ({
isCompleted={isCompleted}
userAuth={memberRole}
/>
) : (
) : issueView === "calendar" ? (
<CalendarView
handleEditIssue={handleEditIssue}
handleDeleteIssue={handleDeleteIssue}
@ -536,6 +538,8 @@ export const IssuesView: React.FC<Props> = ({
isCompleted={isCompleted}
userAuth={memberRole}
/>
) : (
issueView === "gantt_chart" && <GanttChartView />
)}
</>
) : type === "issue" ? (

View File

@ -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<Props> = ({ cycles }) => {
// rendering issues on gantt sidebar
const GanttSidebarBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: "#858e96" }}
/>
<div className="text-brand-base text-sm">{data?.name}</div>
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: { data: ICycle }) => (
<div className="relative flex w-full h-full overflow-hidden">
<div className="flex-shrink-0 w-[4px] h-auto" style={{ backgroundColor: "#858e96" }} />
<div className="inline-block text-brand-base text-sm whitespace-nowrap py-[4px] px-1.5">
{data?.name}
</div>
</div>
);
// 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 (
<div className="w-full h-full overflow-y-auto">
<GanttChartRoot
title={"Cycles"}
loaderTitle="Cycles"
blocks={cycles ? blockFormat(cycles) : null}
blockUpdateHandler={handleUpdateDates}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
/>
</div>
);
};

View File

@ -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<Props> = ({
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<Props> = ({
);
return (
<div>
<>
<div className="flex gap-4 justify-between">
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
<div className="flex items-center gap-x-1">
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
cyclesView === "list" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setCyclesView("list")}
>
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
cyclesView === "grid" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setCyclesView("grid")}
>
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
cyclesView === "gantt_chart" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setCyclesView("gantt_chart")}
>
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
</button>
</div>
</div>
<Tab.Group
as={React.Fragment}
defaultIndex={currentTabValue(cycleTab)}
onChange={(i) => {
switch (i) {
@ -94,59 +127,32 @@ export const CyclesView: React.FC<Props> = ({
}
}}
>
{" "}
<div className="flex justify-between">
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => (
<Tab
key={index}
className={({ selected }) =>
`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}
</Tab>
))}
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => {
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
return null;
return (
<Tab
key={index}
className={({ selected }) =>
`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}
</Tab>
);
})}
</Tab.List>
{cycleTab !== "Active" && (
<div className="flex items-center gap-x-1">
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 ${
cycleView === "list" ? "bg-brand-surface-2" : ""
} hover:bg-brand-surface-2`}
onClick={() => setCycleView("list")}
>
<ListBulletIcon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 ${
cycleView === "board" ? "bg-brand-surface-2" : ""
} hover:bg-brand-surface-2`}
onClick={() => setCycleView("board")}
>
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 ${
cycleView === "gantt" ? "bg-brand-surface-2" : ""
} hover:bg-brand-surface-2`}
onClick={() => setCycleView("gantt")}
>
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
</button>
</div>
)}
</div>
<Tab.Panels>
<Tab.Panel as="div" className="mt-7 space-y-5">
{cycleView === "list" && (
<Tab.Panels as={React.Fragment}>
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
{cyclesView === "list" && (
<AllCyclesList
cycles={cyclesCompleteList}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
@ -154,7 +160,7 @@ export const CyclesView: React.FC<Props> = ({
type="current"
/>
)}
{cycleView === "board" && (
{cyclesView === "grid" && (
<AllCyclesBoard
cycles={cyclesCompleteList}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
@ -162,29 +168,26 @@ export const CyclesView: React.FC<Props> = ({
type="current"
/>
)}
{cycleView === "gantt" && (
<EmptyState
type="cycle"
title="Create New Cycle"
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
imgURL={emptyCycle}
/>
{cyclesView === "gantt_chart" && (
<CyclesListGanttChartView cycles={cyclesCompleteList ?? []} />
)}
</Tab.Panel>
<Tab.Panel as="div" className="mt-7 space-y-5">
{currentAndUpcomingCycles?.current_cycle?.[0] ? (
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
) : (
<EmptyState
type="cycle"
title="Create New Cycle"
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
imgURL={emptyCycle}
/>
)}
</Tab.Panel>
<Tab.Panel as="div" className="mt-7 space-y-5">
{cycleView === "list" && (
{cyclesView !== "gantt_chart" && (
<Tab.Panel as="div" className="mt-7 space-y-5">
{currentAndUpcomingCycles?.current_cycle?.[0] ? (
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
) : (
<EmptyState
type="cycle"
title="Create New Cycle"
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
imgURL={emptyCycle}
/>
)}
</Tab.Panel>
)}
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
{cyclesView === "list" && (
<AllCyclesList
cycles={currentAndUpcomingCycles?.upcoming_cycle}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
@ -192,7 +195,7 @@ export const CyclesView: React.FC<Props> = ({
type="upcoming"
/>
)}
{cycleView === "board" && (
{cyclesView === "board" && (
<AllCyclesBoard
cycles={currentAndUpcomingCycles?.upcoming_cycle}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
@ -200,50 +203,39 @@ export const CyclesView: React.FC<Props> = ({
type="upcoming"
/>
)}
{cycleView === "gantt" && (
<EmptyState
type="cycle"
title="Create New Cycle"
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
imgURL={emptyCycle}
/>
{cyclesView === "gantt_chart" && (
<CyclesListGanttChartView cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []} />
)}
</Tab.Panel>
<Tab.Panel as="div" className="mt-7 space-y-5">
<CompletedCycles
cycleView={cycleView ?? "list"}
cycleView={cyclesView ?? "list"}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
/>
</Tab.Panel>
<Tab.Panel as="div" className="mt-7 space-y-5">
{cycleView === "list" && (
<AllCyclesList
cycles={draftCycles?.draft_cycles}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
type="draft"
/>
)}
{cycleView === "board" && (
<AllCyclesBoard
cycles={draftCycles?.draft_cycles}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
type="draft"
/>
)}
{cycleView === "gantt" && (
<EmptyState
type="cycle"
title="Create New Cycle"
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
imgURL={emptyCycle}
/>
)}
</Tab.Panel>
{cyclesView !== "gantt_chart" && (
<Tab.Panel as="div" className="mt-7 space-y-5">
{cyclesView === "list" && (
<AllCyclesList
cycles={draftCycles?.draft_cycles}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
type="draft"
/>
)}
{cyclesView === "board" && (
<AllCyclesBoard
cycles={draftCycles?.draft_cycles}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
setSelectedCycle={setSelectedCycle}
type="draft"
/>
)}
</Tab.Panel>
)}
</Tab.Panels>
</Tab.Group>
</div>
</>
);
};

View File

@ -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<Props> = ({}) => {
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) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="text-brand-base text-sm">{data?.name}</div>
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full overflow-hidden">
<div
className="flex-shrink-0 w-[4px] h-auto"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="inline-block text-brand-base text-sm whitespace-nowrap py-[4px] px-1.5">
{data?.name}
</div>
</div>
);
// 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 (
<div className="w-full h-full p-3">
<GanttChartRoot
title="Cycles"
loaderTitle="Cycles"
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
blockUpdateHandler={handleUpdateDates}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
/>
</div>
);
};

View File

@ -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";

View File

@ -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 (
<div
className="relative z-10 mt-[58px] h-full w-[4000px] divide-x divide-gray-300 overflow-hidden overflow-y-auto bg-[#999] bg-opacity-5"
style={{ width: `${itemsContainerWidth}px` }}
>
<div className="w-full divide-y divide-brand-base">
{blocks &&
blocks.length > 0 &&
blocks.map((block: any, _idx: number) => (
<>
{block.start_date && block.target_date && (
<ChartDraggable
className="relative flex h-[36.5px] items-center"
key={`blocks-${_idx}`}
block={block}
handleBlock={handleChartBlockPosition}
>
<div
className="relative group inline-flex cursor-pointer items-center font-medium transition-all"
style={{ marginLeft: `${block?.position?.marginLeft}px` }}
>
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
<div className="absolute right-0 mr-[5px] rounded-sm bg-brand-surface-1 px-2 py-0.5 text-xs font-medium">
{block?.start_date ? datePreview(block?.start_date, true) : "-"}
</div>
</div>
<div
className="rounded-sm shadow-sm bg-brand-base overflow-hidden relative flex items-center"
style={{
width: `${block?.position?.width}px`,
}}
>
<div className="w-full h-full relative overflow-hidden">
{blockRender({ ...block?.data })}
</div>
</div>
<div className="flex-shrink-0 relative w-0 h-0 flex items-center invisible group-hover:visible whitespace-nowrap">
<div className="absolute left-0 ml-[5px] mr-[5px] rounded-sm bg-brand-surface-1 px-2 py-0.5 text-xs font-medium">
{block?.target_date ? datePreview(block?.target_date, true) : "-"}
</div>
</div>
</div>
</ChartDraggable>
)}
</>
))}
</div>
{/* sidebar */}
{/* <div className="fixed top-0 bottom-0 w-[300px] flex-shrink-0 divide-y divide-brand-base border-r border-brand-base overflow-y-auto">
{blocks &&
blocks.length > 0 &&
blocks.map((block: any, _idx: number) => (
<div className="relative h-[36.5px] bg-brand-base" key={`sidebar-blocks-${_idx}`}>
{sidebarBlockRender(block?.data)}
</div>
))}
</div> */}
</div>
);
};

View File

@ -0,0 +1,56 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const BiWeekChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
? `bg-brand-surface-2`
: ``
}`}
>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -0,0 +1,56 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const DayChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
? `bg-gray-100`
: ``
}`}
>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -0,0 +1,56 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const HourChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
? `bg-gray-100`
: ``
}`}
>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -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<any>;
blockRender: FC<any>;
};
export const ChartViewRoot: FC<ChartViewRootProps> = ({
title,
blocks = null,
loaderTitle,
blockUpdateHandler,
sidebarBlockRender,
blockRender,
}) => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
const [itemsContainerWidth, setItemsContainerWidth] = useState<number>(0);
const [fullScreenMode, setFullScreenMode] = useState<boolean>(false);
const [blocksSidebarView, setBlocksSidebarView] = useState<boolean>(false);
// blocks state management starts
const [chartBlocks, setChartBlocks] = useState<any[] | null>(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 (
<div
className={`${
fullScreenMode ? `fixed top-0 bottom-0 left-0 right-0 z-[999999] bg-brand-base` : `relative`
} flex h-full flex-col rounded-sm border border-brand-base select-none`}
>
{/* chart title */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-5 gap-y-3 whitespace-nowrap p-2 border-b border-brand-base">
{title && (
<div className="text-lg font-medium flex gap-2 items-center">
<div>{title}</div>
<div className="text-xs rounded-full px-2 py-1 font-bold border border-brand-accent/75 bg-brand-accent/5 text-brand-base">
Gantt View Beta
</div>
</div>
)}
{blocks === null ? (
<div className="text-sm font-medium ml-auto">Loading...</div>
) : (
<div className="text-sm font-medium ml-auto">
{blocks.length} {loaderTitle}
</div>
)}
</div>
{/* chart header */}
<div className="flex w-full flex-shrink-0 flex-wrap items-center gap-5 gap-y-3 whitespace-nowrap p-2">
{/* <div
className="transition-all border border-brand-base w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-brand-surface-2"
onClick={() => setBlocksSidebarView(() => !blocksSidebarView)}
>
{blocksSidebarView ? (
<XMarkIcon className="h-5 w-5" />
) : (
<Bars4Icon className="h-4 w-4" />
)}
</div> */}
<div className="mr-auto text-sm font-medium">
{`${datePreview(currentViewData?.data?.startDate)} - ${datePreview(
currentViewData?.data?.endDate
)}`}
</div>
<div className="flex flex-wrap items-center gap-2">
{allViews &&
allViews.length > 0 &&
allViews.map((_chatView: any, _idx: any) => (
<div
key={_chatView?.key}
className={`cursor-pointer rounded-sm border border-brand-base p-1 px-2 text-sm font-medium ${
currentView === _chatView?.key ? `bg-brand-surface-2` : `hover:bg-brand-surface-1`
}`}
onClick={() => handleChartView(_chatView?.key)}
>
{_chatView?.title}
</div>
))}
</div>
<div className="flex items-center gap-1">
<div
className={`cursor-pointer rounded-sm border border-brand-base p-1 px-2 text-sm font-medium hover:bg-brand-surface-2`}
onClick={handleToday}
>
Today
</div>
</div>
<div
className="transition-all border border-brand-base w-[30px] h-[30px] flex justify-center items-center cursor-pointer rounded-sm hover:bg-brand-surface-2"
onClick={() => setFullScreenMode(() => !fullScreenMode)}
>
{fullScreenMode ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</div>
</div>
{/* content */}
<div className="relative flex h-full w-full flex-1 overflow-hidden border-t border-brand-base">
<div
className="relative flex h-full w-full flex-1 flex-col overflow-hidden overflow-x-auto"
id="scroll-container"
>
{/* blocks components */}
{currentView && currentViewData && (
<GanttChartBlocks
itemsContainerWidth={itemsContainerWidth}
blocks={chartBlocks}
sidebarBlockRender={sidebarBlockRender}
blockRender={blockRender}
/>
)}
{/* chart */}
{/* {currentView && currentView === "hours" && <HourChartView />} */}
{/* {currentView && currentView === "day" && <DayChartView />} */}
{/* {currentView && currentView === "week" && <WeekChartView />} */}
{/* {currentView && currentView === "bi_week" && <BiWeekChartView />} */}
{currentView && currentView === "month" && <MonthChartView />}
{/* {currentView && currentView === "quarter" && <QuarterChartView />} */}
{/* {currentView && currentView === "year" && <YearChartView />} */}
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,56 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const MonthChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
? `bg-brand-surface-2`
: ``
}`}
>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -0,0 +1,50 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const QuarterChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div className={`relative h-full w-full flex-1 flex justify-center`}>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -0,0 +1,56 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const WeekChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div
className={`relative h-full w-full flex-1 flex justify-center ${
["sat", "sun"].includes(_item?.dayData?.shortTitle || "")
? `bg-brand-surface-2`
: ``
}`}
>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -0,0 +1,50 @@
import { FC } from "react";
// context
import { useChart } from "../hooks";
export const YearChartView: FC<any> = () => {
const { currentView, currentViewData, renderView, dispatch, allViews } = useChart();
return (
<>
<div className="absolute flex h-full flex-grow divide-x divide-brand-base">
{renderView &&
renderView.length > 0 &&
renderView.map((_itemRoot: any, _idxRoot: any) => (
<div key={`title-${_idxRoot}`} className="relative flex flex-col">
<div className="relative border-b border-brand-base">
<div className="sticky left-0 inline-flex whitespace-nowrap px-2 py-1 text-sm font-medium capitalize">
{_itemRoot?.title}
</div>
</div>
<div className="flex h-full w-full divide-x divide-brand-base">
{_itemRoot.children &&
_itemRoot.children.length > 0 &&
_itemRoot.children.map((_item: any, _idx: any) => (
<div
key={`sub-title-${_idxRoot}-${_idx}`}
className="relative flex h-full flex-col overflow-hidden whitespace-nowrap"
style={{ width: `${currentViewData.data.width}px` }}
>
<div
className={`flex-shrink-0 border-b py-1 text-center text-sm capitalize font-medium ${
_item?.today ? `text-red-500 border-red-500` : `border-brand-base`
}`}
>
<div>{_item.title}</div>
</div>
<div className={`relative h-full w-full flex-1 flex justify-center`}>
{_item?.today && (
<div className="absolute top-0 bottom-0 border border-red-500"> </div>
)}
</div>
</div>
))}
</div>
</div>
))}
</div>
</>
);
};

View File

@ -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<ChartContextReducer | undefined>(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<ChartContextData>({
currentView: initialView,
currentViewData: currentViewDataWithView(initialView),
renderView: [],
allViews: allViewsWithData,
});
const handleDispatch = (action: ChartContextActionPayload): ChartContextData => {
const newState = chartReducer(state, action);
dispatch(() => newState);
return newState;
};
return (
<ChartContext.Provider value={{ ...state, dispatch: handleDispatch }}>
{children}
</ChartContext.Provider>
);
};

View File

@ -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;
};

View File

@ -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 (
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
className={`${className ? className : ``}`}
>
{children}
</div>
);
};
// 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 (
// <div
// id={id}
// draggable
// onDragStart={handleDragStart}
// onDragEnd={handleDragEnd}
// onDragOver={handleDragOver}
// onDrop={handleDrop}
// className={`${className} ${dragging ? "dragging" : ""}`}
// style={style}
// >
// {children}
// </div>
// );
// };

View File

@ -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;
};

View File

@ -0,0 +1 @@
export * from "./root";

View File

@ -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<any>;
blockRender: FC<any>;
};
export const GanttChartRoot: FC<GanttChartRootProps> = ({
title = null,
blocks,
loaderTitle = "blocks",
blockUpdateHandler,
sidebarBlockRender,
blockRender,
}) => (
<ChartContextProvider>
<ChartViewRoot
title={title}
blocks={blocks}
loaderTitle={loaderTitle}
blockUpdateHandler={blockUpdateHandler}
sidebarBlockRender={sidebarBlockRender}
blockRender={blockRender}
/>
</ChartContextProvider>
);

View File

@ -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;
}

View File

@ -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;
};

View File

@ -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 };
};

View File

@ -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;
};

View File

@ -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 };
};

View File

@ -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";

View File

@ -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 };
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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<Props> = ({}) => {
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) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="text-brand-base text-sm">{data?.name}</div>
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full overflow-hidden">
<div
className="flex-shrink-0 w-[4px] h-auto"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="inline-block text-brand-base text-sm whitespace-nowrap py-[4px] px-1.5">
{data?.name}
</div>
</div>
);
// 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 (
<div className="w-full h-full p-3">
<GanttChartRoot
title="Issues"
loaderTitle="Issues"
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
blockUpdateHandler={handleUpdateDates}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
/>
</div>
);
};

View File

@ -58,6 +58,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
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<IssuesModalProps> = ({
? 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<IIssue>) => {
if (!workspaceSlug) return;
await issuesService
.createIssues(workspaceSlug as string, activeProject ?? "", payload)
.then(async (res) => {
@ -144,6 +155,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
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();

View File

@ -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<Props> = ({}) => {
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) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="text-brand-base text-sm">{data?.name}</div>
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full overflow-hidden">
<div
className="flex-shrink-0 w-[4px] h-auto"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="inline-block text-brand-base text-sm whitespace-nowrap py-[4px] px-1.5">
{data?.name}
</div>
</div>
);
// 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 (
<div className="w-full h-full p-3">
<GanttChartRoot
title="Modules"
loaderTitle="Modules"
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
blockUpdateHandler={handleUpdateDates}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
/>
</div>
);
};

View File

@ -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";

View File

@ -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<Props> = ({ modules }) => {
// rendering issues on gantt sidebar
const GanttSidebarBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{
backgroundColor: MODULE_STATUS.find((s) => s.value === data.status)?.color,
}}
/>
<div className="text-brand-base text-sm">{data?.name}</div>
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: { data: IModule }) => (
<div className="relative flex w-full h-full overflow-hidden">
<div
className="flex-shrink-0 w-[4px] h-auto"
style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === data.status)?.color }}
/>
<div className="inline-block text-brand-base text-sm whitespace-nowrap py-[4px] px-1.5">
{data?.name}
</div>
</div>
);
// 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 (
<div className="w-full h-full overflow-y-auto">
<GanttChartRoot
title="Modules"
loaderTitle="Modules"
blocks={modules ? blockFormat(modules) : null}
blockUpdateHandler={handleUpdateDates}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
/>
</div>
);
};

View File

@ -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<Props> = ({}) => {
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) => (
<div className="relative flex w-full h-full items-center p-1 overflow-hidden gap-1">
<div
className="rounded-sm flex-shrink-0 w-[10px] h-[10px] flex justify-center items-center"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="text-brand-base text-sm">{data?.name}</div>
</div>
);
// rendering issues on gantt card
const GanttBlockView = ({ data }: any) => (
<div className="relative flex w-full h-full overflow-hidden">
<div
className="flex-shrink-0 w-[4px] h-auto"
style={{ backgroundColor: data?.state_detail?.color || "#858e96" }}
/>
<div className="inline-block text-brand-base text-sm whitespace-nowrap py-[4px] px-1.5">
{data?.name}
</div>
</div>
);
// 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 (
<div className="w-full h-full p-3">
<GanttChartRoot
title="Issue Views"
loaderTitle="Issue Views"
blocks={ganttIssues ? blockFormat(ganttIssues) : null}
blockUpdateHandler={handleUpdateDates}
sidebarBlockRender={(data: any) => <GanttSidebarBlockView data={data} />}
blockRender={(data: any) => <GanttBlockView data={data} />}
/>
</div>
);
};

View File

@ -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";

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -9,7 +9,7 @@ type Props = {
};
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => (
<div className="relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-y-4 border border-b border-brand-base bg-brand-sidebar px-5 py-4">
<div className="relative flex w-full flex-shrink-0 flex-row items-center justify-between gap-y-4 border-b border-brand-base bg-brand-sidebar px-5 py-4">
<div className="flex items-center gap-2">
<div className="block md:hidden">
<button

View File

@ -116,17 +116,14 @@ const ProjectCycles: NextPage = () => {
handleClose={() => setCreateUpdateCycleModal(false)}
data={selectedCycle}
/>
<div className="space-y-8 p-8">
<div className="flex flex-col gap-5">
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
<CyclesView
setSelectedCycle={setSelectedCycle}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
cyclesCompleteList={cyclesCompleteList}
currentAndUpcomingCycles={currentAndUpcomingCycles}
draftCycles={draftCycles}
/>
</div>
<div className="space-y-5 p-8 h-full flex flex-col overflow-hidden">
<CyclesView
setSelectedCycle={setSelectedCycle}
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
cyclesCompleteList={cyclesCompleteList}
currentAndUpcomingCycles={currentAndUpcomingCycles}
draftCycles={draftCycles}
/>
</div>
</ProjectAuthorizationWrapper>
);

View File

@ -10,12 +10,16 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
import projectService from "services/project.service";
import modulesService from "services/modules.service";
// components
import { CreateUpdateModuleModal, SingleModuleCard } from "components/modules";
import {
CreateUpdateModuleModal,
ModulesListGanttChartView,
SingleModuleCard,
} from "components/modules";
// ui
import { EmptyState, Loader, PrimaryButton } from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon } from "@heroicons/react/24/outline";
import { ChartBarIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
// images
import emptyModule from "public/empty-state/empty-module.svg";
// types
@ -28,6 +32,8 @@ const ProjectModules: NextPage = () => {
const [selectedModule, setSelectedModule] = useState<SelectModuleType>();
const [createUpdateModule, setCreateUpdateModule] = useState(false);
const [modulesView, setModulesView] = useState<"grid" | "gantt_chart">("grid");
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
@ -89,19 +95,44 @@ const ProjectModules: NextPage = () => {
/>
{modules ? (
modules.length > 0 ? (
<div className="space-y-5 p-8">
<div className="flex flex-col gap-5">
<div className="space-y-5 p-8 flex flex-col h-full overflow-hidden">
<div className="flex gap-4 justify-between">
<h3 className="text-2xl font-semibold text-brand-base">Modules</h3>
<div className="grid grid-cols-1 gap-9 sm:grid-cols-2 lg:grid-cols-3">
{modules.map((module) => (
<SingleModuleCard
key={module.id}
module={module}
handleEditModule={() => handleEditModule(module)}
/>
))}
<div className="flex items-center gap-x-1">
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
modulesView === "grid" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setModulesView("grid")}
>
<Squares2X2Icon className="h-4 w-4 text-brand-secondary" />
</button>
<button
type="button"
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
modulesView === "gantt_chart" ? "bg-brand-surface-2" : ""
}`}
onClick={() => setModulesView("gantt_chart")}
>
<ChartBarIcon className="h-4 w-4 text-brand-secondary" />
</button>
</div>
</div>
{modulesView === "grid" && (
<div className="h-full overflow-y-auto">
<div className="grid grid-cols-1 gap-9 sm:grid-cols-2 lg:grid-cols-3">
{modules.map((module) => (
<SingleModuleCard
key={module.id}
module={module}
handleEditModule={() => handleEditModule(module)}
/>
))}
</div>
</div>
)}
{modulesView === "gantt_chart" && <ModulesListGanttChartView modules={modules} />}
</div>
) : (
<EmptyState

View File

@ -217,3 +217,11 @@ body {
}
/* end react datepicker styling */
/* lineclamp */
.lineclamp {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
}

View File

@ -263,7 +263,7 @@ export interface IIssueFilterOptions {
created_by: string[] | null;
}
export type TIssueViewOptions = "list" | "kanban" | "calendar";
export type TIssueViewOptions = "list" | "kanban" | "calendar" | "gantt_chart";
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;