list of pager + change status
This commit is contained in:
@@ -875,5 +875,15 @@
|
|||||||
"poor_quality": "کیفیت پایین",
|
"poor_quality": "کیفیت پایین",
|
||||||
"overpriced": "قیمت بالا",
|
"overpriced": "قیمت بالا",
|
||||||
"unfriendly_service": "خدمات نامناسب"
|
"unfriendly_service": "خدمات نامناسب"
|
||||||
|
},
|
||||||
|
"pager": {
|
||||||
|
"status": {
|
||||||
|
"pending": "در انتظار",
|
||||||
|
"acknowledged": "تایید شده",
|
||||||
|
"rejected": "رد شده",
|
||||||
|
"completed": "تکمیل شده"
|
||||||
|
},
|
||||||
|
"change_status": "تغییر وضعیت",
|
||||||
|
"save": "ذخیره"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-44
@@ -1,19 +1,21 @@
|
|||||||
import { type FC, useEffect, useState, useMemo } from 'react'
|
import { type FC, useMemo, useState } from 'react'
|
||||||
import { useSocket } from '@/pages/pager/hooks/useSocket'
|
import type { Pager } from './types/Types'
|
||||||
import { getToken } from '@/config/func'
|
|
||||||
import type { PagersType, Pager } from './types/Types'
|
|
||||||
import Table from '@/components/Table'
|
import Table from '@/components/Table'
|
||||||
import Filters from '@/components/Filters'
|
import Filters from '@/components/Filters'
|
||||||
import { usePagerFilters } from './hooks/usePagerFilters'
|
import { usePagerFilters } from './hooks/usePagerFilters'
|
||||||
import { getPagerTableColumns } from './components/PagerTableColumns'
|
import { getPagerTableColumns } from './components/PagerTableColumns'
|
||||||
import { usePagerFiltersFields } from './components/PagerFiltersFields'
|
import { usePagerFiltersFields } from './components/PagerFiltersFields'
|
||||||
import type { RowDataType } from '@/components/types/TableTypes'
|
import type { RowDataType } from '@/components/types/TableTypes'
|
||||||
|
import { useGetPagers } from './hooks/usePagerData'
|
||||||
|
import PagerDetailModal from './components/PagerDetailModal'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
const PagersList: FC = () => {
|
const PagersList: FC = () => {
|
||||||
const token = `Bearer ${getToken()}`
|
const { t } = useTranslation('global')
|
||||||
const { connect, isConnected, socket } = useSocket(import.meta.env.VITE_SOCKET_URL, token)
|
const [selectedPager, setSelectedPager] = useState<Pager | null>(null)
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
|
|
||||||
const [pagers, setPagers] = useState<PagersType>()
|
const { data: pagersData, isLoading } = useGetPagers()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
filters,
|
filters,
|
||||||
@@ -25,42 +27,9 @@ const PagersList: FC = () => {
|
|||||||
totalPages,
|
totalPages,
|
||||||
} = usePagerFilters()
|
} = 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(() => {
|
const pagersList = useMemo(() => {
|
||||||
return pagers?.pagers || []
|
return pagersData?.data || []
|
||||||
}, [pagers])
|
}, [pagersData])
|
||||||
|
|
||||||
const filteredPagers = useMemo(() => {
|
const filteredPagers = useMemo(() => {
|
||||||
return filteredData(pagersList) as Pager[]
|
return filteredData(pagersList) as Pager[]
|
||||||
@@ -74,7 +43,17 @@ const PagersList: FC = () => {
|
|||||||
return totalPages(filteredPagers.length)
|
return totalPages(filteredPagers.length)
|
||||||
}, [filteredPagers.length, totalPages])
|
}, [filteredPagers.length, totalPages])
|
||||||
|
|
||||||
const columns = getPagerTableColumns()
|
const handleViewDetails = (pager: Pager) => {
|
||||||
|
setSelectedPager(pager)
|
||||||
|
setIsModalOpen(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setIsModalOpen(false)
|
||||||
|
setSelectedPager(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const columns = getPagerTableColumns({ onViewDetails: handleViewDetails, t })
|
||||||
const filterFields = usePagerFiltersFields()
|
const filterFields = usePagerFiltersFields()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -95,13 +74,19 @@ const PagersList: FC = () => {
|
|||||||
<Table<Pager & RowDataType>
|
<Table<Pager & RowDataType>
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={paginatedPagers as (Pager & RowDataType)[]}
|
data={paginatedPagers as (Pager & RowDataType)[]}
|
||||||
isloading={false}
|
isloading={isLoading}
|
||||||
pagination={{
|
pagination={{
|
||||||
currentPage,
|
currentPage,
|
||||||
totalPages: totalPagesCount,
|
totalPages: totalPagesCount,
|
||||||
onPageChange: handlePageChange,
|
onPageChange: handlePageChange,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PagerDetailModal
|
||||||
|
open={isModalOpen}
|
||||||
|
close={handleCloseModal}
|
||||||
|
pager={selectedPager}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { type FC, useState, useEffect } from 'react'
|
||||||
|
import DefaulModal from '@/components/DefaulModal'
|
||||||
|
import type { Pager } from '../types/Types'
|
||||||
|
import { formatOptionalDate } from '@/helpers/func'
|
||||||
|
import Status from '@/components/Status'
|
||||||
|
import Select from '@/components/Select'
|
||||||
|
import Button from '@/components/Button'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useUpdatePagerStatus } from '../hooks/usePagerData'
|
||||||
|
import { PagerStatus } from '../enum/Enum'
|
||||||
|
import type { ItemsSelectType } from '@/components/Select'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
close: () => void
|
||||||
|
pager: Pager | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const PagerDetailModal: FC<Props> = ({ open, close, pager }) => {
|
||||||
|
const { t } = useTranslation('global')
|
||||||
|
const [selectedStatus, setSelectedStatus] = useState<string>('')
|
||||||
|
const { mutate: updateStatus, isPending } = useUpdatePagerStatus()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (pager) {
|
||||||
|
setSelectedStatus(pager.status)
|
||||||
|
}
|
||||||
|
}, [pager])
|
||||||
|
|
||||||
|
if (!pager) return null
|
||||||
|
|
||||||
|
const statusOptions: ItemsSelectType[] = [
|
||||||
|
{ value: PagerStatus.Pending, label: t('pager.status.pending') },
|
||||||
|
{ value: PagerStatus.Acknowledged, label: t('pager.status.acknowledged') },
|
||||||
|
{ value: PagerStatus.Rejected, label: t('pager.status.rejected') },
|
||||||
|
{ value: PagerStatus.Completed, label: t('pager.status.completed') },
|
||||||
|
]
|
||||||
|
|
||||||
|
const getStatusVariant = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case PagerStatus.Pending:
|
||||||
|
return 'warning'
|
||||||
|
case PagerStatus.Acknowledged:
|
||||||
|
return 'info'
|
||||||
|
case PagerStatus.Rejected:
|
||||||
|
return 'error'
|
||||||
|
case PagerStatus.Completed:
|
||||||
|
return 'success'
|
||||||
|
default:
|
||||||
|
return 'pending'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
const option = statusOptions.find(opt => opt.value === status)
|
||||||
|
return option?.label || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSaveStatus = () => {
|
||||||
|
if (selectedStatus && selectedStatus !== pager.status) {
|
||||||
|
updateStatus(
|
||||||
|
{ id: pager.id, status: selectedStatus as PagerStatus },
|
||||||
|
{
|
||||||
|
onSuccess: () => {
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusVariant = getStatusVariant(pager.status)
|
||||||
|
const statusLabel = getStatusLabel(pager.status)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaulModal
|
||||||
|
open={open}
|
||||||
|
close={close}
|
||||||
|
isHeader
|
||||||
|
title_header="جزئیات پیجر"
|
||||||
|
width={600}
|
||||||
|
>
|
||||||
|
<div className="mt-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">
|
||||||
|
شماره میز
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{pager.tableNumber || '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">
|
||||||
|
وضعیت
|
||||||
|
</label>
|
||||||
|
<div className="mt-1">
|
||||||
|
<Status variant={statusVariant} label={statusLabel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">
|
||||||
|
کاربر
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{pager.user || '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pager.deletedAt && (
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">
|
||||||
|
تاریخ حذف
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-medium">
|
||||||
|
{formatOptionalDate(pager.deletedAt, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">
|
||||||
|
پیام
|
||||||
|
</label>
|
||||||
|
<p className="text-sm font-medium bg-gray-50 p-3 rounded-lg whitespace-pre-wrap break-words">
|
||||||
|
{pager.message || '-'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='border-t border-gray-200 pt-4'>
|
||||||
|
<label className="text-sm text-gray-500 mb-1 block">
|
||||||
|
{t('pager.change_status')}
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
key={pager.id}
|
||||||
|
items={statusOptions}
|
||||||
|
defaultValue={selectedStatus}
|
||||||
|
onChange={(e) => setSelectedStatus(e.target.value)}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-6">
|
||||||
|
<Button
|
||||||
|
label={t('pager.save')}
|
||||||
|
onClick={handleSaveStatus}
|
||||||
|
isloading={isPending}
|
||||||
|
disabled={selectedStatus === pager.status || !selectedStatus}
|
||||||
|
className="w-fit px-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DefaulModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PagerDetailModal
|
||||||
@@ -8,15 +8,5 @@ export const usePagerFiltersFields = (): FieldType[] => {
|
|||||||
name: 'search',
|
name: 'search',
|
||||||
placeholder: 'جستجو',
|
placeholder: 'جستجو',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'select',
|
|
||||||
name: 'status',
|
|
||||||
placeholder: 'وضعیت',
|
|
||||||
options: [
|
|
||||||
{ label: 'همه', value: '' },
|
|
||||||
{ label: 'فعال', value: 'active' },
|
|
||||||
{ label: 'غیرفعال', value: 'inactive' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
], [])
|
], [])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,47 @@ import type { ColumnType } from '@/components/types/TableTypes'
|
|||||||
import type { Pager } from '../types/Types'
|
import type { Pager } from '../types/Types'
|
||||||
import { formatOptionalDate } from '@/helpers/func'
|
import { formatOptionalDate } from '@/helpers/func'
|
||||||
import Status from '@/components/Status'
|
import Status from '@/components/Status'
|
||||||
|
import { Eye } from 'iconsax-react'
|
||||||
|
import { PagerStatus } from '../enum/Enum'
|
||||||
|
import type { TFunction } from 'i18next'
|
||||||
|
|
||||||
|
interface GetPagerTableColumnsProps {
|
||||||
|
onViewDetails: (pager: Pager) => void
|
||||||
|
t: TFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusVariant = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case PagerStatus.Pending:
|
||||||
|
return 'warning'
|
||||||
|
case PagerStatus.Acknowledged:
|
||||||
|
return 'info'
|
||||||
|
case PagerStatus.Rejected:
|
||||||
|
return 'error'
|
||||||
|
case PagerStatus.Completed:
|
||||||
|
return 'success'
|
||||||
|
default:
|
||||||
|
return 'pending'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPagerTableColumns = (props?: GetPagerTableColumnsProps): ColumnType<Pager>[] => {
|
||||||
|
const getStatusLabel = (status: string) => {
|
||||||
|
if (!props?.t) return status
|
||||||
|
switch (status) {
|
||||||
|
case PagerStatus.Pending:
|
||||||
|
return props.t('pager.status.pending')
|
||||||
|
case PagerStatus.Acknowledged:
|
||||||
|
return props.t('pager.status.acknowledged')
|
||||||
|
case PagerStatus.Rejected:
|
||||||
|
return props.t('pager.status.rejected')
|
||||||
|
case PagerStatus.Completed:
|
||||||
|
return props.t('pager.status.completed')
|
||||||
|
default:
|
||||||
|
return status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getPagerTableColumns = (): ColumnType<Pager>[] => {
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: 'tableNumber',
|
key: 'tableNumber',
|
||||||
@@ -16,15 +55,21 @@ export const getPagerTableColumns = (): ColumnType<Pager>[] => {
|
|||||||
key: 'message',
|
key: 'message',
|
||||||
title: 'پیام',
|
title: 'پیام',
|
||||||
render: (item: Pager) => {
|
render: (item: Pager) => {
|
||||||
return item.message || '-'
|
const message = item.message || '-'
|
||||||
|
const truncatedMessage = message.length > 50 ? `${message.substring(0, 50)}...` : message
|
||||||
|
return (
|
||||||
|
<div className="max-w-xs truncate" title={message}>
|
||||||
|
{truncatedMessage}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'status',
|
key: 'status',
|
||||||
title: 'وضعیت',
|
title: 'وضعیت',
|
||||||
render: (item: Pager) => {
|
render: (item: Pager) => {
|
||||||
const statusLabel = item.status === 'active' ? 'فعال' : 'غیرفعال'
|
const statusLabel = getStatusLabel(item.status)
|
||||||
const statusVariant = item.status === 'active' ? 'success' : 'error'
|
const statusVariant = getStatusVariant(item.status)
|
||||||
return (
|
return (
|
||||||
<Status variant={statusVariant} label={statusLabel} />
|
<Status variant={statusVariant} label={statusLabel} />
|
||||||
)
|
)
|
||||||
@@ -56,5 +101,19 @@ export const getPagerTableColumns = (): ColumnType<Pager>[] => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'details',
|
||||||
|
title: '',
|
||||||
|
render: (item: Pager) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => props?.onViewDetails(item)}
|
||||||
|
className="cursor-pointer hover:opacity-70 transition-opacity"
|
||||||
|
>
|
||||||
|
<Eye size={20} color="#8C90A3" />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export const enum PagerStatus {
|
||||||
|
Pending = "pending",
|
||||||
|
Acknowledged = "acknowledged",
|
||||||
|
Rejected = "rejected",
|
||||||
|
Completed = "completed",
|
||||||
|
}
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
# 🚀 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 <div>{isConnected ? 'متصل' : 'قطع'}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### با Token
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
function MyComponent() {
|
|
||||||
const token = localStorage.getItem('token') || '';
|
|
||||||
const { isConnected, emit, on } = useSocket('http://localhost:3000', token);
|
|
||||||
|
|
||||||
return <div>{isConnected ? 'متصل' : 'قطع'}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### با تنظیمات کامل
|
|
||||||
|
|
||||||
```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 (
|
|
||||||
<div>
|
|
||||||
{error && <p>خطا: {error.message}</p>}
|
|
||||||
<p>{isConnected ? 'متصل' : 'قطع'}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🎯 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 (
|
|
||||||
<div>
|
|
||||||
{messages.map((msg, i) => (
|
|
||||||
<div key={i}>{msg.text}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 (
|
|
||||||
<div>
|
|
||||||
{notifications.map((n, i) => (
|
|
||||||
<div key={i}>{n.message}</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 (
|
|
||||||
<div>
|
|
||||||
<div className={isConnected ? 'text-green-500' : 'text-red-500'}>
|
|
||||||
{isConnected ? 'آنلاین' : 'آفلاین'}
|
|
||||||
</div>
|
|
||||||
<button onClick={() => callPage('101')}>
|
|
||||||
فراخوانی
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4️⃣ استفاده با useAuth
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
function MyComponent() {
|
|
||||||
const { token } = useAuth(); // hook شما برای احراز هویت
|
|
||||||
const { isConnected, emit } = useSocket('http://localhost:3000', token);
|
|
||||||
|
|
||||||
return <div>{isConnected ? 'متصل' : 'قطع'}</div>;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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 (
|
|
||||||
<div className={`
|
|
||||||
px-3 py-1 rounded-full text-sm font-medium
|
|
||||||
${isConnected
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: 'bg-red-100 text-red-800'}
|
|
||||||
`}>
|
|
||||||
{isConnected ? '🟢 متصل' : '🔴 قطع'}
|
|
||||||
{error && ` - خطا: ${error.message}`}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔥 نکات مهم
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div>
|
|
||||||
{isConnected ? (
|
|
||||||
<button onClick={disconnect}>قطع</button>
|
|
||||||
) : (
|
|
||||||
<button onClick={connect}>اتصال</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
### چند event یکجا
|
|
||||||
|
|
||||||
```tsx
|
|
||||||
useEffect(() => {
|
|
||||||
on('event1', handler1);
|
|
||||||
on('event2', handler2);
|
|
||||||
on('event3', handler3);
|
|
||||||
}, [on]);
|
|
||||||
```
|
|
||||||
|
|
||||||
## ⚙️ تنظیمات محیط (.env)
|
|
||||||
|
|
||||||
```env
|
|
||||||
VITE_SOCKET_URL=http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**ساخته شده با ❤️ برای Danak**
|
|
||||||
|
|
||||||
موفق باشی! 🚀
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import * as api from "../service/PagerService";
|
||||||
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import type { PagerStatus } from "../enum/Enum";
|
||||||
|
|
||||||
|
export const useGetPagers = () => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ["pagers"],
|
||||||
|
queryFn: api.getPagers,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdatePagerStatus = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, status }: { id: string; status: PagerStatus }) =>
|
||||||
|
api.updatePagerStatus(id, status),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["pagers"] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import axios from "@/config/axios";
|
||||||
|
import type { GetPagersResponse } from "../types/Types";
|
||||||
|
import type { PagerStatus } from "../enum/Enum";
|
||||||
|
|
||||||
|
export const getPagers = async (): Promise<GetPagersResponse> => {
|
||||||
|
const { data } = await axios.get<GetPagersResponse>("/admin/pager");
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updatePagerStatus = async (id: string, status: PagerStatus) => {
|
||||||
|
const { data } = await axios.patch(`/admin/pager/${id}/status`, { status });
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
|
import type { IResponse } from "@/types/response.types";
|
||||||
|
|
||||||
export type Pager = {
|
export type Pager = {
|
||||||
id: string;
|
id: string;
|
||||||
tableNumber: string;
|
|
||||||
message: string;
|
|
||||||
status: string;
|
|
||||||
cookieId: string;
|
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
deletedAt: string | null;
|
||||||
|
cookieId: string;
|
||||||
|
restaurant: string;
|
||||||
|
tableNumber: string;
|
||||||
|
user: string | null;
|
||||||
|
staff: string | null;
|
||||||
|
message: string;
|
||||||
|
status: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Pagers = {
|
export type GetPagersResponse = IResponse<Pager[]>;
|
||||||
pagers: Pager[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PagersType = Pagers;
|
|
||||||
|
|||||||
Reference in New Issue
Block a user