component table + orders in home

This commit is contained in:
hamid zarghami
2025-10-18 10:38:12 +03:30
parent 2f2a27b083
commit 3b382a973b
18 changed files with 1268 additions and 1 deletions
+1
View File
@@ -1,6 +1,7 @@
import { type FC } from 'react'
import '@/assets/fonts/irancell/style.css'
import 'swiper/swiper-bundle.css';
import 'react-loading-skeleton/dist/skeleton.css'
import { BrowserRouter } from 'react-router-dom'
// import { getToken } from './config/func'
import MainRouter from './router/MainRouter'
+30
View File
@@ -0,0 +1,30 @@
import { type FC, Fragment } from 'react'
import Td from './Td'
import Skeleton from 'react-loading-skeleton'
type Props = {
tdCount: number,
trCount?: number,
}
const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
return (
<Fragment>
{
Array.from({ length: trCount }).map((_, rowIndex) => (
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
{
Array.from({ length: tdCount }).map((_, colIndex) => (
<Td text={''} key={colIndex}>
<Skeleton />
</Td>
))
}
</tr>
))
}
</Fragment>
)
}
export default DefaultTableSkeleton
+109
View File
@@ -0,0 +1,109 @@
import React from "react";
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
}) => {
// اگر فقط یک صفحه داریم، pagination نمایش داده نمی‌شود
if (totalPages <= 1) return null;
const getPageNumbers = () => {
const pageNumbers: (number | string)[] = [];
const maxVisiblePages = window.innerWidth < 768 ? 3 : 5; // تعداد کمتر برای موبایل
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
} else {
// نمایش صفحه اول
pageNumbers.push(1);
if (currentPage > 3) {
pageNumbers.push("...");
}
// صفحات میانی
const start = Math.max(2, Math.min(currentPage - 1, totalPages - 3));
const end = Math.min(totalPages - 1, Math.max(currentPage + 1, 4));
for (let i = start; i <= end; i++) {
if (!pageNumbers.includes(i)) {
pageNumbers.push(i);
}
}
if (currentPage < totalPages - 2) {
pageNumbers.push("...");
}
// نمایش صفحه آخر
if (!pageNumbers.includes(totalPages)) {
pageNumbers.push(totalPages);
}
}
return pageNumbers;
};
const pageNumbers = getPageNumbers();
return (
<div className="flex justify-center items-center gap-1 md:gap-2 py-4 md:py-6 mt-3 md:mt-4 border-t border-gray-100">
{/* دکمه صفحه قبل */}
<button
className={`px-2 md:px-3 py-2 text-xs md:text-sm rounded-lg transition-colors ${currentPage === 1
? "text-gray-400 cursor-not-allowed"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
قبلی
</button>
{/* شماره صفحات */}
<div className="flex gap-1">
{pageNumbers.map((page, index) =>
typeof page === "number" ? (
<button
key={index}
className={`w-8 h-8 md:w-10 md:h-10 text-xs md:text-sm rounded-lg transition-colors ${currentPage === page
? "bg-primary text-white shadow-sm"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => onPageChange(page)}
>
{page}
</button>
) : (
<span key={index} className="flex items-center px-1 md:px-2 text-gray-400 text-xs md:text-sm">
{page}
</span>
)
)}
</div>
{/* دکمه صفحه بعد */}
<button
className={`px-2 md:px-3 py-2 text-xs md:text-sm rounded-lg transition-colors ${currentPage === totalPages
? "text-gray-400 cursor-not-allowed"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
بعدی
</button>
</div>
);
};
export default Pagination;
+145
View File
@@ -0,0 +1,145 @@
import React, { useState, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { More } from 'iconsax-react'
export interface RowActionItem {
label: string;
icon?: React.ReactNode;
onClick: () => void;
className?: string;
}
interface RowActionsDropdownProps {
actions: RowActionItem[];
className?: string;
trigger?: React.ReactNode;
topOffset?: number;
leftOffset?: number;
topOffsetMobile?: number;
leftOffsetMobile?: number;
}
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
actions,
className = '',
trigger,
topOffset = 0,
leftOffset = 0,
topOffsetMobile = 0,
leftOffsetMobile = 0
}) => {
const [isOpen, setIsOpen] = useState(false)
const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 })
const dropdownRef = useRef<HTMLDivElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => {
document.removeEventListener('mousedown', handleClickOutside)
}
}, [])
const calculatePosition = (rect: DOMRect) => {
const isMobile = window.innerWidth < 768
const dropdownWidth = isMobile ? 140 : 150
const dropdownHeight = actions.length * (isMobile ? 36 : 40)
// انتخاب offset بر اساس سایز صفحه
const currentTopOffset = isMobile ? topOffsetMobile : topOffset
const currentLeftOffset = isMobile ? leftOffsetMobile : leftOffset
let left = rect.left + window.scrollX - dropdownWidth + rect.width + currentLeftOffset
let top = rect.bottom + window.scrollY + currentTopOffset
if (left + dropdownWidth > window.innerWidth) {
left = rect.left + window.scrollX - dropdownWidth + currentLeftOffset
}
if (left < 0) {
left = isMobile ? 20 : 40
}
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - dropdownHeight + currentTopOffset
}
// اطمینان از اینکه dropdown از صفحه خارج نشود
if (left < 0) left = 20
if (left + dropdownWidth > window.innerWidth) left = window.innerWidth - dropdownWidth - 20
if (top < 0) top = 20
if (top + dropdownHeight > window.innerHeight) top = window.innerHeight - dropdownHeight - 20
return { top, left }
}
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation()
if (!isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect()
const calculatedPosition = calculatePosition(rect)
setDropdownPosition(calculatedPosition)
}
setIsOpen(!isOpen)
}
const handleActionClick = (action: RowActionItem) => {
action.onClick()
setIsOpen(false)
}
const renderDropdown = () => {
if (!isOpen) return null
return createPortal(
<div
ref={dropdownRef}
className="fixed py-3 md:py-3 bg-white dark:bg-[#252526] rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px] border border-[#d0d0d0]"
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
}}
>
{actions.map((action, index) => (
<button
key={index}
onClick={() => handleActionClick(action)}
className={`w-full mt-1 text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 dark:hover:bg-[#2d2d30] ${index === 0 ? 'rounded-t-lg' : ''} ${action.className || ''}`}
>
{action.icon && <span className="ml-2">{action.icon}</span>}
<span className={`${action.label === 'حذف' ? 'text-red-500' : ''} dark:text-gray-200`}>{action.label}</span>
</button>
))}
</div>,
document.body
)
}
return (
<div className={className}>
<button
ref={buttonRef}
onClick={handleToggle}
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
aria-haspopup="menu"
aria-expanded={isOpen}
>
{trigger ? trigger : (
<More size={16} color={'black'} className="rotate-90" />
)}
</button>
{renderDropdown()}
</div>
)
}
export default RowActionsDropdown
+363
View File
@@ -0,0 +1,363 @@
import React, { Fragment, useState } from 'react';
import DefaultTableSkeleton from '@/components/DefaultTableSkeleton';
import Td from '@/components/Td';
import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes';
import { Checkbox } from '@/components/ui/checkbox';
import RowActionsDropdown from '@/components/RowActionsDropdown';
import Pagination from '@/components/Pagination';
const Table = <T extends RowDataType>({
columns,
data,
isLoading = false,
onRowClick,
className = '',
rowClassName = '',
noDataMessage = 'هیچ داده‌ای یافت نشد',
headerClassName = 'bg-gray-50',
emptyRowsCount = 5,
showHeader = true,
actions = null,
actionsPosition = 'top',
selectable = false,
selectedActions = null,
onSelectionChange,
rowActions,
pagination,
}: TableProps<T>): React.ReactElement => {
const [selectedRows, setSelectedRows] = useState<T[]>([]);
const getRowClassName = (item: T, index: number): string => {
if (typeof rowClassName === 'function') {
return rowClassName(item, index);
}
return rowClassName;
};
const getCellClassName = (column: ColumnType<T>): 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();
};
const handleRowClick = (item: T) => {
if (onRowClick) {
onRowClick(item.id, item);
}
};
const handleSelectAll = () => {
const dataArray = data || [];
const newSelectedRows = selectedRows.length === dataArray.length ? [] : [...dataArray];
setSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
};
const handleSingleRowSelect = (item: T, checked: boolean) => {
const newSelectedRows = checked
? [...selectedRows, item]
: selectedRows.filter((row) => row.id !== item.id);
setSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
};
const isRowSelected = (item: T): boolean => {
return selectedRows.some((row) => row.id === item.id);
};
// Gmail-style layout برای وقتی header نمایش داده نمی‌شود
const renderGmailStyleRows = () => {
const hasActions = (actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions));
const roundedClass = hasActions ? 'rounded-b-2xl' : 'rounded-2xl';
if (isLoading) {
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
{Array.from({ length: emptyRowsCount }).map((_, index) => (
<div key={index} className={`h-[48px] bg-gray-100 animate-pulse ${index !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}></div>
))}
</div>
);
}
if (!data || data.length === 0) {
return (
<div className={`bg-white ${roundedClass} py-6 md:py-8 text-center text-gray-500`}>
{noDataMessage}
</div>
);
}
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
{data.map((item, rowIndex) => (
<div
key={item.id}
className={`w-full min-h-[48px] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<div
className="flex-shrink-0 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</div>
)}
<div className="flex-1 min-w-0">
{/* محتوای اصلی - ستون اول */}
<div className="flex items-center mb-0.5 text-sm font-medium">
{columns[0]?.render ? columns[0].render(item, rowIndex) : (item[columns[0]?.key] as React.ReactNode)}
</div>
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
{columns.length > 1 && (
<div className="flex items-center gap-3 text-xs text-gray-500">
{columns.slice(1).map((col) => (
<div key={col.key} className="truncate">
{col.render ? col.render(item, rowIndex) : (item[col.key] as React.ReactNode)}
</div>
))}
</div>
)}
</div>
{rowActions && (
<div
className="flex-shrink-0 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<RowActionsDropdown actions={rowActions(item)} />
</div>
)}
</div>
))}
</div>
);
};
const renderTableBody = () => {
if (isLoading) {
return (
<Fragment>
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
</Fragment>
);
}
if (!data || data.length === 0) {
return (
<tr>
<td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 py-6 md:py-8 text-center text-gray-500">
{noDataMessage}
</td>
</tr>
);
}
return (data || []).map((item, rowIndex) => (
<tr
key={item.id}
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<td
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</td>
)}
{columns.map((col) => (
<td
key={`${item.id}-${col.key}`}
className={getCellClassName(col)}
style={{ width: col.width }}
>
{col.render ? col.render(item, rowIndex) : item[col.key] as React.ReactNode}
</td>
))}
{rowActions && (
<td
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<RowActionsDropdown actions={rowActions(item)} />
</td>
)}
</tr>
));
};
const renderHeader = () => {
if (!showHeader || actionsPosition === 'header-replace') return null;
return (
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
<tr>
{selectable && (
<th
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5] first:rounded-tr-2xl md:first:rounded-tr-3xl"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={(data?.length || 0) > 0 && selectedRows.length === (data?.length || 0)}
onCheckedChange={handleSelectAll}
/>
</th>
)}
{columns.map((col) => (
<Td key={col.key} text={col.title} />
))}
{rowActions && (
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 rounded-tl-2xl md:rounded-tl-3xl"></th>
)}
</tr>
</thead >
);
};
const renderActions = () => {
if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null;
return (
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-white flex items-center px-3 md:px-6">
<div className='flex items-center gap-2 md:gap-4'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div>
</div>
);
};
// اگر header نشان داده نمی‌شود، از Gmail-style layout فقط در موبایل استفاده کن
if (!showHeader) {
return (
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
{/* نسخه موبایل - Gmail style */}
<div className={`md:hidden`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[64px] bg-white flex items-center px-4 rounded-t-2xl border-b border-[#EAEDF5]">
<div className='flex items-center gap-2'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div>
</div>
)}
{renderGmailStyleRows()}
</div>
{/* نسخه دسکتاپ - Table معمولی */}
<div className={`hidden md:block`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
<div className="h-[74px] bg-white flex items-center px-6 rounded-t-3xl">
<div className='flex items-center gap-4'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div>
</div>
)}
<div className={`overflow-x-auto overflow-hidden ${(actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-white`}>
<table className="w-full text-sm border-collapse min-w-[600px]">
<tbody>
{renderTableBody()}
</tbody>
</table>
</div>
</div>
{pagination && (
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={pagination.onPageChange}
/>
)}
</div>
);
}
return (
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
<div className={`overflow-x-auto overflow-hidden ${showHeader && actionsPosition !== 'header-replace' ? 'rounded-2xl md:rounded-3xl' : 'rounded-b-2xl md:rounded-b-3xl'} bg-white`}>
<table className="w-full text-sm border-collapse min-w-[600px]">
{renderHeader()}
{actionsPosition === 'header-replace' && (
<thead>
<tr>
<th colSpan={columns.length} className="p-0">
{renderActions()}
</th>
</tr>
</thead>
)}
<tbody>
{renderTableBody()}
</tbody>
</table>
</div>
{pagination && (
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={pagination.onPageChange}
/>
)}
</div>
);
};
export default Table;
/*
مثال استفاده با pagination:
import Table from '@/components/Table';
import { useState } from 'react';
const MyComponent = () => {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = 10;
const columns = [
{ title: 'نام', key: 'name' },
{ title: 'ایمیل', key: 'email' },
];
const data = [
{ id: 1, name: 'علی', email: 'ali@example.com' },
{ id: 2, name: 'فاطمه', email: 'fateme@example.com' },
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
// اینجا می‌توانید API call جدید برای دریافت داده‌های صفحه جدید بزنید
};
return (
<Table
columns={columns}
data={data}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange
}}
/>
);
};
*/
+23
View File
@@ -0,0 +1,23 @@
import type { FC, ReactNode } from 'react'
interface Props {
text: string | number | ReactNode,
children?: ReactNode,
dir?: string,
className?: string,
}
const Td: FC<Props> = (props: Props) => {
return (
<td className={`px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${props.className}`} style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
{
props.text ?
props.text
:
props.children
}
</td>
)
}
export default Td
+47
View File
@@ -0,0 +1,47 @@
export interface RowDataType {
id: string | number;
[key: string]: unknown;
}
export interface ColumnType<T extends RowDataType> {
title: string;
key: string;
align?: "left" | "center" | "right";
className?: string;
width?: string | number;
render?: (item: T, index?: number) => React.ReactNode;
}
export interface PaginationType {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
export interface ActionType {
label: string;
onClick: () => void;
icon?: React.ReactNode;
className?: string;
disabled?: boolean;
}
export interface TableProps<T extends RowDataType> {
columns: ColumnType<T>[];
data?: T[];
isLoading?: boolean;
onRowClick?: (id: string | number, item: T) => void;
className?: string;
rowClassName?: string | ((item: T, index: number) => string);
noDataMessage?: string;
headerClassName?: string;
emptyRowsCount?: number;
showHeader?: boolean;
actions?: React.ReactNode;
actionsPosition?: "top" | "above-header" | "bottom" | "header-replace";
selectable?: boolean;
selectedActions?: React.ReactNode;
onSelectionChange?: (selectedRows: T[]) => void;
rowActions?: (item: T) => ActionType[];
pagination?: PaginationType;
}
+37
View File
@@ -0,0 +1,37 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { clx } from "@/helpers/utils"
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={clx(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={clx("flex items-center justify-center text-current")}
>
<svg
className="h-4 w-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20,6 9,17 4,12" />
</svg>
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }
+4
View File
@@ -45,3 +45,7 @@ textarea::placeholder {
--color-border: #f5f7fc;
--color-description: #c3c7dd;
}
tbody tr:nth-child(odd) {
background-color: #f5f7fc;
}
+2
View File
@@ -1,6 +1,7 @@
import { type FC } from 'react'
import Stats from './components/Stats'
import Services from './components/Services'
import Orders from './components/Orders'
const Home: FC = () => {
return (
@@ -8,6 +9,7 @@ const Home: FC = () => {
<div className='flex-1'>
<Stats />
<Services />
<Orders />
</div>
</div>
)
@@ -0,0 +1,106 @@
import { type FC } from 'react';
import { Eye } from 'iconsax-react';
import StatusCircle from '@/components/StatusCircle';
import { type Order, type OrderStatus } from './types/OrderTypes';
// کامپوننت رندر شماره سفارش
export const OrderNumberRenderer: FC<{ order: Order }> = ({ order }) => {
return (
<span className="text-sm font-medium text-gray-900">
{order.number}
</span>
);
};
// کامپوننت رندر عنوان سفارش
export const OrderTitleRenderer: FC<{ order: Order }> = ({ order }) => {
return (
<span className="text-sm font-medium text-gray-900">
{order.title}
</span>
);
};
// کامپوننت رندر تاریخ
export const OrderDateRenderer: FC<{ order: Order }> = ({ order }) => {
return (
<div className="text-xs text-gray-500">
<div className="font-medium">
({order.creationDate.gregorian})
</div>
<div>
{order.creationDate.persian}
</div>
</div>
);
};
// کامپوننت رندر وضعیت سفارش
export const OrderStatusRenderer: FC<{ order: Order }> = ({ order }) => {
const getStatusConfig = (status: OrderStatus) => {
switch (status) {
case 'در حال طراحی':
return {
color: 'text-blue-600',
bgColor: 'bg-blue-50'
};
case 'تهیه متریال':
return {
color: 'text-gray-600',
bgColor: 'bg-gray-50'
};
case 'در حال چاپ':
return {
color: 'text-orange-600',
bgColor: 'bg-orange-50'
};
case 'تکمیل شده':
return {
color: 'text-green-600',
bgColor: 'bg-green-50'
};
default:
return {
color: 'text-gray-600',
bgColor: 'bg-gray-50'
};
}
};
const config = getStatusConfig(order.status);
return (
<span className={`text-sm font-medium ${config.color}`}>
{order.status}
</span>
);
};
// کامپوننت رندر پیام‌ها
export const OrderMessagesRenderer: FC<{ order: Order }> = ({ order }) => {
const hasUnreadMessages = order.unreadMessages > 0;
return (
<div className="flex items-center gap-2">
<StatusCircle color={hasUnreadMessages ? '#EF4444' : '#3B82F6'} />
<span className="text-xs text-gray-500">
{order.unreadMessages} پیام خوانده نشده
</span>
</div>
);
};
// کامپوننت رندر آیکون مشاهده
export const OrderViewRenderer: FC<{ order: Order }> = ({ order }) => {
return (
<button
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
onClick={() => {
// اینجا می‌توانید منطق مشاهده جزئیات را اضافه کنید
console.log('View order:', order.id);
}}
>
<Eye color='black' size={16} />
</button>
);
};
+64
View File
@@ -0,0 +1,64 @@
import Table from '@/components/Table'
import { type FC } from 'react'
import { type ColumnType } from '@/components/types/TableTypes'
import { type Order } from './types/OrderTypes'
import { ordersData } from './data/ordersData'
import {
OrderNumberRenderer,
OrderTitleRenderer,
OrderDateRenderer,
OrderStatusRenderer,
OrderMessagesRenderer,
OrderViewRenderer
} from './OrderTableComponents'
const Orders: FC = () => {
const columns: ColumnType<Order>[] = [
{
title: 'شماره',
key: 'number',
render: (order) => <OrderNumberRenderer order={order} />
},
{
title: 'عنوان سفارش',
key: 'title',
render: (order) => <OrderTitleRenderer order={order} />
},
{
title: 'تاریخ ایجاد',
key: 'creationDate',
render: (order) => <OrderDateRenderer order={order} />
},
{
title: 'وضعیت سفارش',
key: 'status',
render: (order) => <OrderStatusRenderer order={order} />
},
{
title: 'پیام ها',
key: 'unreadMessages',
render: (order) => <OrderMessagesRenderer order={order} />
},
{
title: '',
key: 'actions',
render: (order) => <OrderViewRenderer order={order} />,
width: '60px'
}
]
return (
<div className='mt-6 bg-white p-6 rounded-3xl'>
<h4 className='text-lg font-light'>
سفارش های در حال انجام
</h4>
<Table<Order>
columns={columns}
data={ordersData}
/>
</div>
)
}
export default Orders
@@ -0,0 +1,37 @@
import { type Order } from '../types/OrderTypes';
export const ordersData: Order[] = [
{
id: 1,
number: '123455',
title: 'کارت ویزیت',
creationDate: {
gregorian: '2024-11-01',
persian: '1403/09/01'
},
status: 'در حال طراحی',
unreadMessages: 2
},
{
id: 2,
number: '123455',
title: 'شاپینگ بگ',
creationDate: {
gregorian: '2024-11-01',
persian: '1403/09/01'
},
status: 'تهیه متریال',
unreadMessages: 0
},
{
id: 3,
number: '123455',
title: 'کارت ویزیت',
creationDate: {
gregorian: '2024-11-01',
persian: '1403/09/01'
},
status: 'در حال چاپ',
unreadMessages: 2
}
];
@@ -0,0 +1,22 @@
import { type RowDataType } from '@/components/types/TableTypes';
export interface Order extends RowDataType {
id: string | number;
number: string;
title: string;
creationDate: {
gregorian: string;
persian: string;
};
status: OrderStatus;
unreadMessages: number;
[key: string]: unknown;
}
export type OrderStatus = 'در حال طراحی' | 'تهیه متریال' | 'در حال چاپ' | 'تکمیل شده';
export interface OrderStatusConfig {
status: OrderStatus;
color: string;
textColor: string;
}