c670b9e454
- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解 - 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护) - 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理 - 专业图标体系替换全部emoji、看板美化 - 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC - 444单测+204前端测试+51 e2e
353 lines
21 KiB
TypeScript
353 lines
21 KiB
TypeScript
/**
|
||
* 费率管理页面 — 地域费率套维护(与评估引擎对齐)。
|
||
*
|
||
* 专业设计:
|
||
* - 以「地域费率套」为单位维护(一套 = 五险单位费率 + 公积金 + 增值税 + 附加税)
|
||
* - 展示引擎内置默认费率(全国/上海/北京/广东)作为对照基准
|
||
* - 与默认对比:偏离默认值的项高亮
|
||
* - 复核流程:编辑即重置待复核,复核后驱动评估盈利测算
|
||
* - 实时显示社保单位合计、用工成本加载估算
|
||
*/
|
||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import { colorVar, FONT_FAMILY, RADIUS, space, typographyStyle } from '../design-system/components/styles.js';
|
||
import { Card, Icon } from '../design-system/index.js';
|
||
import {
|
||
fetchEngineDefaults,
|
||
fetchRegionRates,
|
||
saveRegionRate,
|
||
reviewRegionRate,
|
||
deleteRegionRate,
|
||
fetchMinWages,
|
||
saveMinWage,
|
||
deleteMinWageApi,
|
||
type RegionRates,
|
||
type RegionRateRecord,
|
||
type MinWageItem,
|
||
} from '../api/client.js';
|
||
import { useAuthStore } from '../stores/authStore.js';
|
||
|
||
/** 费率项定义(路径 + 标签 + 分组 + 单位/个人说明)。 */
|
||
const RATE_FIELDS: ReadonlyArray<{ path: string; label: string; group: string; hint?: string }> = [
|
||
{ path: 'socialInsurance.pension', label: '养老保险(单位)', group: '社会保险(单位部分)', hint: '通常 14%~16%' },
|
||
{ path: 'socialInsurance.medical', label: '医疗保险(单位)', group: '社会保险(单位部分)', hint: '通常 4.5%~10%' },
|
||
{ path: 'socialInsurance.unemployment', label: '失业保险(单位)', group: '社会保险(单位部分)', hint: '通常 0.5%~0.8%' },
|
||
{ path: 'socialInsurance.injury', label: '工伤保险(单位)', group: '社会保险(单位部分)', hint: '按行业风险 0.16%~1.9%' },
|
||
{ path: 'socialInsurance.maternity', label: '生育保险(单位)', group: '社会保险(单位部分)', hint: '与医疗并轨地区填 0' },
|
||
{ path: 'housingFund', label: '住房公积金(单位)', group: '公积金', hint: '5%~12%,按属地' },
|
||
{ path: 'vatGeneralRate', label: '增值税(一般计税)', group: '税率', hint: '现代服务业 6%' },
|
||
{ path: 'vatSimplifiedRate', label: '增值税(简易/差额)', group: '税率', hint: '劳务派遣差额 5%' },
|
||
{ path: 'surchargeRate', label: '附加税费(占增值税)', group: '税率', hint: '城建+教育附加,约 12%' },
|
||
];
|
||
|
||
const REGIONS = ['全国默认', '上海', '北京', '广东', '深圳', '江苏', '浙江', '河北', '四川', '重庆', '湖北', '天津'];
|
||
|
||
function getPath(obj: RegionRates, path: string): number {
|
||
const parts = path.split('.');
|
||
let cur: unknown = obj;
|
||
for (const p of parts) cur = (cur as Record<string, unknown>)?.[p];
|
||
return typeof cur === 'number' ? cur : 0;
|
||
}
|
||
|
||
function setPath(obj: RegionRates, path: string, value: number): RegionRates {
|
||
const clone: RegionRates = JSON.parse(JSON.stringify(obj));
|
||
const parts = path.split('.');
|
||
let cur: Record<string, unknown> = clone as unknown as Record<string, unknown>;
|
||
for (let i = 0; i < parts.length - 1; i += 1) cur = cur[parts[i]!] as Record<string, unknown>;
|
||
cur[parts[parts.length - 1]!] = value;
|
||
return clone;
|
||
}
|
||
|
||
function emptyRates(regionName: string): RegionRates {
|
||
return {
|
||
regionName,
|
||
socialInsurance: { pension: 0, medical: 0, unemployment: 0, injury: 0, maternity: 0 },
|
||
housingFund: 0, vatGeneralRate: 0.06, vatSimplifiedRate: 0.05, surchargeRate: 0.12,
|
||
};
|
||
}
|
||
|
||
function socialTotal(r: RegionRates): number {
|
||
const s = r.socialInsurance;
|
||
return s.pension + s.medical + s.unemployment + s.injury + s.maternity;
|
||
}
|
||
|
||
/** 全成本加载系数估算(应发=1,加社保+公积金)。 */
|
||
function loadingFactor(r: RegionRates): number {
|
||
return 1 + socialTotal(r) + r.housingFund;
|
||
}
|
||
|
||
export function RateManagement(): JSX.Element {
|
||
const { user } = useAuthStore();
|
||
const [defaults, setDefaults] = useState<{ national: RegionRates; regions: Record<string, RegionRates> } | null>(null);
|
||
const [records, setRecords] = useState<RegionRateRecord[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const [editRegion, setEditRegion] = useState('');
|
||
const [editRates, setEditRates] = useState<RegionRates | null>(null);
|
||
|
||
const load = useCallback(() => {
|
||
setLoading(true);
|
||
Promise.all([fetchEngineDefaults(), fetchRegionRates()])
|
||
.then(([d, r]) => { setDefaults(d); setRecords(r); })
|
||
.finally(() => setLoading(false));
|
||
}, []);
|
||
|
||
useEffect(() => { load(); }, [load]);
|
||
|
||
// 当前编辑地域的"引擎默认"基准(用于对比高亮)
|
||
const baseline = useMemo<RegionRates | null>(() => {
|
||
if (defaults === null || editRegion === '') return null;
|
||
const key = ['上海', '北京', '广东'].find((k) => editRegion.includes(k));
|
||
return key ? (defaults.regions[key] ?? defaults.national) : defaults.national;
|
||
}, [defaults, editRegion]);
|
||
|
||
function startEdit(region: string): void {
|
||
const existing = records.find((r) => r.region === region);
|
||
if (existing) {
|
||
setEditRates(existing.rates);
|
||
} else {
|
||
// 以引擎默认为初始值
|
||
const key = ['上海', '北京', '广东'].find((k) => region.includes(k));
|
||
const init = defaults ? (key ? (defaults.regions[key] ?? defaults.national) : defaults.national) : emptyRates(region);
|
||
setEditRates({ ...JSON.parse(JSON.stringify(init)), regionName: region });
|
||
}
|
||
setEditRegion(region);
|
||
}
|
||
|
||
async function handleSave(): Promise<void> {
|
||
if (editRates === null || editRegion === '') return;
|
||
await saveRegionRate(editRegion, { ...editRates, regionName: editRegion }, user?.username);
|
||
setEditRegion('');
|
||
setEditRates(null);
|
||
load();
|
||
}
|
||
|
||
const inputStyle: React.CSSProperties = {
|
||
padding: `${space(1)}px ${space(2)}px`,
|
||
border: `1px solid ${colorVar('color.border.default')}`,
|
||
borderRadius: `${RADIUS.sm}px`,
|
||
fontFamily: FONT_FAMILY, ...typographyStyle('body'),
|
||
backgroundColor: colorVar('color.bg.canvas'), color: colorVar('color.text.primary'),
|
||
width: 90, textAlign: 'right',
|
||
};
|
||
|
||
const groups = [...new Set(RATE_FIELDS.map((f) => f.group))];
|
||
|
||
return (
|
||
<div style={{ fontFamily: FONT_FAMILY, maxWidth: 1200, margin: '0 auto' }}>
|
||
<div style={{ marginBottom: `${space(4)}px` }}>
|
||
<h1 style={{ ...typographyStyle('heading'), color: colorVar('color.text.primary'), margin: 0 }}>费率管理</h1>
|
||
<p style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), margin: `${space(1)}px 0 0` }}>
|
||
以「地域费率套」维护社保、公积金与税率,与评估引擎对齐。维护并<strong>复核</strong>后,该地域评估将自动采用此费率(覆盖引擎内置默认)。所有费率为行业近似值,须经财务复核。
|
||
</p>
|
||
</div>
|
||
|
||
{loading ? <p style={{ color: colorVar('color.text.secondary') }}>加载中…</p> : (
|
||
<>
|
||
{/* 最低工资标准(驱动"低于最低工资"红线) */}
|
||
<div style={{ marginBottom: `${space(4)}px` }}>
|
||
<MinWagePanel />
|
||
</div>
|
||
|
||
{/* 已维护的地域费率套 */}
|
||
<Card title={`已维护地域费率套(${records.length})`}>
|
||
{records.length === 0 ? (
|
||
<p style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary') }}>
|
||
尚未维护任何地域费率套。系统当前使用引擎内置默认费率。点击下方地域按钮开始维护。
|
||
</p>
|
||
) : (
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead><tr>
|
||
{['地域', '社保单位合计', '公积金', '增值税(一般)', '附加税', '加载系数', '复核状态', '更新', '操作'].map((h) => (
|
||
<th key={h} style={{ textAlign: 'left', padding: `${space(2)}px`, borderBottom: `2px solid ${colorVar('color.border.default')}`, color: colorVar('color.text.secondary'), ...typographyStyle('caption'), fontWeight: 700 }}>{h}</th>
|
||
))}
|
||
</tr></thead>
|
||
<tbody>
|
||
{records.map((rec) => (
|
||
<tr key={rec.region}>
|
||
<td style={tdStyle}>{rec.region}</td>
|
||
<td style={tdStyle}>{(socialTotal(rec.rates) * 100).toFixed(2)}%</td>
|
||
<td style={tdStyle}>{(rec.rates.housingFund * 100).toFixed(1)}%</td>
|
||
<td style={tdStyle}>{(rec.rates.vatGeneralRate * 100).toFixed(1)}%</td>
|
||
<td style={tdStyle}>{(rec.rates.surchargeRate * 100).toFixed(0)}%</td>
|
||
<td style={{ ...tdStyle, fontWeight: 700, color: colorVar('color.brand.primary') }}>{loadingFactor(rec.rates).toFixed(3)}×</td>
|
||
<td style={tdStyle}>{rec.reviewed ? <span style={{ color: '#15803D', fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="check-circle" size={14} /> 已生效</span> : <span style={{ color: '#B45309', display: 'inline-flex', alignItems: 'center', gap: 4 }}><Icon name="clock" size={14} /> 待复核</span>}</td>
|
||
<td style={{ ...tdStyle, ...typographyStyle('caption'), color: colorVar('color.text.secondary') }}>{rec.updatedBy ?? '—'}</td>
|
||
<td style={tdStyle}>
|
||
<div style={{ display: 'flex', gap: `${space(2)}px` }}>
|
||
<button onClick={() => startEdit(rec.region)} style={linkBtn(colorVar('color.brand.primary'))}>编辑</button>
|
||
{!rec.reviewed && <button onClick={() => reviewRegionRate(rec.region).then(load)} style={linkBtn('#15803D')}>复核生效</button>}
|
||
<button onClick={() => deleteRegionRate(rec.region).then(load)} style={linkBtn(colorVar('color.risk.critical'))}>删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
)}
|
||
<div style={{ marginTop: `${space(3)}px`, display: 'flex', gap: `${space(2)}px`, flexWrap: 'wrap', alignItems: 'center' }}>
|
||
<span style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary') }}>新增/维护地域:</span>
|
||
{REGIONS.filter((r) => !records.some((rec) => rec.region === r)).map((r) => (
|
||
<button key={r} onClick={() => startEdit(r)} style={{ padding: `${space(1)}px ${space(3)}px`, borderRadius: `${RADIUS.md}px`, border: `1px dashed ${colorVar('color.brand.primary')}`, background: 'transparent', color: colorVar('color.brand.primary'), cursor: 'pointer', ...typographyStyle('caption') }}>+ {r}</button>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 编辑表单 */}
|
||
{editRates !== null && (
|
||
<div style={{ marginTop: `${space(4)}px` }}>
|
||
<Card title={`维护「${editRegion}」费率套`}>
|
||
<p style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), margin: `0 0 ${space(3)}px` }}>
|
||
填写各项费率(小数,如 0.16 表示 16%)。<span style={{ color: '#B45309' }}>橙色</span>表示偏离引擎默认值。保存后需复核方生效。
|
||
</p>
|
||
{groups.map((g) => (
|
||
<div key={g} style={{ marginBottom: `${space(3)}px` }}>
|
||
<div style={{ ...typographyStyle('caption'), fontWeight: 700, color: colorVar('color.text.secondary'), marginBottom: `${space(1)}px` }}>{g}</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: `${space(2)}px` }}>
|
||
{RATE_FIELDS.filter((f) => f.group === g).map((f) => {
|
||
const val = getPath(editRates, f.path);
|
||
const baseVal = baseline ? getPath(baseline, f.path) : val;
|
||
const deviated = Math.abs(val - baseVal) > 1e-6;
|
||
return (
|
||
<div key={f.path} style={{ display: 'flex', flexDirection: 'column', gap: '2px' }}>
|
||
<label style={{ ...typographyStyle('caption'), color: colorVar('color.text.primary') }}>{f.label}</label>
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: `${space(1)}px` }}>
|
||
<input
|
||
style={{ ...inputStyle, borderColor: deviated ? '#B45309' : colorVar('color.border.default'), color: deviated ? '#B45309' : colorVar('color.text.primary'), fontWeight: deviated ? 700 : 400 }}
|
||
value={val}
|
||
onChange={(e) => setEditRates((r) => r ? setPath(r, f.path, Number(e.target.value) || 0) : r)}
|
||
inputMode="decimal"
|
||
/>
|
||
<span style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary') }}>{(val * 100).toFixed(2)}%</span>
|
||
</div>
|
||
<span style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), fontSize: '11px' }}>
|
||
{f.hint}{deviated ? ` · 默认 ${(baseVal * 100).toFixed(2)}%` : ''}
|
||
</span>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
))}
|
||
{/* 汇总 */}
|
||
<div style={{ display: 'flex', gap: `${space(4)}px`, padding: `${space(3)}px`, backgroundColor: colorVar('color.bg.surface'), borderRadius: `${RADIUS.md}px`, marginBottom: `${space(3)}px`, flexWrap: 'wrap' }}>
|
||
<Summary label="社保单位合计" value={`${(socialTotal(editRates) * 100).toFixed(2)}%`} />
|
||
<Summary label="公积金" value={`${(editRates.housingFund * 100).toFixed(1)}%`} />
|
||
<Summary label="全成本加载系数" value={`${loadingFactor(editRates).toFixed(3)}×`} highlight />
|
||
<Summary label="说明" value="加载系数 = 1 + 社保 + 公积金(不含福利/摊销)" small />
|
||
</div>
|
||
<div style={{ display: 'flex', gap: `${space(2)}px` }}>
|
||
<button onClick={handleSave} style={{ padding: `${space(2)}px ${space(5)}px`, borderRadius: `${RADIUS.md}px`, border: 'none', backgroundColor: colorVar('color.brand.primary'), color: '#fff', cursor: 'pointer', fontWeight: 600 }}>保存(待复核)</button>
|
||
<button onClick={() => { setEditRegion(''); setEditRates(null); }} style={{ padding: `${space(2)}px ${space(4)}px`, borderRadius: `${RADIUS.md}px`, border: `1px solid ${colorVar('color.border.default')}`, background: 'transparent', cursor: 'pointer' }}>取消</button>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
|
||
{/* 引擎内置默认费率(只读对照) */}
|
||
{defaults !== null && (
|
||
<div style={{ marginTop: `${space(4)}px` }}>
|
||
<Card title="引擎内置默认费率(只读对照基准)">
|
||
<p style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), margin: `0 0 ${space(2)}px` }}>
|
||
未维护费率套的地域评估时采用以下默认值。
|
||
</p>
|
||
<div style={{ overflowX: 'auto' }}>
|
||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||
<thead><tr>
|
||
{['地域', '养老', '医疗', '失业', '工伤', '生育', '公积金', '增值税', '附加税', '加载系数'].map((h) => (
|
||
<th key={h} style={{ textAlign: 'left', padding: `${space(1)}px ${space(2)}px`, borderBottom: `1px solid ${colorVar('color.border.default')}`, color: colorVar('color.text.secondary'), ...typographyStyle('caption'), fontWeight: 700 }}>{h}</th>
|
||
))}
|
||
</tr></thead>
|
||
<tbody>
|
||
{[defaults.national, ...Object.values(defaults.regions).filter((r) => r.regionName !== '全国(默认)')].map((r) => (
|
||
<tr key={r.regionName}>
|
||
<td style={defTd}>{r.regionName}</td>
|
||
<td style={defTd}>{(r.socialInsurance.pension * 100).toFixed(1)}%</td>
|
||
<td style={defTd}>{(r.socialInsurance.medical * 100).toFixed(1)}%</td>
|
||
<td style={defTd}>{(r.socialInsurance.unemployment * 100).toFixed(2)}%</td>
|
||
<td style={defTd}>{(r.socialInsurance.injury * 100).toFixed(2)}%</td>
|
||
<td style={defTd}>{(r.socialInsurance.maternity * 100).toFixed(2)}%</td>
|
||
<td style={defTd}>{(r.housingFund * 100).toFixed(1)}%</td>
|
||
<td style={defTd}>{(r.vatGeneralRate * 100).toFixed(1)}%</td>
|
||
<td style={defTd}>{(r.surchargeRate * 100).toFixed(0)}%</td>
|
||
<td style={{ ...defTd, fontWeight: 700, color: colorVar('color.brand.primary') }}>{loadingFactor(r).toFixed(3)}×</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const tdStyle: React.CSSProperties = { padding: `${space(2)}px`, borderBottom: '1px solid var(--color-border-default)', fontSize: '14px' };
|
||
const defTd: React.CSSProperties = { padding: `${space(1)}px ${space(2)}px`, borderBottom: '1px solid var(--color-border-default)', fontSize: '13px', whiteSpace: 'nowrap' };
|
||
|
||
function linkBtn(color: string): React.CSSProperties {
|
||
return { cursor: 'pointer', border: 'none', background: 'none', color, fontWeight: 600, fontSize: '12px' };
|
||
}
|
||
|
||
function Summary({ label, value, highlight, small }: { label: string; value: string; highlight?: boolean; small?: boolean }): JSX.Element {
|
||
return (
|
||
<div>
|
||
<div style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary') }}>{label}</div>
|
||
<div style={{ ...(small ? typographyStyle('caption') : typographyStyle('title')), fontWeight: small ? 400 : 700, color: highlight ? colorVar('color.brand.primary') : colorVar('color.text.primary'), marginTop: 2 }}>{value}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** 最低工资标准维护面板:驱动"低于最低工资"红线自动比对。 */
|
||
function MinWagePanel(): JSX.Element {
|
||
const [items, setItems] = useState<MinWageItem[]>([]);
|
||
const [region, setRegion] = useState('');
|
||
const [wage, setWage] = useState('');
|
||
|
||
const reload = useCallback(() => {
|
||
fetchMinWages().then(setItems).catch(() => setItems([]));
|
||
}, []);
|
||
useEffect(() => { reload(); }, [reload]);
|
||
|
||
const inputStyle: React.CSSProperties = {
|
||
padding: `${space(1)}px ${space(2)}px`, border: `1px solid ${colorVar('color.border.default')}`,
|
||
borderRadius: `${RADIUS.md}px`, fontFamily: FONT_FAMILY, ...typographyStyle('caption'),
|
||
backgroundColor: colorVar('color.bg.canvas'), color: colorVar('color.text.primary'),
|
||
};
|
||
|
||
async function handleSave(): Promise<void> {
|
||
const w = Number(wage);
|
||
if (region.trim() === '' || !Number.isFinite(w) || w <= 0) return;
|
||
await saveMinWage(region.trim(), w);
|
||
setRegion(''); setWage('');
|
||
reload();
|
||
}
|
||
|
||
return (
|
||
<Card title={`各地域最低工资标准(${items.length})`}>
|
||
<p style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), margin: `0 0 ${space(2)}px` }}>
|
||
评估时按岗位应发工资与所在地域最低工资比对,低于标准的岗位数将驱动「低于最低工资」红线。近似默认值须经 HR/财务按当地官方标准复核。
|
||
</p>
|
||
<div style={{ display: 'flex', gap: `${space(2)}px`, flexWrap: 'wrap', alignItems: 'center', marginBottom: `${space(2)}px` }}>
|
||
<input style={{ ...inputStyle, width: 120 }} value={region} onChange={(e) => setRegion(e.target.value)} placeholder="地域(如 北京)" />
|
||
<input style={{ ...inputStyle, width: 140 }} value={wage} onChange={(e) => setWage(e.target.value)} placeholder="月最低工资(元)" inputMode="decimal" />
|
||
<button type="button" onClick={handleSave} style={{ padding: `${space(1)}px ${space(3)}px`, borderRadius: `${RADIUS.md}px`, border: 'none', backgroundColor: colorVar('color.brand.primary'), color: '#fff', cursor: 'pointer', fontWeight: 600 }}>保存/更新</button>
|
||
</div>
|
||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(180px, 1fr))', gap: `${space(2)}px` }}>
|
||
{items.map((m) => (
|
||
<div key={m.region} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: `${space(1)}px ${space(2)}px`, border: `1px solid ${colorVar('color.border.default')}`, borderRadius: `${RADIUS.md}px`, ...typographyStyle('caption') }}>
|
||
<span style={{ color: colorVar('color.text.primary') }}>{m.region}</span>
|
||
<div style={{ display: 'flex', gap: `${space(2)}px`, alignItems: 'center' }}>
|
||
<span style={{ fontWeight: 700 }}>{m.monthlyWage.toLocaleString('zh-CN')} 元</span>
|
||
<button type="button" onClick={() => { void deleteMinWageApi(m.region).then(reload); }} style={{ display: 'inline-flex', alignItems: 'center', border: 'none', background: 'transparent', color: colorVar('color.risk.critical'), cursor: 'pointer' }} aria-label="删除"><Icon name="close" size={15} /></button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
);
|
||
}
|