import { useState, useMemo, useCallback, useEffect, useRef } from 'react'
import { useRouter } from 'next/navigation'
import dynamic from 'next/dynamic'
import type { ColDef, ICellRendererParams } from 'ag-grid-community'
import URLCellRenderer from './renderers/URLCellRenderer'
import StripeCellRenderer from './renderers/StripeCellRenderer'
import SessionCellRenderer from './renderers/SessionCellRenderer'
import { logout } from '@/server/auth'
import { fetchBusinesses } from '@/server/businesses'
import { extractPicName } from '@/lib/utils'
import type { BusinessRecord } from '@/types/business'
import type { Language } from '@/types/common'
import { getTranslation } from '@/i18n'
const AgGridTable = dynamic(() => import('./AgGridTable'), {
<div style={{ height: 560, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', border: '3px solid #e2e7f5', borderTopColor: '#4a6cf7', animation: 'spin 0.8s linear infinite' }} />
const AUTO_REFRESH_MS = 2 60 1000
interface BusinessTableProps {
initialData: BusinessRecord[]
export default function BusinessTable({ initialData, lang }: BusinessTableProps) {
const router = useRouter()
const [rowData, setRowData] = useState<BusinessRecord[]>(initialData)
const [loading, setLoading] = useState(false)
const [search, setSearch] = useState('')
const [lastUpdated, setLastUpdated] = useState<Date>(new Date())
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const t = getTranslation(lang)
const fetchData = useCallback(async (silent = false) => {
if (!silent) setLoading(true)
const data = await fetchBusinesses()
setLastUpdated(new Date())
if (err instanceof Error && err.message === 'UNAUTHORIZED') {
console.error('Failed to refresh:', err)
if (!silent) setLoading(false)
intervalRef.current = setInterval(() => {
if (intervalRef.current) clearInterval(intervalRef.current)
const handleRefresh = useCallback(async () => {
if (intervalRef.current) clearInterval(intervalRef.current)
intervalRef.current = setInterval(() => fetchData(true), AUTO_REFRESH_MS)
function formatLastUpdated(date: Date): string {
return date.toLocaleString('en-US', {
const columnDefs = useMemo((): ColDef<BusinessRecord>[] => [
{ headerName: t.colNumber, valueGetter: 'node.rowIndex + 1', width: 60, pinned: 'left', sortable: false, filter: false, cellStyle: { color: '#a8b4d4', fontSize: 12, fontWeight: 600, display: 'flex', alignItems: 'center' } },
{ field: 'business_name', headerName: t.colBusinessName, minWidth: 180, pinned: 'left', cellStyle: { fontWeight: 600, color: '#0d1b4b', fontSize: 13, display: 'flex', alignItems: 'center' } },
headerName: t.colLoginMethod,
cellStyle: { display: 'flex', alignItems: 'center', fontSize: 13, color: '#0d1b4b', fontFamily: 'DM Sans, sans-serif' },
headerName: t.colEmailOrPhone,
cellStyle: { display: 'flex', alignItems: 'center' }
field: 'approval_status',
headerName: t.colApprovalStatus,
cellStyle: { display: 'flex', alignItems: 'center' },
cellRenderer: (p: ICellRendererParams) => {
const color = p.value === 'APPROVED' ? '#16a34a' : p.value === 'PENDING' ? '#d97706' : '#dc2626'
const bg = p.value === 'APPROVED' ? '#f0fdf4' : p.value === 'PENDING' ? '#fffbeb' : '#fef2f2'
<span style={{ fontSize: 12, fontWeight: 500, padding: '2px 10px', borderRadius: 20, background: bg, color, fontFamily: 'DM Sans, sans-serif' }}>
{ field: 'pic_name', headerName: t.colPicName, minWidth: 150, cellStyle: { display: 'flex', alignItems: 'center' }, valueFormatter: (p) => extractPicName(p.value ?? '') },
{ field: 'support_phone', headerName: t.colSupportPhone, minWidth: 160, cellStyle: { display: 'flex', alignItems: 'center' } },
{ field: 'support_email', headerName: t.colSupportEmail, minWidth: 200, cellStyle: { display: 'flex', alignItems: 'center' } },
field: 'menu_category_published',
headerName: t.colMenuPublished,
valueGetter: (p) => p.data?.menu_category_published === true,
cellRenderer: (p: ICellRendererParams) => (
<span style={{ fontSize: 13, fontWeight: 500, color: '#0d1b4b', fontFamily: 'DM Sans, sans-serif' }}>
cellStyle: { display: 'flex', alignItems: 'center' },
headerName: t.colBusinessPublished,
valueGetter: (p) => p.data?.status === 'PUBLISHED',
cellRenderer: (p: ICellRendererParams) => (
<span style={{ fontSize: 13, fontWeight: 500, color: '#0d1b4b', fontFamily: 'DM Sans, sans-serif' }}>
cellStyle: { display: 'flex', alignItems: 'center' },
{ field: 'last_session_accessed', headerName: t.colLastSession, minWidth: 180, cellRenderer: SessionCellRenderer, cellStyle: { display: 'flex', alignItems: 'center' } },
{ field: 'business_page_url', headerName: t.colBusinessPageUrl, minWidth: 240, cellRenderer: URLCellRenderer, cellStyle: { display: 'flex', alignItems: 'center' } },
{ field: 'stripe_account_id', headerName: t.colStripeAccountId, minWidth: 200, cellRenderer: StripeCellRenderer, cellStyle: { display: 'flex', alignItems: 'center' } },
const defaultColDef = useMemo<ColDef<BusinessRecord>>(
() => ({ sortable: true, filter: true, resizable: true, floatingFilter: false }),
const filteredData = useMemo(() => {
if (!search.trim()) return rowData
const q = search.toLowerCase()
return rowData.filter((r) =>
extractPicName(r.pic_name ?? '').toLowerCase().includes(q) ||
r.support_email?.toLowerCase().includes(q) ||
<div style={{ background: '#fff', borderRadius: 16, boxShadow: '0 2px 20px rgba(13,27,75,0.08)', border: '1px solid #e2e7f5', overflow: 'hidden', height: 'calc(100vh - 260px)' }}>
<div style={{ padding: '18px 24px', borderBottom: '1px solid #eef1fa', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16 }}>
<h2 style={{ fontFamily: 'Syne, sans-serif', fontWeight: 700, fontSize: 17, color: '#0d1b4b', margin: 0 }}>{t.tableTitle}</h2>
<p style={{ fontSize: 12, color: '#7a87b0', margin: '2px 0 0' }}>{filteredData.length} {t.of} {rowData.length} {t.businesses}</p>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<div style={{ width: 7, height: 7, borderRadius: '50%', background: '#10b981', animation: 'pulse-dot 2s ease-in-out infinite' }} />
<span style={{ fontSize: 11, color: '#7a87b0', fontFamily: 'DM Sans, sans-serif' }}>
{t.lastUpdated} {formatLastUpdated(lastUpdated)}
<div style={{ position: 'relative' }}>
<span style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: '#a8b4d4' }}>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M11.742 10.344a6.5 6.5 0 10-1.397 1.398h-.001c.03.04.062.078.098.115l3.85 3.85a1 1 0 001.415-1.414l-3.85-3.85a1.007 1.007 0 00-.115-.099zM12 6.5a5.5 5.5 0 11-11 0 5.5 5.5 0 0111 0z" /></svg>
type="text" placeholder={t.searchPlaceholder} value={search} onChange={(e) => setSearch(e.target.value)}
style={{ paddingLeft: 32, paddingRight: 12, paddingTop: 8, paddingBottom: 8, borderRadius: 8, border: '1px solid #e2e7f5', background: '#f8f9fe', fontSize: 13, color: '#0d1b4b', outline: 'none', width: 220, fontFamily: 'DM Sans, sans-serif' }}
<button onClick={handleRefresh} disabled={loading}
style={{ padding: '8px 14px', borderRadius: 8, border: '1px solid #e2e7f5', background: '#f8f9fe', color: '#3d4d7a', fontSize: 13, cursor: loading ? 'not-allowed' : 'pointer', display: 'flex', alignItems: 'center', gap: 6, fontFamily: 'DM Sans, sans-serif' }}>
<svg width="13" height="13" viewBox="0 0 16 16" fill="currentColor" style={{ animation: loading ? 'spin 1s linear infinite' : 'none' }}>
<path fillRule="evenodd" d="M8 3a5 5 0 104.546 2.914.5.5 0 00-.908-.417A4 4 0 118 4V3z" />
<path d="M8 4.466V.534a.25.25 0 00-.41-.192L5.23 2.308a.25.25 0 000 .384l2.36 1.966A.25.25 0 008 4.466z" />
<div style={{ height: 400, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 14 }}>
<div style={{ width: 40, height: 40, borderRadius: '50%', border: '3px solid #e2e7f5', borderTopColor: '#4a6cf7', animation: 'spin 0.8s linear infinite' }} />
<p style={{ color: '#7a87b0', fontSize: 14 }}>{t.loading}</p>
<div className="ag-theme-alpine" style={{ height: 560, width: '100%' }}>
defaultColDef={defaultColDef}
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes pulse-dot { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.5; transform: scale(0.8); } }