From abae41914f03ae22cfd2b0f50c0ae3893088d77b Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Wed, 10 Dec 2025 12:34:55 +0330 Subject: [PATCH] use socket --- .env | 4 +- package-lock.json | 138 +++++++- package.json | 1 + src/config/Pages.ts | 3 + src/langs/fa.json | 3 +- src/pages/pager/List.tsx | 109 ++++++ .../pager/components/PagerFiltersFields.tsx | 22 ++ .../pager/components/PagerTableColumns.tsx | 60 ++++ src/pages/pager/hooks/README.md | 317 ++++++++++++++++++ src/pages/pager/hooks/usePagerFilters.ts | 70 ++++ src/pages/pager/hooks/useSocket.ts | 189 +++++++++++ src/pages/pager/types/Types.ts | 15 + src/router/Main.tsx | 3 + src/shared/SideBar.tsx | 11 +- 14 files changed, 941 insertions(+), 4 deletions(-) create mode 100644 src/pages/pager/List.tsx create mode 100644 src/pages/pager/components/PagerFiltersFields.tsx create mode 100644 src/pages/pager/components/PagerTableColumns.tsx create mode 100644 src/pages/pager/hooks/README.md create mode 100644 src/pages/pager/hooks/usePagerFilters.ts create mode 100644 src/pages/pager/hooks/useSocket.ts create mode 100644 src/pages/pager/types/Types.ts diff --git a/.env b/.env index 588a700..4cf5cac 100644 --- a/.env +++ b/.env @@ -1,4 +1,6 @@ VITE_TOKEN_NAME = 'dmnu_a_t' VITE_REFRESH_TOKEN_NAME = 'dmnu-a-rt' VITE_BASE_URL = 'https://dmenuplus-api.dev.danakcorp.com' -# VITE_BASE_URL = 'http://192.168.1.112:4001' \ No newline at end of file +# VITE_BASE_URL = 'http://192.168.1.112:4001' +# VITE_SOCKET_URL = 'https://dmenuplus-api.dev.danakcorp.com/pager' +VITE_SOCKET_URL = 'http://192.168.99.242:4000/pager' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1186468..d108fa4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32,6 +32,7 @@ "react-router-dom": "^7.8.2", "react-spinners": "^0.17.0", "react-toastify": "^11.0.5", + "socket.io-client": "^4.8.1", "swiper": "^11.2.10", "tailwind-merge": "^3.3.1", "yup": "^1.7.0", @@ -1766,6 +1767,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -2933,6 +2940,45 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -4303,7 +4349,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -4935,6 +4980,68 @@ "node": ">=8" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5403,6 +5510,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 8319312..3a8e993 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "react-router-dom": "^7.8.2", "react-spinners": "^0.17.0", "react-toastify": "^11.0.5", + "socket.io-client": "^4.8.1", "swiper": "^11.2.10", "tailwind-merge": "^3.3.1", "yup": "^1.7.0", diff --git a/src/config/Pages.ts b/src/config/Pages.ts index 2130395..b06f4f9 100644 --- a/src/config/Pages.ts +++ b/src/config/Pages.ts @@ -75,4 +75,7 @@ export const Pages = { list: "/reviews/list", detail: "/reviews/detail/", }, + pagers: { + list: "/pagers/list", + }, }; diff --git a/src/langs/fa.json b/src/langs/fa.json index 60e53cf..41d6724 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -65,7 +65,8 @@ "payment_methods": "روش های پرداخت", "admins": "مدیران", "shipment_methods": "روش های ارسال", - "reviews": "نظرات" + "reviews": "نظرات", + "pagers": "پیجرها" }, "shipment": { "dineIn": "سرو در محل", diff --git a/src/pages/pager/List.tsx b/src/pages/pager/List.tsx new file mode 100644 index 0000000..8941b16 --- /dev/null +++ b/src/pages/pager/List.tsx @@ -0,0 +1,109 @@ +import { type FC, useEffect, useState, useMemo } from 'react' +import { useSocket } from '@/pages/pager/hooks/useSocket' +import { getToken } from '@/config/func' +import type { PagersType, Pager } from './types/Types' +import Table from '@/components/Table' +import Filters from '@/components/Filters' +import { usePagerFilters } from './hooks/usePagerFilters' +import { getPagerTableColumns } from './components/PagerTableColumns' +import { usePagerFiltersFields } from './components/PagerFiltersFields' +import type { RowDataType } from '@/components/types/TableTypes' + +const PagersList: FC = () => { + const token = `Bearer ${getToken()}` + const { connect, isConnected, socket } = useSocket(import.meta.env.VITE_SOCKET_URL, token) + + const [pagers, setPagers] = useState() + + const { + filters, + currentPage, + handleFiltersChange, + handlePageChange, + filteredData, + paginatedData, + totalPages, + } = usePagerFilters() + + useEffect(() => { + connect() + }, [connect]) + + const handleJoinRestaurant = () => { + socket?.emit('get:pagers') + } + + const handlePagersList = (data: PagersType) => { + setPagers(data) + } + + useEffect(() => { + if (isConnected && socket) { + socket.emit('join:restaurant') + } + }, [isConnected, socket]) + + useEffect(() => { + if (socket) { + socket.on('joined', handleJoinRestaurant) + socket.on('pagers:list', handlePagersList) + } + + return () => { + if (socket) { + socket.off('joined', handleJoinRestaurant) + socket.off('pagers:list', handlePagersList) + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [socket]) + + const pagersList = useMemo(() => { + return pagers?.pagers || [] + }, [pagers]) + + const filteredPagers = useMemo(() => { + return filteredData(pagersList) as Pager[] + }, [pagersList, filteredData]) + + const paginatedPagers = useMemo(() => { + return paginatedData(filteredPagers) as Pager[] + }, [filteredPagers, paginatedData]) + + const totalPagesCount = useMemo(() => { + return totalPages(filteredPagers.length) + }, [filteredPagers.length, totalPages]) + + const columns = getPagerTableColumns() + const filterFields = usePagerFiltersFields() + + return ( +
+
+

لیست پیجرها

+
+ +
+ +
+ + + columns={columns} + data={paginatedPagers as (Pager & RowDataType)[]} + isloading={false} + pagination={{ + currentPage, + totalPages: totalPagesCount, + onPageChange: handlePageChange, + }} + /> +
+ ) +} + +export default PagersList \ No newline at end of file diff --git a/src/pages/pager/components/PagerFiltersFields.tsx b/src/pages/pager/components/PagerFiltersFields.tsx new file mode 100644 index 0000000..a76ffcb --- /dev/null +++ b/src/pages/pager/components/PagerFiltersFields.tsx @@ -0,0 +1,22 @@ +import { useMemo } from 'react' +import type { FieldType } from '@/components/Filters' + +export const usePagerFiltersFields = (): FieldType[] => { + return useMemo(() => [ + { + type: 'input', + name: 'search', + placeholder: 'جستجو', + }, + { + type: 'select', + name: 'status', + placeholder: 'وضعیت', + options: [ + { label: 'همه', value: '' }, + { label: 'فعال', value: 'active' }, + { label: 'غیرفعال', value: 'inactive' }, + ], + }, + ], []) +} diff --git a/src/pages/pager/components/PagerTableColumns.tsx b/src/pages/pager/components/PagerTableColumns.tsx new file mode 100644 index 0000000..7b954e2 --- /dev/null +++ b/src/pages/pager/components/PagerTableColumns.tsx @@ -0,0 +1,60 @@ +import type { ColumnType } from '@/components/types/TableTypes' +import type { Pager } from '../types/Types' +import { formatOptionalDate } from '@/helpers/func' +import Status from '@/components/Status' + +export const getPagerTableColumns = (): ColumnType[] => { + return [ + { + key: 'tableNumber', + title: 'شماره میز', + render: (item: Pager) => { + return item.tableNumber || '-' + } + }, + { + key: 'message', + title: 'پیام', + render: (item: Pager) => { + return item.message || '-' + } + }, + { + key: 'status', + title: 'وضعیت', + render: (item: Pager) => { + const statusLabel = item.status === 'active' ? 'فعال' : 'غیرفعال' + const statusVariant = item.status === 'active' ? 'success' : 'error' + return ( + + ) + } + }, + { + key: 'createdAt', + title: 'تاریخ ایجاد', + render: (item: Pager) => { + return formatOptionalDate(item.createdAt, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } + }, + { + key: 'updatedAt', + title: 'آخرین بروزرسانی', + render: (item: Pager) => { + return formatOptionalDate(item.updatedAt, { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }) + } + }, + ] +} diff --git a/src/pages/pager/hooks/README.md b/src/pages/pager/hooks/README.md new file mode 100644 index 0000000..5a78c35 --- /dev/null +++ b/src/pages/pager/hooks/README.md @@ -0,0 +1,317 @@ +# 🚀 useSocket Hook - راهنمای استفاده + +یک Hook قدرتمند و ساده برای مدیریت Socket.IO در React + +## ✨ ویژگی‌ها + +- ✅ دریافت URL و Token مستقیم از کاربر +- ✅ اتصال خودکار +- ✅ Reconnection هوشمند +- ✅ مدیریت Event Listeners +- ✅ TypeScript Support +- ✅ مدیریت خطاها +- ✅ Clean up خودکار + +## 📦 نصب + +```bash +npm install socket.io-client +``` + +## 📖 استفاده + +### ساده‌ترین روش (بدون token) + +```tsx +import { useSocket } from '@/pages/pager/hooks/useSocket'; + +function MyComponent() { + const { isConnected, emit, on } = useSocket('http://localhost:3000'); + + useEffect(() => { + on('message', (data) => { + console.log('پیام:', data); + }); + }, [on]); + + return
{isConnected ? 'متصل' : 'قطع'}
; +} +``` + +### با Token + +```tsx +function MyComponent() { + const token = localStorage.getItem('token') || ''; + const { isConnected, emit, on } = useSocket('http://localhost:3000', token); + + return
{isConnected ? 'متصل' : 'قطع'}
; +} +``` + +### با تنظیمات کامل + +```tsx +function MyComponent() { + const url = 'http://localhost:3000'; + const token = 'your-jwt-token'; + + const { isConnected, emit, on, error } = useSocket(url, token, { + reconnectionAttempts: 10, + reconnectionDelay: 2000, + query: { + userId: '123', + room: 'general', + }, + }); + + return ( +
+ {error &&

خطا: {error.message}

} +

{isConnected ? 'متصل' : 'قطع'}

+
+ ); +} +``` + +## 🎯 API + +### پارامترها + +```tsx +useSocket(url: string, token?: string, options?: UseSocketOptions) +``` + +| نام | نوع | الزامی | توضیح | +|-----|-----|--------|-------| +| `url` | string | ✅ | آدرس سرور سوکت | +| `token` | string | ❌ | توکن احراز هویت | +| `options` | object | ❌ | تنظیمات اضافی | + +### Options + +| نام | نوع | پیش‌فرض | توضیح | +|-----|-----|---------|-------| +| `reconnection` | boolean | true | تلاش مجدد برای اتصال | +| `reconnectionAttempts` | number | 5 | تعداد تلاش مجدد | +| `reconnectionDelay` | number | 1000 | تاخیر بین تلاش‌ها (ms) | +| `query` | object | - | پارامترهای query string | + +### Return Values + +| نام | نوع | توضیح | +|-----|-----|-------| +| `socket` | Socket \| null | instance سوکت | +| `isConnected` | boolean | وضعیت اتصال | +| `error` | Error \| null | خطای اتصال | +| `emit` | function | ارسال event | +| `on` | function | گوش دادن به event | +| `off` | function | حذف listener | +| `connect` | function | اتصال مجدد | +| `disconnect` | function | قطع اتصال | + +## 💡 مثال‌های کاربردی + +### 1️⃣ صفحه چت + +```tsx +function ChatPage() { + const [messages, setMessages] = useState([]); + const token = localStorage.getItem('token') || ''; + + const { isConnected, emit, on } = useSocket( + 'http://localhost:3000', + token + ); + + useEffect(() => { + on('chat:message', (message) => { + setMessages(prev => [...prev, message]); + }); + }, [on]); + + const sendMessage = (text: string) => { + emit('chat:send', { text, timestamp: Date.now() }); + }; + + return ( +
+ {messages.map((msg, i) => ( +
{msg.text}
+ ))} +
+ ); +} +``` + +### 2️⃣ دریافت اعلان‌ها + +```tsx +function Notifications() { + const [notifications, setNotifications] = useState([]); + const token = localStorage.getItem('authToken') || ''; + + const { on } = useSocket('http://localhost:4000', token); + + useEffect(() => { + on('notification:new', (notif) => { + setNotifications(prev => [notif, ...prev]); + }); + }, [on]); + + return ( +
+ {notifications.map((n, i) => ( +
{n.message}
+ ))} +
+ ); +} +``` + +### 3️⃣ صفحه Pager + +```tsx +function PagerList() { + const socketUrl = import.meta.env.VITE_SOCKET_URL; + const token = localStorage.getItem('token') || ''; + + const { isConnected, emit, on } = useSocket(socketUrl, token); + + useEffect(() => { + on('page:new', (page) => { + console.log('پیج جدید:', page); + }); + + on('page:status', (status) => { + console.log('وضعیت:', status); + }); + }, [on]); + + const callPage = (number: string) => { + emit('page:call', { number }); + }; + + return ( +
+
+ {isConnected ? 'آنلاین' : 'آفلاین'} +
+ +
+ ); +} +``` + +### 4️⃣ استفاده با useAuth + +```tsx +function MyComponent() { + const { token } = useAuth(); // hook شما برای احراز هویت + const { isConnected, emit } = useSocket('http://localhost:3000', token); + + return
{isConnected ? 'متصل' : 'قطع'}
; +} +``` + +### 5️⃣ Custom Hook برای استفاده راحت‌تر + +```tsx +// hooks/useAppSocket.ts +export const useAppSocket = () => { + const token = localStorage.getItem('token') || ''; + const url = import.meta.env.VITE_SOCKET_URL || 'http://localhost:3000'; + + return useSocket(url, token, { + reconnectionAttempts: 3, + reconnectionDelay: 1500, + }); +}; + +// استفاده در کامپوننت +function MyComponent() { + const { isConnected, emit, on } = useAppSocket(); + + // ... +} +``` + +## 🎨 نمایش وضعیت اتصال + +```tsx +function ConnectionStatus() { + const { isConnected, error } = useSocket('http://localhost:3000'); + + return ( +
+ {isConnected ? '🟢 متصل' : '🔴 قطع'} + {error && ` - خطا: ${error.message}`} +
+ ); +} +``` + +## 🔥 نکات مهم + +1. **Token Security**: توکن رو توی localStorage نگه دار +2. **URL از .env**: آدرس سوکت رو توی `.env` بذار +3. **Clean Up خودکار**: نگران پاک کردن listeners نباش +4. **یک instance در هر کامپوننت**: هر کامپوننت instance جدید می‌سازه + +## 🐛 رفع مشکلات + +### خطای اتصال + +```tsx +const { error, isConnected } = useSocket(url, token); + +if (error) { + console.error('خطا:', error.message); + // نمایش پیام به کاربر +} +``` + +### قطع و وصل دستی + +```tsx +const { connect, disconnect, isConnected } = useSocket(url, token); + +return ( +
+ {isConnected ? ( + + ) : ( + + )} +
+); +``` + +### چند event یکجا + +```tsx +useEffect(() => { + on('event1', handler1); + on('event2', handler2); + on('event3', handler3); +}, [on]); +``` + +## ⚙️ تنظیمات محیط (.env) + +```env +VITE_SOCKET_URL=http://localhost:3000 +``` + +--- + +**ساخته شده با ❤️ برای Danak** + +موفق باشی! 🚀 diff --git a/src/pages/pager/hooks/usePagerFilters.ts b/src/pages/pager/hooks/usePagerFilters.ts new file mode 100644 index 0000000..9ee5666 --- /dev/null +++ b/src/pages/pager/hooks/usePagerFilters.ts @@ -0,0 +1,70 @@ +import { useState, useMemo } from "react"; +import type { FilterValues } from "@/components/Filters"; + +const DEFAULT_PAGE = 1; +const DEFAULT_LIMIT = 10; + +export const usePagerFilters = () => { + const [filters, setFilters] = useState({}); + const [currentPage, setCurrentPage] = useState(DEFAULT_PAGE); + const [limit] = useState(DEFAULT_LIMIT); + + const handleFiltersChange = (newFilters: FilterValues) => { + setFilters(newFilters); + setCurrentPage(DEFAULT_PAGE); + }; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + }; + + const filteredData = useMemo(() => { + return (data: unknown[]) => { + let filtered = [...data]; + + if (filters.search) { + const searchTerm = (filters.search as string).toLowerCase(); + filtered = filtered.filter((item: unknown) => { + const pager = item as { tableNumber?: string; message?: string }; + return ( + pager.tableNumber?.toLowerCase().includes(searchTerm) || + pager.message?.toLowerCase().includes(searchTerm) + ); + }); + } + + if (filters.status && filters.status !== "") { + filtered = filtered.filter((item: unknown) => { + const pager = item as { status?: string }; + return pager.status === filters.status; + }); + } + + return filtered; + }; + }, [filters]); + + const paginatedData = useMemo(() => { + return (data: unknown[]) => { + const startIndex = (currentPage - 1) * limit; + return data.slice(startIndex, startIndex + limit); + }; + }, [currentPage, limit]); + + const totalPages = useMemo(() => { + return (dataLength: number) => { + return Math.ceil(dataLength / limit) || 1; + }; + }, [limit]); + + return { + filters, + currentPage, + handleFiltersChange, + handlePageChange, + limit, + filteredData, + paginatedData, + totalPages, + }; +}; diff --git a/src/pages/pager/hooks/useSocket.ts b/src/pages/pager/hooks/useSocket.ts new file mode 100644 index 0000000..95df3a4 --- /dev/null +++ b/src/pages/pager/hooks/useSocket.ts @@ -0,0 +1,189 @@ +import { useEffect, useRef, useState, useCallback } from "react"; +import { io, Socket } from "socket.io-client"; + +interface UseSocketOptions { + reconnection?: boolean; + reconnectionAttempts?: number; + reconnectionDelay?: number; + query?: Record; +} + +interface UseSocketReturn { + socket: Socket | null; + isConnected: boolean; + error: Error | null; + emit: (event: string, data?: unknown) => void; + on: (event: string, callback: (...args: unknown[]) => void) => void; + off: (event: string, callback?: (...args: unknown[]) => void) => void; + connect: () => void; + disconnect: () => void; +} + +export const useSocket = ( + url: string, + token?: string, + options: UseSocketOptions = {} +): UseSocketReturn => { + const socketRef = useRef(null); + const coreListenersAttachedRef = useRef(false); + const [isConnected, setIsConnected] = useState(false); + const [error, setError] = useState(null); + const listenersRef = useRef void>>>( + new Map() + ); + + const { + reconnection = true, + reconnectionAttempts = 5, + reconnectionDelay = 1000, + query, + } = options; + + const handleConnect = useCallback(() => { + setIsConnected(true); + setError(null); + }, []); + + const handleDisconnect = useCallback(() => { + setIsConnected(false); + }, []); + + const handleConnectError = useCallback((err: Error) => { + setError(err); + setIsConnected(false); + }, []); + + const attachCoreListeners = useCallback( + (socket: Socket) => { + if (coreListenersAttachedRef.current) return; + socket.on("connect", handleConnect); + socket.on("disconnect", handleDisconnect); + socket.on("connect_error", handleConnectError); + coreListenersAttachedRef.current = true; + }, + [handleConnect, handleDisconnect, handleConnectError] + ); + + const connect = useCallback(() => { + if (socketRef.current?.connected) return; + + try { + if (!socketRef.current) { + const socketOptions: { + autoConnect: boolean; + reconnection: boolean; + reconnectionAttempts: number; + reconnectionDelay: number; + auth?: { token: string }; + query?: Record; + } = { + autoConnect: false, + reconnection, + reconnectionAttempts, + reconnectionDelay, + }; + + if (token) { + socketOptions.auth = { token }; + } + + if (query) { + socketOptions.query = query; + } + + socketRef.current = io(url, socketOptions); + attachCoreListeners(socketRef.current); + } else { + attachCoreListeners(socketRef.current); + } + + socketRef.current.connect(); + } catch (err) { + setError(err instanceof Error ? err : new Error("خطا در اتصال به سوکت")); + } + }, [ + url, + token, + reconnection, + reconnectionAttempts, + reconnectionDelay, + query, + attachCoreListeners, + ]); + + const disconnect = useCallback(() => { + if (socketRef.current) { + socketRef.current.disconnect(); + } + }, []); + + const emit = useCallback((event: string, data?: unknown) => { + if (socketRef.current?.connected) { + socketRef.current.emit(event, data); + } else { + console.warn("سوکت متصل نیست. امکان ارسال داده وجود ندارد."); + } + }, []); + + const on = useCallback( + (event: string, callback: (...args: unknown[]) => void) => { + if (!socketRef.current) return; + + socketRef.current.on(event, callback); + + if (!listenersRef.current.has(event)) { + listenersRef.current.set(event, new Set()); + } + listenersRef.current.get(event)!.add(callback); + }, + [] + ); + + const off = useCallback( + (event: string, callback?: (...args: unknown[]) => void) => { + if (!socketRef.current) return; + + if (callback) { + socketRef.current.off(event, callback); + listenersRef.current.get(event)?.delete(callback); + } else { + socketRef.current.off(event); + listenersRef.current.delete(event); + } + }, + [] + ); + + useEffect(() => { + connect(); + + return () => { + listenersRef.current.forEach((callbacks, event) => { + callbacks.forEach((callback) => { + socketRef.current?.off(event, callback); + }); + }); + listenersRef.current.clear(); + + if (socketRef.current) { + socketRef.current.off("connect", handleConnect); + socketRef.current.off("disconnect", handleDisconnect); + socketRef.current.off("connect_error", handleConnectError); + coreListenersAttachedRef.current = false; + socketRef.current.disconnect(); + socketRef.current = null; + } + }; + }, [connect, handleConnect, handleDisconnect, handleConnectError]); + + return { + socket: socketRef.current, + isConnected, + error, + emit, + on, + off, + connect, + disconnect, + }; +}; diff --git a/src/pages/pager/types/Types.ts b/src/pages/pager/types/Types.ts new file mode 100644 index 0000000..6e05ec0 --- /dev/null +++ b/src/pages/pager/types/Types.ts @@ -0,0 +1,15 @@ +export type Pager = { + id: string; + tableNumber: string; + message: string; + status: string; + cookieId: string; + createdAt: string; + updatedAt: string; +}; + +export type Pagers = { + pagers: Pager[]; +}; + +export type PagersType = Pagers; diff --git a/src/router/Main.tsx b/src/router/Main.tsx index 478f6ec..e4f741f 100644 --- a/src/router/Main.tsx +++ b/src/router/Main.tsx @@ -38,6 +38,7 @@ import CreateCoupon from '@/pages/coupon/Create' import UpdateCoupon from '@/pages/coupon/Update' import ReviewsList from '@/pages/review/List' import DetailReview from '@/pages/review/Detail' +import PagersList from '@/pages/pager/List' const MainRouter: FC = () => { const { hasSubMenu } = useSharedStore() @@ -92,6 +93,8 @@ const MainRouter: FC = () => { } /> } /> + + } /> diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index 8f58d97..4de094a 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -2,7 +2,7 @@ import { type FC, useEffect } from 'react' import LogoImage from '../assets/images/logo.svg' import LogoSmall from '../assets/images/logo-small.svg' import { useTranslation } from 'react-i18next' -import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Chart, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1 } from 'iconsax-react' +import { DocumentText, Home2, Logout, Message, NotificationStatus, People, Setting2, TicketDiscount, Chart, Calendar, Security, Card, SecurityUser, TruckFast, Element3, Star1, NotificationBing } from 'iconsax-react' import SideBarItem from './SideBarItem' import { useLocation } from 'react-router-dom' import { Pages } from '../config/Pages' @@ -129,6 +129,15 @@ const SideBar: FC = () => { activeName='reviews' /> + } + title={t('sidebar.pagers')} + isActive={isActive('pagers')} + link={Pages.pagers.list} + name='pagers' + activeName='pagers' + /> + } title={t('sidebar.admins')}