feat: 商业保险新增员工汇总Tab
- 后端新增 /commercial-insurance/employee-summary 接口,按员工维度汇总商险 - 前端商业保险页面新增Tab切换:方案管理 + 员工汇总 - 员工汇总展示每人参保项、年保费合计、保额合计,底部带总计行
This commit is contained in:
@@ -129,4 +129,43 @@ router.post('/enrollments/:enrollmentId/terminate', async (req: AuthRequest, res
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
// 员工商险汇总(按员工维度)
|
||||
router.get('/employee-summary', async (req: AuthRequest, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const enrollments = await prisma.commercialInsuranceEnrollment.findMany({
|
||||
where: { orgId: req.user!.orgId, status: 'ACTIVE' },
|
||||
include: {
|
||||
employee: { select: { id: true, name: true, department: true } },
|
||||
plan: { select: { id: true, name: true, type: true, provider: true, coverageAmount: true } },
|
||||
},
|
||||
})
|
||||
const summary: Record<string, any> = {}
|
||||
for (const e of enrollments) {
|
||||
if (!summary[e.employeeId]) {
|
||||
summary[e.employeeId] = {
|
||||
employeeId: e.employeeId,
|
||||
name: e.employee.name,
|
||||
department: e.employee.department,
|
||||
insurances: [],
|
||||
totalPremium: 0,
|
||||
totalCoverage: 0,
|
||||
}
|
||||
}
|
||||
summary[e.employeeId].insurances.push({
|
||||
planId: e.planId,
|
||||
planName: e.plan.name,
|
||||
type: e.plan.type,
|
||||
provider: e.plan.provider,
|
||||
premium: e.premium,
|
||||
coverageAmount: e.plan.coverageAmount,
|
||||
effectiveFrom: e.effectiveFrom,
|
||||
effectiveTo: e.effectiveTo,
|
||||
})
|
||||
summary[e.employeeId].totalPremium += e.premium
|
||||
summary[e.employeeId].totalCoverage += e.plan.coverageAmount
|
||||
}
|
||||
res.json({ success: true, data: Object.values(summary) })
|
||||
} catch (err) { next(err) }
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -578,6 +578,9 @@ export const commercialInsuranceApi = {
|
||||
/** 退保 */
|
||||
terminateEnrollment: (enrollmentId: string, effectiveTo?: string) =>
|
||||
post(`/commercial-insurance/enrollments/${enrollmentId}/terminate`, { effectiveTo }),
|
||||
/** 员工商险汇总 */
|
||||
employeeSummary: () =>
|
||||
get('/commercial-insurance/employee-summary').then(unwrap<any[]>()),
|
||||
}
|
||||
|
||||
// ========== 员工福利 ==========
|
||||
|
||||
@@ -1,8 +1,33 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Shield } from 'lucide-react'
|
||||
import PageGuide from '../components/ui/PageGuide'
|
||||
import Card from '../components/ui/Card'
|
||||
import { commercialInsuranceApi } from '../lib/api-services'
|
||||
import CommercialInsuranceTab from './social-insurance/CommercialInsuranceTab'
|
||||
|
||||
const fmt = (n: number) => (n || 0).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })
|
||||
|
||||
const INSURANCE_TYPES: Record<string, { label: string; color: string }> = {
|
||||
ACCIDENT: { label: '意外伤害险', color: 'bg-orange-50 text-orange-700 border border-orange-200' },
|
||||
SUPPLEMENTARY_MEDICAL: { label: '补充医疗保险', color: 'bg-blue-50 text-blue-700 border border-blue-200' },
|
||||
EMPLOYER_LIABILITY: { label: '雇主责任险', color: 'bg-purple-50 text-purple-700 border border-purple-200' },
|
||||
CRITICAL_ILLNESS: { label: '重大疾病险', color: 'bg-rose-50 text-rose-700 border border-rose-200' },
|
||||
GROUP_LIFE: { label: '团体寿险', color: 'bg-teal-50 text-teal-700 border border-teal-200' },
|
||||
OTHER: { label: '其他', color: 'bg-gray-50 text-gray-700 border border-gray-200' },
|
||||
}
|
||||
|
||||
export default function CommercialInsurance() {
|
||||
const [tab, setTab] = useState<'plans' | 'summary'>('plans')
|
||||
|
||||
const { data: employeeSummary = [] } = useQuery<any[]>({
|
||||
queryKey: ['commercial-insurance-employee-summary'],
|
||||
queryFn: async () => {
|
||||
return await commercialInsuranceApi.employeeSummary()
|
||||
},
|
||||
enabled: tab === 'summary',
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -17,7 +42,80 @@ export default function CommercialInsurance() {
|
||||
点击「新增方案」创建商险计划,选择方案后可查看参保人员列表。
|
||||
<span className="text-primary"> 商业保险是系统增值服务模块,支持方案管理、参保人员追踪、保费统计。</span>
|
||||
</PageGuide>
|
||||
<CommercialInsuranceTab />
|
||||
|
||||
{/* Tab 切换 */}
|
||||
<div className="flex items-center gap-4 border-b">
|
||||
{(['plans', 'summary'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
className={`px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
tab === t ? 'border-primary text-primary' : 'border-transparent text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
onClick={() => setTab(t)}
|
||||
>
|
||||
{t === 'plans' ? '方案管理' : '员工汇总'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ========== 方案管理 Tab ========== */}
|
||||
{tab === 'plans' && <CommercialInsuranceTab />}
|
||||
|
||||
{/* ========== 员工汇总 Tab ========== */}
|
||||
{tab === 'summary' && (
|
||||
<Card>
|
||||
{employeeSummary.length === 0 ? (
|
||||
<div className="text-center py-8 text-gray-400 text-sm">暂无员工商险数据</div>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b text-xs text-gray-500">
|
||||
<th className="py-2 px-3 text-left">姓名</th>
|
||||
<th className="py-2 px-3 text-left">部门</th>
|
||||
<th className="py-2 px-3 text-left">保险项</th>
|
||||
<th className="py-2 px-3 text-right">年保费合计</th>
|
||||
<th className="py-2 px-3 text-right">保额合计</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{employeeSummary.map((e: any) => (
|
||||
<tr key={e.employeeId} className="border-b last:border-0 hover:bg-gray-50">
|
||||
<td className="py-2 px-3 font-medium">{e.name}</td>
|
||||
<td className="py-2 px-3 text-gray-500">{e.department}</td>
|
||||
<td className="py-2 px-3">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{e.insurances.map((ins: any, i: number) => {
|
||||
const typeCfg = INSURANCE_TYPES[ins.type] || INSURANCE_TYPES.OTHER
|
||||
return (
|
||||
<span key={i} className={`px-1.5 py-0.5 rounded text-xs ${typeCfg.color}`}>
|
||||
{ins.planName} · {ins.provider} · ¥{fmt(ins.premium)}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right font-medium text-primary">¥{fmt(e.totalPremium)}</td>
|
||||
<td className="py-2 px-3 text-right font-medium">¥{fmt(e.totalCoverage)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr className="border-t-2 font-medium">
|
||||
<td className="py-2 px-3" colSpan={3}>合计({employeeSummary.length}人)</td>
|
||||
<td className="py-2 px-3 text-right text-primary">
|
||||
¥{fmt(employeeSummary.reduce((sum: number, e: any) => sum + e.totalPremium, 0))}
|
||||
</td>
|
||||
<td className="py-2 px-3 text-right">
|
||||
¥{fmt(employeeSummary.reduce((sum: number, e: any) => sum + e.totalCoverage, 0))}
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user