feat: 商业保险新增员工汇总Tab

- 后端新增 /commercial-insurance/employee-summary 接口,按员工维度汇总商险
- 前端商业保险页面新增Tab切换:方案管理 + 员工汇总
- 员工汇总展示每人参保项、年保费合计、保额合计,底部带总计行
This commit is contained in:
freedakgmail
2026-08-05 07:25:14 +08:00
parent c328172e7d
commit 13c9192b0a
3 changed files with 141 additions and 1 deletions
@@ -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
+3
View File
@@ -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[]>()),
}
// ========== 员工福利 ==========
+99 -1
View File
@@ -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>
)
}