From 03fc293ee909a19b8c07a82cfd57bd007995e49f Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sun, 7 Jun 2026 14:47:11 +0330 Subject: [PATCH] list of requests --- src/components/DefaultTableSkeleton.tsx | 25 ++ src/components/EditorTable.tsx | 114 +++++++++ src/components/Table.tsx | 216 ++++++++---------- src/components/Td.tsx | 21 ++ src/components/types/TableTypes.ts | 27 +++ src/pages/designer/List.tsx | 94 ++++++++ src/pages/designer/hooks/useDesignerData.ts | 9 +- src/pages/designer/service/DesignerService.ts | 19 +- src/pages/designer/types/Types.ts | 17 ++ .../components/canvas/ObjectRenderer.tsx | 4 +- src/router/MainRouter.tsx | 45 ++-- src/shared/SideBar.tsx | 8 +- 12 files changed, 430 insertions(+), 169 deletions(-) create mode 100644 src/components/DefaultTableSkeleton.tsx create mode 100644 src/components/EditorTable.tsx create mode 100644 src/components/Td.tsx create mode 100644 src/components/types/TableTypes.ts create mode 100644 src/pages/designer/List.tsx diff --git a/src/components/DefaultTableSkeleton.tsx b/src/components/DefaultTableSkeleton.tsx new file mode 100644 index 0000000..b042f70 --- /dev/null +++ b/src/components/DefaultTableSkeleton.tsx @@ -0,0 +1,25 @@ +import Td from "@/components/Td"; +import { type FC, Fragment } from "react"; + +type Props = { + tdCount: number; + trCount?: number; +}; + +const DefaultTableSkeleton: FC = ({ tdCount, trCount = 5 }) => { + return ( + + {Array.from({ length: trCount }).map((_, rowIndex) => ( + + {Array.from({ length: tdCount }).map((_, colIndex) => ( + +
+ + ))} + + ))} + + ); +}; + +export default DefaultTableSkeleton; diff --git a/src/components/EditorTable.tsx b/src/components/EditorTable.tsx new file mode 100644 index 0000000..52f6610 --- /dev/null +++ b/src/components/EditorTable.tsx @@ -0,0 +1,114 @@ +import React from "react"; +import { Group, Rect, Text } from "react-konva"; +import Konva from "konva"; +import type { EditorObject } from "@/pages/editor/store/editorStore"; + +type EditorTableProps = { + obj: EditorObject; + selectedCellId: string | null; + onCellClick: (tableId: string, cellId: string, node: Konva.Node, e?: Konva.KonvaEventObject) => void; + onCellDblClick: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void; + onDragEnd: (tableId: string, x: number, y: number) => void; + draggable: boolean; +}; + +const EditorTable = ({ obj, selectedCellId, onCellClick, onCellDblClick, onDragEnd, draggable }: EditorTableProps) => { + const groupRef = React.useRef(null); + const tableData = obj.tableData; + + React.useEffect(() => { + if (groupRef.current) { + groupRef.current.setAttr("id", obj.id); + } + }, [obj.id]); + + if (!tableData) return null; + + const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = tableData; + + const handleCellClick = (cellId: string, e: Konva.KonvaEventObject) => { + e.cancelBubble = true; + if (groupRef.current) { + onCellClick(obj.id, cellId, groupRef.current, e); + } + }; + + const handleCellDblClick = (cellId: string, e: Konva.KonvaEventObject) => { + e.cancelBubble = true; + const stage = e.target.getStage(); + if (!stage || !groupRef.current) return; + + const cell = cells[cellId]; + if (!cell) return; + + const targetNode = e.target; + const box = targetNode.getClientRect(); + const stageBox = stage.container().getBoundingClientRect(); + const x = stageBox.left + box.x; + const y = stageBox.top + box.y; + const width = box.width; + const height = box.height; + + onCellDblClick(obj.id, cellId, x, y, width, height, cell.text); + }; + + const handleDragEnd = () => { + if (groupRef.current) { + const pos = groupRef.current.position(); + onDragEnd(obj.id, pos.x, pos.y); + } + }; + + return ( + + {Array.from({ length: rows }).map((_, row) => + Array.from({ length: cols }).map((_, col) => { + const cellId = `cell-${row}-${col}`; + const cell = cells[cellId]; + if (!cell) return null; + + const x = col * cellWidth; + const y = row * cellHeight; + const isCellSelected = selectedCellId === cellId; + const isTopLeft = row === 0 && col === 0; + const isTopRight = row === 0 && col === cols - 1; + + return ( + + handleCellClick(cellId, e)} + onDblClick={(e) => handleCellDblClick(cellId, e)} + /> + {cell.text && ( + handleCellClick(cellId, e)} + onDblClick={(e) => handleCellDblClick(cellId, e)} + /> + )} + + ); + }), + )} + + ); +}; + +export default EditorTable; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index 949f586..078dbb7 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -1,134 +1,108 @@ -import React from "react"; -import { Group, Rect, Text } from "react-konva"; -import Konva from "konva"; -import type { EditorObject } from "@/pages/editor/store/editorStore"; +import DefaultTableSkeleton from "@/components/DefaultTableSkeleton"; +import Td from "@/components/Td"; +import type { ColumnType, RowDataType, TableProps } from "@/components/types/TableTypes"; +import { type ReactElement } from "react"; -type TableProps = { - obj: EditorObject; - selectedCellId: string | null; - onCellClick: (tableId: string, cellId: string, node: Konva.Node, e?: Konva.KonvaEventObject) => void; - onCellDblClick: (tableId: string, cellId: string, x: number, y: number, width: number, height: number, text: string) => void; - onDragEnd: (tableId: string, x: number, y: number) => void; - draggable: boolean; -}; +const Table = ({ + columns, + data, + isLoading = false, + onRowClick, + className = "", + rowClassName = "", + noDataMessage = "هیچ داده‌ای یافت نشد", + headerClassName = "bg-gray-50", + emptyRowsCount = 5, + showHeader = true, + actions = null, + actionsPosition = "top", +}: TableProps): ReactElement => { + const getRowClassName = (item: T, index: number): string => { + if (typeof rowClassName === "function") { + return rowClassName(item, index); + } + return rowClassName; + }; -const Table = ({ - obj, - selectedCellId, - onCellClick, - onCellDblClick, - onDragEnd, - draggable, -}: TableProps) => { - const groupRef = React.useRef(null); - const tableData = obj.tableData; + const getCellClassName = (column: ColumnType): string => { + const alignClass = column.align ? `text-${column.align}` : ""; + return `px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${alignClass} ${column.className || ""}`.trim(); + }; - React.useEffect(() => { - if (groupRef.current) { - groupRef.current.setAttr("id", obj.id); - } - }, [obj.id]); + const handleRowClick = (item: T) => { + if (onRowClick) { + onRowClick(item.id, item); + } + }; - if (!tableData) return null; + const renderTableBody = () => { + if (isLoading) { + return ; + } - const { rows, cols, cells, cellWidth, cellHeight, stroke, strokeWidth } = tableData; + if (!data || data.length === 0) { + return ( + + + {noDataMessage} + + + ); + } - const handleCellClick = (cellId: string, e: Konva.KonvaEventObject) => { - e.cancelBubble = true; - if (groupRef.current) { - onCellClick(obj.id, cellId, groupRef.current, e); - } - }; + return data.map((item, rowIndex) => ( + handleRowClick(item)} + > + {columns.map((col) => ( + + {col.render ? col.render(item) : (item[col.key] as React.ReactNode)} + + ))} + + )); + }; - const handleCellDblClick = (cellId: string, e: Konva.KonvaEventObject) => { - e.cancelBubble = true; - const stage = e.target.getStage(); - if (!stage || !groupRef.current) return; - - const cell = cells[cellId]; - if (!cell) return; - - // Get the target node (Rect or Text) - const targetNode = e.target; - - // Get the client rect which includes all transformations (scale, position, etc.) - const box = targetNode.getClientRect(); - const stageBox = stage.container().getBoundingClientRect(); - - // Calculate position relative to viewport - const x = stageBox.left + box.x; - const y = stageBox.top + box.y; - const width = box.width; - const height = box.height; - - onCellDblClick(obj.id, cellId, x, y, width, height, cell.text); - }; - - const handleDragEnd = () => { - if (groupRef.current) { - const pos = groupRef.current.position(); - onDragEnd(obj.id, pos.x, pos.y); - } - }; + const renderHeader = () => { + if (!showHeader || actionsPosition === "header-replace") return null; return ( - - {Array.from({ length: rows }).map((_, row) => - Array.from({ length: cols }).map((_, col) => { - const cellId = `cell-${row}-${col}`; - const cell = cells[cellId]; - if (!cell) return null; - - const x = col * cellWidth; - const y = row * cellHeight; - const isCellSelected = selectedCellId === cellId; - - const isTopLeft = row === 0 && col === 0; - const isTopRight = row === 0 && col === cols - 1; - - return ( - - handleCellClick(cellId, e)} - onDblClick={(e) => handleCellDblClick(cellId, e)} - /> - {cell.text && ( - handleCellClick(cellId, e)} - onDblClick={(e) => handleCellDblClick(cellId, e)} - /> - )} - - ); - }) - )} - + + + {columns.map((col) => ( + + ))} + + ); + }; + + const renderActions = () => { + if (!actions) return null; + + return ( +
+
{actions}
+
+ ); + }; + + return ( +
+ {(actionsPosition === "top" || actionsPosition === "above-header") && renderActions()} + +
+ + {renderHeader()} + {renderTableBody()} +
+
+
+ ); }; export default Table; - diff --git a/src/components/Td.tsx b/src/components/Td.tsx new file mode 100644 index 0000000..01c3ff1 --- /dev/null +++ b/src/components/Td.tsx @@ -0,0 +1,21 @@ +import { type FC, type ReactNode } from "react"; + +type Props = { + text?: string | number | ReactNode; + children?: ReactNode; + dir?: string; + className?: string; +}; + +const Td: FC = ({ text, children, dir, className }) => { + return ( + + {text ?? children} + + ); +}; + +export default Td; diff --git a/src/components/types/TableTypes.ts b/src/components/types/TableTypes.ts new file mode 100644 index 0000000..a806a96 --- /dev/null +++ b/src/components/types/TableTypes.ts @@ -0,0 +1,27 @@ +import type { ReactNode } from "react"; + +export type RowDataType = Record & { id: string | number }; + +export type ColumnType = { + title: string; + key: string; + render?: (item: T) => ReactNode; + width?: string | number; + align?: "left" | "center" | "right"; + className?: string; +}; + +export type TableProps = { + columns: ColumnType[]; + data?: T[]; + isLoading?: boolean; + onRowClick?: (id: T["id"], item: T) => void; + className?: string; + rowClassName?: string | ((item: T, index: number) => string); + noDataMessage?: ReactNode; + headerClassName?: string; + emptyRowsCount?: number; + showHeader?: boolean; + actions?: ReactNode; + actionsPosition?: "top" | "header-replace" | "above-header"; +}; diff --git a/src/pages/designer/List.tsx b/src/pages/designer/List.tsx new file mode 100644 index 0000000..2a1bb71 --- /dev/null +++ b/src/pages/designer/List.tsx @@ -0,0 +1,94 @@ +import Button from "@/components/Button"; +import Table from "@/components/Table"; +import type { ColumnType } from "@/components/types/TableTypes"; +import { Paths } from "@/config/Paths"; +import { formatDateShort, formatPrice } from "@/helpers/func"; +import { usePageTitle } from "@/hooks/usePageTitle"; +import { getTotalPrice } from "@/pages/designer/designerRequestForm"; +import { useGetDesignRequests } from "@/pages/designer/hooks/useDesignerData"; +import type { RequestDesignItemType } from "@/pages/designer/types/Types"; +import { type FC, useMemo } from "react"; +import { Link } from "react-router-dom"; + +const getInvoiceUrl = (invoiceId: string) => `${import.meta.env.VITE_INVOICE_URL}${invoiceId}`; + +const RequestDesignList: FC = () => { + usePageTitle("لیست درخواست‌های طراحی"); + const { data, isPending } = useGetDesignRequests(); + const requests = data?.data ?? []; + + const columns = useMemo[]>( + () => [ + { title: "عنوان", key: "title", className: "font-medium text-black" }, + { title: "تاریخ ثبت", key: "createdAt", render: (item) => formatDateShort(item.createdAt) }, + { title: "زمان تحویل", key: "expectedDate", render: (item) => formatDateShort(item.expectedDate) }, + { title: "تعداد صفحات", key: "count", render: (item) => item.count.toLocaleString("fa-IR") }, + { + title: "مبلغ", + key: "totalPrice", + render: (item) => `${formatPrice(getTotalPrice(item.count))} تومان`, + }, + { + title: "وضعیت", + key: "paidAt", + render: (item) => { + const isPaid = Boolean(item.paidAt); + return ( + {isPaid ? "پرداخت شده" : "در انتظار پرداخت"} + ); + }, + }, + { + title: "عملیات", + key: "actions", + render: (item) => { + if (item.paidAt) { + return ; + } + + return ( + + ); + }, + }, + ], + [], + ); + + return ( +
+
+

درخواست‌های طراحی

+ + + +
+ + + هنوز درخواست طراحی ثبت نشده است. + + + + + } + /> + + ); +}; + +export default RequestDesignList; diff --git a/src/pages/designer/hooks/useDesignerData.ts b/src/pages/designer/hooks/useDesignerData.ts index a334030..7a5b1b6 100644 --- a/src/pages/designer/hooks/useDesignerData.ts +++ b/src/pages/designer/hooks/useDesignerData.ts @@ -1,4 +1,4 @@ -import { useMutation } from "@tanstack/react-query"; +import { useMutation, useQuery } from "@tanstack/react-query"; import * as api from "../service/DesignerService"; export const useInitiateDesignRequest = () => { @@ -6,3 +6,10 @@ export const useInitiateDesignRequest = () => { mutationFn: api.initiateDesignRequest, }); }; + +export const useGetDesignRequests = () => { + return useQuery({ + queryKey: ["design-requests"], + queryFn: api.getDesignRequests, + }); +}; diff --git a/src/pages/designer/service/DesignerService.ts b/src/pages/designer/service/DesignerService.ts index 989ecb2..2cc4f20 100644 --- a/src/pages/designer/service/DesignerService.ts +++ b/src/pages/designer/service/DesignerService.ts @@ -1,15 +1,12 @@ import axios from "@/config/axios"; -import type { - DesignRequestInitiateParams, - RequestDesignResponse, -} from "../types/Types"; +import type { DesignRequestInitiateParams, DesignRequestsResponse, RequestDesignResponse } from "../types/Types"; -export const initiateDesignRequest = async ( - params: DesignRequestInitiateParams, -): Promise => { - const { data } = await axios.post( - "/admin/design-request/initiate", - params, - ); +export const initiateDesignRequest = async (params: DesignRequestInitiateParams): Promise => { + const { data } = await axios.post("/admin/design-request/initiate", params); + return data; +}; + +export const getDesignRequests = async (): Promise => { + const { data } = await axios.get("/admin/design-request"); return data; }; diff --git a/src/pages/designer/types/Types.ts b/src/pages/designer/types/Types.ts index 04b46a3..95c89c8 100644 --- a/src/pages/designer/types/Types.ts +++ b/src/pages/designer/types/Types.ts @@ -47,3 +47,20 @@ export type DesignRequestType = { }; export type RequestDesignResponse = BaseResponse; + +export type RequestDesignItemType = { + id: string; + createdAt: string; + updatedAt: string; + deletedAt: string | null; + title: string; + count: number; + desc: string; + expectedDate: string; + attachments: string[]; + invoiceId: string; + paidAt: string | null; + business: string; +}; + +export type DesignRequestsResponse = BaseResponse; \ No newline at end of file diff --git a/src/pages/editor/components/canvas/ObjectRenderer.tsx b/src/pages/editor/components/canvas/ObjectRenderer.tsx index 0ad0157..e27e94b 100644 --- a/src/pages/editor/components/canvas/ObjectRenderer.tsx +++ b/src/pages/editor/components/canvas/ObjectRenderer.tsx @@ -15,7 +15,7 @@ import { VideoShape, AudioShape, } from "../tools"; -import Table from "@/components/Table"; +import EditorTable from "@/components/EditorTable"; type ObjectRendererProps = { obj: EditorObject; @@ -234,7 +234,7 @@ const ObjectRenderer = ({ break; case "grid": shapeElement = ( -
{ const isBusinessCatalogRoute = isBusinessCatalogPath(location.pathname); const hideAppChrome = isViewerRoute || isBusinessCatalogRoute; const hiddenSideBarRoutes = [Paths.editor]; - const shouldRenderSideBar = - !hiddenSideBarRoutes.includes(location.pathname) && !hideAppChrome; + const shouldRenderSideBar = !hiddenSideBarRoutes.includes(location.pathname) && !hideAppChrome; const routeHasLocalSidebar = location.pathname.includes("editor"); const hasSidebarSpace = shouldRenderSideBar || routeHasLocalSidebar; const headerSidebarSize = routeHasLocalSidebar ? "wide" : "default"; return ( -
+
{!hideAppChrome && shouldRenderSideBar ? : null} - {!hideAppChrome ? ( -
- ) : null} + {!hideAppChrome ?
: null}
-
+
{ } /> - } - /> + } /> { } /> + + + + } + />
diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index a488590..8ef4daa 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -68,11 +68,11 @@ const SideBar: FC = () => { /> } + icon={} title={t("sidebar.request_design")} - isActive={isActive(Paths.designer.request)} - link={Paths.designer.request} - activeName={Paths.designer.request} + isActive={isActive(Paths.designer.list)} + link={Paths.designer.list} + activeName={Paths.designer.list} />