From a3ada4a56beac3a401431347a12e7fba4a824e9c Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 5 Nov 2025 10:18:02 +0330 Subject: [PATCH] food admin --- package-lock.json | 65 ++++++++ package.json | 2 + src/components/Filters.tsx | 224 ++++++++++++++++++++++++++ src/components/Input.tsx | 4 +- src/components/RowActionsDropdown.tsx | 12 +- src/pages/food/Category.tsx | 212 ++++++++++++++++++++++++ src/router/Main.tsx | 2 + 7 files changed, 513 insertions(+), 8 deletions(-) create mode 100644 src/components/Filters.tsx create mode 100644 src/pages/food/Category.tsx diff --git a/package-lock.json b/package-lock.json index 3db9e99..7859a7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "formik": "^2.4.6", "i18next": "^25.4.2", "iconsax-react": "^0.0.8", + "moment-jalaali": "^0.10.4", "rc-rate": "^2.13.1", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -36,6 +37,7 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", + "@types/moment-jalaali": "^0.7.9", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", @@ -96,6 +98,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2143,11 +2146,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/moment-jalaali": { + "version": "0.7.9", + "resolved": "https://registry.npmjs.org/@types/moment-jalaali/-/moment-jalaali-0.7.9.tgz", + "integrity": "sha512-gsDOoAzRnCfQTbfdlUrCvX6R0wIto6CvwfvV2C3j4qJLK+DEiTK8Rl/xlOCBO9C6qeUfX8oyZ2UfjnXJTOvHSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "moment": ">=2.14.0" + } + }, "node_modules/@types/react": { "version": "19.1.11", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.11.tgz", "integrity": "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ==", "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2158,6 +2172,7 @@ "integrity": "sha512-xG7xaBMJCpcK0RpN8jDbAACQo54ycO6h4dSSmgv8+fu6ZIAdANkx/WsawASUjVXYfy+J9AbUpRMNNEsXCDfDBQ==", "devOptional": true, "license": "MIT", + "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2208,6 +2223,7 @@ "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.41.0", "@typescript-eslint/types": "8.41.0", @@ -2460,6 +2476,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2632,6 +2649,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001735", "electron-to-chromium": "^1.5.204", @@ -3007,6 +3025,7 @@ "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -3604,6 +3623,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -3705,6 +3725,12 @@ "dev": true, "license": "ISC" }, + "node_modules/jalaali-js": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/jalaali-js/-/jalaali-js-1.2.8.tgz", + "integrity": "sha512-Jl/EwY84JwjW2wsWqeU4pNd22VNQ7EkjI36bDuLw31wH98WQW4fPjD0+mG7cdCK+Y8D6s9R3zLiQ3LaKu6bD8A==", + "license": "MIT" + }, "node_modules/jiti": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", @@ -4201,6 +4227,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-jalaali": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/moment-jalaali/-/moment-jalaali-0.10.4.tgz", + "integrity": "sha512-/eD0HeyvATznb5iE0G1BHjKRZAFEpJ9ZNUkcHwXhNgt1WJJVVzHD7+uDmqzZWVFLdbGme2gvIXKb3ezDYOXcZA==", + "license": "MIT", + "dependencies": { + "jalaali-js": "^1.2.7", + "moment": "^2.29.4", + "moment-timezone": "^0.5.46" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4380,6 +4438,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -4503,6 +4562,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -4518,6 +4578,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -4988,6 +5049,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -5064,6 +5126,7 @@ "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -5151,6 +5214,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -5242,6 +5306,7 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, diff --git a/package.json b/package.json index 340ff76..ac54be2 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "formik": "^2.4.6", "i18next": "^25.4.2", "iconsax-react": "^0.0.8", + "moment-jalaali": "^0.10.4", "rc-rate": "^2.13.1", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -38,6 +39,7 @@ }, "devDependencies": { "@eslint/js": "^9.33.0", + "@types/moment-jalaali": "^0.7.9", "@types/react": "^19.1.10", "@types/react-dom": "^19.1.7", "@vitejs/plugin-react": "^5.0.0", diff --git a/src/components/Filters.tsx b/src/components/Filters.tsx new file mode 100644 index 0000000..eaaef37 --- /dev/null +++ b/src/components/Filters.tsx @@ -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; + +interface FiltersProps { + fields: FieldType[]; + onChange: (filters: FilterValues) => void; + initialValues?: FilterValues; + className?: string; + fieldClassName?: string; + searchField?: string; +} + +const Filters: FC = ({ + 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({}); + const [isFiltersOpen, setIsFiltersOpen] = useState(false); + const [inputValues, setInputValues] = useState>({}); + const onChangeRef = useRef(onChange); + + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + useEffect(() => { + if (Object.keys(initialValues).length > 0) { + setFilters(initialValues); + const inputFields: Record = {}; + 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 = {}; + 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 = {}; + + 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) => { + 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 ( + handleChange(field.name, moment(value, 'jYYYY/jMM/jDD').format('YYYY-MM-DD'))} + defaulValue={currentValue || field.defaultValue || ''} + /> + ); + + case 'select': + return ( + handleInputChange(field.name, e)} + /> + ); + } + }; + + const searchFieldObj = fields.find(field => field.name === searchField); + const otherFields = fields.filter(field => field.name !== searchField); + + return ( +
+
+ + {searchFieldObj && ( +
+ {renderField(searchFieldObj)} +
+ )} +
+ +
+
+ {fields.map(renderField)} +
+
+ + setIsFiltersOpen(false)} + isHeader={true} + title_header="فیلتر کردن نتایج" + > +
+ {otherFields.map(field => ( +
+ +
+ {renderField(field)} +
+
+ ))} +
+
+
+
+
+ ); +}; + +export default Filters; \ No newline at end of file diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 0830371..da6bc88 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -34,9 +34,9 @@ const Input: FC = (props: Props) => { const [search, setSearch] = useState('') 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 ); diff --git a/src/components/RowActionsDropdown.tsx b/src/components/RowActionsDropdown.tsx index 06aa578..6f48910 100644 --- a/src/components/RowActionsDropdown.tsx +++ b/src/components/RowActionsDropdown.tsx @@ -102,7 +102,7 @@ const RowActionsDropdown: React.FC = ({ return createPortal(
= ({ ))}
, @@ -128,12 +128,12 @@ const RowActionsDropdown: React.FC = ({ diff --git a/src/pages/food/Category.tsx b/src/pages/food/Category.tsx new file mode 100644 index 0000000..117ddac --- /dev/null +++ b/src/pages/food/Category.tsx @@ -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(null) + const [currentPage, setCurrentPage] = useState(1) + + // فرم دسته‌بندی جدید + const [formData, setFormData] = useState({ + isActive: true, + categoryStatus: '', + title: '', + order: '', + icon: [] as File[] + }) + + // داده‌های تستی + const [categories] = useState([ + { 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) => ( +
+ {item.icon} +
+ ) + }, + { + title: 'عنوان', + key: 'name', + }, + { + title: 'ترتیب نمایش', + key: 'order', + }, + { + title: 'تعداد غذا', + key: 'foodCount', + }, + { + title: 'وضعیت', + key: 'status', + render: (item: CategoryData) => ( + console.log('تغییر وضعیت:', item.id, value)} + /> + ) + } + ] + + return ( +
+ {/* بخش اصلی - جدول */} +
+
+

دسته بندی غذا

+
+
+ { }} + /> +
+ + 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 + }} + /> + + + {/* سایدبار - فرم اضافه کردن */} +
+

اضافه کردن دسته بندی

+ +
+
فعال
+ setFormData({ ...formData, isActive: value })} + /> +
+ +
+ setFormData({ ...formData, categoryStatus: e.target.value })} + /> +
+ +
+ setFormData({ ...formData, title: e.target.value })} + /> +
+ +
+