Files
TurboHR/frontend/src/pages/platform/PlatformOrgs.tsx
T

247 lines
12 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 { useEffect, useState } from 'react'
import { Search, Building2, Eye, Trash2, Edit2 } from 'lucide-react'
import api from '../../lib/api'
import { Input, Select, Label } from '../../components/ui/Input'
import Button from '../../components/ui/Button'
interface Org {
id: string; name: string; plan: string; maxEmployees: number
city: string | null; contactName: string | null; contactPhone: string | null
payrollFrequency: number; retirementReminderEnabled: boolean
createdAt: string; updatedAt: string
employeeCount: number; userCount: number; contractCount: number; payslipCount: number
}
const PLAN_LABELS: Record<string, string> = { FREE: '免费版', PRO: '专业版', ENTERPRISE: '企业版' }
const PLAN_COLORS: Record<string, string> = { FREE: 'bg-gray-100 text-gray-700', PRO: 'bg-blue-50 text-blue-700', ENTERPRISE: 'bg-purple-50 text-purple-700' }
export default function PlatformOrgs() {
const [orgs, setOrgs] = useState<Org[]>([])
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [pageSize] = useState(20)
const [search, setSearch] = useState('')
const [planFilter, setPlanFilter] = useState('')
const [loading, setLoading] = useState(true)
const [editOrg, setEditOrg] = useState<Org | null>(null)
const [deleteOrg, setDeleteOrg] = useState<Org | null>(null)
const fetchOrgs = async () => {
setLoading(true)
try {
const params: any = { page, pageSize }
if (search) params.search = search
if (planFilter) params.plan = planFilter
const res = await api.get('/platform/orgs', { params }) as any
setOrgs(res.data.list)
setTotal(res.data.total)
} finally {
setLoading(false)
}
}
useEffect(() => { fetchOrgs() }, [page, planFilter])
useEffect(() => { setPage(1) }, [search, planFilter])
const totalPages = Math.ceil(total / pageSize)
const handleSaveEdit = async () => {
if (!editOrg) return
try {
await api.put(`/platform/orgs/${editOrg.id}`, {
name: editOrg.name,
plan: editOrg.plan,
maxEmployees: editOrg.maxEmployees,
city: editOrg.city,
contactName: editOrg.contactName,
contactPhone: editOrg.contactPhone,
})
setEditOrg(null)
fetchOrgs()
} catch (err: any) {
alert(err.response?.data?.error?.message || '保存失败')
}
}
const handleDelete = async () => {
if (!deleteOrg) return
try {
await api.delete(`/platform/orgs/${deleteOrg.id}`)
setDeleteOrg(null)
fetchOrgs()
} catch (err: any) {
alert(err.response?.data?.error?.message || '删除失败')
}
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-white"></h1>
<p className="text-sm text-slate-400 mt-1"> {total} </p>
</div>
{/* 搜索栏 */}
<div className="flex flex-wrap gap-3">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500" />
<Input
placeholder="搜索企业名称、城市、联系人..."
className="bg-slate-900 border-slate-700 text-white placeholder:text-slate-500 pl-10"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && fetchOrgs()}
/>
</div>
<Select
className="bg-slate-900 border-slate-700 text-white w-32"
value={planFilter}
onChange={(e) => setPlanFilter(e.target.value)}
>
<option value=""></option>
<option value="FREE"></option>
<option value="PRO"></option>
<option value="ENTERPRISE"></option>
</Select>
<Button variant="secondary" onClick={fetchOrgs} className="bg-slate-800 text-slate-200 border border-slate-700 hover:bg-slate-700">
</Button>
</div>
{/* 表格 */}
<div className="bg-slate-900 border border-slate-800 rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-800 text-slate-400 text-xs">
<th className="text-left px-4 py-3 font-medium"></th>
<th className="text-left px-4 py-3 font-medium"></th>
<th className="text-left px-4 py-3 font-medium"></th>
<th className="text-right px-4 py-3 font-medium"></th>
<th className="text-right px-4 py-3 font-medium"></th>
<th className="text-right px-4 py-3 font-medium"></th>
<th className="text-left px-4 py-3 font-medium"></th>
<th className="text-center px-4 py-3 font-medium"></th>
</tr>
</thead>
<tbody>
{loading ? (
<tr><td colSpan={8} className="text-center py-12 text-slate-400">...</td></tr>
) : orgs.length === 0 ? (
<tr><td colSpan={8} className="text-center py-12 text-slate-400"></td></tr>
) : orgs.map((org) => (
<tr key={org.id} className="border-b border-slate-800/50 hover:bg-slate-800/30">
<td className="px-4 py-3">
<div className="text-white font-medium">{org.name}</div>
<div className="text-xs text-slate-500">{org.contactName || '-'} {org.contactPhone || ''}</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${PLAN_COLORS[org.plan] || 'bg-gray-100 text-gray-700'}`}>
{PLAN_LABELS[org.plan] || org.plan}
</span>
</td>
<td className="px-4 py-3 text-slate-300">{org.city || '-'}</td>
<td className="px-4 py-3 text-right text-slate-300">{org.employeeCount}</td>
<td className="px-4 py-3 text-right text-slate-300">{org.userCount}</td>
<td className="px-4 py-3 text-right text-slate-300">{org.contractCount}</td>
<td className="px-4 py-3 text-slate-400 text-xs">{new Date(org.createdAt).toLocaleDateString('zh-CN')}</td>
<td className="px-4 py-3">
<div className="flex items-center justify-center gap-1">
<button
onClick={() => setEditOrg(org)}
className="p-1.5 rounded text-slate-400 hover:text-amber-400 hover:bg-slate-800"
title="编辑"
>
<Edit2 className="w-4 h-4" />
</button>
<button
onClick={() => setDeleteOrg(org)}
className="p-1.5 rounded text-slate-400 hover:text-red-400 hover:bg-slate-800"
title="删除"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
{/* 分页 */}
{totalPages > 1 && (
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-800">
<span className="text-xs text-slate-400"> {page} / {totalPages} {total} </span>
<div className="flex gap-2">
<Button size="sm" variant="secondary" disabled={page <= 1} onClick={() => setPage(page - 1)} className="bg-slate-800 text-slate-200 border border-slate-700"></Button>
<Button size="sm" variant="secondary" disabled={page >= totalPages} onClick={() => setPage(page + 1)} className="bg-slate-800 text-slate-200 border border-slate-700"></Button>
</div>
</div>
)}
</div>
{/* 编辑弹窗 */}
{editOrg && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setEditOrg(null)}>
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-md" onClick={(e) => e.stopPropagation()}>
<h2 className="text-lg font-semibold text-white mb-4"></h2>
<div className="space-y-3">
<div>
<Label className="text-slate-300"></Label>
<Input className="bg-slate-800 border-slate-700 text-white" value={editOrg.name} onChange={(e) => setEditOrg({ ...editOrg, name: e.target.value })} />
</div>
<div>
<Label className="text-slate-300"></Label>
<Select className="bg-slate-800 border-slate-700 text-white" value={editOrg.plan} onChange={(e) => setEditOrg({ ...editOrg, plan: e.target.value })}>
<option value="FREE"></option>
<option value="PRO"></option>
<option value="ENTERPRISE"></option>
</Select>
</div>
<div>
<Label className="text-slate-300"></Label>
<Input type="number" className="bg-slate-800 border-slate-700 text-white" value={editOrg.maxEmployees} onChange={(e) => setEditOrg({ ...editOrg, maxEmployees: parseInt(e.target.value) || 0 })} />
</div>
<div>
<Label className="text-slate-300"></Label>
<Input className="bg-slate-800 border-slate-700 text-white" value={editOrg.city || ''} onChange={(e) => setEditOrg({ ...editOrg, city: e.target.value })} />
</div>
<div className="flex gap-2 pt-2">
<Button className="bg-amber-400 text-slate-950 hover:bg-amber-300 flex-1" onClick={handleSaveEdit}></Button>
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setEditOrg(null)}></Button>
</div>
</div>
</div>
</div>
)}
{/* 删除确认 */}
{deleteOrg && (
<div className="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4" onClick={() => setDeleteOrg(null)}>
<div className="bg-slate-900 border border-slate-700 rounded-lg p-6 w-full max-w-sm" onClick={(e) => e.stopPropagation()}>
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-full bg-red-950 flex items-center justify-center">
<Trash2 className="w-5 h-5 text-red-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-white"></h2>
<p className="text-xs text-slate-400"></p>
</div>
</div>
<p className="text-sm text-slate-300 mb-4">
<span className="text-white font-medium">{deleteOrg.name}</span>
{deleteOrg.employeeCount} {deleteOrg.contractCount} {deleteOrg.payslipCount}
</p>
<div className="flex gap-2">
<Button variant="danger" className="flex-1" onClick={handleDelete}></Button>
<Button variant="secondary" className="bg-slate-800 text-slate-200 border border-slate-700" onClick={() => setDeleteOrg(null)}></Button>
</div>
</div>
</div>
)}
</div>
)
}