food admin

This commit is contained in:
hamid zarghami
2025-11-05 10:18:02 +03:30
parent 1465a9a9d4
commit a3ada4a56b
7 changed files with 513 additions and 8 deletions
+224
View File
@@ -0,0 +1,224 @@
import { type FC, useEffect, useState, type ChangeEvent, useRef } from 'react';
import { Filter } from 'iconsax-react';
import DatePicker from './DatePicker';
import Input from './Input';
import Select, { type ItemsSelectType } from './Select';
import DefaulModal from './DefaulModal';
import moment from 'moment-jalaali'
import Button from './Button';
export type DateFieldType = {
type: 'date';
name: string;
placeholder: string;
defaultValue?: string;
};
export type SelectFieldType = {
type: 'select';
name: string;
placeholder: string;
options: ItemsSelectType[];
defaultValue?: string;
};
export type InputFieldType = {
type: 'input';
name: string;
placeholder: string;
defaultValue?: string;
};
export type FieldType = DateFieldType | SelectFieldType | InputFieldType;
export type FilterValues = Record<string, string | null>;
interface FiltersProps {
fields: FieldType[];
onChange: (filters: FilterValues) => void;
initialValues?: FilterValues;
className?: string;
fieldClassName?: string;
searchField?: string;
}
const Filters: FC<FiltersProps> = ({
fields,
onChange,
initialValues = {},
className = "flex flex-col md:flex-row justify-between items-start md:items-center gap-4",
fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto",
searchField = "search"
}) => {
const [filters, setFilters] = useState<FilterValues>({});
const [isFiltersOpen, setIsFiltersOpen] = useState(false);
const [inputValues, setInputValues] = useState<Record<string, string>>({});
const onChangeRef = useRef(onChange);
useEffect(() => {
onChangeRef.current = onChange;
}, [onChange]);
useEffect(() => {
if (Object.keys(initialValues).length > 0) {
setFilters(initialValues);
const inputFields: Record<string, string> = {};
fields.forEach(field => {
if (field.type === 'input' && initialValues[field.name]) {
inputFields[field.name] = initialValues[field.name] as string;
}
});
setInputValues(inputFields);
} else {
const defaultValues: FilterValues = {};
const defaultInputs: Record<string, string> = {};
fields.forEach(field => {
if ('defaultValue' in field && field.defaultValue !== undefined) {
defaultValues[field.name] = field.defaultValue;
if (field.type === 'input') {
defaultInputs[field.name] = field.defaultValue;
}
}
});
if (Object.keys(defaultValues).length > 0) {
setFilters(defaultValues);
setInputValues(defaultInputs);
onChange(defaultValues);
}
}
}, [fields, initialValues, onChange]);
useEffect(() => {
const timeouts: Record<string, number> = {};
Object.keys(inputValues).forEach(fieldName => {
timeouts[fieldName] = setTimeout(() => {
setFilters(currentFilters => {
const currentFilter = currentFilters[fieldName];
const newValue = inputValues[fieldName];
if (currentFilter !== newValue) {
const newFilters = { ...currentFilters, [fieldName]: newValue || null };
onChangeRef.current(newFilters);
return newFilters;
}
return currentFilters;
});
}, 1000);
});
return () => {
Object.values(timeouts).forEach(timeout => clearTimeout(timeout));
};
}, [inputValues]);
const handleChange = (name: string, value: string | null) => {
const newFilters = { ...filters, [name]: value };
setFilters(newFilters);
onChange(newFilters);
};
const handleInputChange = (name: string, event: ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setInputValues(prev => ({ ...prev, [name]: value }));
};
const renderField = (field: FieldType) => {
const currentValue = field.type === 'input' ? inputValues[field.name] : filters[field.name];
switch (field.type) {
case 'date':
return (
<DatePicker
key={field.name}
placeholder={field.placeholder}
onChange={(value) => handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))}
defaulValue={currentValue || field.defaultValue || ''}
/>
);
case 'select':
return (
<Select
key={field.name}
placeholder={field.placeholder}
onChange={(e) => handleChange(field.name, e.target.value)}
defaultValue={currentValue || field.defaultValue || ''}
items={field.options}
/>
);
case 'input':
return (
<Input
key={field.name}
placeholder={field.placeholder}
variant="search"
className="w-full md:min-w-[230px] md:max-w-[230px]"
value={currentValue || field.defaultValue || ''}
onChange={(e) => handleInputChange(field.name, e)}
/>
);
}
};
const searchFieldObj = fields.find(field => field.name === searchField);
const otherFields = fields.filter(field => field.name !== searchField);
return (
<div className={className}>
<div className="flex md:hidden items-center justify-between w-full mb-2">
<button
onClick={() => setIsFiltersOpen(!isFiltersOpen)}
className='flex items-center gap-2 px-3 py-2 rounded-lg transition-colors bg-card text-card-foreground border border-border hover:bg-secondary hover:bg-opacity-80'
>
<Filter
size={18}
color='#000000'
/>
<span className="text-sm">فیلترها</span>
</button>
{searchFieldObj && (
<div className="flex-1 mr-4">
{renderField(searchFieldObj)}
</div>
)}
</div>
<div className="hidden md:flex md:flex-row justify-between items-center gap-4 w-full">
<div className={fieldClassName}>
{fields.map(renderField)}
</div>
</div>
<DefaulModal
open={isFiltersOpen}
close={() => setIsFiltersOpen(false)}
isHeader={true}
title_header="فیلتر کردن نتایج"
>
<div className="pb-5 gap-4 flex flex-col-reverse mt-3">
{otherFields.map(field => (
<div key={field.name} className="w-full">
<label className="block text-sm font-medium text-foreground mb-2">
{field.placeholder}
</label>
<div className="w-full">
{renderField(field)}
</div>
</div>
))}
</div>
<div className='mt-6'>
<Button
label='بستن'
onClick={() => setIsFiltersOpen(false)}
/>
</div>
</DefaulModal>
</div>
);
};
export default Filters;
+2 -2
View File
@@ -34,9 +34,9 @@ const Input: FC<Props> = (props: Props) => {
const [search, setSearch] = useState<string>('')
const inputClass = clx(
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-border',
'w-full bg-white h-10 text-black block px-4 text-xs rounded-xl border border-border',
props.readOnly && 'bg-gray-100 border-0 text-description',
props.variant === 'search' && 'bg-[#EEF0F7] border-0 ps-10',
props.variant === 'search' && 'border-0 ps-10',
props.className
);
+6 -6
View File
@@ -102,7 +102,7 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
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]"
className="fixed py-2 bg-white rounded-lg shadow-xl z-[5] min-w-[140px] md:min-w-[150px] border border-gray-200"
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
@@ -112,10 +112,10 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
<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 || ''}`}
className={`w-full text-right px-3 py-2.5 text-sm flex items-center gap-2 hover:bg-gray-50 transition-colors ${index === 0 ? 'rounded-t-lg' : ''} ${index === actions.length - 1 ? 'rounded-b-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>
{action.icon && <span className="ml-2 flex-shrink-0">{action.icon}</span>}
<span className={`flex-1 ${action.label === 'حذف' ? 'text-red-500' : 'text-gray-700'}`}>{action.label}</span>
</button>
))}
</div>,
@@ -128,12 +128,12 @@ const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({
<button
ref={buttonRef}
onClick={handleToggle}
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
className={`p-1.5 hover:bg-gray-100 rounded-lg transition-colors ${isOpen ? 'bg-gray-100' : ''}`}
aria-haspopup="menu"
aria-expanded={isOpen}
>
{trigger ? trigger : (
<More size={16} color={'black'} className="rotate-90" />
<More size={18} color="#6B7280" className="rotate-90" />
)}
</button>
+212
View File
@@ -0,0 +1,212 @@
import { type FC, useState } from 'react'
import Table from '@/components/Table'
import Button from '@/components/Button'
import Input from '@/components/Input'
import Select from '@/components/Select'
import SwitchComponent from '@/components/Switch'
import UploadBox from '@/components/UploadBox'
import { type RowActionItem } from '@/components/RowActionsDropdown'
import { Add } from 'iconsax-react'
import Filters from '@/components/Filters'
interface CategoryData {
id: number
name: string
icon: string
iconColor: string
order: number
foodCount: number
status: boolean
[key: string]: string | number | boolean
}
const CategoryFood: FC = () => {
const [selectedRow, setSelectedRow] = useState<number | null>(null)
const [currentPage, setCurrentPage] = useState(1)
// فرم دسته‌بندی جدید
const [formData, setFormData] = useState({
isActive: true,
categoryStatus: '',
title: '',
order: '',
icon: [] as File[]
})
// داده‌های تستی
const [categories] = useState<CategoryData[]>([
{ id: 1, name: 'ایرانی', icon: '🍕', iconColor: '#FF6B6B', order: 1, foodCount: 14, status: true },
{ id: 2, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 2, foodCount: 14, status: true },
{ id: 3, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 3, foodCount: 14, status: true },
{ id: 4, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 4, foodCount: 14, status: true },
{ id: 5, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 5, foodCount: 14, status: true },
{ id: 6, name: 'پیتزا آمریکایی', icon: '🍕', iconColor: '#FF6B6B', order: 6, foodCount: 14, status: true },
])
const orderOptions = Array.from({ length: 20 }, (_, i) => ({
value: String(i + 1),
label: String(i + 1)
}))
const handleSave = () => {
console.log('ذخیره:', formData)
// پاک کردن فرم بعد از ذخیره
setFormData({
isActive: true,
categoryStatus: '',
title: '',
order: '',
icon: []
})
}
const rowActions = (item: CategoryData): RowActionItem[] => [
{
label: 'ویرایش',
onClick: () => {
console.log('ویرایش', item)
// بارگذاری داده‌ها در فرم
setFormData({
isActive: item.status,
categoryStatus: '',
title: item.name,
order: String(item.order),
icon: []
})
}
},
{
label: 'حذف',
onClick: () => console.log('حذف', item)
}
]
const columns = [
{
title: 'آیکون',
key: 'icon',
render: (item: CategoryData) => (
<div
className="w-8 h-8 rounded-lg flex items-center justify-center text-lg"
style={{ backgroundColor: item.iconColor }}
>
{item.icon}
</div>
)
},
{
title: 'عنوان',
key: 'name',
},
{
title: 'ترتیب نمایش',
key: 'order',
},
{
title: 'تعداد غذا',
key: 'foodCount',
},
{
title: 'وضعیت',
key: 'status',
render: (item: CategoryData) => (
<SwitchComponent
active={item.status}
onChange={(value) => console.log('تغییر وضعیت:', item.id, value)}
/>
)
}
]
return (
<div className="flex gap-6 p-6">
{/* بخش اصلی - جدول */}
<div className="flex-1">
<div className="flex items-center justify-between mb-6">
<h1 className="text-lg font-light">دسته بندی غذا</h1>
</div>
<div>
<Filters
fields={[
{
type: 'input',
name: 'search',
placeholder: 'جستجو',
}
]}
onChange={() => { }}
/>
</div>
<Table
columns={columns}
data={categories}
selectable
rowActions={rowActions}
onRowClick={(id) => setSelectedRow(id as number)}
rowClassName={(item) =>
selectedRow === item.id ? 'bg-blue-50 border-2 border-blue-400 border-dashed' : ''
}
pagination={{
currentPage,
totalPages: 2,
onPageChange: setCurrentPage
}}
/>
</div>
{/* سایدبار - فرم اضافه کردن */}
<div className="w-[300px] bg-white rounded-3xl p-6 h-fit sticky top-6">
<h2 className="font-light mb-6">اضافه کردن دسته بندی</h2>
<div className="flex items-center justify-between mb-6">
<div className="text-sm">فعال</div>
<SwitchComponent
active={formData.isActive}
onChange={(value) => setFormData({ ...formData, isActive: value })}
/>
</div>
<div className="mb-4">
<Input
label="وضعیت دسته بندی"
value={formData.categoryStatus}
onChange={(e) => setFormData({ ...formData, categoryStatus: e.target.value })}
/>
</div>
<div className="mb-4">
<Input
label="عنوان دسته بندی"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
/>
</div>
<div className="mb-4">
<Select
label="ترتیب نمایش"
items={orderOptions}
value={formData.order}
onChange={(e) => setFormData({ ...formData, order: e.target.value })}
placeholder="انتخاب کنید"
/>
</div>
<div className="mb-6">
<UploadBox
label="آیکون دسته بندی"
onChange={(files) => setFormData({ ...formData, icon: files })}
/>
</div>
<Button onClick={handleSave} className="w-full">
<Add size={20} className="ml-2" />
ذخیره
</Button>
</div>
</div>
)
}
export default CategoryFood
+2
View File
@@ -8,6 +8,7 @@ import { useSharedStore } from '../shared/store/sharedStore'
import FoodsList from '@/pages/food/List'
import { Pages } from '@/config/Pages'
import CreateFood from '@/pages/food/Create'
import CategoryFood from '@/pages/food/Category'
const MainRouter: FC = () => {
@@ -28,6 +29,7 @@ const MainRouter: FC = () => {
<Routes>
<Route path={Pages.foods.list} element={<FoodsList />} />
<Route path={Pages.foods.add} element={<CreateFood />} />
<Route path={Pages.foods.category} element={<CategoryFood />} />
</Routes>
</div>
</div>