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