forked from github/plane
chore: new estimates workflow (#927)
* chore: new services for estimates * chore: new estimates workflow
This commit is contained in:
parent
cb814dd68b
commit
c638b6aba6
@ -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>
|
||||||
|
@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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"
|
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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({
|
||||||
|
@ -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}
|
||||||
|
@ -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()}`;
|
|
||||||
|
@ -31,3 +31,5 @@ export const orderArrayBy = (
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const checkDuplicates = (array: any[]) => new Set(array).size !== array.length;
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -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) => (
|
||||||
|
@ -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();
|
||||||
|
12
apps/app/types/estimate.d.ts
vendored
12
apps/app/types/estimate.d.ts
vendored
@ -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;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user