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) }
|
} 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
|
export default router
|
||||||
|
|||||||
@@ -578,6 +578,9 @@ export const commercialInsuranceApi = {
|
|||||||
/** 退保 */
|
/** 退保 */
|
||||||
terminateEnrollment: (enrollmentId: string, effectiveTo?: string) =>
|
terminateEnrollment: (enrollmentId: string, effectiveTo?: string) =>
|
||||||
post(`/commercial-insurance/enrollments/${enrollmentId}/terminate`, { effectiveTo }),
|
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 { Shield } from 'lucide-react'
|
||||||
import PageGuide from '../components/ui/PageGuide'
|
import PageGuide from '../components/ui/PageGuide'
|
||||||
|
import Card from '../components/ui/Card'
|
||||||
|
import { commercialInsuranceApi } from '../lib/api-services'
|
||||||
import CommercialInsuranceTab from './social-insurance/CommercialInsuranceTab'
|
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() {
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -17,7 +42,80 @@ export default function CommercialInsurance() {
|
|||||||
点击「新增方案」创建商险计划,选择方案后可查看参保人员列表。
|
点击「新增方案」创建商险计划,选择方案后可查看参保人员列表。
|
||||||
<span className="text-primary"> 商业保险是系统增值服务模块,支持方案管理、参保人员追踪、保费统计。</span>
|
<span className="text-primary"> 商业保险是系统增值服务模块,支持方案管理、参保人员追踪、保费统计。</span>
|
||||||
</PageGuide>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user