list of pager + change status

This commit is contained in:
hamid zarghami
2025-12-10 15:15:40 +03:30
parent abae41914f
commit e1f6499ba0
10 changed files with 321 additions and 384 deletions
+10
View File
@@ -875,5 +875,15 @@
"poor_quality": "کیفیت پایین",
"overpriced": "قیمت بالا",
"unfriendly_service": "خدمات نامناسب"
},
"pager": {
"status": {
"pending": "در انتظار",
"acknowledged": "تایید شده",
"rejected": "رد شده",
"completed": "تکمیل شده"
},
"change_status": "تغییر وضعیت",
"save": "ذخیره"
}
}
+29 -44
View File
@@ -1,19 +1,21 @@
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 { type FC, useMemo, useState } from 'react'
import type { 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'
import { useGetPagers } from './hooks/usePagerData'
import PagerDetailModal from './components/PagerDetailModal'
import { useTranslation } from 'react-i18next'
const PagersList: FC = () => {
const token = `Bearer ${getToken()}`
const { connect, isConnected, socket } = useSocket(import.meta.env.VITE_SOCKET_URL, token)
const { t } = useTranslation('global')
const [selectedPager, setSelectedPager] = useState<Pager | null>(null)
const [isModalOpen, setIsModalOpen] = useState(false)
const [pagers, setPagers] = useState<PagersType>()
const { data: pagersData, isLoading } = useGetPagers()
const {
filters,
@@ -25,42 +27,9 @@ const PagersList: FC = () => {
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])
return pagersData?.data || []
}, [pagersData])
const filteredPagers = useMemo(() => {
return filteredData(pagersList) as Pager[]
@@ -74,7 +43,17 @@ const PagersList: FC = () => {
return totalPages(filteredPagers.length)
}, [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()
return (
@@ -95,13 +74,19 @@ const PagersList: FC = () => {
<Table<Pager & RowDataType>
columns={columns}
data={paginatedPagers as (Pager & RowDataType)[]}
isloading={false}
isloading={isLoading}
pagination={{
currentPage,
totalPages: totalPagesCount,
onPageChange: handlePageChange,
}}
/>
<PagerDetailModal
open={isModalOpen}
close={handleCloseModal}
pager={selectedPager}
/>
</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',
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 { formatOptionalDate } from '@/helpers/func'
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 [
{
key: 'tableNumber',
@@ -16,15 +55,21 @@ export const getPagerTableColumns = (): ColumnType<Pager>[] => {
key: 'message',
title: 'پیام',
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',
title: 'وضعیت',
render: (item: Pager) => {
const statusLabel = item.status === 'active' ? 'فعال' : 'غیرفعال'
const statusVariant = item.status === 'active' ? 'success' : 'error'
const statusLabel = getStatusLabel(item.status)
const statusVariant = getStatusVariant(item.status)
return (
<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>
)
}
},
]
}
+6
View File
@@ -0,0 +1,6 @@
export const enum PagerStatus {
Pending = "pending",
Acknowledged = "acknowledged",
Rejected = "rejected",
Completed = "completed",
}
-317
View File
@@ -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**
موفق باشی! 🚀
+21
View File
@@ -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"] });
},
});
};
+13
View File
@@ -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;
};
+11 -9
View File
@@ -1,15 +1,17 @@
import type { IResponse } from "@/types/response.types";
export type Pager = {
id: string;
tableNumber: string;
message: string;
status: string;
cookieId: string;
createdAt: 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 = {
pagers: Pager[];
};
export type PagersType = Pagers;
export type GetPagersResponse = IResponse<Pager[]>;