diff --git a/apps/app/components/cycles/completed-cycles-list.tsx b/apps/app/components/cycles/completed-cycles-list.tsx new file mode 100644 index 000000000..899a17bb2 --- /dev/null +++ b/apps/app/components/cycles/completed-cycles-list.tsx @@ -0,0 +1,82 @@ +// react +import { useState } from "react"; +// next +import { useRouter } from "next/router"; +import useSWR from "swr"; + +// components +import { DeleteCycleModal, SingleCycleCard } from "components/cycles"; +// types +import { ICycle, SelectCycleType } from "types"; +import { CompletedCycleIcon } from "components/icons"; +import cyclesService from "services/cycles.service"; +import { CYCLE_COMPLETE_LIST } from "constants/fetch-keys"; + +export interface CompletedCyclesListProps { + setCreateUpdateCycleModal: React.Dispatch>; + setSelectedCycle: React.Dispatch>; +} + +export const CompletedCyclesList: React.FC = ({ + setCreateUpdateCycleModal, + setSelectedCycle, +}) => { + const [cycleDeleteModal, setCycleDeleteModal] = useState(false); + const [selectedCycleForDelete, setSelectedCycleForDelete] = useState(); + + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { data: completedCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_COMPLETE_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => cyclesService.getCompletedCycles(workspaceSlug as string, projectId as string) + : null + ); + + const handleDeleteCycle = (cycle: ICycle) => { + setSelectedCycleForDelete({ ...cycle, actionType: "delete" }); + setCycleDeleteModal(true); + }; + + const handleEditCycle = (cycle: ICycle) => { + setSelectedCycle({ ...cycle, actionType: "edit" }); + setCreateUpdateCycleModal(true); + }; + + return ( + <> + {completedCycles && ( + <> + + {completedCycles?.completed_cycles.length > 0 ? ( + completedCycles.completed_cycles.map((cycle) => ( + handleDeleteCycle(cycle)} + handleEditCycle={() => handleEditCycle(cycle)} + /> + )) + ) : ( +
+ +

+ No completed cycles yet. Create with{" "} +
Q
. +

+
+ )} + + )} + + ); +}; diff --git a/apps/app/components/cycles/cycles-list-view.tsx b/apps/app/components/cycles/cycles-list.tsx similarity index 97% rename from apps/app/components/cycles/cycles-list-view.tsx rename to apps/app/components/cycles/cycles-list.tsx index 8491190e8..e55f0e6f1 100644 --- a/apps/app/components/cycles/cycles-list-view.tsx +++ b/apps/app/components/cycles/cycles-list.tsx @@ -13,7 +13,7 @@ type TCycleStatsViewProps = { type: "current" | "upcoming" | "completed"; }; -export const CyclesListView: React.FC = ({ +export const CyclesList: React.FC = ({ cycles, setCreateUpdateCycleModal, setSelectedCycle, diff --git a/apps/app/components/cycles/form.tsx b/apps/app/components/cycles/form.tsx index 58f57ba14..863b9b57a 100644 --- a/apps/app/components/cycles/form.tsx +++ b/apps/app/components/cycles/form.tsx @@ -1,11 +1,16 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +// toast +import useToast from "hooks/use-toast"; // react-hook-form import { Controller, useForm } from "react-hook-form"; // ui import { Button, CustomDatePicker, CustomSelect, Input, TextArea } from "components/ui"; // types import { ICycle } from "types"; +// services +import cyclesService from "services/cycles.service"; type Props = { handleFormSubmit: (values: Partial) => Promise; @@ -17,17 +22,24 @@ type Props = { const defaultValues: Partial = { name: "", description: "", - status: "draft", - start_date: "", - end_date: "", + start_date: null, + end_date: null, }; export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, status, data }) => { + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + const { setToastAlert } = useToast(); + + const [isDateValid, setIsDateValid] = useState(true); + const { register, formState: { errors, isSubmitting }, handleSubmit, control, + watch, reset, } = useForm({ defaultValues, @@ -41,6 +53,31 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat }); }; + const dateChecker = async (payload: any) => { + await cyclesService + .cycleDateCheck(workspaceSlug as string, projectId as string, payload) + .then((res) => { + if (res.status) { + setIsDateValid(true); + } else { + setIsDateValid(false); + setToastAlert({ + type: "error", + title: "Error!", + message: + "You have a cycle already on the given dates, if you want to create your draft cycle you can do that by removing dates", + }); + } + }) + .catch((err) => { + console.log(err); + }); + }; + + const checkEmptyDate = + (watch("start_date") === "" && watch("end_date") === "") || + (!watch("start_date") && !watch("end_date")); + useEffect(() => { reset({ ...defaultValues, @@ -84,30 +121,7 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat register={register} /> -
-
Status
- ( - {field.value ?? "Select Status"}} - input - > - {[ - { label: "Draft", value: "draft" }, - { label: "Started", value: "started" }, - { label: "Completed", value: "completed" }, - ].map((item) => ( - - {item.label} - - ))} - - )} - /> -
+
Start Date
@@ -115,12 +129,19 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat ( { + onChange(val); + watch("end_date") + ? dateChecker({ + start_date: val, + end_date: watch("end_date"), + }) + : ""; + }} error={errors.start_date ? true : false} /> )} @@ -136,12 +157,19 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat ( { + onChange(val); + watch("start_date") + ? dateChecker({ + start_date: watch("start_date"), + end_date: val, + }) + : ""; + }} error={errors.end_date ? true : false} /> )} @@ -158,7 +186,18 @@ export const CycleForm: React.FC = ({ handleFormSubmit, handleClose, stat -
diff --git a/apps/app/constants/cycle.ts b/apps/app/constants/cycle.ts deleted file mode 100644 index 93336fb8c..000000000 --- a/apps/app/constants/cycle.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const CYCLE_STATUS = [ - { label: "Started", value: "started", color: "#5e6ad2" }, - { label: "Completed", value: "completed", color: "#eb5757" }, - { label: "Draft", value: "draft", color: "#f2c94c" }, - ]; \ No newline at end of file diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index a2831d818..42a3c3051 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -35,6 +35,9 @@ export const PROJECT_GITHUB_REPOSITORY = (projectId: string) => export const CYCLE_LIST = (projectId: string) => `CYCLE_LIST_${projectId}`; export const CYCLE_ISSUES = (cycleId: string) => `CYCLE_ISSUES_${cycleId}`; export const CYCLE_DETAILS = (cycleId: string) => `CYCLE_DETAIL_${cycleId}`; +export const CYCLE_CURRENT_AND_UPCOMING_LIST = (projectId: string) => `CYCLE_CURRENT_AND_UPCOMING_LIST_${projectId}`; +export const CYCLE_DRAFT_LIST = (projectId: string) => `CYCLE_DRAFT_LIST_${projectId}`; +export const CYCLE_COMPLETE_LIST = (projectId: string) => `CYCLE_COMPLETE_LIST_${projectId}`; export const STATE_LIST = (projectId: string) => `STATE_LIST_${projectId}`; export const STATE_DETAIL = "STATE_DETAIL"; diff --git a/apps/app/helpers/date-time.helper.ts b/apps/app/helpers/date-time.helper.ts index 4d179ef0d..1fdd58f58 100644 --- a/apps/app/helpers/date-time.helper.ts +++ b/apps/app/helpers/date-time.helper.ts @@ -88,3 +88,17 @@ export const timeAgo = (time: any) => { } return time; }; + +export const getDateRangeStatus = (startDate: string , endDate: string ) => { + const now = new Date(); + const start = new Date(startDate); + const end = new Date(endDate); + + if (end < now) { + return "completed"; + } else if (start <= now && end >= now) { + return "current"; + } else { + return "upcoming"; + } +} \ No newline at end of file diff --git a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx index 771387877..efafd3ce3 100644 --- a/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx +++ b/apps/app/pages/[workspaceSlug]/projects/[projectId]/cycles/[cycleId].tsx @@ -25,6 +25,7 @@ import { CustomMenu, EmptySpace, EmptySpaceItem, Spinner } from "components/ui"; import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs"; // helpers import { truncateText } from "helpers/string.helper"; +import { getDateRangeStatus } from "helpers/date-time.helper"; // types import { CycleIssueResponse, UserAuth } from "types"; // fetch-keys @@ -78,6 +79,11 @@ const SingleCycle: React.FC = (props) => { : null ); + const cycleStatus = + cycleDetails?.start_date && cycleDetails?.end_date + ? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) + : "draft"; + const { data: cycleIssues } = useSWR( workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null, workspaceSlug && projectId && cycleId @@ -218,6 +224,7 @@ const SingleCycle: React.FC = (props) => {
)} ( + () => import("components/cycles").then((a) => a.CompletedCyclesList), + { + ssr: false, + loading: () => ( + + + + ), + } +); const ProjectCycles: NextPage = () => { const [selectedCycle, setSelectedCycle] = useState(); @@ -34,43 +51,25 @@ const ProjectCycles: NextPage = () => { query: { workspaceSlug, projectId }, } = useRouter(); - const { data: activeWorkspace } = useSWR( - workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null, - () => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null) - ); - const { data: activeProject } = useSWR( - activeWorkspace && projectId ? PROJECT_DETAILS(projectId as string) : null, - activeWorkspace && projectId - ? () => projectService.getProject(activeWorkspace.slug, projectId as string) + workspaceSlug && projectId ? PROJECT_DETAILS(projectId as string) : null, + workspaceSlug && projectId + ? () => projectService.getProject(workspaceSlug as string, projectId as string) : null ); - const { data: cycles } = useSWR( - activeWorkspace && projectId ? CYCLE_LIST(projectId as string) : null, - activeWorkspace && projectId - ? () => cycleService.getCycles(activeWorkspace.slug, projectId as string) + const { data: draftCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_DRAFT_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => cycleService.getDraftCycles(workspaceSlug as string, projectId as string) : null ); - const getCycleStatus = (startDate: string, endDate: string) => { - const today = new Date(); - - if (today < new Date(startDate)) return "upcoming"; - else if (today > new Date(endDate)) return "completed"; - else return "current"; - }; - - const currentCycles = cycles?.filter( - (c) => getCycleStatus(c.start_date, c.end_date) === "current" - ); - - const upcomingCycles = cycles?.filter( - (c) => getCycleStatus(c.start_date, c.end_date) === "upcoming" - ); - - const completedCycles = cycles?.filter( - (c) => getCycleStatus(c.start_date, c.end_date) === "completed" + const { data: currentAndUpcomingCycles } = useSWR( + workspaceSlug && projectId ? CYCLE_CURRENT_AND_UPCOMING_LIST(projectId as string) : null, + workspaceSlug && projectId + ? () => cycleService.getCurrentAndUpcomingCycles(workspaceSlug as string, projectId as string) + : null ); useEffect(() => { @@ -110,92 +109,71 @@ const ProjectCycles: NextPage = () => { handleClose={() => setCreateUpdateCycleModal(false)} data={selectedCycle} /> - {cycles ? ( - cycles.length > 0 ? ( -
-

Current Cycle

-
- +

Current Cycle

+
+ +
+
+ + + + `w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` + } + > + Upcoming + + + `w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` + } + > + Completed + + + ` w-1/3 rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` + } + > + Draft + + + + + + + + + + + + -
-
- - - - `rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` - } - > - Upcoming - - - `rounded-lg px-6 py-2 ${selected ? "bg-gray-300" : "hover:bg-gray-200"}` - } - > - Completed - - - - - - - - - - - -
-
- ) : ( -
- - - Use
Q
shortcut to - create a new cycle - - } - Icon={PlusIcon} - action={() => { - const e = new KeyboardEvent("keydown", { - key: "q", - }); - document.dispatchEvent(e); - }} - /> -
-
- ) - ) : ( - - - - - )} + + +
+
); }; diff --git a/apps/app/services/cycles.service.ts b/apps/app/services/cycles.service.ts index 109b62108..eac138030 100644 --- a/apps/app/services/cycles.service.ts +++ b/apps/app/services/cycles.service.ts @@ -1,7 +1,7 @@ // services import APIService from "services/api.service"; // types -import type { ICycle } from "types"; +import type { CompletedCyclesResponse, CurrentAndUpcomingCyclesResponse, DraftCyclesResponse, ICycle } from "types"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; @@ -87,6 +87,47 @@ class ProjectCycleServices extends APIService { throw error?.response?.data; }); } + + async cycleDateCheck(workspaceSlug: string, projectId: string, data: { + start_date: string, + end_date: string + }): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/date-check/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCurrentAndUpcomingCycles(workspaceSlug: string, projectId: string): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/current-upcoming-cycles/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getDraftCycles(workspaceSlug: string, projectId: string): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/draft-cycles/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getCompletedCycles(workspaceSlug: string, projectId: string): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/completed-cycles/` + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } export default new ProjectCycleServices(); diff --git a/apps/app/types/cycles.d.ts b/apps/app/types/cycles.d.ts index dbd7573bf..928166a32 100644 --- a/apps/app/types/cycles.d.ts +++ b/apps/app/types/cycles.d.ts @@ -7,16 +7,32 @@ export interface ICycle { updated_at: Date; name: string; description: string; - start_date: string; - end_date: string; - status: string; + start_date: string | null; + end_date: string | null; created_by: string; updated_by: string; project: string; workspace: string; issue: string; + current_cycle: []; + upcoming_cycle: []; + past_cycles: []; } +export interface CurrentAndUpcomingCyclesResponse { + current_cycle : ICycle[]; + upcoming_cycle : ICycle[]; +} + + +export interface DraftCyclesResponse { + draft_cycles : ICycle[]; + } + +export interface CompletedCyclesResponse { + completed_cycles : ICycle[]; + } + export interface CycleIssueResponse { id: string; issue_detail: IIssue;