Files
rikkei_simple_care/management/src/components/WifiPoolModal.tsx
2026-06-30 09:31:33 +07:00

169 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { apiFetchWifiPool } from '../api';
import type { WifiPoolItem } from '../api';
function bssidKey(bssid: string): string {
return bssid.toLowerCase().replace(/[^0-9a-f]/g, '');
}
interface WifiPoolModalProps {
open: boolean;
onClose: () => void;
onSelect: (ssid: string, bssid: string) => void;
acceptedBssidKeys: string;
}
export const WifiPoolModal: React.FC<WifiPoolModalProps> = ({
open,
onClose,
onSelect,
acceptedBssidKeys,
}) => {
const [search, setSearch] = useState('');
const [debouncedQ, setDebouncedQ] = useState('');
const [items, setItems] = useState<WifiPoolItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const acceptedSet = useMemo(() => {
return new Set(
acceptedBssidKeys.split(',').map(s => s.trim()).filter(s => s.length === 12)
);
}, [acceptedBssidKeys]);
useEffect(() => {
if (!open) return;
const t = setTimeout(() => setDebouncedQ(search.trim()), 300);
return () => clearTimeout(t);
}, [search, open]);
const loadPool = useCallback(async (q: string) => {
try {
setLoading(true);
setError(null);
const res = await apiFetchWifiPool(q, 80);
setItems(res.data || []);
} catch (e: any) {
setItems([]);
setError(e?.message || 'Không tải được kho WiFi');
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (!open) return;
loadPool(debouncedQ);
const interval = setInterval(() => loadPool(debouncedQ), 20000);
return () => clearInterval(interval);
}, [open, debouncedQ, loadPool]);
useEffect(() => {
if (!open) {
setSearch('');
setDebouncedQ('');
setItems([]);
setError(null);
}
}, [open]);
if (!open) return null;
const handleSelect = (ssid: string, bssid: string) => {
const key = bssidKey(bssid);
if (!ssid.trim() || key.length !== 12 || acceptedSet.has(key)) return;
onSelect(ssid, bssid);
};
return (
<div className="modal-overlay app-pool-overlay" onClick={onClose}>
<div className="modal-container app-pool-modal" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<div>
<h2 className="modal-title" style={{ margin: 0 }}>Kho WiFi</h2>
<p className="app-pool-modal-sub">
Điểm phát thật từ máy sinh viên (SSID + BSSID). Chọn đ thêm vào danh sách chấp nhận.
</p>
</div>
<button type="button" className="btn btn-secondary" onClick={onClose}>Đóng</button>
</div>
<div className="app-pool-toolbar">
<input
type="search"
className="app-pool-search"
placeholder="Tìm SSID hoặc BSSID..."
value={search}
onChange={e => setSearch(e.target.value)}
autoFocus
/>
<button type="button" className="btn btn-secondary" onClick={() => loadPool(debouncedQ)} disabled={loading}>
{loading ? 'Đang tải...' : 'Làm mới'}
</button>
</div>
<div className="app-pool-body">
{error ? (
<div className="discovered-apps-empty discovered-apps-error">{error}</div>
) : loading && items.length === 0 ? (
<div className="app-pool-status">Đang tải...</div>
) : items.length === 0 ? (
<div className="app-pool-status">
{debouncedQ ? `Không tìm thấy "${debouncedQ}"` : 'Chưa có WiFi nào trong kho.'}
</div>
) : (
<table className="data-table app-pool-table">
<thead>
<tr>
<th>SSID</th>
<th>BSSID (MAC)</th>
<th>Lần ghi nhận</th>
<th>Gần nhất</th>
<th></th>
</tr>
</thead>
<tbody>
{items.map(item => {
const key = bssidKey(item.bssid || '');
const added = key.length === 12 && acceptedSet.has(key);
return (
<tr key={item.id} className={added ? 'app-pool-row--added' : ''}>
<td><code className="app-pool-kw">{item.ssid}</code></td>
<td className="app-pool-muted" style={{ fontFamily: 'monospace', fontSize: '0.8rem' }}>
{item.bssid || '—'}
</td>
<td className="app-pool-hit">{item.hitCount}×</td>
<td className="app-pool-muted">
{item.lastSeenAt ? new Date(item.lastSeenAt).toLocaleString('vi-VN') : '—'}
</td>
<td>
{!item.bssid || key.length !== 12 ? (
<span className="app-pool-muted">Thiếu BSSID</span>
) : added ? (
<span className="app-pool-added-tag">Đã </span>
) : (
<button
type="button"
className="btn btn-primary app-pool-add-btn"
onClick={() => handleSelect(item.ssid, item.bssid)}
>
+ Thêm
</button>
)}
</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
<div className="app-pool-footer">
Hiển thị {items.length} kết quả{debouncedQ ? ` cho "${debouncedQ}"` : ''}
</div>
</div>
</div>
);
};