feat(inquiries): Add change status functionality to inquiries
- Create ChangeStatusForm component with status, reason, and description fields - Add change status modal to inquiries table with form integration - Implement useChangeInquiryStatus hook integration in presenter - Add edit icon button to trigger status change modal for each inquiry - Update Status component to accept status prop for proper styling - Update FormFooter component to support form submission handling - Add isChangeStatusModalOpen state management to presenter - Update TInquiries type definitions to include change status credentials - Enhance inquiries list with status change workflow alongside existing delete functionality
This commit is contained in:
@@ -0,0 +1,76 @@
|
|||||||
|
"use client";
|
||||||
|
import InputGroup from "@/components/FormElements/InputGroup";
|
||||||
|
import { TextAreaGroup } from "@/components/FormElements/InputGroup/text-area";
|
||||||
|
import { Select } from "@/components/FormElements/select";
|
||||||
|
import FormFooter from "@/components/ui/FormFooter";
|
||||||
|
import { INQUIRY_STATUS } from "@/constants/enums";
|
||||||
|
import { TChangeInquiryStatusCredentials } from "@/lib/api";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
interface ChangeStatusFormProps {
|
||||||
|
onSubmit: (data: TChangeInquiryStatusCredentials) => void;
|
||||||
|
onCancel: () => void;
|
||||||
|
currentStatus: string;
|
||||||
|
isSubmitting?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChangeStatusForm = ({
|
||||||
|
onSubmit,
|
||||||
|
onCancel,
|
||||||
|
currentStatus,
|
||||||
|
isSubmitting,
|
||||||
|
}: ChangeStatusFormProps) => {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<TChangeInquiryStatusCredentials>({
|
||||||
|
defaultValues: {
|
||||||
|
status: currentStatus as any,
|
||||||
|
reason: "",
|
||||||
|
description: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusOptions = Object.values(INQUIRY_STATUS).map((status) => ({
|
||||||
|
label: status.charAt(0).toUpperCase() + status.slice(1),
|
||||||
|
value: status,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
|
<Select
|
||||||
|
{...register("status", { required: "Status is required" })}
|
||||||
|
name="status"
|
||||||
|
label="Status"
|
||||||
|
items={statusOptions}
|
||||||
|
defaultValue={currentStatus}
|
||||||
|
errors={errors}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputGroup
|
||||||
|
{...register("reason", { required: "Reason is required" })}
|
||||||
|
name="reason"
|
||||||
|
label="Reason"
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter reason for status change"
|
||||||
|
errors={errors}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextAreaGroup
|
||||||
|
{...register("description", { required: "Description is required" })}
|
||||||
|
name="description"
|
||||||
|
label="Description"
|
||||||
|
placeholder="Enter detailed description"
|
||||||
|
errors={errors}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormFooter onCancel={onCancel} />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangeStatusForm;
|
||||||
@@ -18,9 +18,22 @@ import TableSkeleton from "@/components/ui/TableSkeleton";
|
|||||||
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
import { _TooltipDefaultParams } from "@/constants/tooltip";
|
||||||
import { formatPhoneNumber, unixToDate } from "@/utils/shared";
|
import { formatPhoneNumber, unixToDate } from "@/utils/shared";
|
||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
|
import ChangeStatusForm from "./ChangeStatusForm";
|
||||||
import InquiriesListFilters from "./InquiriesListFilters";
|
import InquiriesListFilters from "./InquiriesListFilters";
|
||||||
import useInquiriesListPresenter from "./useInquiriesListPresenter";
|
import useInquiriesListPresenter from "./useInquiriesListPresenter";
|
||||||
|
|
||||||
|
const EditIcon = (props: React.SVGProps<SVGSVGElement>) => (
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="currentColor"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M13.7 2.3c.4-.4 1-.4 1.4 0l.6.6c.4.4.4 1 0 1.4l-1 1-2-2 1-1zm-2 2l-8 8v2h2l8-8-2-2z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const InquiriesTable = () => {
|
const InquiriesTable = () => {
|
||||||
const {
|
const {
|
||||||
columns,
|
columns,
|
||||||
@@ -31,9 +44,13 @@ const InquiriesTable = () => {
|
|||||||
isPending,
|
isPending,
|
||||||
currentInquiry,
|
currentInquiry,
|
||||||
isDeleteModalOpen,
|
isDeleteModalOpen,
|
||||||
|
isChangeStatusModalOpen,
|
||||||
setCurrentInquiry,
|
setCurrentInquiry,
|
||||||
setIsDeleteModalOpen,
|
setIsDeleteModalOpen,
|
||||||
|
setIsChangeStatusModalOpen,
|
||||||
deleteInquiry,
|
deleteInquiry,
|
||||||
|
handleChangeStatus,
|
||||||
|
isChangingStatus,
|
||||||
filterRegister,
|
filterRegister,
|
||||||
handleFilterSubmit,
|
handleFilterSubmit,
|
||||||
search,
|
search,
|
||||||
@@ -44,7 +61,7 @@ const InquiriesTable = () => {
|
|||||||
} = useInquiriesListPresenter();
|
} = useInquiriesListPresenter();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
<div className="flex flex-col rounded-[10px] bg-white px-7.5 pb-4 pt-7.5 shadow-1 dark:bg-gray-dark dark:shadow-card">
|
||||||
<ListHeader onFilter={() => setIsFilterModalOpen(true)} />
|
<ListHeader onFilter={() => setIsFilterModalOpen(true)} hasCreate={false}/>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="border-none uppercase [&>th]:text-start">
|
<TableRow className="border-none uppercase [&>th]:text-start">
|
||||||
@@ -77,13 +94,31 @@ const InquiriesTable = () => {
|
|||||||
{formatPhoneNumber(inquiry?.phone_number)}
|
{formatPhoneNumber(inquiry?.phone_number)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
<Status>{inquiry.status}</Status>
|
<Status status={inquiry?.status ?? "pending"}>
|
||||||
|
{inquiry?.status}
|
||||||
|
</Status>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start" colSpan={100}>
|
<TableCell className="text-start" colSpan={100}>
|
||||||
{unixToDate({ unix: inquiry.created_at, hasTime: true })}
|
{unixToDate({ unix: inquiry.created_at, hasTime: true })}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-start xl:pr-7.5">
|
<TableCell className="text-start xl:pr-7.5">
|
||||||
<div className="flex items-center justify-start gap-x-3.5">
|
<div className="flex items-center justify-start gap-x-3.5">
|
||||||
|
<button
|
||||||
|
data-tooltip-id="change-status"
|
||||||
|
data-tooltip-content="Change Status"
|
||||||
|
data-tooltip-place="top"
|
||||||
|
className="hover:text-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentInquiry(inquiry);
|
||||||
|
setIsChangeStatusModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
{..._TooltipDefaultParams({ id: "change-status" })}
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Change Status</span>
|
||||||
|
<EditIcon />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
data-tooltip-id="delete"
|
data-tooltip-id="delete"
|
||||||
data-tooltip-content="Delete"
|
data-tooltip-content="Delete"
|
||||||
@@ -134,6 +169,24 @@ const InquiriesTable = () => {
|
|||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<Modal
|
||||||
|
isOpen={isChangeStatusModalOpen}
|
||||||
|
onClose={() => setIsChangeStatusModalOpen(false)}
|
||||||
|
classNames="w-full xl:w-1/2"
|
||||||
|
showButtons={false}
|
||||||
|
>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h3 className="text-xl font-semibold text-dark dark:text-white">
|
||||||
|
Change Status - {currentInquiry?.full_name}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<ChangeStatusForm
|
||||||
|
onSubmit={handleChangeStatus}
|
||||||
|
onCancel={() => setIsChangeStatusModalOpen(false)}
|
||||||
|
currentStatus={currentInquiry?.status ?? "pending"}
|
||||||
|
isSubmitting={isChangingStatus}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
{isDeleteModalOpen && (
|
{isDeleteModalOpen && (
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
isOpen={isDeleteModalOpen}
|
isOpen={isDeleteModalOpen}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import {
|
import {
|
||||||
|
useChangeInquiryStatus,
|
||||||
useDeleteInquiry,
|
useDeleteInquiry,
|
||||||
useGetInquiries,
|
useGetInquiries,
|
||||||
} from "@/hooks/queries/useInquiryQueries";
|
} from "@/hooks/queries/useInquiryQueries";
|
||||||
@@ -13,6 +14,7 @@ import { useForm } from "react-hook-form";
|
|||||||
const useInquiriesListPresenter = () => {
|
const useInquiriesListPresenter = () => {
|
||||||
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
const [isFilterModalOpen, setIsFilterModalOpen] = useState(false);
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||||
|
const [isChangeStatusModalOpen, setIsChangeStatusModalOpen] = useState(false);
|
||||||
const [currentInquiry, setCurrentInquiry] = useState<TInquiries | null>(null);
|
const [currentInquiry, setCurrentInquiry] = useState<TInquiries | null>(null);
|
||||||
const [params, setParams] = useState<Record<string, any>>({});
|
const [params, setParams] = useState<Record<string, any>>({});
|
||||||
const columns = [
|
const columns = [
|
||||||
@@ -51,6 +53,8 @@ const useInquiriesListPresenter = () => {
|
|||||||
|
|
||||||
const { data, isPending } = useGetInquiries(params);
|
const { data, isPending } = useGetInquiries(params);
|
||||||
const { mutate } = useDeleteInquiry(() => setIsDeleteModalOpen(false));
|
const { mutate } = useDeleteInquiry(() => setIsDeleteModalOpen(false));
|
||||||
|
const { mutate: changeStatus, isPending: isChangingStatus } =
|
||||||
|
useChangeInquiryStatus(() => setIsChangeStatusModalOpen(false));
|
||||||
|
|
||||||
const search = (data: any) => {
|
const search = (data: any) => {
|
||||||
const newParams = { ...params, ...data, offset: 0 };
|
const newParams = { ...params, ...data, offset: 0 };
|
||||||
@@ -65,6 +69,14 @@ const useInquiriesListPresenter = () => {
|
|||||||
mutate(currentInquiry.id);
|
mutate(currentInquiry.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleChangeStatus = (data: any) => {
|
||||||
|
if (!currentInquiry?.id) return;
|
||||||
|
changeStatus({
|
||||||
|
id: currentInquiry.id,
|
||||||
|
credentials: data,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
columns,
|
columns,
|
||||||
inquiries: data?.data.inquiries,
|
inquiries: data?.data.inquiries,
|
||||||
@@ -74,9 +86,13 @@ const useInquiriesListPresenter = () => {
|
|||||||
isPending,
|
isPending,
|
||||||
isDeleteModalOpen,
|
isDeleteModalOpen,
|
||||||
setIsDeleteModalOpen,
|
setIsDeleteModalOpen,
|
||||||
|
isChangeStatusModalOpen,
|
||||||
|
setIsChangeStatusModalOpen,
|
||||||
currentInquiry,
|
currentInquiry,
|
||||||
setCurrentInquiry,
|
setCurrentInquiry,
|
||||||
deleteInquiry,
|
deleteInquiry,
|
||||||
|
handleChangeStatus,
|
||||||
|
isChangingStatus,
|
||||||
filterRegister,
|
filterRegister,
|
||||||
handleFilterSubmit,
|
handleFilterSubmit,
|
||||||
search,
|
search,
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { useIsMutating } from "@tanstack/react-query";
|
import { useIsMutating } from "@tanstack/react-query";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface IProps {}
|
interface IProps {
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
const FormFooter = ({}: IProps) => {
|
const FormFooter = ({ onCancel }: IProps) => {
|
||||||
const isMutating = useIsMutating();
|
const isMutating = useIsMutating();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
@@ -21,7 +23,7 @@ const FormFooter = ({}: IProps) => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={router.back}
|
onClick={onCancel ?? router.back}
|
||||||
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
className="mt-6 flex w-fit justify-center rounded-lg bg-error p-[13px] font-medium text-white hover:bg-opacity-90"
|
||||||
>
|
>
|
||||||
Opt out
|
Opt out
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const Status = ({ status = "draft", children }: IProps) => {
|
|||||||
"published",
|
"published",
|
||||||
"sent",
|
"sent",
|
||||||
];
|
];
|
||||||
const blues = ["awarded", "processing", "in-review", "scheduled"];
|
const blues = ["awarded", "processing", "in-review", "scheduled", "reviewed"];
|
||||||
const yellows = ["pending", "warning", "on-hold", "delayed"];
|
const yellows = ["pending", "warning", "on-hold", "delayed"];
|
||||||
const oranges = ["expired", "expiring", "limited"];
|
const oranges = ["expired", "expiring", "limited"];
|
||||||
const grays = ["draft", "disabled", "archived", "paused", "suspended"];
|
const grays = ["draft", "disabled", "archived", "paused", "suspended"];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createApiResponseSchema } from "./Factory";
|
|||||||
const changeInquiryStatusCredentials = z.object({
|
const changeInquiryStatusCredentials = z.object({
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
reason: z.string(),
|
reason: z.string(),
|
||||||
status: z.enum(INQUIRY_STATUS),
|
status: z.nativeEnum(INQUIRY_STATUS),
|
||||||
});
|
});
|
||||||
const inquiriesSchema = z.object({
|
const inquiriesSchema = z.object({
|
||||||
company_name: z.string(),
|
company_name: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user