forked from github/plane
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:
parent
9ccc35d181
commit
e1e9a5ed96
26
apps/app/components/core/gantt-chart-view/index.tsx
Normal file
26
apps/app/components/core/gantt-chart-view/index.tsx
Normal 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 />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./board-view";
|
export * from "./board-view";
|
||||||
export * from "./calendar-view";
|
export * from "./calendar-view";
|
||||||
|
export * from "./gantt-chart-view";
|
||||||
export * from "./list-view";
|
export * from "./list-view";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./bulk-delete-issues-modal";
|
export * from "./bulk-delete-issues-modal";
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
ListBulletIcon,
|
ListBulletIcon,
|
||||||
Squares2X2Icon,
|
Squares2X2Icon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
|
ChartBarIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// helpers
|
// helpers
|
||||||
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
import { replaceUnderscoreIfSnakeCase } from "helpers/string.helper";
|
||||||
@ -82,6 +83,15 @@ export const IssuesFilterView: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
<CalendarDaysIcon className="h-4 w-4 text-brand-secondary" />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
<SelectFilters
|
<SelectFilters
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
@ -18,10 +18,11 @@ import { useProjectMyMembership } from "contexts/project-member.context";
|
|||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useIssuesView from "hooks/use-issues-view";
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
// components
|
// 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 { CreateUpdateIssueModal, DeleteIssueModal } from "components/issues";
|
||||||
import { CreateUpdateViewModal } from "components/views";
|
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
|
// ui
|
||||||
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
|
import { EmptySpace, EmptySpaceItem, EmptyState, PrimaryButton, Spinner } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -47,6 +48,7 @@ import {
|
|||||||
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
STATES_LIST,
|
STATES_LIST,
|
||||||
} from "constants/fetch-keys";
|
} from "constants/fetch-keys";
|
||||||
|
import { ModuleIssuesGanttChartView } from "components/modules";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
type?: "issue" | "cycle" | "module";
|
type?: "issue" | "cycle" | "module";
|
||||||
@ -528,7 +530,7 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
isCompleted={isCompleted}
|
isCompleted={isCompleted}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : issueView === "calendar" ? (
|
||||||
<CalendarView
|
<CalendarView
|
||||||
handleEditIssue={handleEditIssue}
|
handleEditIssue={handleEditIssue}
|
||||||
handleDeleteIssue={handleDeleteIssue}
|
handleDeleteIssue={handleDeleteIssue}
|
||||||
@ -536,6 +538,8 @@ export const IssuesView: React.FC<Props> = ({
|
|||||||
isCompleted={isCompleted}
|
isCompleted={isCompleted}
|
||||||
userAuth={memberRole}
|
userAuth={memberRole}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
issueView === "gantt_chart" && <GanttChartView />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : type === "issue" ? (
|
) : type === "issue" ? (
|
||||||
|
67
apps/app/components/cycles/cycles-list-gantt-chart.tsx
Normal file
67
apps/app/components/cycles/cycles-list-gantt-chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { useEffect } from "react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Tab } from "@headlessui/react";
|
import { Tab } from "@headlessui/react";
|
||||||
@ -10,7 +10,7 @@ import {
|
|||||||
CompletedCyclesListProps,
|
CompletedCyclesListProps,
|
||||||
AllCyclesBoard,
|
AllCyclesBoard,
|
||||||
AllCyclesList,
|
AllCyclesList,
|
||||||
CompletedCycles,
|
CyclesListGanttChartView,
|
||||||
} from "components/cycles";
|
} from "components/cycles";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Loader } from "components/ui";
|
import { EmptyState, Loader } from "components/ui";
|
||||||
@ -41,7 +41,7 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
draftCycles,
|
draftCycles,
|
||||||
}) => {
|
}) => {
|
||||||
const { storedValue: cycleTab, setValue: setCycleTab } = useLocalStorage("cycleTab", "All");
|
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) => {
|
const currentTabValue = (tab: string | null) => {
|
||||||
switch (tab) {
|
switch (tab) {
|
||||||
@ -73,8 +73,41 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
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
|
<Tab.Group
|
||||||
|
as={React.Fragment}
|
||||||
defaultIndex={currentTabValue(cycleTab)}
|
defaultIndex={currentTabValue(cycleTab)}
|
||||||
onChange={(i) => {
|
onChange={(i) => {
|
||||||
switch (i) {
|
switch (i) {
|
||||||
@ -94,59 +127,32 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{" "}
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
<Tab.List as="div" className="flex flex-wrap items-center justify-start gap-4 text-base">
|
||||||
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => (
|
{["All", "Active", "Upcoming", "Completed", "Drafts"].map((tab, index) => {
|
||||||
<Tab
|
if (cyclesView === "gantt_chart" && (tab === "Active" || tab === "Drafts"))
|
||||||
key={index}
|
return null;
|
||||||
className={({ selected }) =>
|
|
||||||
`rounded-3xl border px-6 py-1 outline-none ${
|
return (
|
||||||
selected
|
<Tab
|
||||||
? "border-brand-accent bg-brand-accent text-white font-medium"
|
key={index}
|
||||||
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
className={({ selected }) =>
|
||||||
}`
|
`rounded-3xl border px-6 py-1 outline-none ${
|
||||||
}
|
selected
|
||||||
>
|
? "border-brand-accent bg-brand-accent text-white font-medium"
|
||||||
{tab}
|
: "border-brand-base bg-brand-base hover:bg-brand-surface-2"
|
||||||
</Tab>
|
}`
|
||||||
))}
|
}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Tab>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Tab.List>
|
</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>
|
</div>
|
||||||
<Tab.Panels>
|
<Tab.Panels as={React.Fragment}>
|
||||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
||||||
{cycleView === "list" && (
|
{cyclesView === "list" && (
|
||||||
<AllCyclesList
|
<AllCyclesList
|
||||||
cycles={cyclesCompleteList}
|
cycles={cyclesCompleteList}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
@ -154,7 +160,7 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
type="current"
|
type="current"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cycleView === "board" && (
|
{cyclesView === "grid" && (
|
||||||
<AllCyclesBoard
|
<AllCyclesBoard
|
||||||
cycles={cyclesCompleteList}
|
cycles={cyclesCompleteList}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
@ -162,29 +168,26 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
type="current"
|
type="current"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cycleView === "gantt" && (
|
{cyclesView === "gantt_chart" && (
|
||||||
<EmptyState
|
<CyclesListGanttChartView cycles={cyclesCompleteList ?? []} />
|
||||||
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>
|
||||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
{cyclesView !== "gantt_chart" && (
|
||||||
{currentAndUpcomingCycles?.current_cycle?.[0] ? (
|
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||||
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
|
{currentAndUpcomingCycles?.current_cycle?.[0] ? (
|
||||||
) : (
|
<ActiveCycleDetails cycle={currentAndUpcomingCycles?.current_cycle?.[0]} />
|
||||||
<EmptyState
|
) : (
|
||||||
type="cycle"
|
<EmptyState
|
||||||
title="Create New Cycle"
|
type="cycle"
|
||||||
description="Sprint more effectively with Cycles by confining your project to a fixed amount of time. Create new cycle now."
|
title="Create New Cycle"
|
||||||
imgURL={emptyCycle}
|
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">
|
</Tab.Panel>
|
||||||
{cycleView === "list" && (
|
)}
|
||||||
|
<Tab.Panel as="div" className="mt-7 space-y-5 h-full overflow-y-auto">
|
||||||
|
{cyclesView === "list" && (
|
||||||
<AllCyclesList
|
<AllCyclesList
|
||||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
@ -192,7 +195,7 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
type="upcoming"
|
type="upcoming"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cycleView === "board" && (
|
{cyclesView === "board" && (
|
||||||
<AllCyclesBoard
|
<AllCyclesBoard
|
||||||
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
cycles={currentAndUpcomingCycles?.upcoming_cycle}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
@ -200,50 +203,39 @@ export const CyclesView: React.FC<Props> = ({
|
|||||||
type="upcoming"
|
type="upcoming"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{cycleView === "gantt" && (
|
{cyclesView === "gantt_chart" && (
|
||||||
<EmptyState
|
<CyclesListGanttChartView cycles={currentAndUpcomingCycles?.upcoming_cycle ?? []} />
|
||||||
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>
|
||||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||||
<CompletedCycles
|
<CompletedCycles
|
||||||
cycleView={cycleView ?? "list"}
|
cycleView={cyclesView ?? "list"}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
setSelectedCycle={setSelectedCycle}
|
setSelectedCycle={setSelectedCycle}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tab.Panel>
|
||||||
<Tab.Panel as="div" className="mt-7 space-y-5">
|
{cyclesView !== "gantt_chart" && (
|
||||||
{cycleView === "list" && (
|
<Tab.Panel as="div" className="mt-7 space-y-5">
|
||||||
<AllCyclesList
|
{cyclesView === "list" && (
|
||||||
cycles={draftCycles?.draft_cycles}
|
<AllCyclesList
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
cycles={draftCycles?.draft_cycles}
|
||||||
setSelectedCycle={setSelectedCycle}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
type="draft"
|
setSelectedCycle={setSelectedCycle}
|
||||||
/>
|
type="draft"
|
||||||
)}
|
/>
|
||||||
{cycleView === "board" && (
|
)}
|
||||||
<AllCyclesBoard
|
{cyclesView === "board" && (
|
||||||
cycles={draftCycles?.draft_cycles}
|
<AllCyclesBoard
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
cycles={draftCycles?.draft_cycles}
|
||||||
setSelectedCycle={setSelectedCycle}
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
type="draft"
|
setSelectedCycle={setSelectedCycle}
|
||||||
/>
|
type="draft"
|
||||||
)}
|
/>
|
||||||
{cycleView === "gantt" && (
|
)}
|
||||||
<EmptyState
|
</Tab.Panel>
|
||||||
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.Panels>
|
</Tab.Panels>
|
||||||
</Tab.Group>
|
</Tab.Group>
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
81
apps/app/components/cycles/gantt-chart.tsx
Normal file
81
apps/app/components/cycles/gantt-chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,10 +1,12 @@
|
|||||||
export * from "./active-cycle-details";
|
export * from "./active-cycle-details";
|
||||||
export * from "./cycles-view";
|
export * from "./cycles-view";
|
||||||
export * from "./completed-cycles";
|
export * from "./completed-cycles";
|
||||||
|
export * from "./cycles-list-gantt-chart";
|
||||||
export * from "./all-cycles-board";
|
export * from "./all-cycles-board";
|
||||||
export * from "./all-cycles-list";
|
export * from "./all-cycles-list";
|
||||||
export * from "./delete-cycle-modal";
|
export * from "./delete-cycle-modal";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
|
export * from "./gantt-chart";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
export * from "./select";
|
export * from "./select";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
|
81
apps/app/components/gantt-chart/blocks/index.tsx
Normal file
81
apps/app/components/gantt-chart/blocks/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
56
apps/app/components/gantt-chart/chart/bi-week.tsx
Normal file
56
apps/app/components/gantt-chart/chart/bi-week.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
56
apps/app/components/gantt-chart/chart/day.tsx
Normal file
56
apps/app/components/gantt-chart/chart/day.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
56
apps/app/components/gantt-chart/chart/hours.tsx
Normal file
56
apps/app/components/gantt-chart/chart/hours.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
322
apps/app/components/gantt-chart/chart/index.tsx
Normal file
322
apps/app/components/gantt-chart/chart/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
56
apps/app/components/gantt-chart/chart/month.tsx
Normal file
56
apps/app/components/gantt-chart/chart/month.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
50
apps/app/components/gantt-chart/chart/quarter.tsx
Normal file
50
apps/app/components/gantt-chart/chart/quarter.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
56
apps/app/components/gantt-chart/chart/week.tsx
Normal file
56
apps/app/components/gantt-chart/chart/week.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
50
apps/app/components/gantt-chart/chart/year.tsx
Normal file
50
apps/app/components/gantt-chart/chart/year.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
48
apps/app/components/gantt-chart/contexts/index.tsx
Normal file
48
apps/app/components/gantt-chart/contexts/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
144
apps/app/components/gantt-chart/data/index.ts
Normal file
144
apps/app/components/gantt-chart/data/index.ts
Normal 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;
|
||||||
|
};
|
138
apps/app/components/gantt-chart/helpers/draggable.tsx
Normal file
138
apps/app/components/gantt-chart/helpers/draggable.tsx
Normal 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>
|
||||||
|
// );
|
||||||
|
// };
|
15
apps/app/components/gantt-chart/hooks/index.tsx
Normal file
15
apps/app/components/gantt-chart/hooks/index.tsx
Normal 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;
|
||||||
|
};
|
1
apps/app/components/gantt-chart/index.ts
Normal file
1
apps/app/components/gantt-chart/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./root";
|
34
apps/app/components/gantt-chart/root.tsx
Normal file
34
apps/app/components/gantt-chart/root.tsx
Normal 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>
|
||||||
|
);
|
43
apps/app/components/gantt-chart/types/index.ts
Normal file
43
apps/app/components/gantt-chart/types/index.ts
Normal 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;
|
||||||
|
}
|
163
apps/app/components/gantt-chart/views/bi-week-view.ts
Normal file
163
apps/app/components/gantt-chart/views/bi-week-view.ts
Normal 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;
|
||||||
|
};
|
186
apps/app/components/gantt-chart/views/day-view.ts
Normal file
186
apps/app/components/gantt-chart/views/day-view.ts
Normal 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 };
|
||||||
|
};
|
93
apps/app/components/gantt-chart/views/helpers.ts
Normal file
93
apps/app/components/gantt-chart/views/helpers.ts
Normal 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;
|
||||||
|
};
|
186
apps/app/components/gantt-chart/views/hours-view.ts
Normal file
186
apps/app/components/gantt-chart/views/hours-view.ts
Normal 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 };
|
||||||
|
};
|
7
apps/app/components/gantt-chart/views/index.ts
Normal file
7
apps/app/components/gantt-chart/views/index.ts
Normal 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";
|
199
apps/app/components/gantt-chart/views/month-view.ts
Normal file
199
apps/app/components/gantt-chart/views/month-view.ts
Normal 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 };
|
||||||
|
};
|
117
apps/app/components/gantt-chart/views/quater-view.ts
Normal file
117
apps/app/components/gantt-chart/views/quater-view.ts
Normal 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;
|
||||||
|
};
|
163
apps/app/components/gantt-chart/views/week-view.ts
Normal file
163
apps/app/components/gantt-chart/views/week-view.ts
Normal 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;
|
||||||
|
};
|
116
apps/app/components/gantt-chart/views/year-view.ts
Normal file
116
apps/app/components/gantt-chart/views/year-view.ts
Normal 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;
|
||||||
|
};
|
81
apps/app/components/issues/gantt-chart.tsx
Normal file
81
apps/app/components/issues/gantt-chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -58,6 +58,7 @@ export const CreateUpdateIssueModal: React.FC<IssuesModalProps> = ({
|
|||||||
|
|
||||||
const { issueView, params } = useIssuesView();
|
const { issueView, params } = useIssuesView();
|
||||||
const { params: calendarParams } = useCalendarIssuesView();
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
|
const { order_by, group_by, ...viewGanttParams } = params;
|
||||||
|
|
||||||
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
if (cycleId) prePopulateData = { ...prePopulateData, cycle: cycleId as string };
|
||||||
if (moduleId) prePopulateData = { ...prePopulateData, module: moduleId 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)
|
? VIEW_ISSUES(viewId.toString(), calendarParams)
|
||||||
: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.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>) => {
|
const createIssue = async (payload: Partial<IIssue>) => {
|
||||||
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
await issuesService
|
await issuesService
|
||||||
.createIssues(workspaceSlug as string, activeProject ?? "", payload)
|
.createIssues(workspaceSlug as string, activeProject ?? "", payload)
|
||||||
.then(async (res) => {
|
.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 (payload.module && payload.module !== "") await addIssueToModule(res.id, payload.module);
|
||||||
|
|
||||||
if (issueView === "calendar") mutate(calendarFetchKey);
|
if (issueView === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (issueView === "gantt_chart") mutate(ganttFetchKey);
|
||||||
|
|
||||||
if (!createMore) handleClose();
|
if (!createMore) handleClose();
|
||||||
|
|
||||||
|
81
apps/app/components/modules/gantt-chart.tsx
Normal file
81
apps/app/components/modules/gantt-chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -2,6 +2,8 @@ export * from "./select";
|
|||||||
export * from "./sidebar-select";
|
export * from "./sidebar-select";
|
||||||
export * from "./delete-module-modal";
|
export * from "./delete-module-modal";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
|
export * from "./gantt-chart";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
|
export * from "./modules-list-gantt-chart";
|
||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./single-module-card";
|
export * from "./single-module-card";
|
||||||
|
74
apps/app/components/modules/modules-list-gantt-chart.tsx
Normal file
74
apps/app/components/modules/modules-list-gantt-chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
81
apps/app/components/views/gantt-chart.tsx
Normal file
81
apps/app/components/views/gantt-chart.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,6 @@
|
|||||||
export * from "./delete-view-modal";
|
export * from "./delete-view-modal";
|
||||||
export * from "./form";
|
export * from "./form";
|
||||||
|
export * from "./gantt-chart";
|
||||||
export * from "./modal";
|
export * from "./modal";
|
||||||
export * from "./select-filters";
|
export * from "./select-filters";
|
||||||
export * from "./single-view-item"
|
export * from "./single-view-item";
|
||||||
|
32
apps/app/hooks/gantt-chart/cycle-issues-view.tsx
Normal file
32
apps/app/hooks/gantt-chart/cycle-issues-view.tsx
Normal 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;
|
23
apps/app/hooks/gantt-chart/issue-view.tsx
Normal file
23
apps/app/hooks/gantt-chart/issue-view.tsx
Normal 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;
|
32
apps/app/hooks/gantt-chart/module-issues-view.tsx
Normal file
32
apps/app/hooks/gantt-chart/module-issues-view.tsx
Normal 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;
|
37
apps/app/hooks/gantt-chart/view-issues-view.tsx
Normal file
37
apps/app/hooks/gantt-chart/view-issues-view.tsx
Normal 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;
|
@ -9,7 +9,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Header: React.FC<Props> = ({ breadcrumbs, left, right, setToggleSidebar }) => (
|
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="flex items-center gap-2">
|
||||||
<div className="block md:hidden">
|
<div className="block md:hidden">
|
||||||
<button
|
<button
|
||||||
|
@ -116,17 +116,14 @@ const ProjectCycles: NextPage = () => {
|
|||||||
handleClose={() => setCreateUpdateCycleModal(false)}
|
handleClose={() => setCreateUpdateCycleModal(false)}
|
||||||
data={selectedCycle}
|
data={selectedCycle}
|
||||||
/>
|
/>
|
||||||
<div className="space-y-8 p-8">
|
<div className="space-y-5 p-8 h-full flex flex-col overflow-hidden">
|
||||||
<div className="flex flex-col gap-5">
|
<CyclesView
|
||||||
<h3 className="text-2xl font-semibold text-brand-base">Cycles</h3>
|
setSelectedCycle={setSelectedCycle}
|
||||||
<CyclesView
|
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
||||||
setSelectedCycle={setSelectedCycle}
|
cyclesCompleteList={cyclesCompleteList}
|
||||||
setCreateUpdateCycleModal={setCreateUpdateCycleModal}
|
currentAndUpcomingCycles={currentAndUpcomingCycles}
|
||||||
cyclesCompleteList={cyclesCompleteList}
|
draftCycles={draftCycles}
|
||||||
currentAndUpcomingCycles={currentAndUpcomingCycles}
|
/>
|
||||||
draftCycles={draftCycles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ProjectAuthorizationWrapper>
|
</ProjectAuthorizationWrapper>
|
||||||
);
|
);
|
||||||
|
@ -10,12 +10,16 @@ import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
|
|||||||
import projectService from "services/project.service";
|
import projectService from "services/project.service";
|
||||||
import modulesService from "services/modules.service";
|
import modulesService from "services/modules.service";
|
||||||
// components
|
// components
|
||||||
import { CreateUpdateModuleModal, SingleModuleCard } from "components/modules";
|
import {
|
||||||
|
CreateUpdateModuleModal,
|
||||||
|
ModulesListGanttChartView,
|
||||||
|
SingleModuleCard,
|
||||||
|
} from "components/modules";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState, Loader, PrimaryButton } from "components/ui";
|
import { EmptyState, Loader, PrimaryButton } from "components/ui";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
||||||
// icons
|
// icons
|
||||||
import { PlusIcon } from "@heroicons/react/24/outline";
|
import { ChartBarIcon, PlusIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
|
||||||
// images
|
// images
|
||||||
import emptyModule from "public/empty-state/empty-module.svg";
|
import emptyModule from "public/empty-state/empty-module.svg";
|
||||||
// types
|
// types
|
||||||
@ -28,6 +32,8 @@ const ProjectModules: NextPage = () => {
|
|||||||
const [selectedModule, setSelectedModule] = useState<SelectModuleType>();
|
const [selectedModule, setSelectedModule] = useState<SelectModuleType>();
|
||||||
const [createUpdateModule, setCreateUpdateModule] = useState(false);
|
const [createUpdateModule, setCreateUpdateModule] = useState(false);
|
||||||
|
|
||||||
|
const [modulesView, setModulesView] = useState<"grid" | "gantt_chart">("grid");
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
|
||||||
@ -89,19 +95,44 @@ const ProjectModules: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
{modules ? (
|
{modules ? (
|
||||||
modules.length > 0 ? (
|
modules.length > 0 ? (
|
||||||
<div className="space-y-5 p-8">
|
<div className="space-y-5 p-8 flex flex-col h-full overflow-hidden">
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex gap-4 justify-between">
|
||||||
<h3 className="text-2xl font-semibold text-brand-base">Modules</h3>
|
<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">
|
<div className="flex items-center gap-x-1">
|
||||||
{modules.map((module) => (
|
<button
|
||||||
<SingleModuleCard
|
type="button"
|
||||||
key={module.id}
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-brand-surface-2 ${
|
||||||
module={module}
|
modulesView === "grid" ? "bg-brand-surface-2" : ""
|
||||||
handleEditModule={() => handleEditModule(module)}
|
}`}
|
||||||
/>
|
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>
|
||||||
</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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
@ -217,3 +217,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* end react datepicker styling */
|
/* end react datepicker styling */
|
||||||
|
|
||||||
|
/* lineclamp */
|
||||||
|
.lineclamp {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
}
|
||||||
|
2
apps/app/types/issues.d.ts
vendored
2
apps/app/types/issues.d.ts
vendored
@ -263,7 +263,7 @@ export interface IIssueFilterOptions {
|
|||||||
created_by: string[] | null;
|
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;
|
export type TIssueGroupByOptions = "state" | "priority" | "labels" | "created_by" | null;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user