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
|
||||
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>
|
||||
|
@ -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 "./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 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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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({
|
||||
|
@ -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}
|
||||
|
@ -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()}`;
|
||||
|
@ -31,3 +31,5 @@ export const orderArrayBy = (
|
||||
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 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,
|
||||
};
|
||||
};
|
||||
|
@ -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) => (
|
||||
|
@ -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();
|
||||
|
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;
|
||||
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;
|
||||
}[];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user