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
import { useForm } from "react-hook-form";
// headless ui
import { Dialog, Transition } from "@headlessui/react";
// services
import estimatesService from "services/estimates.service";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
import { Dialog, Transition } from "@headlessui/react";
// hooks
import useToast from "hooks/use-toast";
// ui
import { Input, PrimaryButton, SecondaryButton, TextArea } from "components/ui";
// helpers
import { checkDuplicates } from "helpers/array.helper";
// types
import { IEstimate } from "types";
import { IEstimate, IEstimateFormData } from "types";
// fetch-keys
import { ESTIMATES_LIST } from "constants/fetch-keys";
import { ESTIMATES_LIST, ESTIMATE_DETAILS } from "constants/fetch-keys";
type Props = {
isOpen: boolean;
@ -26,18 +27,35 @@ type Props = {
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: "",
description: "",
value1: "",
value2: "",
value3: "",
value4: "",
value5: "",
value6: "",
};
export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data, isOpen }) => {
const {
register,
formState: { errors, isSubmitting },
formState: { isSubmitting },
handleSubmit,
reset,
} = useForm<IEstimate>({
} = useForm<FormValues>({
defaultValues,
});
@ -51,47 +69,48 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
const { setToastAlert } = useToast();
const createEstimate = async (formData: IEstimate) => {
const createEstimate = async (payload: IEstimateFormData) => {
if (!workspaceSlug || !projectId) return;
const payload = {
name: formData.name,
description: formData.description,
};
await estimatesService
.createEstimate(workspaceSlug as string, projectId as string, payload)
.then((res) => {
mutate<IEstimate[]>(
ESTIMATES_LIST(projectId as string),
(prevData) => [res, ...(prevData ?? [])],
false
);
.then(() => {
mutate(ESTIMATES_LIST(projectId as string));
onClose();
})
.catch(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Error: Estimate could not be created",
});
.catch((err) => {
if (err.status === 400)
setToastAlert({
type: "error",
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;
const payload = {
name: formData.name,
description: formData.description,
};
mutate<IEstimate[]>(
ESTIMATES_LIST(projectId as string),
ESTIMATES_LIST(projectId.toString()),
(prevData) =>
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;
}),
@ -100,23 +119,116 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
await estimatesService
.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(() => {
setToastAlert({
type: "error",
title: "Error!",
message: "Error: Estimate could not be updated",
message: "Estimate could not be updated. Please try again.",
});
});
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(() => {
reset({
...defaultValues,
...data,
});
if (data)
reset({
...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]);
return (
@ -146,10 +258,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
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={data ? handleSubmit(updateEstimate) : handleSubmit(createEstimate)}
>
<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 onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-3">
<div className="text-lg font-medium leading-6">
{data ? "Update" : "Create"} Estimate
@ -161,17 +271,8 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
type="name"
placeholder="Title"
autoComplete="off"
mode="transparent"
className="resize-none text-xl"
error={errors.name}
register={register}
validations={{
required: "Title is required",
maxLength: {
value: 255,
message: "Title should be less than 255 characters",
},
}}
/>
</div>
<div>
@ -180,11 +281,107 @@ export const CreateUpdateEstimateModal: React.FC<Props> = ({ handleClose, data,
name="description"
placeholder="Description"
className="h-32 resize-none text-sm"
mode="transparent"
error={errors.description}
register={register}
/>
</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 className="mt-5 flex justify-end gap-2">
<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 "./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 useSWR from "swr";
// services
import estimatesService from "services/estimates.service";
import projectService from "services/project.service";
// hooks
import useToast from "hooks/use-toast";
import useProjectDetails from "hooks/use-project-details";
// components
import { EstimatePointsModal, DeleteEstimateModal } from "components/estimates";
import { DeleteEstimateModal } from "components/estimates";
// ui
import { CustomMenu } from "components/ui";
import { CustomMenu, SecondaryButton } from "components/ui";
//icons
import {
PencilIcon,
TrashIcon,
SquaresPlusIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types
import { IEstimate } from "types";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
type Props = {
estimate: IEstimate;
@ -39,7 +29,6 @@ export const SingleEstimate: React.FC<Props> = ({
editEstimate,
handleEstimateDelete,
}) => {
const [isEstimatePointsModalOpen, setIsEstimatePointsModalOpen] = useState(false);
const [isDeleteEstimateModalOpen, setIsDeleteEstimateModalOpen] = useState(false);
const router = useRouter();
@ -49,18 +38,6 @@ export const SingleEstimate: React.FC<Props> = ({
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 () => {
if (!workspaceSlug || !projectId) return;
@ -87,78 +64,61 @@ export const SingleEstimate: React.FC<Props> = ({
return (
<>
<EstimatePointsModal
isOpen={isEstimatePointsModalOpen}
estimate={estimate}
onClose={() => setIsEstimatePointsModalOpen(false)}
data={estimatePoints ? orderArrayBy(estimatePoints, "key") : undefined}
/>
<div className="gap-2 py-3">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<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}
{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
</span>
)}
</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}
</p>
</div>
<CustomMenu ellipsis>
{projectDetails?.estimate !== estimate.id &&
estimatePoints &&
estimatePoints.length > 0 && (
<CustomMenu.MenuItem onClick={handleUseEstimate}>
<div className="flex items-center justify-start gap-2">
<SquaresPlusIcon className="h-3.5 w-3.5" />
<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 && (
<div className="flex items-center gap-2">
{projectDetails?.estimate !== estimate.id && estimate.points.length > 0 && (
<SecondaryButton onClick={handleUseEstimate} className="py-1">
Use
</SecondaryButton>
)}
<CustomMenu ellipsis>
<CustomMenu.MenuItem
onClick={() => {
setIsDeleteEstimateModalOpen(true);
editEstimate(estimate);
}}
>
<div className="flex items-center justify-start gap-2">
<TrashIcon className="h-3.5 w-3.5" />
<span>Delete estimate</span>
<PencilIcon className="h-3.5 w-3.5" />
<span>Edit estimate</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
{projectDetails?.estimate !== estimate.id && (
<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>
{estimatePoints && estimatePoints.length > 0 ? (
<div className="flex text-sm text-gray-400">
{estimate.points.length > 0 ? (
<div className="flex text-xs text-brand-secondary">
Estimate points (
<span className="flex gap-1">
{estimatePoints.map((point, index) => (
<h6 key={point.id}>
{orderArrayBy(estimate.points, "key").map((point, index) => (
<h6 key={point.id} className="text-brand-secondary">
{point.value}
{index !== estimatePoints.length - 1 && ","}{" "}
{index !== estimate.points.length - 1 && ","}{" "}
</h6>
))}
</span>
@ -166,7 +126,7 @@ export const SingleEstimate: React.FC<Props> = ({
</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>

View File

@ -22,7 +22,7 @@ export const IssueEstimateSelect: React.FC<Props> = ({ value, onChange }) => {
value={value}
label={
<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"}`}>
{estimatePoints?.find((e) => e.key === value)?.value ?? "Estimate"}
</span>

View File

@ -56,7 +56,7 @@ export const ViewEstimateSelect: React.FC<Props> = ({
}}
label={
<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" />
{estimateValue ?? "Estimate"}
</div>

View File

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

View File

@ -1,7 +1,7 @@
import * as React from "react";
// common
import { Props } from "./types";
// types
import { Props } from "./types";
export const Input: React.FC<Props> = ({
label,
@ -21,7 +21,7 @@ export const Input: React.FC<Props> = ({
}) => (
<>
{label && (
<label htmlFor={id} className="mb-2 text-brand-muted-1">
<label htmlFor={id} className="text-brand-muted-1 mb-2">
{label}
</label>
)}
@ -42,7 +42,7 @@ export const Input: React.FC<Props> = ({
: mode === "trueTransparent"
? "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" : ""
} ${size === "rg" ? "px-3 py-2" : size === "lg" ? "p-3" : ""} ${className}`}
{...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 ESTIMATE_DETAILS = (estimateId: string) =>
`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;
});
};
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 useSWR from "swr";
@ -11,7 +9,7 @@ import useProjectDetails from "hooks/use-project-details";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// fetch-keys
import { ESTIMATE_POINTS_LIST } from "constants/fetch-keys";
import { ESTIMATE_DETAILS } from "constants/fetch-keys";
const useEstimateOption = (estimateKey?: number | null) => {
const router = useRouter();
@ -19,31 +17,26 @@ const useEstimateOption = (estimateKey?: number | null) => {
const { projectDetails } = useProjectDetails();
const { data: estimatePoints, error: estimatePointsError } = useSWR(
const { data: estimateDetails, error: estimateDetailsError } = useSWR(
workspaceSlug && projectId && projectDetails && projectDetails?.estimate
? ESTIMATE_POINTS_LIST(projectDetails.estimate as string)
? ESTIMATE_DETAILS(projectDetails.estimate as string)
: null,
workspaceSlug && projectId && projectDetails && projectDetails.estimate
? () =>
estimatesService.getEstimatesPointsList(
workspaceSlug as string,
projectId as string,
estimatesService.getEstimateDetails(
workspaceSlug.toString(),
projectId.toString(),
projectDetails.estimate as string
)
: null
);
const estimateValue: any =
(estimateKey && estimatePoints?.find((e) => e.key === estimateKey)?.value) ?? "None";
useEffect(() => {
if (estimatePointsError?.status === 404) router.push("/404");
else if (estimatePointsError) router.push("/error");
}, [estimatePointsError, router]);
(estimateKey && estimateDetails?.points?.find((e) => e.key === estimateKey)?.value) ?? "None";
return {
isEstimateActive: projectDetails?.estimate ? true : false,
estimatePoints: orderArrayBy(estimatePoints ?? [], "key"),
estimatePoints: orderArrayBy(estimateDetails?.points ?? [], "key"),
estimateValue,
};
};

View File

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

View File

@ -1,7 +1,7 @@
// services
import APIService from "services/api.service";
// types
import type { IEstimate, IEstimatePoint } from "types";
import type { IEstimate, IEstimateFormData, IEstimatePoint } from "types";
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");
}
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)
.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)
.catch((error) => {
throw error?.response?.data;
});
}
async getEstimate(
async getEstimateDetails(
workspaceSlug: string,
projectId: 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> {
return this.delete(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/estimates/${estimateId}/`
@ -65,72 +69,6 @@ class ProjectEstimateServices extends APIService {
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();

View File

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