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:
AmirReza Jamali
2025-11-25 14:53:16 +03:30
parent 6da653cb15
commit 2ec752df0c
6 changed files with 154 additions and 7 deletions
@@ -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;
+55 -2
View File
@@ -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,
+5 -3
View File
@@ -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
+1 -1
View File
@@ -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"];
+1 -1
View File
@@ -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(),