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:
@@ -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>
|
</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) {
|
export function PasswordIcon(props: IconProps) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<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";
|
} from "@/components/ui/table";
|
||||||
import useTenderListPresenter from "./useTenderListPresenter";
|
import useTenderListPresenter from "./useTenderListPresenter";
|
||||||
|
|
||||||
import { ExclamationIcon } from "@/assets/icons";
|
import { ExclamationIcon, FeedbackIcon } from "@/assets/icons";
|
||||||
import Pagination from "@/components/ui/pagination";
|
import Pagination from "@/components/ui/pagination";
|
||||||
import TableSkeleton from "@/components/ui/TableSkeleton";
|
import TableSkeleton from "@/components/ui/TableSkeleton";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -65,7 +65,10 @@ const TendersTable = () => {
|
|||||||
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
||||||
<Status status={item.status}>{item.status}</Status>
|
<Status status={item.status}>{item.status}</Status>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className={cn(`text capitalize`)} colSpan={100}>
|
<TableCell
|
||||||
|
className={cn(`text flex gap-2 capitalize`)}
|
||||||
|
colSpan={100}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
|
className="text-gray-40 rounded-full bg-gray-dark bg-opacity-5 hover:text-green-dark dark:bg-gray dark:bg-opacity-5"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -74,6 +77,13 @@ const TendersTable = () => {
|
|||||||
>
|
>
|
||||||
<ExclamationIcon />
|
<ExclamationIcon />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
router.push(`${pathName}/feedback/${item.id}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FeedbackIcon />
|
||||||
|
</button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -528,7 +528,6 @@ const CreateCompany = ({ defaultValues, editMode, id }: IProps) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FormFooter />
|
<FormFooter />
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ export * from "./useCompaniesQueries";
|
|||||||
export * from "./useTenders";
|
export * from "./useTenders";
|
||||||
export * from "./useCustomerQueries";
|
export * from "./useCustomerQueries";
|
||||||
export * from "./useProfileQueries";
|
export * from "./useProfileQueries";
|
||||||
|
export * from "./useFeedbackQueries"
|
||||||
@@ -45,6 +45,9 @@ export const useUpdateCompany = (id: string) => {
|
|||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
|
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL],
|
||||||
});
|
});
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
|
||||||
|
});
|
||||||
router.push("/companies");
|
router.push("/companies");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -52,7 +55,7 @@ export const useUpdateCompany = (id: string) => {
|
|||||||
|
|
||||||
export const useCompanyDetails = (id: string) => {
|
export const useCompanyDetails = (id: string) => {
|
||||||
const queryKey = useMemo(
|
const queryKey = useMemo(
|
||||||
() => [API_ENDPOINTS.COMPANIES.DETAILS(id), "details"],
|
() => [API_ENDPOINTS.COMPANIES.READ_ALL, "details"],
|
||||||
[id],
|
[id],
|
||||||
);
|
);
|
||||||
return useQuery({
|
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) =>
|
ASSIGN_COMPANY_TO_CUSTOMER: (id: string) =>
|
||||||
`customers/${id}/companies/assign`,
|
`customers/${id}/companies/assign`,
|
||||||
},
|
},
|
||||||
|
FEEDBACK: {
|
||||||
|
READ_ALL: "feedback",
|
||||||
|
DETAILS: (id: string) => `feedback/${id}`,
|
||||||
|
DELETE: (id: string) => `feedback/${id}`,
|
||||||
|
},
|
||||||
} as const;
|
} 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({
|
export const companySchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
email: z.string().email().optional(),
|
email: z.string().optional(),
|
||||||
phone: z.string().optional(),
|
phone: z.string().optional(),
|
||||||
website: z.string(),
|
website: z.string(),
|
||||||
type: z.string(),
|
type: z.string(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const TenderSchema = z.object({
|
export const TenderSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
buyer_organization: z.object({
|
buyer_organization: z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user