feat(feedback): add feedback button to tenders table
This commit introduces the ability for users to navigate to a feedback page directly from the tenders list. A new feedback icon and a corresponding button have been added to the actions column of the tenders table. Clicking this button redirects the user to the feedback page associated with that specific tender. To support this feature, the following changes were made: - Added a new `FeedbackIcon` SVG component. - Defined API endpoints and created query hooks for the feedback module. - Fixed a query key inconsistency for company details to ensure data is correctly refetched after updates.
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
import Breadcrumb from "@/components/Breadcrumbs/Breadcrumb";
|
||||
import TenderFeedbackTable from "@/components/Tables/tenders/feedback";
|
||||
import { BreadcrumbItem } from "@/types/shared";
|
||||
import { use } from "react";
|
||||
interface IProps {
|
||||
params: Promise<{
|
||||
id: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
const TenderFeedbackPage = ({ params }: IProps) => {
|
||||
const { id } = use(params);
|
||||
|
||||
const breadcrumbItems: BreadcrumbItem[] = [
|
||||
{ href: "/", name: "Dashboard" },
|
||||
{ href: "/tenders", name: "Tenders" },
|
||||
{ href: `/tenders/feedback/${id}`, name: "Tender feedback" },
|
||||
];
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb items={breadcrumbItems} />
|
||||
<TenderFeedbackTable id={id} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TenderFeedbackPage;
|
||||
+14
-1
@@ -297,7 +297,20 @@ export function EmailIcon(props: IconProps) {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeedbackIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="24px"
|
||||
viewBox="0 -960 960 960"
|
||||
width="24px"
|
||||
fill="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path d="M480-360q17 0 28.5-11.5T520-400q0-17-11.5-28.5T480-440q-17 0-28.5 11.5T440-400q0 17 11.5 28.5T480-360Zm-40-160h80v-240h-80v240ZM80-80v-720q0-33 23.5-56.5T160-880h640q33 0 56.5 23.5T880-800v480q0 33-23.5 56.5T800-240H240L80-80Zm126-240h594v-480H160v525l46-45Zm-46 0v-480 480Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
export function PasswordIcon(props: IconProps) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
"use client";
|
||||
|
||||
import { ExclamationIcon } from "@/assets/icons";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
} from "@/components/ui/table";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { unixToDate } from "@/utils/shared";
|
||||
import { TableRow } from "../../../ui/table";
|
||||
import useTenderFeedbackPresenter from "./useTenderFeedbackPresenter";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface IProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const TenderFeedbackTable = ({ id }: IProps) => {
|
||||
const { feedback, isLoading, columns, pathName, router } =
|
||||
useTenderFeedbackPresenter({ id });
|
||||
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">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead className="capitalize" key={column} colSpan={100}>
|
||||
{column}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
{isLoading && <TableSkeleton column={columns.length} />}
|
||||
<TableBody>
|
||||
{feedback?.map((item, index) =>
|
||||
!item ? (
|
||||
<h6 key={index}>No feedback is submitted for this tender</h6>
|
||||
) : (
|
||||
<TableRow
|
||||
key={item.id}
|
||||
className="odd:bg-gray-2 dark:odd:bg-gray-7"
|
||||
>
|
||||
<TableCell className="text-start" colSpan={100}>
|
||||
{index + 1}
|
||||
</TableCell>
|
||||
<TableCell className="uppercase" colSpan={100}>
|
||||
{item.feedback_type}
|
||||
</TableCell>
|
||||
<TableCell className="uppercase" colSpan={100}>
|
||||
{unixToDate(item.updated_at)}
|
||||
</TableCell>
|
||||
<TableCell className="uppercase" colSpan={100}>
|
||||
{item?.tender?.title ?? "-"}
|
||||
</TableCell>
|
||||
<TableCell className="uppercase" colSpan={100}>
|
||||
{unixToDate(item.created_at)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={cn(`text flex gap-2 capitalize`)}
|
||||
colSpan={100}
|
||||
>
|
||||
<button
|
||||
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
|
||||
onClick={() => {
|
||||
router.push(`${pathName}/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<ExclamationIcon />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TenderFeedbackTable;
|
||||
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
import { useGetFeedback } from "@/hooks/queries";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
const useTenderFeedbackPresenter = ({ id }: { id: string }) => {
|
||||
const { data, isLoading } = useGetFeedback({ tender_id: id });
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const columns = [
|
||||
"row",
|
||||
"feedback type",
|
||||
"updated at",
|
||||
"tender name",
|
||||
"created at",
|
||||
"actions",
|
||||
];
|
||||
return { feedback: data?.data, isLoading, columns, pathName, router };
|
||||
};
|
||||
|
||||
export default useTenderFeedbackPresenter;
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import useTenderListPresenter from "./useTenderListPresenter";
|
||||
|
||||
import { ExclamationIcon } from "@/assets/icons";
|
||||
import { ExclamationIcon, FeedbackIcon } from "@/assets/icons";
|
||||
import Pagination from "@/components/ui/pagination";
|
||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -65,7 +65,10 @@ const TendersTable = () => {
|
||||
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||
<Status status={item.status}>{item.status}</Status>
|
||||
</TableCell>
|
||||
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||
<TableCell
|
||||
className={cn(`text flex gap-2 capitalize`)}
|
||||
colSpan={100}
|
||||
>
|
||||
<button
|
||||
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
|
||||
onClick={() => {
|
||||
@@ -74,6 +77,13 @@ const TendersTable = () => {
|
||||
>
|
||||
<ExclamationIcon />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
router.push(`${pathName}/feedback/${item.id}`);
|
||||
}}
|
||||
>
|
||||
<FeedbackIcon />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
),
|
||||
|
||||
@@ -528,7 +528,6 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
||||
</div>
|
||||
|
||||
<FormFooter />
|
||||
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from "./useCompaniesQueries";
|
||||
export * from "./useTenders";
|
||||
export * from "./useCustomerQueries";
|
||||
export * from "./useProfileQueries";
|
||||
export * from "./useFeedbackQueries"
|
||||
@@ -45,6 +45,9 @@ export const useUpdateCompany = (id: string) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
|
||||
});
|
||||
router.push("/companies");
|
||||
},
|
||||
});
|
||||
@@ -52,7 +55,7 @@ export const useUpdateCompany = (id: string) => {
|
||||
|
||||
export const useCompanyDetails = (id: string) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.COMPANIES.DETAILS(id), "details"],
|
||||
() => [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
|
||||
[id],
|
||||
);
|
||||
return useQuery({
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { API_ENDPOINTS } from "@/lib/api";
|
||||
import { feedbackService } from "@/lib/api/services/feedback-service";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export const useGetFeedSingleFeedback = (id: string) => {
|
||||
const queryKey = useMemo(() => [API_ENDPOINTS.FEEDBACK.DETAILS(id)], [id]);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => feedbackService.getSingleFeedback(id),
|
||||
});
|
||||
};
|
||||
export const useGetFeedback = (params: Record<string, any>) => {
|
||||
const queryKey = useMemo(
|
||||
() => [API_ENDPOINTS.FEEDBACK.READ_ALL, params],
|
||||
[params],
|
||||
);
|
||||
return useQuery({
|
||||
queryKey,
|
||||
queryFn: () => feedbackService.getFeedbacks(params),
|
||||
});
|
||||
};
|
||||
@@ -45,4 +45,9 @@ export const API_ENDPOINTS = {
|
||||
ASSIGN_COMPANY_TO_CUSTOMER: (id: string) =>
|
||||
`customers/${id}/companies/assign`,
|
||||
},
|
||||
FEEDBACK: {
|
||||
READ_ALL: "feedback",
|
||||
DETAILS: (id: string) => `feedback/${id}`,
|
||||
DELETE: (id: string) => `feedback/${id}`,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import api from "../axios";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import { ApiResponse } from "../types";
|
||||
import { TFeedbackResponse } from "../types/Feedback";
|
||||
|
||||
export const feedbackService = {
|
||||
getFeedbacks: async (
|
||||
params: Record<string, any>,
|
||||
): Promise<TFeedbackResponse> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.FEEDBACK.READ_ALL, { params })).data;
|
||||
} catch (error) {
|
||||
console.error("ERROR Caught in feedback service => getFeedbacks", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
getSingleFeedback: async (
|
||||
id: string,
|
||||
): Promise<ApiResponse<TFeedbackResponse>> => {
|
||||
try {
|
||||
return (await api.get(API_ENDPOINTS.FEEDBACK.DETAILS(id))).data;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"ERROR Caught in feedback service => getSingleFeedback",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import { z } from "zod";
|
||||
import { TenderSchema } from "./Tenders";
|
||||
export const feedbackSchema = z.object({
|
||||
company_id: z.string(),
|
||||
created_at: z.number(),
|
||||
customer_id: z.string(),
|
||||
feedback_type: z.string(),
|
||||
id: z.string(),
|
||||
tender: TenderSchema.or(z.null()),
|
||||
updated_at: z.number(),
|
||||
});
|
||||
export const feedbackResponseSchema = z.object({
|
||||
data: z.array(feedbackSchema),
|
||||
});
|
||||
export type TFeedback = z.infer<typeof feedbackSchema>;
|
||||
export type TFeedbackResponse = z.infer<typeof feedbackResponseSchema>;
|
||||
@@ -52,7 +52,7 @@ export interface ICreateCompanyCredentials {
|
||||
export const companySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
email: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
website: z.string(),
|
||||
type: z.string(),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const TenderSchema = z.object({
|
||||
export const TenderSchema = z.object({
|
||||
id: z.string(),
|
||||
buyer_organization: z.object({
|
||||
name: z.string(),
|
||||
|
||||
Reference in New Issue
Block a user