Merge pull request 'feat(feedback): add feedback button to tenders table' (#34) from tender-feedbacks into develop

Reviewed-on: https://repo.ravanertebat.com/TM/tm_panel/pulls/34
Reviewed-by: Sina Nakhostin <s.nakhostin@ravanertebat.com>
This commit is contained in:
Sina Nakhostin
2025-09-23 12:55:49 +03:30
15 changed files with 237 additions and 7 deletions
BIN
View File
Binary file not shown.
+28
View File
@@ -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
View File
@@ -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;
+12 -2
View File
@@ -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>
);
};
+1
View File
@@ -3,3 +3,4 @@ export * from "./useCompaniesQueries";
export * from "./useTenders";
export * from "./useCustomerQueries";
export * from "./useProfileQueries";
export * from "./useFeedbackQueries"
+4 -1
View File
@@ -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({
+22
View File
@@ -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),
});
};
+5
View File
@@ -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;
+30
View File
@@ -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;
}
},
};
+16
View File
@@ -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>;
+1 -1
View File
@@ -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 -1
View File
@@ -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(),