chore: new estimates workflow (#927)

* chore: new services for estimates

* chore: new estimates workflow
This commit is contained in:
Aaryan Khandelwal 2023-04-22 00:59:57 +05:30 committed by GitHub
parent cb814dd68b
commit c638b6aba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 349 additions and 587 deletions

View File

@ -6,19 +6,20 @@ import { mutate } from "swr";
// react-hook-form // react-hook-form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services // services
import estimatesService from "services/estimates.service"; import estimatesService from "services/estimates.service";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
import { Dialog, Transition } from "@headlessui/react";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import { checkDuplicates } from "helpers/array.helper";
// types // types
import { IEstimate } from "types"; import { IEstimate, IEstimateFormData } from "types";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST } from "constants/fetch-keys"; import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -26,18 +27,35 @@ type Props = {
data?: IEstimate; data?: IEstimate;
}; };
const defaultValues: Partial<IEstimate> = { type FormValues = {
name: string;
description: string;
value1: string;
value2: string;
value3: string;
value4: string;
value5: string;
value6: string;
};
const defaultValues: Partial<FormValues> = {
name: "", name: "",
description: "", description: "",
value1: "",
value2: "",
value3: "",
value4: "",
value5: "",
value6: "",
}; };
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => { export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
const { const {
register, register,
formState: { errors, isSubmitting }, formState: { isSubmitting },
handleSubmit, handleSubmit,
reset, reset,
} = useForm<IEstimate>({ } = useForm<FormValues>({
defaultValues, defaultValues,
}); });
@ -51,47 +69,48 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
const { setToastAlert } = useToast(); const { setToastAlert } = useToast();
const createEstimate = async (formData: IEstimate) => { const createEstimate = async (payload: IEstimateFormData) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
const payload = {
name: formData.name,
description: formData.description,
};
await estimatesService await estimatesService
.createEstimate(workspaceSlug as string, projectId as string, payload) .createEstimate(workspaceSlug as string, projectId as string, payload)
.then((res) => { .then(() => {
mutate<IEstimate[]>( mutate(ESTIMATES_LIST(projectId as string));
ESTIMATES_LIST(projectId as string), onClose();
(prevData) => [res, ...(prevData ?? [])],
false
);
}) })
.catch(() => { .catch((err) => {
setToastAlert({ if (err.status === 400)
type: "error", setToastAlert({
title: "Error!", type: "error",
message: "Error: Estimate could not be created", title: "Error!",
}); message: "Estimate with that name already exists. Please try again with another name.",
});
else
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate could not be created. Please try again.",
});
}); });
onClose();
}; };
const updateEstimate = async (formData: IEstimate) => { const updateEstimate = async (payload: IEstimateFormData) => {
if (!workspaceSlug || !projectId || !data) return; if (!workspaceSlug || !projectId || !data) return;
const payload = {
name: formData.name,
description: formData.description,
};
mutate<IEstimate[]>( mutate<IEstimate[]>(
ESTIMATES_LIST(projectId as string), ESTIMATES_LIST(projectId.toString()),
(prevData) => (prevData) =>
prevData?.map((p) => { prevData?.map((p) => {
if (p.id === data.id) return { ...p, ...payload }; if (p.id === data.id)
return {
...p,
name: payload.estimate.name,
description: payload.estimate.description,
points: p.points.map((point, index) => ({
...point,
value: payload.estimate_points[index].value,
})),
};
return p; return p;
}), }),
@ -100,23 +119,116 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
await estimatesService await estimatesService
.patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload) .patchEstimate(workspaceSlug as string, projectId as string, data?.id as string, payload)
.then(() => handleClose()) .then(() => {
mutate(ESTIMATES_LIST(projectId.toString()));
mutate(ESTIMATE_DETAILS(data.id));
handleClose();
})
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: "Error: Estimate could not be updated", message: "Estimate could not be updated. Please try again.",
}); });
}); });
onClose(); onClose();
}; };
const onSubmit = async (formData: FormValues) => {
if (!formData.name || formData.name === "") {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate title cannot be empty.",
});
return;
}
if (
formData.value1 === "" ||
formData.value2 === "" ||
formData.value3 === "" ||
formData.value4 === "" ||
formData.value5 === "" ||
formData.value6 === ""
) {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate point cannot be empty.",
});
return;
}
if (
checkDuplicates([
formData.value1,
formData.value2,
formData.value3,
formData.value4,
formData.value5,
formData.value6,
])
) {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate points cannot have duplicate values.",
});
return;
}
const payload: IEstimateFormData = {
estimate: {
name: formData.name,
description: formData.description,
},
estimate_points: [
{
key: 0,
value: formData.value1,
},
{
key: 1,
value: formData.value2,
},
{
key: 2,
value: formData.value3,
},
{
key: 3,
value: formData.value4,
},
{
key: 4,
value: formData.value5,
},
{
key: 5,
value: formData.value6,
},
],
};
if (data) await updateEstimate(payload);
else await createEstimate(payload);
};
useEffect(() => { useEffect(() => {
reset({ if (data)
...defaultValues, reset({
...data, ...defaultValues,
}); ...data,
value1: data.points[0]?.value,
value2: data.points[1]?.value,
value3: data.points[2]?.value,
value4: data.points[3]?.value,
value5: data.points[4]?.value,
value6: data.points[5]?.value,
});
else reset({ ...defaultValues });
}, [data, reset]); }, [data, reset]);
return ( return (
@ -146,10 +258,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
> >
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6"> <Dialog.Panel className="relative transform rounded-lg bg-brand-base px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form <form onSubmit={handleSubmit(onSubmit)}>
onSubmit={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)}
>
<div className="space-y-3"> <div className="space-y-3">
<div className="text-lg font-medium leading-6"> <div className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate {data ? "Update" : "Create"} Estimate
@ -161,17 +271,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
type="name" type="name"
placeholder="Title" placeholder="Title"
autoComplete="off" autoComplete="off"
mode="transparent"
className="resize-none text-xl" className="resize-none text-xl"
error={errors.name}
register={register} register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/> />
</div> </div>
<div> <div>
@ -180,11 +281,107 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
name="description" name="description"
placeholder="Description" placeholder="Description"
className="h-32 resize-none text-sm" className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register} register={register}
/> />
</div> </div>
<div className="grid grid-cols-3 gap-3">
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">1</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value1"
type="name"
className="rounded-l-none"
placeholder="Point 1"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">2</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value2"
type="name"
className="rounded-l-none"
placeholder="Point 2"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">3</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value3"
type="name"
className="rounded-l-none"
placeholder="Point 3"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">4</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value4"
type="name"
className="rounded-l-none"
placeholder="Point 4"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">5</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value5"
type="name"
className="rounded-l-none"
placeholder="Point 5"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="flex h-full items-center rounded-lg bg-brand-surface-2">
<span className="rounded-lg px-2 text-sm text-brand-secondary">6</span>
<span className="rounded-r-lg bg-brand-base">
<Input
id="name"
name="value6"
type="name"
className="rounded-l-none"
placeholder="Point 6"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
</div>
</div> </div>
<div className="mt-5 flex justify-end gap-2"> <div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={handleClose}>Cancel</SecondaryButton> <SecondaryButton onClick={handleClose}>Cancel</SecondaryButton>

View File

@ -1,329 +0,0 @@
import React, { useEffect } from "react";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
import { mutate } from "swr";
// services
import estimatesService from "services/estimates.service";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton } from "components/ui";
// types
import type { IEstimate, IEstimatePoint } from "types";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
data?: IEstimatePoint[];
estimate: IEstimate | null;
onClose: () => void;
};
interface FormValues {
value1: string;
value2: string;
value3: string;
value4: string;
value5: string;
value6: string;
}
const defaultValues: FormValues = {
value1: "",
value2: "",
value3: "",
value4: "",
value5: "",
value6: "",
};
export const EstimatePointsModal: React.FC<Props> = ({ isOpen, data, estimate, onClose }) => {
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { setToastAlert } = useToast();
const {
register,
formState: { isSubmitting },
handleSubmit,
reset,
} = useForm<FormValues>({ defaultValues });
const handleClose = () => {
onClose();
reset();
};
const createEstimatePoints = async (formData: FormValues) => {
if (!workspaceSlug || !projectId) return;
const payload = {
estimate_points: [
{
key: 0,
value: formData.value1,
},
{
key: 1,
value: formData.value2,
},
{
key: 2,
value: formData.value3,
},
{
key: 3,
value: formData.value4,
},
{
key: 4,
value: formData.value5,
},
{
key: 5,
value: formData.value6,
},
],
};
await estimatesService
.createEstimatePoints(
workspaceSlug as string,
projectId as string,
estimate?.id as string,
payload
)
.then(() => {
handleClose();
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate points could not be created. Please try again.",
});
});
};
const updateEstimatePoints = async (formData: FormValues) => {
if (!workspaceSlug || !projectId || !data || data.length === 0) return;
const payload = {
estimate_points: data.map((d, index) => ({
id: d.id,
value: (formData as any)[`value${index + 1}`],
})),
};
await estimatesService
.patchEstimatePoints(
workspaceSlug as string,
projectId as string,
estimate?.id as string,
payload
)
.then(() => {
handleClose();
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Estimate points could not be created. Please try again.",
});
});
};
const onSubmit = async (formData: FormValues) => {
let c = 0;
Object.keys(formData).map((key) => {
if (formData[key as keyof FormValues] === "") c++;
});
if (c !== 0) {
setToastAlert({
type: "error",
title: "Error!",
message: "Please fill all the fields.",
});
return;
}
if (data && data.length !== 0) await updateEstimatePoints(formData);
else await createEstimatePoints(formData);
if (estimate) mutate(ESTIMATE_POINTS_LIST(estimate.id));
};
useEffect(() => {
if (!data || data.length < 6) return;
reset({
...defaultValues,
value1: data[0].value,
value2: data[1].value,
value3: data[2].value,
value4: data[3].value,
value5: data[4].value,
value6: data[5].value,
});
}, [data, reset]);
return (
<Transition.Root show={isOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-20" onClose={() => handleClose()}>
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>
<div className="fixed inset-0 z-20 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center sm:p-0">
<Transition.Child
as={React.Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel className="relative transform rounded-lg bg-white px-5 py-8 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-2xl sm:p-6">
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-3">
<div className="flex flex-col gap-3">
<h4 className="text-lg font-medium leading-6">
{data && data.length > 0 ? "Update" : "Create"} Estimate Points
</h4>
<div className="grid grid-cols-3 gap-3">
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">1</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value1"
type="name"
placeholder="Point 1"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">2</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value2"
type="name"
placeholder="Point 2"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">3</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value3"
type="name"
placeholder="Point 3"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">4</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value4"
type="name"
placeholder="Point 4"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">5</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value5"
type="name"
placeholder="Point 5"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
<div className="flex items-center">
<span className="bg-gray-100 h-full flex items-center rounded-lg">
<span className="px-2 rounded-lg text-sm text-gray-600">6</span>
<span className="bg-white rounded-lg">
<Input
id="name"
name="value6"
type="name"
placeholder="Point 6"
autoComplete="off"
register={register}
/>
</span>
</span>
</div>
</div>
</div>
</div>
<div className="mt-5 flex justify-end gap-2">
<SecondaryButton onClick={() => handleClose()}>Cancel</SecondaryButton>
<PrimaryButton type="submit" loading={isSubmitting}>
{data && data.length > 0
? isSubmitting
? "Updating Points..."
: "Update Points"
: isSubmitting
? "Creating Points..."
: "Create Points"}
</PrimaryButton>
</div>
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
);
};

View File

@ -1,4 +1,3 @@
export * from "./create-update-estimate-modal"; export * from "./create-update-estimate-modal";
export * from "./single-estimate"; export * from "./single-estimate";
export * from "./estimate-points-modal" export * from "./delete-estimate-modal";
export * from "./delete-estimate-modal"

View File

@ -2,31 +2,21 @@ import React, { useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr";
// services // services
import estimatesService from "services/estimates.service";
import projectService from "services/project.service"; import projectService from "services/project.service";
// hooks // hooks
import useToast from "hooks/use-toast"; import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
// components // components
import { EstimatePointsModal, DeleteEstimateModal } from "components/estimates"; import { DeleteEstimateModal } from "components/estimates";
// ui // ui
import { CustomMenu } from "components/ui"; import { CustomMenu, SecondaryButton } from "components/ui";
//icons //icons
import { import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
PencilIcon,
TrashIcon,
SquaresPlusIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
// helpers // helpers
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IEstimate } from "types"; import { IEstimate } from "types";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = { type Props = {
estimate: IEstimate; estimate: IEstimate;
@ -39,7 +29,6 @@ export const SingleEstimate: React.FC<Props> = ({
editEstimate, editEstimate,
handleEstimateDelete, handleEstimateDelete,
}) => { }) => {
const [isEstimatePointsModalOpen, setIsEstimatePointsModalOpen] = useState(false);
const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false); const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false);
const router = useRouter(); const router = useRouter();
@ -49,18 +38,6 @@ export const SingleEstimate: React.FC<Props> = ({
const { projectDetails, mutateProjectDetails } = useProjectDetails(); const { projectDetails, mutateProjectDetails } = useProjectDetails();
const { data: estimatePoints } = useSWR(
workspaceSlug && projectId ? ESTIMATE_POINTS_LIST(estimate.id) : null,
workspaceSlug && projectId
? () =>
estimatesService.getEstimatesPointsList(
workspaceSlug as string,
projectId as string,
estimate.id
)
: null
);
const handleUseEstimate = async () => { const handleUseEstimate = async () => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
@ -87,78 +64,61 @@ export const SingleEstimate: React.FC<Props> = ({
return ( return (
<> <>
<EstimatePointsModal
isOpen={isEstimatePointsModalOpen}
estimate={estimate}
onClose={() => setIsEstimatePointsModalOpen(false)}
data={estimatePoints ? orderArrayBy(estimatePoints, "key") : undefined}
/>
<div className="gap-2 py-3"> <div className="gap-2 py-3">
<div className="flex justify-between items-center"> <div className="flex items-center justify-between">
<div> <div>
<h6 className="flex items-center gap-2 font-medium text-base w-[40vw] truncate"> <h6 className="flex w-[40vw] items-center gap-2 truncate text-sm font-medium">
{estimate.name} {estimate.name}
{projectDetails?.estimate && projectDetails?.estimate === estimate.id && ( {projectDetails?.estimate && projectDetails?.estimate === estimate.id && (
<span className="capitalize px-2 py-0.5 text-xs rounded bg-green-100 text-green-500"> <span className="rounded bg-green-500/20 px-2 py-0.5 text-xs capitalize text-green-500">
In use In use
</span> </span>
)} )}
</h6> </h6>
<p className="font-sm text-gray-400 font-normal text-[14px] w-[40vw] truncate"> <p className="font-sm w-[40vw] truncate text-[14px] font-normal text-brand-secondary">
{estimate.description} {estimate.description}
</p> </p>
</div> </div>
<CustomMenu ellipsis> <div className="flex items-center gap-2">
{projectDetails?.estimate !== estimate.id && {projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && (
estimatePoints && <SecondaryButton onClick={handleUseEstimate} className="py-1">
estimatePoints.length > 0 && ( Use
<CustomMenu.MenuItem onClick={handleUseEstimate}> </SecondaryButton>
<div className="flex items-center justify-start gap-2"> )}
<SquaresPlusIcon className="h-3.5 w-3.5" /> <CustomMenu ellipsis>
<span>Use estimate</span>
</div>
</CustomMenu.MenuItem>
)}
<CustomMenu.MenuItem onClick={() => setIsEstimatePointsModalOpen(true)}>
<div className="flex items-center justify-start gap-2">
<ListBulletIcon className="h-3.5 w-3.5" />
<span>
{estimatePoints && estimatePoints?.length > 0 ? "Edit points" : "Create points"}
</span>
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
onClick={() => {
editEstimate(estimate);
}}
>
<div className="flex items-center justify-start gap-2">
<PencilIcon className="h-3.5 w-3.5" />
<span>Edit estimate</span>
</div>
</CustomMenu.MenuItem>
{projectDetails?.estimate !== estimate.id && (
<CustomMenu.MenuItem <CustomMenu.MenuItem
onClick={() => { onClick={() => {
setIsDeleteEstimateModalOpen(true); editEstimate(estimate);
}} }}
> >
<div className="flex items-center justify-start gap-2"> <div className="flex items-center justify-start gap-2">
<TrashIcon className="h-3.5 w-3.5" /> <PencilIcon className="h-3.5 w-3.5" />
<span>Delete estimate</span> <span>Edit estimate</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
)} {projectDetails?.estimate !== estimate.id && (
</CustomMenu> <CustomMenu.MenuItem
onClick={() => {
setIsDeleteEstimateModalOpen(true);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-3.5 w-3.5" />
<span>Delete estimate</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
</div>
</div> </div>
{estimatePoints && estimatePoints.length > 0 ? ( {estimate.points.length > 0 ? (
<div className="flex text-sm text-gray-400"> <div className="flex text-xs text-brand-secondary">
Estimate points ( Estimate points (
<span className="flex gap-1"> <span className="flex gap-1">
{estimatePoints.map((point, index) => ( {orderArrayBy(estimate.points, "key").map((point, index) => (
<h6 key={point.id}> <h6 key={point.id} className="text-brand-secondary">
{point.value} {point.value}
{index !== estimatePoints.length - 1 && ","}{" "} {index !== estimate.points.length - 1 && ","}{" "}
</h6> </h6>
))} ))}
</span> </span>
@ -166,7 +126,7 @@ export const SingleEstimate: React.FC<Props> = ({
</div> </div>
) : ( ) : (
<div> <div>
<p className="text-sm text-gray-400">No estimate points</p> <p className="text-xs text-brand-secondary">No estimate points</p>
</div> </div>
)} )}
</div> </div>

View File

@ -22,7 +22,7 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
value={value} value={value}
label={ label={
<div className="flex items-center gap-2 text-xs"> <div className="flex items-center gap-2 text-xs">
<PlayIcon className="h-4 w-4 text-gray-500 -rotate-90" /> <PlayIcon className="h-4 w-4 -rotate-90 text-gray-500" />
<span className={`${value ? "text-gray-600" : "text-gray-500"}`}> <span className={`${value ? "text-gray-600" : "text-gray-500"}`}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"} {estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
</span> </span>

View File

@ -56,7 +56,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
}} }}
label={ label={
<Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}> <Tooltip tooltipHeading="Estimate" tooltipContent={estimateValue}>
<div className="flex items-center gap-1 text-gray-500"> <div className="flex items-center gap-1 text-brand-secondary">
<PlayIcon className="h-3.5 w-3.5 -rotate-90" /> <PlayIcon className="h-3.5 w-3.5 -rotate-90" />
{estimateValue ?? "Estimate"} {estimateValue ?? "Estimate"}
</div> </div>

View File

@ -98,8 +98,7 @@ export const CreateUpdateStateInline: React.FC<Props> = ({ data, onClose, select
setToastAlert({ setToastAlert({
type: "error", type: "error",
title: "Error!", title: "Error!",
message: message: "State with that name already exists. Please try again with another name.",
"Another state exists with the same name. Please try again with another name.",
}); });
else else
setToastAlert({ setToastAlert({

View File

@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
// common
import { Props } from "./types";
// types // types
import { Props } from "./types";
export const Input: React.FC<Props> = ({ export const Input: React.FC<Props> = ({
label, label,
@ -21,7 +21,7 @@ export const Input: React.FC<Props> = ({
}) => ( }) => (
<> <>
{label && ( {label && (
<label htmlFor={id} className="mb-2 text-brand-muted-1"> <label htmlFor={id} className="text-brand-muted-1 mb-2">
{label} {label}
</label> </label>
)} )}
@ -42,7 +42,7 @@ export const Input: React.FC<Props> = ({
: mode === "trueTransparent" : mode === "trueTransparent"
? "rounded border-none bg-transparent ring-0" ? "rounded border-none bg-transparent ring-0"
: "" : ""
} ${error ? "border-red-500" : ""} ${error && mode === "primary" ? "bg-red-100" : ""} ${ } ${error ? "border-red-500/20" : ""} ${error && mode === "primary" ? "bg-red-500/20" : ""} ${
fullWidth ? "w-full" : "" fullWidth ? "w-full" : ""
} ${size === "rg" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`} } ${size === "rg" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`}
{...rest} {...rest}

View File

@ -163,5 +163,3 @@ export const PAGE_BLOCK_DETAILS = (pageId: string) => `PAGE_BLOCK_DETAILS_${page
export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`; export const ESTIMATES_LIST = (projectId: string) => `ESTIMATES_LIST_${projectId.toUpperCase()}`;
export const ESTIMATE_DETAILS = (estimateId: string) => export const ESTIMATE_DETAILS = (estimateId: string) =>
`ESTIMATE_DETAILS_${estimateId.toUpperCase()}`; `ESTIMATE_DETAILS_${estimateId.toUpperCase()}`;
export const ESTIMATE_POINTS_LIST = (estimateId: string) =>
`ESTIMATES_POINTS_LIST_${estimateId.toUpperCase()}`;

View File

@ -31,3 +31,5 @@ export const orderArrayBy = (
return 0; return 0;
}); });
}; };
export const checkDuplicates = (array: any[]) => new Set(array).size !== array.length;

View File

@ -1,5 +1,3 @@
import { useEffect } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import useSWR from "swr"; import useSWR from "swr";
@ -11,7 +9,7 @@ import useProjectDetails from "hooks/use-project-details";
// helpers // helpers
import { orderArrayBy } from "helpers/array.helper"; import { orderArrayBy } from "helpers/array.helper";
// fetch-keys // fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys"; import { ESTIMATE_DETAILS } from "constants/fetch-keys";
const useEstimateOption = (estimateKey?: number | null) => { const useEstimateOption = (estimateKey?: number | null) => {
const router = useRouter(); const router = useRouter();
@ -19,31 +17,26 @@ const useEstimateOption = (estimateKey?: number | null) => {
const { projectDetails } = useProjectDetails(); const { projectDetails } = useProjectDetails();
const { data: estimatePoints, error: estimatePointsError } = useSWR( const { data: estimateDetails, error: estimateDetailsError } = useSWR(
workspaceSlug && projectId && projectDetails && projectDetails?.estimate workspaceSlug && projectId && projectDetails && projectDetails?.estimate
? ESTIMATE_POINTS_LIST(projectDetails.estimate as string) ? ESTIMATE_DETAILS(projectDetails.estimate as string)
: null, : null,
workspaceSlug && projectId && projectDetails && projectDetails.estimate workspaceSlug && projectId && projectDetails && projectDetails.estimate
? () => ? () =>
estimatesService.getEstimatesPointsList( estimatesService.getEstimateDetails(
workspaceSlug as string, workspaceSlug.toString(),
projectId as string, projectId.toString(),
projectDetails.estimate as string projectDetails.estimate as string
) )
: null : null
); );
const estimateValue: any = const estimateValue: any =
(estimateKey && estimatePoints?.find((e) => e.key === estimateKey)?.value) ?? "None"; (estimateKey && estimateDetails?.points?.find((e) => e.key === estimateKey)?.value) ?? "None";
useEffect(() => {
if (estimatePointsError?.status === 404) router.push("/404");
else if (estimatePointsError) router.push("/error");
}, [estimatePointsError, router]);
return { return {
isEstimateActive: projectDetails?.estimate ? true : false, isEstimateActive: projectDetails?.estimate ? true : false,
estimatePoints: orderArrayBy(estimatePoints ?? [], "key"), estimatePoints: orderArrayBy(estimateDetails?.points ?? [], "key"),
estimateValue, estimateValue,
}; };
}; };

View File

@ -6,6 +6,7 @@ import useSWR, { mutate } from "swr";
// services // services
import estimatesService from "services/estimates.service"; import estimatesService from "services/estimates.service";
import projectService from "services/project.service";
// hooks // hooks
import useProjectDetails from "hooks/use-project-details"; import useProjectDetails from "hooks/use-project-details";
// layouts // layouts
@ -24,7 +25,6 @@ import { IEstimate, IProject } from "types";
import type { NextPage } from "next"; import type { NextPage } from "next";
// fetch-keys // fetch-keys
import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys"; import { ESTIMATES_LIST, PROJECT_DETAILS } from "constants/fetch-keys";
import projectService from "services/project.service";
const EstimatesSettings: NextPage = () => { const EstimatesSettings: NextPage = () => {
const [estimateFormOpen, setEstimateFormOpen] = useState(false); const [estimateFormOpen, setEstimateFormOpen] = useState(false);
@ -61,13 +61,6 @@ const EstimatesSettings: NextPage = () => {
estimatesService estimatesService
.deleteEstimate(workspaceSlug as string, projectId as string, estimateId) .deleteEstimate(workspaceSlug as string, projectId as string, estimateId)
.then(() => {
setToastAlert({
type: "success",
title: "Success!",
message: "Estimate Deleted successfully.",
});
})
.catch(() => { .catch(() => {
setToastAlert({ setToastAlert({
type: "error", type: "error",
@ -127,7 +120,7 @@ const EstimatesSettings: NextPage = () => {
<div className="col-span-12 space-y-5 sm:col-span-7"> <div className="col-span-12 space-y-5 sm:col-span-7">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span <span
className="flex items-center cursor-pointer gap-2 text-theme" className="flex cursor-pointer items-center gap-2 text-theme"
onClick={() => { onClick={() => {
setEstimateToUpdate(undefined); setEstimateToUpdate(undefined);
setEstimateFormOpen(true); setEstimateFormOpen(true);
@ -143,7 +136,7 @@ const EstimatesSettings: NextPage = () => {
</div> </div>
</section> </section>
{estimatesList && estimatesList.length > 0 && ( {estimatesList && estimatesList.length > 0 && (
<section className="mt-4 divide-y px-6 mb-8 rounded-xl border bg-white"> <section className="mt-4 mb-8 divide-y divide-brand-base rounded-xl border border-brand-base bg-brand-base px-6">
<> <>
{estimatesList ? ( {estimatesList ? (
estimatesList.map((estimate) => ( estimatesList.map((estimate) => (

View File

@ -1,7 +1,7 @@
// services // services
import APIService from "services/api.service"; import APIService from "services/api.service";
// types // types
import type { IEstimate, IEstimatePoint } from "types"; import type { IEstimate, IEstimateFormData, IEstimatePoint } from "types";
const { NEXT_PUBLIC_API_BASE_URL } = process.env; const { NEXT_PUBLIC_API_BASE_URL } = process.env;
@ -10,15 +10,35 @@ class ProjectEstimateServices extends APIService {
super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000"); super(NEXT_PUBLIC_API_BASE_URL || "http://localhost:8000");
} }
async createEstimate(workspaceSlug: string, projectId: string, data: any): Promise<any> { async createEstimate(
workspaceSlug: string,
projectId: string,
data: IEstimateFormData
): Promise<any> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data) return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async patchEstimate(
workspaceSlug: string,
projectId: string,
estimateId: string,
data: IEstimateFormData
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,
data
)
.then((response) => response?.data) .then((response) => response?.data)
.catch((error) => { .catch((error) => {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async getEstimate( async getEstimateDetails(
workspaceSlug: string, workspaceSlug: string,
projectId: string, projectId: string,
estimateId: string estimateId: string
@ -40,22 +60,6 @@ class ProjectEstimateServices extends APIService {
}); });
} }
async patchEstimate(
workspaceSlug: string,
projectId: string,
estimateId: string,
data: Partial<IEstimate>
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`,
data
)
.then((res) => res?.data)
.catch((err) => {
throw err?.response?.data;
});
}
async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise<any> { async deleteEstimate(workspaceSlug: string, projectId: string, estimateId: string): Promise<any> {
return this.delete( return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/` `/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
@ -65,72 +69,6 @@ class ProjectEstimateServices extends APIService {
throw error?.response?.data; throw error?.response?.data;
}); });
} }
async createEstimatePoints(
workspaceSlug: string,
projectId: string,
estimateId: string,
data: {
estimate_points: {
key: number;
value: string;
}[];
}
): Promise<any> {
return this.post(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/bulk-estimate-points/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getEstimatesPointDetails(
workspaceSlug: string,
projectId: string,
estimateId: string,
estimatePointId: string
): Promise<any> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimate/${estimateId}/estimate-points/${estimatePointId}`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getEstimatesPointsList(
workspaceSlug: string,
projectId: string,
estimateId: string
): Promise<IEstimatePoint[]> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/estimate-points/`
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async patchEstimatePoints(
workspaceSlug: string,
projectId: string,
estimateId: string,
data: any
): Promise<any> {
return this.patch(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/bulk-estimate-points/`,
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
} }
export default new ProjectEstimateServices(); export default new ProjectEstimateServices();

View File

@ -6,6 +6,7 @@ export interface IEstimate {
description: string; description: string;
created_by: string; created_by: string;
updated_by: string; updated_by: string;
points: IEstimatePoint[];
project: string; project: string;
workspace: string; workspace: string;
} }
@ -23,3 +24,14 @@ export interface IEstimatePoint {
value: string; value: string;
workspace: string; workspace: string;
} }
export interface IEstimateFormData {
estimate: {
name: string;
description: string;
};
estimate_points: {
key: number;
value: string;
}[];
}