diff --git a/web端/财务管理/还车应结款-费用明细.jsx b/web端/财务管理/还车应结款-费用明细.jsx index 5410785..c45be54 100644 --- a/web端/财务管理/还车应结款-费用明细.jsx +++ b/web端/财务管理/还车应结款-费用明细.jsx @@ -56,6 +56,35 @@ const Component = function () { return isNaN(n) ? '' : n.toFixed(2); } + function filterSignedMoneyInput(raw) { + var s = String(raw == null ? '' : raw); + var neg = s.charAt(0) === '-'; + s = s.replace(/[^\d.]/g, ''); + var parts = s.split('.'); + if (parts.length > 2) { + s = parts[0] + '.' + parts.slice(1).join(''); + parts = s.split('.'); + } + if (parts[1] && parts[1].length > 2) s = parts[0] + '.' + parts[1].slice(0, 2); + return (neg ? '-' : '') + s; + } + + function formatSignedMoney2(raw) { + if (raw === null || raw === undefined) return ''; + var v = String(raw).trim(); + if (!v || v === '-') return v === '-' ? '-' : ''; + var n = parseFloat(v); + return isNaN(n) ? '' : n.toFixed(2); + } + + function isValidSignedMoney2(s) { + if (s === null || s === undefined) return false; + var v = String(s).trim(); + if (!v || v === '-') return false; + if (!/^-?\d+(\.\d{1,2})?$/.test(v)) return false; + return !isNaN(parseFloat(v)); + } + // 页面样式 var layoutStyle = { padding: '16px 24px 88px', background: '#f5f5f5', minHeight: '100vh' }; var cardStyle = { marginBottom: 16 }; @@ -125,7 +154,7 @@ const Component = function () { }; // 业务服务组 - var businessServiceFixedItems = ['违章处理违约金', '保险上浮', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费']; + var businessServiceFixedItems = ['租金', 'ETC-客户未缴费用', 'ETC卡缺损费', 'ETC设备缺损费']; var businessServiceRowsState = useState( businessServiceFixedItems.map(function (name, i) { return { key: 'bs-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; @@ -474,7 +503,50 @@ const Component = function () { }); }, []); - // 安全组(示例数据) + // 安全组 + var safetyFixedItems = ['违章处理违约金', '保险上浮', '其他违规费用']; + var safetyFeeRowsState = useState( + safetyFixedItems.map(function (name, i) { + return { key: 'sf-' + i, seq: i + 1, feeItem: name, amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: true }; + }) + ); + var safetyFeeRows = safetyFeeRowsState[0]; + var setSafetyFeeRows = safetyFeeRowsState[1]; + + function recalcSafetyTotal(list) { + var sum = 0; + (list || []).forEach(function (r) { sum += (parseFloat(r.amount) || 0); }); + return sum.toFixed(2); + } + + var safetyTotalComputed = useMemo(function () { + return recalcSafetyTotal(safetyFeeRows || []); + }, [safetyFeeRows]); + + var addSafetyFeeRow = useCallback(function () { + setSafetyFeeRows(function (p) { + var next = p.slice(); + next.push({ key: 'sf-new-' + Date.now(), seq: next.length + 1, feeItem: '', amount: '', remark: '', lastUpdateTime: '', photos: [], attachments: [], fixed: false }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var removeSafetyFeeRow = useCallback(function (key) { + setSafetyFeeRows(function (p) { + var next = p.filter(function (r) { return r.key !== key; }); + return next.map(function (r, idx) { var o = {}; for (var k in r) o[k] = r[k]; o.seq = idx + 1; return o; }); + }); + }, []); + var updateSafetyFeeRow = useCallback(function (key, field, value) { + setSafetyFeeRows(function (p) { + return p.map(function (r) { + if (r.key !== key) return r; + var n = {}; for (var k in r) n[k] = r[k]; + n[field] = value; + return n; + }); + }); + }, []); + var violationList = useMemo(function () { return [{ key: 'w1', @@ -525,6 +597,10 @@ const Component = function () { okText: '确认', cancelText: '取消', onOk: function () { + setSafetyFeeRows(function (p) { + var now = formatDateTimeNow(); + return p.map(function (r) { var n = {}; for (var k in r) n[k] = r[k]; n.lastUpdateTime = now; return n; }); + }); setSafetyMeta(function (m) { var nm = {}; for (var k in m) nm[k] = m[k]; nm.submitBy = nm.submitBy || '安全组-赵六'; nm.status = '已提交'; return nm; }); message.success('安全组已提交'); } @@ -548,8 +624,9 @@ const Component = function () { var rentRefund = parseFloat((billInfo && billInfo.shouldRefundRent) || '') || 0; var op = parseFloat(operationTotalComputed) || 0; var en = (parseFloat(energy.hydrogenSupplement) || 0) + (parseFloat(energy.hydrogenFee) || 0) + (parseFloat(energy.electricFee) || 0) - (parseFloat(energy.prepayRefund) || 0); - return (bs + rentRefund + en + op).toFixed(2); - }, [businessServiceTotalComputed, billInfo.shouldRefundRent, operationTotalComputed, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund]); + var sf = parseFloat(safetyTotalComputed) || 0; + return (bs + rentRefund + en + op + sf).toFixed(2); + }, [businessServiceTotalComputed, billInfo.shouldRefundRent, operationTotalComputed, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, safetyTotalComputed]); var refundTotalComputed = useMemo(function () { var deposit = parseFloat(stats.depositAmount) || 0; @@ -597,9 +674,10 @@ const Component = function () { { key: 'eFee', item: '能源采购组-电费补缴金额', amount: (toFixed2(energy.electricFee) || '0.00') }, { key: 'prepay', item: '能源采购组-预付款退费金额(减)', amount: '-' + (toFixed2(energy.prepayRefund) || '0.00') }, { key: 'op', item: '运维部费用项金额总额', amount: operationTotalComputed || '0.00' }, + { key: 'sf', item: '安全组费用项金额总和', amount: safetyTotalComputed || '0.00' }, { key: 'total', item: '待结算总额', amount: pendingSettleComputed || '0.00', strong: true } ]; - }, [businessServiceTotalComputed, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotalComputed, pendingSettleComputed]); + }, [businessServiceTotalComputed, billInfo.shouldRefundRent, energy.hydrogenSupplement, energy.hydrogenFee, energy.electricFee, energy.prepayRefund, operationTotalComputed, safetyTotalComputed, pendingSettleComputed]); var refundBreakdown = useMemo(function () { return [ @@ -640,7 +718,7 @@ var requirementDocContent = useMemo(function () { 3.还车费用明细: #上方为还车费用统计数据,包括保证金总额、待结算总额、应退还总额、应补缴总额等相关信息,该部分只有业务管理组能查看; 3.1.保证金总额:显示该车辆保证金金额,格式为xx.xx元; -3.2.待结算总额:显示该车辆待结算金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「业务服务部所有费用项-金额总和」+「业务服务部-车辆应退租金」+「能源采购组-氢量差补缴金额」+「能源采购组-氢费补缴金额」+「能源采购组-电费补缴金额」-「能源采购组-预付款退费金额」+「运维部所有费用项-金额总额」; +3.2.待结算总额:显示该车辆待结算金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「业务服务部所有费用项-金额总和」+「业务服务部-车辆应退租金」+「能源采购组-氢量差补缴金额」+「能源采购组-氢费补缴金额」+「能源采购组-电费补缴金额」-「能源采购组-预付款退费金额」+「运维部所有费用项-金额总额」+「安全组费用项金额总和」; 3.3.应退还总额:显示该车辆应退还金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「保证金金额」-「待结算金额」如果是正数,则显示在应退还总额中; 3.4.应补缴总额:显示该车辆应补缴金额,格式为xx.xx元,点击以气泡卡片列表显示:费用项、金额。计算方式为:「保证金金额」-「待结算金额」如果是负数,则显示在应补缴总额中; @@ -655,12 +733,11 @@ var requirementDocContent = useMemo(function () { 下方为列表,列表字段为:序号、费用项、金额、备注、照片、附件、操作; 4.7.序号:1、2、3....依次类推; -4.8.费用项:固定显示违章处理违约金、保险上浮、ETC-客户未缴费用、ETC卡缺损费、ETC设备缺损费,该部分不能删除;可通过点击下方新增一行,添加新的条目(该部分可删除); - 4.8.1.违章处理违约金: - 4.8.2.保险上浮: - 4.8.3.ETC-客户未缴费用: - 4.8.4.ETC卡缺损费: - 4.8.5.ETC设备缺损费: +4.8.费用项:固定显示租金、ETC-客户未缴费用、ETC卡缺损费、ETC设备缺损费,该部分不能删除;可通过点击下方新增一行,添加新的条目(该部分可删除); + 4.8.1.租金:金额输入框支持正数/负数,最多2位小数; + 4.8.2.ETC-客户未缴费用: + 4.8.3.ETC卡缺损费: + 4.8.4.ETC设备缺损费: 4.9.金额:输入框,支持2位小数,后缀为元; 4.10.备注:文本域,支持自定义输入; 4.11.最后更新时间:显示最后更新时间,格式为:YYYY-MM-DD HH:MM; @@ -716,14 +793,16 @@ var requirementDocContent = useMemo(function () { 6.13.附件:附件上传按钮,文案为:上传附件; 6.14.操作:除固定费用项外,其余操作中为删除; -7.安全组:仅由安全组人员进行确认提交;标题栏标题为安全组,后方为提交人、状态(待提交、已提交),该部分只有安全组能查看; -7.1.提交人:显示提交人; -7.2.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); -7.3.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; -7.4.提交:点击进行二次确认,提示信息:请确认业务服务组金额填写无误,点击确认完成提交。如果已提交,隐藏提交按钮; -7.5.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; -7.6.违章清单:违章编码、车牌号、违法行为、违法时间、罚款金额、缴费状态、计分值、是否处理、违章客户、违章照片、备注; -7.7.事故清单:列表显示:事故编码、车牌号、事故时间、事故地点、事故类型、客户名称、我方定损金额、对方定损金额、责任划分、事故状态、结案时间、其他费用、备注; +7.安全组:仅由安全组人员进行确认提交;标题栏标题为安全组,后方为总金额、提交人、状态(待提交、已提交),该部分只有安全组能查看; +7.1.总金额:显示所有费用项金额总和; +7.2.提交人:显示提交人; +7.3.状态:分为待提交(点击保存按钮)、已提交(点击提交按钮); +7.4.保存:点击保存已填写内容,如果已提交,则隐藏保存按钮; +7.5.提交:点击进行二次确认,提示信息:请确认安全组信息无误,点击确认完成提交。如果已提交,隐藏提交按钮; +7.6.撤回:已提交后,显示撤回按钮。点击进行二次确认,提示信息:是否确认撤回,点击确认,重新显示保存、提交按钮; +7.7.费用项列表(样式同业务服务组):固定显示违章处理违约金、保险上浮、其他违规费用,不可删除;可新增自定义费用项;字段含序号、费用项、金额、备注、最后更新时间、照片、附件、操作; +7.8.违章清单:违章编码、车牌号、违法行为、违法时间、罚款金额、缴费状态、计分值、是否处理、违章客户、违章照片、备注; +7.9.事故清单:列表显示:事故编码、车牌号、事故时间、事故地点、事故类型、客户名称、我方定损金额、对方定损金额、责任划分、事故状态、结案时间、其他费用、备注; 8.底部为提交、取消按钮; 8.1.提交审核:还车应结款生成后有15天时间倒计时,倒计时结束并且业务服务组、能源采购组、运维部、安全部均提交后才启用,平时禁用,倒计时显示在按钮上,格式为:x天x小时后可提交审核; @@ -829,6 +908,9 @@ var requirementDocContent = useMemo(function () { tireTreadList: tireTreadList, updateBusinessServiceRow: updateBusinessServiceRow, removeBusinessServiceRow: removeBusinessServiceRow, + safetyMeta: safetyMeta, + updateSafetyFeeRow: updateSafetyFeeRow, + removeSafetyFeeRow: removeSafetyFeeRow, updateOperationRow: updateOperationRow, removeOperationRow: removeOperationRow }; @@ -851,7 +933,21 @@ var requirementDocContent = useMemo(function () { var ref = latestRefs.current || {}; var meta = ref.businessServiceMeta || {}; var readOnly = meta.status === '已提交'; - return React.createElement(Input, { value: v, onChange: function (e) { ref.updateBusinessServiceRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: readOnly }); + var isRent = r.feeItem === '租金'; + return React.createElement(Input, { + value: v, + onChange: function (e) { + var nv = isRent ? filterSignedMoneyInput(e.target.value) : e.target.value; + ref.updateBusinessServiceRow(r.key, 'amount', nv); + }, + onBlur: isRent ? function (e) { + var formatted = formatSignedMoney2(e.target.value); + if (formatted !== e.target.value) ref.updateBusinessServiceRow(r.key, 'amount', formatted); + } : undefined, + placeholder: '0.00', + addonAfter: '元', + disabled: readOnly + }); } }, { @@ -878,6 +974,51 @@ var requirementDocContent = useMemo(function () { ]; }, []); + var safetyFeeColumns = useMemo(function () { + return [ + { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, + { + title: '费用项', dataIndex: 'feeItem', key: 'feeItem', width: 200, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + var readOnly = meta.status === '已提交'; + return r.fixed ? (v || '-') : React.createElement(Input, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'feeItem', e.target.value); }, placeholder: '费用项', disabled: readOnly }); + } + }, + { + title: RequiredLabel('金额'), dataIndex: 'amount', key: 'amount', width: 140, + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(Input, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'amount', e.target.value); }, placeholder: '0.00', addonAfter: '元', disabled: readOnly }); + } + }, + { + title: '备注', dataIndex: 'remark', key: 'remark', + render: function (v, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + var readOnly = meta.status === '已提交'; + return React.createElement(TextArea, { value: v, onChange: function (e) { ref.updateSafetyFeeRow(r.key, 'remark', e.target.value); }, placeholder: '备注', rows: 1, disabled: readOnly }); + } + }, + { title: '最后更新时间', dataIndex: 'lastUpdateTime', key: 'lastUpdateTime', width: 150, render: function (v) { return React.createElement('span', { style: { fontSize: 12, color: '#666' } }, v || '-'); } }, + { title: '照片', key: 'photo', width: 100, render: function () { var ref = latestRefs.current || {}; var meta = ref.safetyMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('照片上传(原型)'); } }, '上传'); } }, + { title: '附件', key: 'attachment', width: 110, render: function () { var ref = latestRefs.current || {}; var meta = ref.safetyMeta || {}; var readOnly = meta.status === '已提交'; return React.createElement(Button, { size: 'small', disabled: readOnly, onClick: function () { message.info('上传附件(原型)'); } }, '上传附件'); } }, + { + title: '操作', key: 'action', width: 80, + render: function (_, r) { + var ref = latestRefs.current || {}; + var meta = ref.safetyMeta || {}; + if (r.fixed || meta.status === '已提交') return null; + return React.createElement(Button, { type: 'link', size: 'small', danger: true, onClick: function () { ref.removeSafetyFeeRow(r.key); } }, '删除'); + } + } + ]; + }, []); + var operationColumns = useMemo(function () { return [ { title: '序号', dataIndex: 'seq', key: 'seq', width: 60 }, @@ -1042,11 +1183,14 @@ var requirementDocContent = useMemo(function () { return !isNaN(parseFloat(v)); } - // 业务服务组:每行“金额”必填 + // 业务服务组:每行“金额”必填;租金支持正负两位小数 for (var i = 0; i < (businessServiceRows || []).length; i++) { var r1 = businessServiceRows[i]; - if (!isValidMoney(r1.amount)) { - message.error('请填写业务服务组第' + (r1.seq || (i + 1)) + '行金额'); + var amountOk = r1.feeItem === '租金' ? isValidSignedMoney2(r1.amount) : isValidMoney(r1.amount); + if (!amountOk) { + message.error(r1.feeItem === '租金' + ? '请填写业务服务组租金金额(支持正负,最多2位小数)' + : '请填写业务服务组第' + (r1.seq || (i + 1)) + '行金额'); return; } } @@ -1074,6 +1218,14 @@ var requirementDocContent = useMemo(function () { } } + for (var k = 0; k < (safetyFeeRows || []).length; k++) { + var r3 = safetyFeeRows[k]; + if (!isValidMoney(r3.amount)) { + message.error('请填写安全组第' + (r3.seq || (k + 1)) + '行金额'); + return; + } + } + if (!canSubmitReview()) { message.error('未满足提交审核条件(倒计时结束且四组均已提交)'); return; @@ -1290,6 +1442,7 @@ return React.createElement('div', { style: layoutStyle }, setCollapsed: safetyCollapsedState[1], title: '安全组', extra: React.createElement(React.Fragment, null, + React.createElement('span', null, '总金额:', safetyTotalComputed, ' 元'), React.createElement('span', null, '提交人:', safetyMeta.submitBy || '-'), React.createElement('span', null, '状态:', safetyMeta.status) ), @@ -1300,7 +1453,13 @@ return React.createElement('div', { style: layoutStyle }, React.createElement(Button, { size: 'small', type: 'primary', onClick: handleSafetySubmit }, '提交') ) }, - React.createElement('div', { style: { marginBottom: 12, fontWeight: 600 } }, '违章清单'), + React.createElement(Table, { rowKey: 'key', columns: safetyFeeColumns, dataSource: safetyFeeRows, pagination: false, bordered: true, size: 'small' }), + safetyMeta.status === '待提交' + ? React.createElement('div', { style: { marginTop: 8, width: '100%' } }, + React.createElement(Button, { type: 'dashed', size: 'small', onClick: addSafetyFeeRow, block: true, style: { width: '100%' } }, '新增一行') + ) + : null, + React.createElement('div', { style: { margin: '16px 0 12px', fontWeight: 600 } }, '违章清单'), React.createElement(Table, { rowKey: 'key', size: 'small',