Просмотр исходного кода

feat(inspection): 实现整改流程串行化及整改内容自动保存

- 新增整改人按顺序逐一完成整改的串行流程,替换原有并行模式
- 添加整改内容自动保存功能,支持输入防抖、冲突检测和保存状态提示
- 新增整改退回功能,支持退回上一整改人并重新进入流程
- 添加审批流程页面滚动位置保存与恢复机制
- 在巡检菜单中增加整改和批复的待办数量角标
- 优化整改内容展示样式,支持换行和长文本自动换行
- 调整附件弹窗表格为固定布局,统一质量和安全巡检附件样式
- 在控制器中新增 _getInspectionMenuCounts 方法获取待办统计
caipin 1 неделя назад
Родитель
Сommit
eb22ba16b1

+ 11 - 0
app/controller/quality_controller.js

@@ -741,6 +741,7 @@ module.exports = app => {
                     tender,
                     inspection: ctx.inspection,
                     auditConst: qualityInspectionConst,
+                    inspectionMenuCounts: await this._getInspectionMenuCounts(ctx),
                     fileList,
                     whiteList,
                     auditType,
@@ -1030,6 +1031,7 @@ module.exports = app => {
                     tender,
                     permission: ctx.permission.inspection,
                     overview: await ctx.service.qualityInspection.getOverviewData(ctx.tender.id, year),
+                    inspectionMenuCounts: await this._getInspectionMenuCounts(ctx),
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.quality.inspection_overview),
                 };
                 await this.layout('quality/inspection_overview.ejs', renderData);
@@ -1063,6 +1065,15 @@ module.exports = app => {
                 ctx.redirect(`/sp/${ctx.subProject.id}/quality/tender/${ctx.tender.id}/inspection`);
             }
         }
+
+        async _getInspectionMenuCounts(ctx) {
+            const status = qualityInspectionConst.filter.status;
+            const [rectification, reply] = await Promise.all([
+                ctx.service.qualityInspection.getCountByStatus(ctx.tender.id, status.rectification),
+                ctx.service.qualityInspection.getCountByStatus(ctx.tender.id, status.reply),
+            ]);
+            return { rectification, reply };
+        }
     }
 
     return QualityController;

+ 11 - 0
app/controller/safe_controller.js

@@ -615,6 +615,7 @@ module.exports = app => {
                     tender,
                     permission: ctx.permission.safe_inspection,
                     overview: await ctx.service.safeInspection.getOverviewData(ctx.tender.id, year),
+                    inspectionMenuCounts: await this._getInspectionMenuCounts(ctx),
                     jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.safe.inspection_overview),
                 };
                 await this.layout('safe/inspection_overview.ejs', renderData);
@@ -810,6 +811,7 @@ module.exports = app => {
                     inspection: ctx.inspection,
                     auditConst: safeInspectionConst,
                     fileList,
+                    inspectionMenuCounts: await this._getInspectionMenuCounts(ctx),
                     whiteList,
                     auditType,
                     shenpiConst,
@@ -1092,6 +1094,15 @@ module.exports = app => {
                 }
             }
         }
+
+        async _getInspectionMenuCounts(ctx) {
+            const status = safeInspectionConst.filter.status;
+            const [rectification, reply] = await Promise.all([
+                ctx.service.safeInspection.getCountByStatus(ctx.tender.id, status.rectification),
+                ctx.service.safeInspection.getCountByStatus(ctx.tender.id, status.reply),
+            ]);
+            return { rectification, reply };
+        }
     }
 
     return SafeController;

+ 46 - 6
app/public/css/main.css

@@ -3367,6 +3367,11 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 .safe-inspection-rectification .flow-rectification-item {
     min-height: 48px;
 }
+.safe-inspection-rectification .rectification-content {
+    min-height: 48px;
+    white-space: pre-wrap;
+    overflow-wrap: break-word;
+}
 .safe-inspection-rectification .flow-rectification-date {
     width: 200px;
     max-width: 100%;
@@ -3541,6 +3546,11 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
 .quality-inspection-rectification .flow-rectification-item {
     min-height: 48px;
 }
+.quality-inspection-rectification .rectification-content {
+    min-height: 48px;
+    white-space: pre-wrap;
+    overflow-wrap: break-word;
+}
 .quality-inspection-rectification .flow-rectification-date {
     width: 200px;
     max-width: 100%;
@@ -3592,18 +3602,48 @@ button.gy-stat:focus-visible{outline:2px solid var(--accent);outline-offset:3px}
     flex-wrap: wrap;
 }
 
-.quality-inspection-attachments .modal-dialog {
-    width: max-content;
+.quality-inspection-attachments .modal-dialog,
+.safe-inspection-attachments .modal-dialog {
+    width: 900px;
     max-width: calc(100% - 32px);
     margin-right: auto;
     margin-left: auto;
 }
-.quality-inspection-attachments table {
-    width: auto;
+.quality-inspection-attachments table,
+.safe-inspection-attachments table {
+    width: 100%;
     max-width: 100%;
-    table-layout: auto;
+    table-layout: fixed;
 }
 .quality-inspection-attachments th,
-.quality-inspection-attachments td {
+.quality-inspection-attachments td,
+.safe-inspection-attachments th,
+.safe-inspection-attachments td {
+    white-space: normal;
+    overflow-wrap: break-word;
+}
+
+.quality-inspection-menu .nav-list a,
+.safe-inspection-menu .nav-list a {
+    padding-right: 8px;
     white-space: nowrap;
 }
+.quality-inspection-menu .nav-list a .quality-inspection-menu-count,
+.safe-inspection-menu .nav-list a .safe-inspection-menu-count {
+    position: static;
+    margin-left: 4px;
+    vertical-align: middle;
+}
+
+.quality-inspection-list-table,
+.safe-inspection-list-table {
+    width: 100%;
+    table-layout: fixed;
+}
+.quality-inspection-list-table th,
+.quality-inspection-list-table td,
+.safe-inspection-list-table th,
+.safe-inspection-list-table td {
+    white-space: normal;
+    overflow-wrap: break-word;
+}

+ 192 - 17
app/public/js/quality_inspection_information.js

@@ -696,6 +696,7 @@ $(document).ready(function () {
             })
         }
     });
+    restoreQualityInspectionFlowScroll();
 })
 
 
@@ -724,14 +725,14 @@ function validateFiles(files) {
 // 表单自动保存和流程提交按顺序执行,避免提交先于最后一次字段保存。
 let qualityInspectionSaveQueue = Promise.resolve(true);
 let qualityInspectionSaving = 0;
-function postQualityInspection(url, data, success, failure) {
-    if (inspection.flow_version !== 1) return postData(url, data, success, failure);
+function postQualityInspection(url, data, success, failure, showWaiting = true) {
+    if (inspection.flow_version !== 1) return postData(url, data, success, failure, showWaiting);
     if (!qualityInspectionSaving) qualityInspectionSaveQueue = Promise.resolve(true);
     qualityInspectionSaving++;
     const payload = Object.assign({}, data, { times: inspection.times, flow_stage: inspection.flow_stage, stage_round: inspection.stage_round });
     const previous = qualityInspectionSaveQueue;
     qualityInspectionSaveQueue = previous.then(function (saved) {
-        if (!saved && data.type !== 'update-field') {
+        if (!saved && !['update-field', 'flow-save-rectification'].includes(data.type)) {
             toastr.error('上一次保存失败,请检查内容并重试');
             if (failure) failure();
             return false;
@@ -743,7 +744,7 @@ function postQualityInspection(url, data, success, failure) {
             }, function () {
                 resolve(false);
                 if (failure) failure.apply(null, arguments);
-            });
+            }, showWaiting);
         });
     }).then(function (saved) {
         qualityInspectionSaving--;
@@ -766,14 +767,19 @@ function bindQualityInspectionFlow() {
         postQualityInspection(preUrl + '/save', data, function () {
             busy = false;
             $button.prop('disabled', false);
+            if (!isWap && ['flow-save-rectification', 'flow-complete-rectification', 'flow-return-rectification'].includes(data.type)) {
+                inspection.flowRectification = { rectification_item: data.rectification_item, rectification_date: data.rectification_date };
+            }
             if (!isWap && data.type === 'start-inspection') qualityInspectionNeedDraft(null);
             if (reload) {
                 if (!inspection.readOnly && ['flow-add-audit', 'flow-del-audit'].includes(data.type)) qualityInspectionNeedDraft(needRectification);
+                if (!isWap && ['flow-add-audit', 'flow-del-audit'].includes(data.type)) saveQualityInspectionFlowScroll(data.target_stage);
                 window.location.reload();
             } else toastr.success('保存成功');
         }, function () {
             busy = false;
             $button.prop('disabled', false);
+            if (rectificationAutosave && ['flow-complete-rectification', 'flow-return-rectification'].includes(data.type)) rectificationAutosave.setLocked(false);
         });
     }
     function rectificationData(auditId) {
@@ -782,13 +788,16 @@ function bindQualityInspectionFlow() {
             toastr.error('未找到当前整改任务,请刷新页面');
             return null;
         }
-        return {
+        const data = {
             auditId: Number($form.data('id')),
             rectification_item: $form.find('.flow-rectification-item').val().trim(),
             rectification_date: $form.find('.flow-rectification-date').val(),
             opinion: $form.find('.flow-rectification-opinion').val().trim(),
         };
+        if (!isWap && inspection.flowRectification) data.rectificationOriginal = Object.assign({}, inspection.flowRectification);
+        return data;
     }
+    const rectificationAutosave = isWap ? null : bindQualityInspectionRectificationAutosave(rectificationData);
     $flow.on('click', '.flow-picker .dropdown-menu', function (event) { event.stopPropagation(); });
     $flow.on('input propertychange', '.flow-user-search', function () {
         const $picker = $(this).closest('.flow-picker');
@@ -845,34 +854,43 @@ function bindQualityInspectionFlow() {
         if (type === 'start-inspection') {
             pending = { type, need_rectification: needRectification };
             message = needRectification ? '确认提交上报?上报通过后进入整改、批复流程。' : '确认提交上报?本次无整改、批复流程。';
-        } else if (type === 'flow-complete-rectification') {
+        } else if (['flow-complete-rectification', 'flow-return-rectification'].includes(type)) {
             const data = rectificationData($(this).data('id'));
             if (!data) {
                 pending = null;
                 return;
             }
             Object.assign(pending, data);
-            if (!pending.rectification_item || !pending.rectification_date) {
+            if (type === 'flow-complete-rectification' && (!pending.rectification_item || !pending.rectification_date)) {
                 pending = null;
                 toastr.error('请填写整改内容和整改日期');
                 return;
             }
-            message = '确认本人整改完成?所有整改人完成后才会进入批复。';
+            if (type === 'flow-return-rectification' && !inspection.flowRectificationReturnTarget) {
+                pending = null;
+                toastr.error('未找到上一审批人,请刷新页面后重试');
+                return;
+            }
+            message = '确认整改通过?通过后交给下一位整改人,最后一位通过后进入批复。';
         } else if (type === 'flow-check') {
-            message = pending.checkType === 3 ? '确认本次审批通过?' : pending.checkType === 4 ? '确认关闭本次巡检?' : inspection.flow_stage === 3 ? '确认退回重新整改?所有整改人将同时开始新一轮整改。' : pending.checkType === 5 ? '确认退回上报人重新上报?' : '确认退回上一个上报审批节点?';
+            const returnToRectification = inspection.flow_stage === 3 && inspection.flowReplyReturnTarget && inspection.flowReplyReturnTarget.flow_stage === 2;
+            message = pending.checkType === 3 ? '确认本次审批通过?' : pending.checkType === 4 ? '确认关闭本次巡检?' : pending.checkType === 5 ? '确认退回上报人重新上报?' : returnToRectification ? '确认退回上一整改人?整改通过后将从第一位批复人重新批复。' : inspection.flow_stage === 3 ? '确认退回上一审批人?' : '确认退回上一个上报审批节点?';
         }
-        const isReturn = type === 'flow-return';
+        const isRectificationReturn = type === 'flow-return-rectification';
+        const isReturn = type === 'flow-return' || isRectificationReturn;
+        const isRectification = !isWap && ['flow-complete-rectification', 'flow-return-rectification'].includes(type);
         $('#quality-flow-title').text($(this).text());
         $('#quality-flow-message').text(message).toggle(!isReturn);
-        $('#quality-flow-return-group').toggleClass('d-none', !isReturn);
+        $('#quality-flow-return-group').toggleClass('d-none', type !== 'flow-return');
+        $('#quality-flow-rectification-return').toggleClass('d-none', !isRectificationReturn);
         $('[name="quality-flow-return-type"]').prop('checked', false);
-        $('#quality-flow-opinion').val('').css('height', '');
-        $('#quality-flow-opinion-group').toggle(type === 'flow-check' || isReturn).find('label').text(isReturn ? '退回意见' : '处理意见');
+        $('#quality-flow-opinion').val(isRectification && !isReturn ? pending.opinion || '' : '').attr('placeholder', isReturn ? '请输入退回意见' : isRectification ? '请输入整改意见' : '').css('height', '');
+        $('#quality-flow-opinion-group').toggle(type === 'flow-check' || isReturn || isRectification).find('label').text(isReturn ? '退回意见' : isRectification ? '整改意见' : '处理意见');
         $('#quality-flow-cancel').text(isReturn ? '关闭' : '取消');
         $('#quality-flow-submit').text(isReturn ? '确认退回' : '确认').toggleClass('btn-warning', isReturn).toggleClass('btn-primary', !isReturn);
         $('#quality-flow-confirm').modal('show');
     });
-    $('#quality-flow-submit').on('click', function () {
+    $('#quality-flow-submit').on('click', async function () {
         if (!pending || busy) return;
         const data = Object.assign({}, pending);
         if (data.type === 'flow-return') {
@@ -884,12 +902,31 @@ function bindQualityInspectionFlow() {
             data.type = 'flow-check';
             data.checkType = checkType;
         }
-        if (data.type === 'flow-check') {
+        if (data.type === 'flow-check' || (!isWap && ['flow-complete-rectification', 'flow-return-rectification'].includes(data.type))) {
             data.opinion = $('#quality-flow-opinion').val().trim();
-            if (data.checkType !== 3 && !data.opinion) {
-                toastr.error(pending.type === 'flow-return' ? '请填写退回意见' : '请填写退回或关闭原因');
+            if ((data.type === 'flow-check' && data.checkType !== 3 || data.type === 'flow-return-rectification') && !data.opinion) {
+                toastr.error(['flow-return', 'flow-return-rectification'].includes(pending.type) ? '请填写退回意见' : '请填写退回或关闭原因');
+                return;
+            }
+        }
+        if (rectificationAutosave && ['flow-complete-rectification', 'flow-return-rectification'].includes(data.type)) {
+            busy = true;
+            $(this).prop('disabled', true);
+            rectificationAutosave.setLocked(true);
+            const saved = await rectificationAutosave.flush();
+            busy = false;
+            $(this).prop('disabled', false);
+            if (!saved) {
+                rectificationAutosave.setLocked(false);
+                return;
+            }
+            const latest = rectificationData(data.auditId);
+            if (!latest || (data.type === 'flow-complete-rectification' && (!latest.rectification_item || !latest.rectification_date))) {
+                rectificationAutosave.setLocked(false);
+                if (latest) toastr.error('请填写整改内容和整改日期');
                 return;
             }
+            Object.assign(data, latest, { opinion: data.opinion });
         }
         request(data, $(this), true);
     });
@@ -928,3 +965,141 @@ function showQualityInspectionNeed(value) {
     $('.quality-inspection-flow .flow-no-rectification').toggleClass('d-none', value !== 0);
     $('.quality-inspection-need-pending').removeClass('quality-inspection-need-pending');
 }
+
+// 只在添加或移除人员成功后的刷新中恢复位置,按用户和当前页面隔离暂存数据。
+function saveQualityInspectionFlowScroll(stage) {
+    const flow = document.querySelector('.quality-inspection-flow');
+    const container = flow && flow.closest('.sjs-height-0');
+    if (!container || ![1, 2, 3].includes(stage)) return;
+    const anchor = flow.querySelector('.flow-picker[data-stage="' + stage + '"] > .dropdown-toggle');
+    const key = 'quality-inspection-flow-scroll:' + cur_uid + ':' + window.location.pathname + window.location.search;
+    try {
+        sessionStorage.setItem(key, JSON.stringify({
+            stage,
+            scrollTop: container.scrollTop,
+            anchorOffset: anchor ? anchor.getBoundingClientRect().top - container.getBoundingClientRect().top : null,
+            times: inspection.times,
+            stageRound: inspection.stage_round,
+            savedAt: Date.now(),
+        }));
+    } catch (error) {
+        console.warn('无法保存审批流程滚动位置', error);
+    }
+}
+
+function restoreQualityInspectionFlowScroll() {
+    if (inspection.flow_version !== 1 || window.location.pathname.indexOf('/wap/') === 0) return;
+    const key = 'quality-inspection-flow-scroll:' + cur_uid + ':' + window.location.pathname + window.location.search;
+    try {
+        const saved = sessionStorage.getItem(key);
+        if (!saved) return;
+        sessionStorage.removeItem(key);
+        const position = JSON.parse(saved);
+        if (!position || ![1, 2, 3].includes(position.stage) || !Number.isFinite(position.scrollTop) || position.scrollTop < 0 ||
+            !Number.isFinite(position.savedAt) || Date.now() - position.savedAt < 0 || Date.now() - position.savedAt > 120000 ||
+            position.times !== inspection.times || position.stageRound !== inspection.stage_round) return;
+        const flow = document.querySelector('.quality-inspection-flow');
+        const container = flow && flow.closest('.sjs-height-0');
+        if (!container) return;
+        // 在文本框高度、附件和审批阶段显示状态初始化后,按最终布局恢复。
+        window.requestAnimationFrame(function () {
+            const anchor = flow.querySelector('.flow-picker[data-stage="' + position.stage + '"] > .dropdown-toggle');
+            let scrollTop = position.scrollTop;
+            if (anchor && anchor.getClientRects().length && Number.isFinite(position.anchorOffset)) {
+                scrollTop = container.scrollTop + anchor.getBoundingClientRect().top - container.getBoundingClientRect().top - position.anchorOffset;
+            }
+            container.scrollTop = Math.max(0, Math.min(scrollTop, container.scrollHeight - container.clientHeight));
+        });
+    } catch (error) {
+        console.warn('无法恢复审批流程滚动位置', error);
+    }
+}
+
+function bindQualityInspectionRectificationAutosave(readData) {
+    const $form = $('.quality-inspection-rectification.flow-rectification');
+    if ($form.length !== 1) return null;
+    const auditId = Number($form.data('id'));
+    const $item = $form.find('.flow-rectification-item');
+    const $date = $form.find('.flow-rectification-date');
+    const $status = $('.quality-inspection-rectification-save .flow-save-status');
+    const $retry = $('.quality-inspection-rectification-save .flow-save-retry');
+    let timer = null;
+    let inFlight = null;
+    let composing = false;
+    let locked = false;
+    let conflict = false;
+
+    $item.on('compositionstart', function () {
+        composing = true;
+        clearTimeout(timer);
+    });
+    $item.on('compositionend', function () {
+        composing = false;
+        schedule();
+    });
+    $item.on('input', function () {
+        if (!composing) schedule();
+    });
+    $item.on('blur', function () {
+        if (!locked && !composing) flush();
+    });
+    $date.on('input change blur', function () {
+        if (!locked) flush();
+    });
+    $retry.on('click', function () {
+        if (!locked) flush();
+    });
+    return { flush, setLocked };
+
+    function showState(message, failed = false, retry = false) {
+        $status.text(message).toggleClass('text-danger', failed).toggleClass('text-muted', !failed);
+        $retry.toggleClass('d-none', !retry).prop('disabled', locked);
+    }
+    function setLocked(value) {
+        locked = value;
+        if (locked) clearTimeout(timer);
+        $item.prop('readOnly', value);
+        $date.prop('readOnly', value);
+        $retry.prop('disabled', value);
+    }
+    function schedule() {
+        clearTimeout(timer);
+        if (locked || conflict) return;
+        showState('未保存');
+        timer = setTimeout(flush, 500);
+    }
+    function flush() {
+        clearTimeout(timer);
+        if (conflict || composing) return Promise.resolve(false);
+        // 新输入始终在上一次保存完成后重新读取,避免原值过期或旧请求覆盖新输入。
+        if (inFlight) return inFlight.then(saved => saved ? flush() : false);
+        const data = readData(auditId);
+        if (!data) return Promise.resolve(false);
+        if (($date[0].validity && !$date[0].validity.valid) ||
+            (data.rectification_date && !moment(data.rectification_date, 'YYYY-MM-DD', true).isValid())) {
+            showState('整改日期格式不正确,尚未保存', true);
+            return Promise.resolve(false);
+        }
+        const original = inspection.flowRectification || {};
+        if (data.rectification_item === original.rectification_item && data.rectification_date === original.rectification_date) {
+            showState('已保存');
+            return Promise.resolve(true);
+        }
+        showState('保存中…');
+        const operation = new Promise(resolve => {
+            postQualityInspection(preUrl + '/save', Object.assign({ type: 'flow-save-rectification' }, data), function () {
+                inspection.flowRectification = { rectification_item: data.rectification_item, rectification_date: data.rectification_date };
+                resolve(true);
+            }, function (message) {
+                conflict = typeof message === 'string' && message.indexOf('其他整改人更新') !== -1;
+                showState(conflict ? '整改单已被其他整改人更新,请保留输入并刷新页面' : '保存失败,请重试', true, !conflict);
+                resolve(false);
+            }, false);
+        });
+        inFlight = operation;
+        return operation.then(saved => {
+            if (inFlight === operation) inFlight = null;
+            return saved ? flush() : false;
+        });
+    }
+}

+ 192 - 17
app/public/js/safe_inspection_information.js

@@ -696,6 +696,7 @@ $(document).ready(function () {
             })
         }
     });
+    restoreSafeInspectionFlowScroll();
 })
 
 
@@ -724,14 +725,14 @@ function validateFiles(files) {
 // 表单自动保存和流程提交按顺序执行,避免提交先于最后一次字段保存。
 let safeInspectionSaveQueue = Promise.resolve(true);
 let safeInspectionSaving = 0;
-function postSafeInspection(url, data, success, failure) {
-    if (inspection.flow_version !== 1) return postData(url, data, success, failure);
+function postSafeInspection(url, data, success, failure, showWaiting = true) {
+    if (inspection.flow_version !== 1) return postData(url, data, success, failure, showWaiting);
     if (!safeInspectionSaving) safeInspectionSaveQueue = Promise.resolve(true);
     safeInspectionSaving++;
     const payload = Object.assign({}, data, { times: inspection.times, flow_stage: inspection.flow_stage, stage_round: inspection.stage_round });
     const previous = safeInspectionSaveQueue;
     safeInspectionSaveQueue = previous.then(function (saved) {
-        if (!saved && data.type !== 'update-field') {
+        if (!saved && !['update-field', 'flow-save-rectification'].includes(data.type)) {
             toastr.error('上一次保存失败,请检查内容并重试');
             if (failure) failure();
             return false;
@@ -743,7 +744,7 @@ function postSafeInspection(url, data, success, failure) {
             }, function () {
                 resolve(false);
                 if (failure) failure.apply(null, arguments);
-            });
+            }, showWaiting);
         });
     }).then(function (saved) {
         safeInspectionSaving--;
@@ -766,14 +767,19 @@ function bindSafeInspectionFlow() {
         postSafeInspection(preUrl + '/save', data, function () {
             busy = false;
             $button.prop('disabled', false);
+            if (!isWap && ['flow-save-rectification', 'flow-complete-rectification', 'flow-return-rectification'].includes(data.type)) {
+                inspection.flowRectification = { rectification_item: data.rectification_item, rectification_date: data.rectification_date };
+            }
             if (!isWap && data.type === 'start-inspection') safeInspectionNeedDraft(null);
             if (reload) {
                 if (!inspection.readOnly && ['flow-add-audit', 'flow-del-audit'].includes(data.type)) safeInspectionNeedDraft(needRectification);
+                if (!isWap && ['flow-add-audit', 'flow-del-audit'].includes(data.type)) saveSafeInspectionFlowScroll(data.target_stage);
                 window.location.reload();
             } else toastr.success('保存成功');
         }, function () {
             busy = false;
             $button.prop('disabled', false);
+            if (rectificationAutosave && ['flow-complete-rectification', 'flow-return-rectification'].includes(data.type)) rectificationAutosave.setLocked(false);
         });
     }
     function rectificationData(auditId) {
@@ -782,13 +788,16 @@ function bindSafeInspectionFlow() {
             toastr.error('未找到当前整改任务,请刷新页面');
             return null;
         }
-        return {
+        const data = {
             auditId: Number($form.data('id')),
             rectification_item: $form.find('.flow-rectification-item').val().trim(),
             rectification_date: $form.find('.flow-rectification-date').val(),
             opinion: $form.find('.flow-rectification-opinion').val().trim(),
         };
+        if (!isWap && inspection.flowRectification) data.rectificationOriginal = Object.assign({}, inspection.flowRectification);
+        return data;
     }
+    const rectificationAutosave = isWap ? null : bindSafeInspectionRectificationAutosave(rectificationData);
     $flow.on('click', '.flow-picker .dropdown-menu', function (event) { event.stopPropagation(); });
     $flow.on('input propertychange', '.flow-user-search', function () {
         const $picker = $(this).closest('.flow-picker');
@@ -845,34 +854,43 @@ function bindSafeInspectionFlow() {
         if (type === 'start-inspection') {
             pending = { type, need_rectification: needRectification };
             message = needRectification ? '确认提交上报?上报通过后进入整改、批复流程。' : '确认提交上报?本次无整改、批复流程。';
-        } else if (type === 'flow-complete-rectification') {
+        } else if (['flow-complete-rectification', 'flow-return-rectification'].includes(type)) {
             const data = rectificationData($(this).data('id'));
             if (!data) {
                 pending = null;
                 return;
             }
             Object.assign(pending, data);
-            if (!pending.rectification_item || !pending.rectification_date) {
+            if (type === 'flow-complete-rectification' && (!pending.rectification_item || !pending.rectification_date)) {
                 pending = null;
                 toastr.error('请填写整改内容和整改日期');
                 return;
             }
-            message = '确认本人整改完成?所有整改人完成后才会进入批复。';
+            if (type === 'flow-return-rectification' && !inspection.flowRectificationReturnTarget) {
+                pending = null;
+                toastr.error('未找到上一审批人,请刷新页面后重试');
+                return;
+            }
+            message = '确认整改通过?通过后交给下一位整改人,最后一位通过后进入批复。';
         } else if (type === 'flow-check') {
-            message = pending.checkType === 3 ? '确认本次审批通过?' : pending.checkType === 4 ? '确认关闭本次巡检?' : inspection.flow_stage === 3 ? '确认退回重新整改?所有整改人将同时开始新一轮整改。' : pending.checkType === 5 ? '确认退回上报人重新上报?' : '确认退回上一个上报审批节点?';
+            const returnToRectification = inspection.flow_stage === 3 && inspection.flowReplyReturnTarget && inspection.flowReplyReturnTarget.flow_stage === 2;
+            message = pending.checkType === 3 ? '确认本次审批通过?' : pending.checkType === 4 ? '确认关闭本次巡检?' : pending.checkType === 5 ? '确认退回上报人重新上报?' : returnToRectification ? '确认退回上一整改人?整改通过后将从第一位批复人重新批复。' : inspection.flow_stage === 3 ? '确认退回上一审批人?' : '确认退回上一个上报审批节点?';
         }
-        const isReturn = type === 'flow-return';
+        const isRectificationReturn = type === 'flow-return-rectification';
+        const isReturn = type === 'flow-return' || isRectificationReturn;
+        const isRectification = !isWap && ['flow-complete-rectification', 'flow-return-rectification'].includes(type);
         $('#safe-flow-title').text($(this).text());
         $('#safe-flow-message').text(message).toggle(!isReturn);
-        $('#safe-flow-return-group').toggleClass('d-none', !isReturn);
+        $('#safe-flow-return-group').toggleClass('d-none', type !== 'flow-return');
+        $('#safe-flow-rectification-return').toggleClass('d-none', !isRectificationReturn);
         $('[name="safe-flow-return-type"]').prop('checked', false);
-        $('#safe-flow-opinion').val('').css('height', '');
-        $('#safe-flow-opinion-group').toggle(type === 'flow-check' || isReturn).find('label').text(isReturn ? '退回意见' : '处理意见');
+        $('#safe-flow-opinion').val(isRectification && !isReturn ? pending.opinion || '' : '').attr('placeholder', isReturn ? '请输入退回意见' : isRectification ? '请输入整改意见' : '').css('height', '');
+        $('#safe-flow-opinion-group').toggle(type === 'flow-check' || isReturn || isRectification).find('label').text(isReturn ? '退回意见' : isRectification ? '整改意见' : '处理意见');
         $('#safe-flow-cancel').text(isReturn ? '关闭' : '取消');
         $('#safe-flow-submit').text(isReturn ? '确认退回' : '确认').toggleClass('btn-warning', isReturn).toggleClass('btn-primary', !isReturn);
         $('#safe-flow-confirm').modal('show');
     });
-    $('#safe-flow-submit').on('click', function () {
+    $('#safe-flow-submit').on('click', async function () {
         if (!pending || busy) return;
         const data = Object.assign({}, pending);
         if (data.type === 'flow-return') {
@@ -884,12 +902,31 @@ function bindSafeInspectionFlow() {
             data.type = 'flow-check';
             data.checkType = checkType;
         }
-        if (data.type === 'flow-check') {
+        if (data.type === 'flow-check' || (!isWap && ['flow-complete-rectification', 'flow-return-rectification'].includes(data.type))) {
             data.opinion = $('#safe-flow-opinion').val().trim();
-            if (data.checkType !== 3 && !data.opinion) {
-                toastr.error(pending.type === 'flow-return' ? '请填写退回意见' : '请填写退回或关闭原因');
+            if ((data.type === 'flow-check' && data.checkType !== 3 || data.type === 'flow-return-rectification') && !data.opinion) {
+                toastr.error(['flow-return', 'flow-return-rectification'].includes(pending.type) ? '请填写退回意见' : '请填写退回或关闭原因');
+                return;
+            }
+        }
+        if (rectificationAutosave && ['flow-complete-rectification', 'flow-return-rectification'].includes(data.type)) {
+            busy = true;
+            $(this).prop('disabled', true);
+            rectificationAutosave.setLocked(true);
+            const saved = await rectificationAutosave.flush();
+            busy = false;
+            $(this).prop('disabled', false);
+            if (!saved) {
+                rectificationAutosave.setLocked(false);
+                return;
+            }
+            const latest = rectificationData(data.auditId);
+            if (!latest || (data.type === 'flow-complete-rectification' && (!latest.rectification_item || !latest.rectification_date))) {
+                rectificationAutosave.setLocked(false);
+                if (latest) toastr.error('请填写整改内容和整改日期');
                 return;
             }
+            Object.assign(data, latest, { opinion: data.opinion });
         }
         request(data, $(this), true);
     });
@@ -928,3 +965,141 @@ function showSafeInspectionNeed(value) {
     $('.safe-inspection-flow .flow-no-rectification').toggleClass('d-none', value !== 0);
     $('.safe-inspection-need-pending').removeClass('safe-inspection-need-pending');
 }
+
+// 只在添加或移除人员成功后的刷新中恢复位置,按用户和当前页面隔离暂存数据。
+function saveSafeInspectionFlowScroll(stage) {
+    const flow = document.querySelector('.safe-inspection-flow');
+    const container = flow && flow.closest('.sjs-height-0');
+    if (!container || ![1, 2, 3].includes(stage)) return;
+    const anchor = flow.querySelector('.flow-picker[data-stage="' + stage + '"] > .dropdown-toggle');
+    const key = 'safe-inspection-flow-scroll:' + cur_uid + ':' + window.location.pathname + window.location.search;
+    try {
+        sessionStorage.setItem(key, JSON.stringify({
+            stage,
+            scrollTop: container.scrollTop,
+            anchorOffset: anchor ? anchor.getBoundingClientRect().top - container.getBoundingClientRect().top : null,
+            times: inspection.times,
+            stageRound: inspection.stage_round,
+            savedAt: Date.now(),
+        }));
+    } catch (error) {
+        console.warn('无法保存审批流程滚动位置', error);
+    }
+}
+
+function restoreSafeInspectionFlowScroll() {
+    if (inspection.flow_version !== 1 || window.location.pathname.indexOf('/wap/') === 0) return;
+    const key = 'safe-inspection-flow-scroll:' + cur_uid + ':' + window.location.pathname + window.location.search;
+    try {
+        const saved = sessionStorage.getItem(key);
+        if (!saved) return;
+        sessionStorage.removeItem(key);
+        const position = JSON.parse(saved);
+        if (!position || ![1, 2, 3].includes(position.stage) || !Number.isFinite(position.scrollTop) || position.scrollTop < 0 ||
+            !Number.isFinite(position.savedAt) || Date.now() - position.savedAt < 0 || Date.now() - position.savedAt > 120000 ||
+            position.times !== inspection.times || position.stageRound !== inspection.stage_round) return;
+        const flow = document.querySelector('.safe-inspection-flow');
+        const container = flow && flow.closest('.sjs-height-0');
+        if (!container) return;
+        // 在文本框高度、附件和审批阶段显示状态初始化后,按最终布局恢复。
+        window.requestAnimationFrame(function () {
+            const anchor = flow.querySelector('.flow-picker[data-stage="' + position.stage + '"] > .dropdown-toggle');
+            let scrollTop = position.scrollTop;
+            if (anchor && anchor.getClientRects().length && Number.isFinite(position.anchorOffset)) {
+                scrollTop = container.scrollTop + anchor.getBoundingClientRect().top - container.getBoundingClientRect().top - position.anchorOffset;
+            }
+            container.scrollTop = Math.max(0, Math.min(scrollTop, container.scrollHeight - container.clientHeight));
+        });
+    } catch (error) {
+        console.warn('无法恢复审批流程滚动位置', error);
+    }
+}
+
+function bindSafeInspectionRectificationAutosave(readData) {
+    const $form = $('.safe-inspection-rectification.flow-rectification');
+    if ($form.length !== 1) return null;
+    const auditId = Number($form.data('id'));
+    const $item = $form.find('.flow-rectification-item');
+    const $date = $form.find('.flow-rectification-date');
+    const $status = $('.safe-inspection-rectification-save .flow-save-status');
+    const $retry = $('.safe-inspection-rectification-save .flow-save-retry');
+    let timer = null;
+    let inFlight = null;
+    let composing = false;
+    let locked = false;
+    let conflict = false;
+
+    $item.on('compositionstart', function () {
+        composing = true;
+        clearTimeout(timer);
+    });
+    $item.on('compositionend', function () {
+        composing = false;
+        schedule();
+    });
+    $item.on('input', function () {
+        if (!composing) schedule();
+    });
+    $item.on('blur', function () {
+        if (!locked && !composing) flush();
+    });
+    $date.on('input change blur', function () {
+        if (!locked) flush();
+    });
+    $retry.on('click', function () {
+        if (!locked) flush();
+    });
+    return { flush, setLocked };
+
+    function showState(message, failed = false, retry = false) {
+        $status.text(message).toggleClass('text-danger', failed).toggleClass('text-muted', !failed);
+        $retry.toggleClass('d-none', !retry).prop('disabled', locked);
+    }
+    function setLocked(value) {
+        locked = value;
+        if (locked) clearTimeout(timer);
+        $item.prop('readOnly', value);
+        $date.prop('readOnly', value);
+        $retry.prop('disabled', value);
+    }
+    function schedule() {
+        clearTimeout(timer);
+        if (locked || conflict) return;
+        showState('未保存');
+        timer = setTimeout(flush, 500);
+    }
+    function flush() {
+        clearTimeout(timer);
+        if (conflict || composing) return Promise.resolve(false);
+        // 新输入始终在上一次保存完成后重新读取,避免原值过期或旧请求覆盖新输入。
+        if (inFlight) return inFlight.then(saved => saved ? flush() : false);
+        const data = readData(auditId);
+        if (!data) return Promise.resolve(false);
+        if (($date[0].validity && !$date[0].validity.valid) ||
+            (data.rectification_date && !moment(data.rectification_date, 'YYYY-MM-DD', true).isValid())) {
+            showState('整改日期格式不正确,尚未保存', true);
+            return Promise.resolve(false);
+        }
+        const original = inspection.flowRectification || {};
+        if (data.rectification_item === original.rectification_item && data.rectification_date === original.rectification_date) {
+            showState('已保存');
+            return Promise.resolve(true);
+        }
+        showState('保存中…');
+        const operation = new Promise(resolve => {
+            postSafeInspection(preUrl + '/save', Object.assign({ type: 'flow-save-rectification' }, data), function () {
+                inspection.flowRectification = { rectification_item: data.rectification_item, rectification_date: data.rectification_date };
+                resolve(true);
+            }, function (message) {
+                conflict = typeof message === 'string' && message.indexOf('其他整改人更新') !== -1;
+                showState(conflict ? '整改单已被其他整改人更新,请保留输入并刷新页面' : '保存失败,请重试', true, !conflict);
+                resolve(false);
+            }, false);
+        });
+        inFlight = operation;
+        return operation.then(saved => {
+            if (inFlight === operation) inFlight = null;
+            return saved ? flush() : false;
+        });
+    }
+}

Разница между файлами не показана из-за своего большого размера
+ 30 - 7
app/service/quality_inspection.js


+ 136 - 21
app/service/quality_inspection_audit.js

@@ -62,7 +62,8 @@ module.exports = app => {
         async getAuditorsByStatus(qiId, status, times = 1) {
             const inspection = await this.ctx.service.qualityInspection.getDataById(qiId);
             if (inspection && inspection.flow_version === 1) {
-                const auditors = await this.getAuditors(qiId, inspection.times);
+                let auditors = await this.getAuditors(qiId, inspection.times);
+                if (await this.prepareSequentialFlow(inspection, auditors)) auditors = await this.getAuditors(qiId, inspection.times);
                 return auditors.filter(x => !x.is_old && x.flow_stage === inspection.flow_stage && x.stage_round === inspection.stage_round && [2, 12].includes(x.status) && [2, 6, 12].includes(inspection.status));
             }
             let auditor = [];
@@ -1351,14 +1352,17 @@ module.exports = app => {
         }
 
         async activateFlow(transaction, inspection, stage) {
-            const records = await transaction.select(this.tableName, { where: { qiid: inspection.id, times: inspection.times, flow_stage: stage, stage_round: inspection.stage_round, is_old: 0, status: 1 }, orders: [['order', 'asc']] });
+            let records = await transaction.select(this.tableName, { where: { qiid: inspection.id, times: inspection.times, flow_stage: stage, stage_round: inspection.stage_round, is_old: 0 }, orders: [['order', 'asc'], ['id', 'asc']] });
+            if (stage === 2) await this.normalizeRectificationFlow(transaction, inspection, records, false);
+            records = records.filter(x => x.status === 1).sort((a, b) => a.order - b.order || a.id - b.id);
             if (!records.length) throw '请先设置' + auditConst.stageString[stage] + '人员';
-            const active = stage === 2 ? records : records.filter(x => x.order === records[0].order);
+            const active = stage === 2 ? records.slice(0, 1) : records.filter(x => x.order === records[0].order);
             const status = stage === 2 ? 12 : 2;
             const begin_time = new Date();
             await transaction.updateRows(this.tableName, active.map(x => ({ id: x.id, status, begin_time })));
-            await transaction.update(this.ctx.service.qualityInspection.tableName, { id: inspection.id, flow_stage: stage, stage_round: inspection.stage_round, status });
-            Object.assign(inspection, { flow_stage: stage, status });
+            const update = { id: inspection.id, flow_stage: stage, stage_round: inspection.stage_round, status };
+            await transaction.update(this.ctx.service.qualityInspection.tableName, update);
+            Object.assign(inspection, update);
         }
 
         async processFlow(inspection, data) {
@@ -1370,6 +1374,7 @@ module.exports = app => {
                 const uid = this.ctx.session.sessionUser.accountId;
                 const ownerEdit = current.uid === uid && [1, 5].includes(current.status);
                 const records = await transaction.select(this.tableName, { where: { qiid: current.id, times: current.times, is_old: 0 }, orders: [['order', 'asc'], ['id', 'asc']] });
+                await this.normalizeRectificationFlow(transaction, current, records);
                 const active = records.filter(x => x.flow_stage === current.flow_stage && x.stage_round === current.stage_round && [2, 12].includes(x.status));
                 const self = [2, 6, 12].includes(current.status) ? active.find(x => x.aid === uid) : null;
                 const report = records.filter(x => x.flow_stage === 1);
@@ -1415,13 +1420,13 @@ module.exports = app => {
                             if (stageRecords.some(x => x.aid === id)) throw '该人员已存在';
                             const users = await this.ctx.service.projectAccount.getAllSubProjectAccount(this.ctx.subProject);
                             if (!users.some(x => x.id === id)) throw '该用户不在当前项目中';
-                            let order = stage === 2 ? 1 : stageRecords.reduce((n, x) => Math.max(n, x.order), 0) + 1;
+                            let order = stageRecords.reduce((n, x) => Math.max(n, x.order), 0) + 1;
                             if (stage === 1 && mode === shenpiConst.sp_status.gdzs && stageRecords.length) {
                                 const last = stageRecords[stageRecords.length - 1];
                                 order = last.order;
                                 await transaction.update(this.tableName, { id: last.id, order: order + 1, audit_order: order + 1 });
                             }
-                            await transaction.insert(this.tableName, this.flowRecord(current, { aid: id, flow_stage: stage, order, audit_order: order, audit_type: stage === 2 ? 2 : 1 }));
+                            await transaction.insert(this.tableName, this.flowRecord(current, { aid: id, flow_stage: stage, order, audit_order: order, audit_type: 1 }));
                             await this.grantFlowPermission(transaction, [id]);
                         } else {
                             const target = stageRecords.find(x => x.aid === id);
@@ -1451,6 +1456,7 @@ module.exports = app => {
                         break;
                     }
                     case 'flow-save-rectification':
+                    case 'flow-return-rectification':
                     case 'flow-complete-rectification': {
                         if (!self || current.flow_stage !== 2 || self.status !== 12 || Number(data.auditId) !== self.id) throw '您没有待处理的整改任务';
                         const item = typeof data.rectification_item === 'string' ? data.rectification_item.trim() : '';
@@ -1459,12 +1465,24 @@ module.exports = app => {
                         if (item.length > 1000 || opinion.length > 1000) throw '整改内容和意见最多1000字';
                         if (date && (typeof date !== 'string' || !moment(date, 'YYYY-MM-DD', true).isValid())) throw '整改日期格式错误';
                         const done = data.type === 'flow-complete-rectification';
+                        const returned = data.type === 'flow-return-rectification';
+                        if (returned && !opinion) throw '请填写退回意见';
                         if (done && (!item || !date)) throw '请填写整改内容和整改日期';
+                        const shared = this.ctx.service.qualityInspection.getFlowRectification(current, rectifiers);
+                        if (data.rectificationOriginal !== undefined && (!data.rectificationOriginal || data.rectificationOriginal.rectification_item !== shared.rectification_item || data.rectificationOriginal.rectification_date !== shared.rectification_date)) {
+                            throw '整改单已被其他整改人更新,请刷新页面后重试';
+                        }
+                        await transaction.update(this.ctx.service.qualityInspection.tableName, { id: current.id, rectification_item: item, rectification_date: date, rectification_uid: uid });
                         const update = { id: self.id, rectification_item: item, rectification_date: date, opinion };
                         if (done) Object.assign(update, { status: 3, end_time: new Date() });
                         await transaction.update(this.tableName, update);
-                        // 所有提交都先锁巡检主记录,再检查本轮其他人的结果。
-                        if (done && rectifiers.every(x => x.id === self.id || x.status === 3)) await this.activateFlow(transaction, current, 3);
+                        Object.assign(self, update);
+                        // 只有当前整改人可提交;通过后激活下一人,最后一人通过才进入批复。
+                        if (done) await this.activateFlow(transaction, current, rectifiers.some(x => x.status === 1) ? 2 : 3);
+                        if (returned) {
+                            await this.returnRectificationFlow(transaction, current, records, self, opinion);
+                            await this.noticeFlow(transaction, current, records, 6, opinion, 2);
+                        }
                         if (done) await this.noticeFlow(transaction, current, records, 3, opinion, 2);
                         break;
                     }
@@ -1520,23 +1538,30 @@ module.exports = app => {
                 inspection.status = 4;
                 return;
             }
-            if (inspection.flow_stage === 3) {
-                // 批复退回:原轮次保留,所有整改人同时开始新一轮整改。
-                const previous = records.filter(x => [2, 3].includes(x.flow_stage) && x.stage_round === inspection.stage_round);
-                inspection.stage_round += 1;
-                const copies = previous.map(x => this.flowRecord(inspection, x));
-                if (copies.length) await transaction.insert(this.tableName, copies);
-                await this.activateFlow(transaction, inspection, 2);
-                return;
-            }
             if (result === 5) {
-                const templates = records.filter(x => x.flow_stage === 1 || x.flow_stage === 3 && x.stage_round === inspection.stage_round);
+                const fromReply = inspection.flow_stage === 3;
+                const shared = fromReply ? this.ctx.service.qualityInspection.getFlowRectification(inspection, records) : null;
+                const templates = records.filter(x => x.flow_stage === 1 || (x.flow_stage === 3 || fromReply && x.flow_stage === 2) && x.stage_round === inspection.stage_round);
                 inspection.times += 1;
                 inspection.stage_round = 1;
                 const copies = templates.map(x => this.flowRecord(inspection, x));
                 if (copies.length) await transaction.insert(this.tableName, copies);
-                await transaction.update(this.ctx.service.qualityInspection.tableName, { id: inspection.id, times: inspection.times, stage_round: 1, flow_stage: 1, status: 5 });
-                inspection.status = 5;
+                const update = { id: inspection.id, times: inspection.times, stage_round: 1, flow_stage: 1, status: 5 };
+                if (shared) Object.assign(update, shared, { rectification_date: shared.rectification_date || null });
+                await transaction.update(this.ctx.service.qualityInspection.tableName, update);
+                Object.assign(inspection, update);
+                return;
+            }
+            if (inspection.flow_stage === 3) {
+                const target = this.getReplyReturnTarget(inspection, records, self);
+                if (!target) throw '未找到上一审批人或整改人,请刷新页面后重试';
+                const shared = this.ctx.service.qualityInspection.getFlowRectification(inspection, records);
+                const replaced = records.filter(x => x.stage_round === inspection.stage_round && (x.id === target.id || x.flow_stage === 3 && (target.flow_stage === 2 || x.order >= target.order)));
+                // 从退回对象重新流转,保留更早的审批结果,原意见及处理时间留作历史。
+                await transaction.updateRows(this.tableName, replaced.map(x => ({ id: x.id, is_old: 1 })));
+                await transaction.insert(this.tableName, replaced.map(x => this.flowRecord(inspection, x)));
+                await transaction.update(this.ctx.service.qualityInspection.tableName, Object.assign({ id: inspection.id }, shared, { rectification_date: shared.rectification_date || null }));
+                await this.activateFlow(transaction, inspection, target.flow_stage);
                 return;
             }
             const report = records.filter(x => x.flow_stage === 1);
@@ -1561,6 +1586,96 @@ module.exports = app => {
             const users = this._.uniq([inspection.uid].concat(records.map(x => x.aid)));
             await transaction.insert('zh_notice', users.map(uid => ({ pid: this.ctx.session.sessionProject.id, tid: inspection.tid, type: pushType.inspection, uid, status: result, content: JSON.stringify(content) })));
         }
+
+        getRectificationSequence(inspection, records) {
+            return records.filter(x => !x.is_old && x.times === inspection.times && x.flow_stage === 2 && x.stage_round === inspection.stage_round)
+                .sort((a, b) => a.audit_order - b.audit_order || a.order - b.order || a.id - b.id);
+        }
+
+        getRectificationUpdates(inspection, records, reconcileActive = true) {
+            if (inspection.flow_version !== 1 || [3, 4].includes(inspection.status)) return [];
+            const sequence = this.getRectificationSequence(inspection, records);
+            const current = sequence.find(x => [1, 12].includes(x.status));
+            const updates = [];
+            sequence.forEach((record, index) => {
+                const update = { id: record.id };
+                if (record.order !== index + 1 || record.audit_order !== index + 1 || record.audit_type !== 1) {
+                    Object.assign(update, { order: index + 1, audit_order: index + 1, audit_type: 1 });
+                }
+                if (reconcileActive && inspection.flow_stage === 2 && inspection.status === 12 && [1, 12].includes(record.status)) {
+                    const status = record === current ? 12 : 1;
+                    if (record.status !== status) {
+                        update.status = status;
+                        if (status === 12) update.begin_time = new Date();
+                    }
+                }
+                if (Object.keys(update).length > 1) updates.push(update);
+            });
+            return updates;
+        }
+
+        async normalizeRectificationFlow(transaction, inspection, records, reconcileActive = true) {
+            const updates = this.getRectificationUpdates(inspection, records, reconcileActive);
+            if (updates.length) {
+                await transaction.updateRows(this.tableName, updates);
+                for (const update of updates) Object.assign(records.find(x => x.id === update.id), update);
+            }
+        }
+
+        async prepareSequentialFlow(inspection, records) {
+            if (!this.getRectificationUpdates(inspection, records).length) return false;
+            const transaction = await this.db.beginTransaction();
+            try {
+                const current = await this.lockFlow(transaction, inspection.id);
+                const audits = await transaction.select(this.tableName, { where: { qiid: current.id, times: current.times, is_old: 0 }, orders: [['order', 'asc'], ['id', 'asc']] });
+                await this.normalizeRectificationFlow(transaction, current, audits);
+                await transaction.commit();
+                Object.assign(inspection, current);
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        getRectificationReturnTarget(inspection, records, self) {
+            if (!self || self.flow_stage !== 2) return null;
+            const sequence = this.getRectificationSequence(inspection, records);
+            const index = sequence.findIndex(x => x.id === self.id);
+            if (index < 0) return null;
+            if (index > 0) return sequence[index - 1].status === 3 ? sequence[index - 1] : null;
+            // 会签/或签终审取实际最后通过的人,其他已通过人的结果继续保留。
+            const report = records.filter(x => !x.is_old && x.times === inspection.times && x.flow_stage === 1 && x.status === 3);
+            report.sort((a, b) => b.audit_order - a.audit_order || new Date(b.end_time).getTime() - new Date(a.end_time).getTime() || b.id - a.id);
+            return report[0] || null;
+        }
+
+        async returnRectificationFlow(transaction, inspection, records, self, opinion) {
+            const target = this.getRectificationReturnTarget(inspection, records, self);
+            if (!target) throw '未找到上一审批人,请刷新页面后重试';
+            await transaction.update(this.tableName, { id: self.id, status: 6, opinion, end_time: new Date() });
+            const sequence = this.getRectificationSequence(inspection, records);
+            const replaced = sequence.filter(x => target.flow_stage === 1 || x.audit_order >= target.audit_order);
+            if (target.flow_stage === 1) replaced.push(target);
+            // 原处理记录留作历史,新建待办,避免旧页面请求操作重新激活的任务。
+            await transaction.updateRows(this.tableName, replaced.map(x => ({ id: x.id, is_old: 1 })));
+            await transaction.insert(this.tableName, replaced.map(x => this.flowRecord(inspection, x)));
+            await this.activateFlow(transaction, inspection, target.flow_stage);
+        }
+
+        getReplyReturnTarget(inspection, records, self) {
+            if (inspection.flow_stage !== 3 || !self) return null;
+            const earlier = records.filter(x => !x.is_old && x.times === inspection.times && x.flow_stage === 3 && x.stage_round === inspection.stage_round && x.order < self.order);
+            if (earlier.length) {
+                const previousOrder = Math.max(...earlier.map(x => x.order));
+                const previous = earlier.filter(x => x.order === previousOrder);
+                const approved = previous.find(x => x.status === 3);
+                return approved ? Object.assign({}, approved, { name: previous.map(x => x.name).join('、') }) : null;
+            }
+            const sequence = this.getRectificationSequence(inspection, records);
+            const last = sequence[sequence.length - 1];
+            return last && last.status === 3 ? last : null;
+        }
     }
 
     return QualityInspectionAudit;

Разница между файлами не показана из-за своего большого размера
+ 30 - 7
app/service/safe_inspection.js


+ 136 - 21
app/service/safe_inspection_audit.js

@@ -62,7 +62,8 @@ module.exports = app => {
         async getAuditorsByStatus(qiId, status, times = 1) {
             const inspection = await this.ctx.service.safeInspection.getDataById(qiId);
             if (inspection && inspection.flow_version === 1) {
-                const auditors = await this.getAuditors(qiId, inspection.times);
+                let auditors = await this.getAuditors(qiId, inspection.times);
+                if (await this.prepareSequentialFlow(inspection, auditors)) auditors = await this.getAuditors(qiId, inspection.times);
                 return auditors.filter(x => !x.is_old && x.flow_stage === inspection.flow_stage && x.stage_round === inspection.stage_round && [2, 12].includes(x.status) && [2, 6, 12].includes(inspection.status));
             }
             let auditor = [];
@@ -1351,14 +1352,17 @@ module.exports = app => {
         }
 
         async activateFlow(transaction, inspection, stage) {
-            const records = await transaction.select(this.tableName, { where: { qiid: inspection.id, times: inspection.times, flow_stage: stage, stage_round: inspection.stage_round, is_old: 0, status: 1 }, orders: [['order', 'asc']] });
+            let records = await transaction.select(this.tableName, { where: { qiid: inspection.id, times: inspection.times, flow_stage: stage, stage_round: inspection.stage_round, is_old: 0 }, orders: [['order', 'asc'], ['id', 'asc']] });
+            if (stage === 2) await this.normalizeRectificationFlow(transaction, inspection, records, false);
+            records = records.filter(x => x.status === 1).sort((a, b) => a.order - b.order || a.id - b.id);
             if (!records.length) throw '请先设置' + auditConst.stageString[stage] + '人员';
-            const active = stage === 2 ? records : records.filter(x => x.order === records[0].order);
+            const active = stage === 2 ? records.slice(0, 1) : records.filter(x => x.order === records[0].order);
             const status = stage === 2 ? 12 : 2;
             const begin_time = new Date();
             await transaction.updateRows(this.tableName, active.map(x => ({ id: x.id, status, begin_time })));
-            await transaction.update(this.ctx.service.safeInspection.tableName, { id: inspection.id, flow_stage: stage, stage_round: inspection.stage_round, status });
-            Object.assign(inspection, { flow_stage: stage, status });
+            const update = { id: inspection.id, flow_stage: stage, stage_round: inspection.stage_round, status };
+            await transaction.update(this.ctx.service.safeInspection.tableName, update);
+            Object.assign(inspection, update);
         }
 
         async processFlow(inspection, data) {
@@ -1370,6 +1374,7 @@ module.exports = app => {
                 const uid = this.ctx.session.sessionUser.accountId;
                 const ownerEdit = current.uid === uid && [1, 5].includes(current.status);
                 const records = await transaction.select(this.tableName, { where: { qiid: current.id, times: current.times, is_old: 0 }, orders: [['order', 'asc'], ['id', 'asc']] });
+                await this.normalizeRectificationFlow(transaction, current, records);
                 const active = records.filter(x => x.flow_stage === current.flow_stage && x.stage_round === current.stage_round && [2, 12].includes(x.status));
                 const self = [2, 6, 12].includes(current.status) ? active.find(x => x.aid === uid) : null;
                 const report = records.filter(x => x.flow_stage === 1);
@@ -1415,13 +1420,13 @@ module.exports = app => {
                             if (stageRecords.some(x => x.aid === id)) throw '该人员已存在';
                             const users = await this.ctx.service.projectAccount.getAllSubProjectAccount(this.ctx.subProject);
                             if (!users.some(x => x.id === id)) throw '该用户不在当前项目中';
-                            let order = stage === 2 ? 1 : stageRecords.reduce((n, x) => Math.max(n, x.order), 0) + 1;
+                            let order = stageRecords.reduce((n, x) => Math.max(n, x.order), 0) + 1;
                             if (stage === 1 && mode === shenpiConst.sp_status.gdzs && stageRecords.length) {
                                 const last = stageRecords[stageRecords.length - 1];
                                 order = last.order;
                                 await transaction.update(this.tableName, { id: last.id, order: order + 1, audit_order: order + 1 });
                             }
-                            await transaction.insert(this.tableName, this.flowRecord(current, { aid: id, flow_stage: stage, order, audit_order: order, audit_type: stage === 2 ? 2 : 1 }));
+                            await transaction.insert(this.tableName, this.flowRecord(current, { aid: id, flow_stage: stage, order, audit_order: order, audit_type: 1 }));
                             await this.grantFlowPermission(transaction, [id]);
                         } else {
                             const target = stageRecords.find(x => x.aid === id);
@@ -1451,6 +1456,7 @@ module.exports = app => {
                         break;
                     }
                     case 'flow-save-rectification':
+                    case 'flow-return-rectification':
                     case 'flow-complete-rectification': {
                         if (!self || current.flow_stage !== 2 || self.status !== 12 || Number(data.auditId) !== self.id) throw '您没有待处理的整改任务';
                         const item = typeof data.rectification_item === 'string' ? data.rectification_item.trim() : '';
@@ -1459,12 +1465,24 @@ module.exports = app => {
                         if (item.length > 1000 || opinion.length > 1000) throw '整改内容和意见最多1000字';
                         if (date && (typeof date !== 'string' || !moment(date, 'YYYY-MM-DD', true).isValid())) throw '整改日期格式错误';
                         const done = data.type === 'flow-complete-rectification';
+                        const returned = data.type === 'flow-return-rectification';
+                        if (returned && !opinion) throw '请填写退回意见';
                         if (done && (!item || !date)) throw '请填写整改内容和整改日期';
+                        const shared = this.ctx.service.safeInspection.getFlowRectification(current, rectifiers);
+                        if (data.rectificationOriginal !== undefined && (!data.rectificationOriginal || data.rectificationOriginal.rectification_item !== shared.rectification_item || data.rectificationOriginal.rectification_date !== shared.rectification_date)) {
+                            throw '整改单已被其他整改人更新,请刷新页面后重试';
+                        }
+                        await transaction.update(this.ctx.service.safeInspection.tableName, { id: current.id, rectification_item: item, rectification_date: date, rectification_uid: uid });
                         const update = { id: self.id, rectification_item: item, rectification_date: date, opinion };
                         if (done) Object.assign(update, { status: 3, end_time: new Date() });
                         await transaction.update(this.tableName, update);
-                        // 所有提交都先锁巡检主记录,再检查本轮其他人的结果。
-                        if (done && rectifiers.every(x => x.id === self.id || x.status === 3)) await this.activateFlow(transaction, current, 3);
+                        Object.assign(self, update);
+                        // 只有当前整改人可提交;通过后激活下一人,最后一人通过才进入批复。
+                        if (done) await this.activateFlow(transaction, current, rectifiers.some(x => x.status === 1) ? 2 : 3);
+                        if (returned) {
+                            await this.returnRectificationFlow(transaction, current, records, self, opinion);
+                            await this.noticeFlow(transaction, current, records, 6, opinion, 2);
+                        }
                         if (done) await this.noticeFlow(transaction, current, records, 3, opinion, 2);
                         break;
                     }
@@ -1520,23 +1538,30 @@ module.exports = app => {
                 inspection.status = 4;
                 return;
             }
-            if (inspection.flow_stage === 3) {
-                // 批复退回:原轮次保留,所有整改人同时开始新一轮整改。
-                const previous = records.filter(x => [2, 3].includes(x.flow_stage) && x.stage_round === inspection.stage_round);
-                inspection.stage_round += 1;
-                const copies = previous.map(x => this.flowRecord(inspection, x));
-                if (copies.length) await transaction.insert(this.tableName, copies);
-                await this.activateFlow(transaction, inspection, 2);
-                return;
-            }
             if (result === 5) {
-                const templates = records.filter(x => x.flow_stage === 1 || x.flow_stage === 3 && x.stage_round === inspection.stage_round);
+                const fromReply = inspection.flow_stage === 3;
+                const shared = fromReply ? this.ctx.service.safeInspection.getFlowRectification(inspection, records) : null;
+                const templates = records.filter(x => x.flow_stage === 1 || (x.flow_stage === 3 || fromReply && x.flow_stage === 2) && x.stage_round === inspection.stage_round);
                 inspection.times += 1;
                 inspection.stage_round = 1;
                 const copies = templates.map(x => this.flowRecord(inspection, x));
                 if (copies.length) await transaction.insert(this.tableName, copies);
-                await transaction.update(this.ctx.service.safeInspection.tableName, { id: inspection.id, times: inspection.times, stage_round: 1, flow_stage: 1, status: 5 });
-                inspection.status = 5;
+                const update = { id: inspection.id, times: inspection.times, stage_round: 1, flow_stage: 1, status: 5 };
+                if (shared) Object.assign(update, shared, { rectification_date: shared.rectification_date || null });
+                await transaction.update(this.ctx.service.safeInspection.tableName, update);
+                Object.assign(inspection, update);
+                return;
+            }
+            if (inspection.flow_stage === 3) {
+                const target = this.getReplyReturnTarget(inspection, records, self);
+                if (!target) throw '未找到上一审批人或整改人,请刷新页面后重试';
+                const shared = this.ctx.service.safeInspection.getFlowRectification(inspection, records);
+                const replaced = records.filter(x => x.stage_round === inspection.stage_round && (x.id === target.id || x.flow_stage === 3 && (target.flow_stage === 2 || x.order >= target.order)));
+                // 从退回对象重新流转,保留更早的审批结果,原意见及处理时间留作历史。
+                await transaction.updateRows(this.tableName, replaced.map(x => ({ id: x.id, is_old: 1 })));
+                await transaction.insert(this.tableName, replaced.map(x => this.flowRecord(inspection, x)));
+                await transaction.update(this.ctx.service.safeInspection.tableName, Object.assign({ id: inspection.id }, shared, { rectification_date: shared.rectification_date || null }));
+                await this.activateFlow(transaction, inspection, target.flow_stage);
                 return;
             }
             const report = records.filter(x => x.flow_stage === 1);
@@ -1561,6 +1586,96 @@ module.exports = app => {
             const users = this._.uniq([inspection.uid].concat(records.map(x => x.aid)));
             await transaction.insert('zh_notice', users.map(uid => ({ pid: this.ctx.session.sessionProject.id, tid: inspection.tid, type: pushType.safeInspection, uid, status: result, content: JSON.stringify(content) })));
         }
+
+        getRectificationSequence(inspection, records) {
+            return records.filter(x => !x.is_old && x.times === inspection.times && x.flow_stage === 2 && x.stage_round === inspection.stage_round)
+                .sort((a, b) => a.audit_order - b.audit_order || a.order - b.order || a.id - b.id);
+        }
+
+        getRectificationUpdates(inspection, records, reconcileActive = true) {
+            if (inspection.flow_version !== 1 || [3, 4].includes(inspection.status)) return [];
+            const sequence = this.getRectificationSequence(inspection, records);
+            const current = sequence.find(x => [1, 12].includes(x.status));
+            const updates = [];
+            sequence.forEach((record, index) => {
+                const update = { id: record.id };
+                if (record.order !== index + 1 || record.audit_order !== index + 1 || record.audit_type !== 1) {
+                    Object.assign(update, { order: index + 1, audit_order: index + 1, audit_type: 1 });
+                }
+                if (reconcileActive && inspection.flow_stage === 2 && inspection.status === 12 && [1, 12].includes(record.status)) {
+                    const status = record === current ? 12 : 1;
+                    if (record.status !== status) {
+                        update.status = status;
+                        if (status === 12) update.begin_time = new Date();
+                    }
+                }
+                if (Object.keys(update).length > 1) updates.push(update);
+            });
+            return updates;
+        }
+
+        async normalizeRectificationFlow(transaction, inspection, records, reconcileActive = true) {
+            const updates = this.getRectificationUpdates(inspection, records, reconcileActive);
+            if (updates.length) {
+                await transaction.updateRows(this.tableName, updates);
+                for (const update of updates) Object.assign(records.find(x => x.id === update.id), update);
+            }
+        }
+
+        async prepareSequentialFlow(inspection, records) {
+            if (!this.getRectificationUpdates(inspection, records).length) return false;
+            const transaction = await this.db.beginTransaction();
+            try {
+                const current = await this.lockFlow(transaction, inspection.id);
+                const audits = await transaction.select(this.tableName, { where: { qiid: current.id, times: current.times, is_old: 0 }, orders: [['order', 'asc'], ['id', 'asc']] });
+                await this.normalizeRectificationFlow(transaction, current, audits);
+                await transaction.commit();
+                Object.assign(inspection, current);
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        getRectificationReturnTarget(inspection, records, self) {
+            if (!self || self.flow_stage !== 2) return null;
+            const sequence = this.getRectificationSequence(inspection, records);
+            const index = sequence.findIndex(x => x.id === self.id);
+            if (index < 0) return null;
+            if (index > 0) return sequence[index - 1].status === 3 ? sequence[index - 1] : null;
+            // 会签/或签终审取实际最后通过的人,其他已通过人的结果继续保留。
+            const report = records.filter(x => !x.is_old && x.times === inspection.times && x.flow_stage === 1 && x.status === 3);
+            report.sort((a, b) => b.audit_order - a.audit_order || new Date(b.end_time).getTime() - new Date(a.end_time).getTime() || b.id - a.id);
+            return report[0] || null;
+        }
+
+        async returnRectificationFlow(transaction, inspection, records, self, opinion) {
+            const target = this.getRectificationReturnTarget(inspection, records, self);
+            if (!target) throw '未找到上一审批人,请刷新页面后重试';
+            await transaction.update(this.tableName, { id: self.id, status: 6, opinion, end_time: new Date() });
+            const sequence = this.getRectificationSequence(inspection, records);
+            const replaced = sequence.filter(x => target.flow_stage === 1 || x.audit_order >= target.audit_order);
+            if (target.flow_stage === 1) replaced.push(target);
+            // 原处理记录留作历史,新建待办,避免旧页面请求操作重新激活的任务。
+            await transaction.updateRows(this.tableName, replaced.map(x => ({ id: x.id, is_old: 1 })));
+            await transaction.insert(this.tableName, replaced.map(x => this.flowRecord(inspection, x)));
+            await this.activateFlow(transaction, inspection, target.flow_stage);
+        }
+
+        getReplyReturnTarget(inspection, records, self) {
+            if (inspection.flow_stage !== 3 || !self) return null;
+            const earlier = records.filter(x => !x.is_old && x.times === inspection.times && x.flow_stage === 3 && x.stage_round === inspection.stage_round && x.order < self.order);
+            if (earlier.length) {
+                const previousOrder = Math.max(...earlier.map(x => x.order));
+                const previous = earlier.filter(x => x.order === previousOrder);
+                const approved = previous.find(x => x.status === 3);
+                return approved ? Object.assign({}, approved, { name: previous.map(x => x.name).join('、') }) : null;
+            }
+            const sequence = this.getRectificationSequence(inspection, records);
+            const last = sequence[sequence.length - 1];
+            return last && last.status === 3 ? last : null;
+        }
     }
 
     return SafeInspectionAudit;

+ 4 - 4
app/view/quality/inspection.ejs

@@ -63,13 +63,13 @@
     <div class="content-wrap">
         <div class="c-body">
             <div class="sjs-height-0">
-                <table class="table table-bordered">
+                <table class="table table-bordered quality-inspection-list-table">
                     <thead class="text-center">
                     <tr>
-                        <th width="13%" id="sort_change">编号</th>
+                        <th width="15%" id="sort_change">编号</th>
 <!--                        <th width="5%">部位</th>-->
-                        <th width="10%">检查项目</th><th width="18%">检查情况</th><th width="7%">是否整改</th><th width="13%">处理要求及措施</th>
-                        <th width="10%">检查日期</th><th width="7%">附件</th><th width="13%">状态</th>
+                        <th width="20%">检查项目</th><th width="15%">检查情况</th><th width="7%">是否整改</th><th width="15%">处理要求及措施</th>
+                        <th width="10%">检查日期</th><th width="5%">附件</th><th width="13%">状态</th>
                     </tr>
                     </thead>
                     <tbody id="changeList">

+ 13 - 15
app/view/quality/inspection_flow.ejs

@@ -15,7 +15,7 @@
                 <% [1, 0].forEach(value => { %>
                     <label class="mr-3 mb-0"><input type="radio" name="quality-need-rectification" value="<%= value %>" <%= inspection.need_rectification === value ? 'checked' : '' %> <%= inspection.readOnly ? 'disabled' : '' %>> <%= value ? '是' : '否' %></label>
                 <% }); %>
-                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,所有整改人完成后进入批复。</small>
+                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,整改人按顺序通过后进入批复。</small>
             </div>
         </div>
     <% } else { %>
@@ -30,7 +30,7 @@
                         <% const people = phase.auditors.map((audit, index) => ({
                             name: audit.name,
                             icon: phase.stage === 3 && index === phase.auditors.length - 1 ? 'fa-stop-circle' : 'fa-chevron-circle-down',
-                            title: phase.stage === 2 ? '整改人(同时处理)' : audit.audit_order + '审',
+                            title: phase.stage === 2 ? '第' + audit.audit_order + '位整改人' : audit.audit_order + '审',
                             placeholder: false,
                         }));
                         if (phase.stage === 1) people.unshift({ name: inspection.user.name, icon: 'fa-play-circle fa-rotate-90', title: '上报人', placeholder: false });
@@ -89,7 +89,7 @@
                         <% if (showTimeline) { %>
                             <% if (phase.stage === 2 && phase.auditors.length) { %>
                                 <div class="flow-stage-progress d-flex justify-content-between flex-wrap text-muted">
-                                    <span>同时整改</span><span>已完成 <%= inspection.rectificationDone %>/<%= inspection.rectificationTotal %></span>
+                                    <span>按顺序整改</span><span>已通过 <%= inspection.rectificationDone %>/<%= inspection.rectificationTotal %></span>
                                 </div>
                             <% } %>
                             <ul class="timeline-list list-unstyled flow-timeline">
@@ -105,7 +105,7 @@
                                         <div class="timeline-item-content">
                                             <div class="d-flex justify-content-between py-1">
                                                 <span class="text-black-50"><%= node.label %></span>
-                                                <span class="text-<%= tone %>"><%= node.origin ? '上报审批' : phase.stage !== 2 && node.status === 3 ? '审批通过' : flowStatusText[node.status] %></span>
+                                                <span class="text-<%= tone %>"><%= node.origin ? '上报审批' : node.status === 3 ? (phase.stage === 2 ? '整改通过' : '审批通过') : phase.stage === 2 && [5, 6].includes(node.status) ? '整改退回' : flowStatusText[node.status] %></span>
                                             </div>
                                             <div class="card">
                                                 <div class="card-body px-3 py-2">
@@ -120,11 +120,7 @@
                                                                     <span class="ml-auto pl-2 text-<%= personTone %>"><% if (!node.origin && node.auditors.length > 1) { %><small class="mr-1"><%= flowStatusText[audit.status] %></small><% } %><i class="fa <%= node.origin || audit.status === 3 ? 'fa-check-circle' : [2, 12].includes(audit.status) ? 'fa-commenting' : [5, 6].includes(audit.status) ? 'fa-reply' : audit.status === 4 ? 'fa-stop-circle' : 'fa-circle-o' %>" aria-hidden="true"></i></span>
                                                                 <% } %>
                                                             </div>
-                                                            <% if (phase.stage === 2) { %>
-                                                                <% if (audit.rectification_item) { %><div class="flow-rectification-result mt-2"><%= audit.rectification_item %></div><% } %>
-                                                                <% if (audit.rectification_date) { %><small class="d-block text-muted mt-1">整改日期:<%= moment(audit.rectification_date).format('YYYY-MM-DD') %></small><% } %>
-                                                            <% } %>
-                                                            <% if (phase.stage !== 2 || audit.opinion) { %><div class="bg-light flow-opinion mt-1" aria-label="<%= phase.stage === 2 ? '说明' : '处理意见' %>"><i class="fa fa-commenting-o mr-1" aria-hidden="true"></i><%= audit.opinion || '' %></div><% } %>
+                                                            <div class="bg-light flow-opinion mt-1" aria-label="<%= phase.stage === 2 ? '整改意见' : '处理意见' %>"><i class="fa fa-commenting-o mr-1" aria-hidden="true"></i><%= audit.opinion || (phase.stage === 2 ? '未填写整改意见' : '') %></div>
                                                         </div>
                                                     <% }); %>
                                                 </div>
@@ -132,7 +128,7 @@
                                         </div>
                                     </li>
                                 <% }); %>
-                                <% if (!phase.nodes.length) { %><li class="flow-timeline-empty text-muted py-2"><%= phase.stage === 2 ? '整改人由上报审批人指派,可设置多人同时处理。' : '尚未设置审批人' %></li><% } %>
+                                <% if (!phase.nodes.length) { %><li class="flow-timeline-empty text-muted py-2"><%= phase.stage === 2 ? '整改人由上报审批人按先后顺序指派,依次处理。' : '尚未设置审批人' %></li><% } %>
                             </ul>
                         <% } else { %>
                         <div class="card">
@@ -141,7 +137,7 @@
                                 <% if (phase.stage === 2 && phase.auditors.length) { %><span class="float-right">第 <%= inspection.stage_round %> 轮 · 已完成 <%= inspection.rectificationDone %>/<%= inspection.rectificationTotal %></span><% } %>
                             </div>
                             <ul class="list-group list-group-flush">
-                                <% if (!phase.auditors.length) { %><li class="list-group-item text-muted"><%= phase.stage === 2 ? '整改人由上报审批人指派,可设置多人同时处理。' : '请设置' + phase.name + '审批人' %></li><% } %>
+                                <% if (!phase.auditors.length) { %><li class="list-group-item text-muted"><%= phase.stage === 2 ? '整改人由上报审批人按先后顺序指派,依次处理。' : '请设置' + phase.name + '审批人' %></li><% } %>
                                 <% phase.auditors.forEach(audit => { %>
                                     <li class="list-group-item">
                                         <div class="d-flex justify-content-between">
@@ -160,14 +156,16 @@
                                                 <label class="d-block">说明<textarea class="form-control form-control-sm flow-rectification-opinion" rows="2" maxlength="1000"><%= audit.opinion || '' %></textarea></label>
                                                 <% if (mobileFlow) { %>
                                                     <button type="button" class="btn btn-outline-primary btn-sm flow-save-rectification" data-id="<%= audit.id %>">保存</button>
-                                                    <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= audit.id %>">提交整改完成</button>
+                                                    <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= audit.id %>">提交整改</button>
                                                 <% } %>
                                             </div>
-                                        <% } else if (phase.stage === 2 && (audit.rectification_item || audit.rectification_date)) { %>
+                                        <% } else if (mobileFlow && phase.stage === 2 && (audit.rectification_item || audit.rectification_date)) { %>
                                             <div class="mt-2"><%= audit.rectification_item %></div>
                                             <small class="text-muted">整改日期:<%= audit.rectification_date ? moment(audit.rectification_date).format('YYYY-MM-DD') : '' %></small>
                                         <% } %>
-                                        <% if (audit.opinion && !(phase.stage === 2 && inspection.rectificationPower && audit.aid === ctx.session.sessionUser.accountId && audit.status === 12)) { %><p class="mb-0 mt-2"><%= audit.opinion %></p><% } %>
+                                        <% if (!mobileFlow && phase.stage === 2) { %>
+                                            <div class="bg-light flow-opinion mt-1" aria-label="整改意见"><i class="fa fa-commenting-o mr-1" aria-hidden="true"></i><%= audit.opinion || '未填写整改意见' %></div>
+                                        <% } else if (audit.opinion && !(phase.stage === 2 && inspection.rectificationPower && audit.aid === ctx.session.sessionUser.accountId && audit.status === 12)) { %><p class="mb-0 mt-2"><%= audit.opinion %></p><% } %>
                                         <% if (mobileFlow && phase.stage !== 2 && inspection.shenpiPower && inspection.flow_stage === phase.stage && audit.aid === ctx.session.sessionUser.accountId && audit.status === 2) { %>
                                             <div class="mt-2">
                                                 <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-check" data-result="3" data-id="<%= audit.id %>"><%= phase.stage === 1 ? '审批' : phase.name %>通过</button>
@@ -175,7 +173,7 @@
                                                     <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-check" data-result="5" data-id="<%= audit.id %>">退回上报人</button>
                                                     <% if (phase.auditors.some(x => x.audit_order < audit.audit_order)) { %><button type="button" class="btn btn-outline-warning btn-sm flow-action" data-action="flow-check" data-result="6" data-id="<%= audit.id %>">退回上一审批节点</button><% } %>
                                                 <% } else { %>
-                                                    <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-check" data-result="5" data-id="<%= audit.id %>">退回重新整改</button>
+                                                    <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-check" data-result="5" data-id="<%= audit.id %>">整改退回</button>
                                                 <% } %>
                                                 <button type="button" class="btn btn-outline-danger btn-sm flow-action" data-action="flow-check" data-result="4" data-id="<%= audit.id %>">关闭巡检</button>
                                             </div>

+ 28 - 10
app/view/quality/inspection_information.ejs

@@ -10,7 +10,7 @@
             </div>
             <div class="ml-auto">
                 <% if (!inspection.readOnly) { %>
-                <a href="#del" data-toggle="modal" data-target="#del" class="btn btn-outline-danger btn-sm mr-5">删除巡检</a>
+                <a href="#del" data-toggle="modal" data-target="#del" class="btn btn-outline-danger btn-sm mr-1">删除巡检</a>
                 <% } %>
                 <% if (inspection.flow_version === 1) { %>
                     <% if (inspection.status !== auditConst.status.uncheck && inspection.status !== auditConst.status.rectification && !([auditConst.stage.report, auditConst.stage.reply].includes(inspection.flow_stage) && [auditConst.status.checking, auditConst.status.checkNoPre].includes(inspection.status))) { %>
@@ -20,11 +20,11 @@
                         <button type="button" class="btn btn-success btn-sm flow-action" data-action="start-inspection">提交上报</button>
                     <% } else if (inspection.flowCurrentAudit && inspection.shenpiPower) { %>
                         <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-check" data-result="3" data-id="<%= inspection.flowCurrentAudit.id %>"><%= inspection.flow_stage === 1 ? '审批' : '批复' %>通过</button>
-                        <button type="button" class="btn btn-warning btn-sm flow-action" data-action="<%= inspection.flow_stage === 1 ? 'flow-return' : 'flow-check' %>" data-result="5" data-id="<%= inspection.flowCurrentAudit.id %>"><%= inspection.flow_stage === 1 ? '审批退回' : '退回重新整改' %></button>
+                        <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-return" data-id="<%= inspection.flowCurrentAudit.id %>">审批退回</button>
                         <button type="button" class="btn btn-danger btn-sm flow-action" data-action="flow-check" data-result="4" data-id="<%= inspection.flowCurrentAudit.id %>">关闭</button>
                     <% } else if (inspection.flowCurrentAudit && inspection.rectificationPower) { %>
-                        <button type="button" class="btn btn-outline-primary btn-sm flow-save-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">保存整改</button>
-                        <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">提交整改完成</button>
+                        <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-return-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">整改退回</button>
+                        <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">整改通过</button>
                     <% } %>
                 <% } else { %>
                 <% if (inspection.status === auditConst.status.uncheck) { %>
@@ -85,7 +85,7 @@
                                 <% [1, 0].forEach(value => { %>
                                     <label class="mr-3 mb-0"><input type="radio" name="quality-need-rectification" value="<%= value %>" <%= inspection.need_rectification === value ? 'checked' : '' %> <%= inspection.readOnly ? 'disabled' : '' %>> <%= value ? '是' : '否' %></label>
                                 <% }); %>
-                                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,所有整改人完成后进入批复。</small>
+                                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,整改人按顺序通过后进入批复。</small>
                             </td>
                         </tr>
                         <% } %>
@@ -158,9 +158,11 @@
                         </tbody>
                     </table>
                     <% } %>
-                    <% if (inspection.flow_version === 1 && inspection.rectificationPower && inspection.flowCurrentAudit && inspection.flowCurrentAudit.status === auditConst.status.rectification) { %>
-                    <% const rectificationAudit = inspection.flowCurrentAudit; %>
-                    <table class="table table-bordered quality-inspection-rectification flow-rectification" data-id="<%= rectificationAudit.id %>">
+                    <% if (inspection.flow_version === 1 && ([auditConst.stage.rectification, auditConst.stage.reply].includes(inspection.flow_stage) || inspection.rectification_uid || inspection.rectification_item || inspection.rectification_date || (inspection.flowRectificationReturns && inspection.flowRectificationReturns.length))) { %>
+                    <% const rectificationAudit = inspection.flowCurrentAudit;
+                       const rectification = inspection.flowRectification;
+                       const canEditRectification = inspection.rectificationPower && inspection.flow_stage === auditConst.stage.rectification && rectificationAudit && rectificationAudit.status === auditConst.status.rectification; %>
+                    <table class="table table-bordered quality-inspection-rectification<%= canEditRectification ? ' flow-rectification' : '' %>"<% if (canEditRectification) { %> data-id="<%= rectificationAudit.id %>"<% } %>>
                         <colgroup><col class="rectification-label"><col></colgroup>
                         <thead>
                         <tr><th colspan="2" class="text-center">整改单</th></tr>
@@ -169,16 +171,32 @@
                         <tr>
                             <th scope="row" class="align-middle">整改情况</th>
                             <td>
-                                <textarea class="form-control form-control-sm flow-rectification-item" rows="2" maxlength="1000" aria-label="整改情况"><%= rectificationAudit.rectification_item || '' %></textarea>
+                                <% if (canEditRectification) { %>
+                                <textarea class="form-control form-control-sm flow-rectification-item" rows="2" maxlength="1000" aria-label="整改情况"><%= rectification.rectification_item %></textarea>
                                 <textarea class="flow-rectification-opinion" hidden><%= rectificationAudit.opinion || '' %></textarea>
+                                <% } else { %>
+                                <div class="rectification-content"><%= rectification.rectification_item || '—' %></div>
+                                <% } %>
                             </td>
                         </tr>
                         <tr>
                             <th scope="row" class="align-middle">整改日期</th>
-                            <td><input class="form-control form-control-sm flow-rectification-date" type="date" aria-label="整改日期" value="<%= rectificationAudit.rectification_date ? moment(rectificationAudit.rectification_date).format('YYYY-MM-DD') : '' %>"></td>
+                            <td>
+                                <% if (canEditRectification) { %>
+                                <input class="form-control form-control-sm flow-rectification-date" type="date" aria-label="整改日期" value="<%= rectification.rectification_date %>">
+                                <% } else { %>
+                                <%= rectification.rectification_date || '—' %>
+                                <% } %>
+                            </td>
                         </tr>
                         </tbody>
                     </table>
+                    <% if (canEditRectification) { %>
+                    <div class="quality-inspection-rectification-save small text-right mb-3" role="status" aria-live="polite">
+                        <span class="flow-save-status text-muted">已保存</span>
+                        <button type="button" class="btn btn-link btn-sm p-0 ml-2 d-none flow-save-retry">重试</button>
+                    </div>
+                    <% } %>
                     <% } %>
                     <table class="table table-bordered mt-3">
                         <thead>

+ 17 - 7
app/view/quality/inspection_information_modal.ejs

@@ -257,25 +257,35 @@
             <div class="modal-header"><h5 class="modal-title" id="quality-flow-title">确认操作</h5></div>
             <div class="modal-body">
                 <p id="quality-flow-message"></p>
-                <% if (inspection.shenpiPower && inspection.flow_stage === 1 && inspection.flowCurrentAudit) { %>
+                <% if (inspection.shenpiPower && [1, 3].includes(inspection.flow_stage) && inspection.flowCurrentAudit) { %>
                 <div class="alert alert-warning d-none" id="quality-flow-return-group">
                     <div class="form-check form-check-inline">
                         <input class="form-check-input" type="radio" name="quality-flow-return-type" id="quality-flow-return-origin" value="5">
-                        <label class="form-check-label" for="quality-flow-return-origin">退回原报</label>
+                        <label class="form-check-label" for="quality-flow-return-origin">退回原报<% if (inspection.flow_stage === 3) { %>:<%= inspection.user.name %><% } %></label>
                     </div>
                     <% if (inspection.flowCanReturnPrevious) {
-                        const report = inspection.flowStages.find(phase => phase.stage === 1).auditors;
-                        const earlier = report.filter(audit => audit.audit_order < inspection.flowCurrentAudit.audit_order);
-                        const previousOrder = Math.max(...earlier.map(audit => audit.audit_order));
-                        const previousNames = report.filter(audit => audit.audit_order === previousOrder).map(audit => audit.name).join('、');
+                        let previousNames;
+                        let previousLabel = '退回上一审批人';
+                        if (inspection.flow_stage === 3) {
+                            previousNames = inspection.flowReplyReturnTarget.name;
+                            previousLabel = inspection.flowReplyReturnTarget.flow_stage === 2 ? '退回上一整改人' : '退回上一审批人';
+                        } else {
+                            const report = inspection.flowStages.find(phase => phase.stage === 1).auditors;
+                            const earlier = report.filter(audit => audit.audit_order < inspection.flowCurrentAudit.audit_order);
+                            const previousOrder = Math.max(...earlier.map(audit => audit.audit_order));
+                            previousNames = report.filter(audit => audit.audit_order === previousOrder).map(audit => audit.name).join('、');
+                        }
                     %>
                     <div class="form-check form-check-inline">
                         <input class="form-check-input" type="radio" name="quality-flow-return-type" id="quality-flow-return-previous" value="6">
-                        <label class="form-check-label" for="quality-flow-return-previous">退回上一审批人:<%= previousNames %></label>
+                        <label class="form-check-label" for="quality-flow-return-previous"><%= previousLabel %>:<%= previousNames %></label>
                     </div>
                     <% } %>
                 </div>
                 <% } %>
+                <% if (inspection.flowRectificationReturnTarget) { %>
+                <div class="alert alert-warning d-none" id="quality-flow-rectification-return">退回上一审批人:<%= inspection.flowRectificationReturnTarget.name %></div>
+                <% } %>
                 <div id="quality-flow-opinion-group"><label for="quality-flow-opinion">处理意见</label><textarea id="quality-flow-opinion" class="form-control" rows="3" maxlength="1000"></textarea></div>
             </div>
             <div class="modal-footer">

+ 2 - 2
app/view/quality/inspection_modal.ejs

@@ -33,8 +33,8 @@
             <div class="modal-body">
                 <div class="modal-height-500" style="overflow: auto;">
                     <table class="table table-sm table-bordered">
-                        <thead>
-                        <tr><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
+                        <thead class="text-center">
+                        <tr><th width="45%">文件名</th><th width="15%">上传人</th><th width="25%">上传时间</th><th width="15%">操作</th></tr>
                         </thead>
                         <tbody id="file-content"></tbody>
                     </table>

+ 20 - 2
app/view/quality/sub_memu_list.ejs

@@ -4,8 +4,26 @@
 <!--<nav-menu title="缺陷管理" url="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/flaw%>" ml="3" active="<%= ctx.url.indexOf('/flaw') %>"></nav-menu>-->
 <!--<nav-menu title="试验报告" url="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/lab%>" ml="3" active="<%= ctx.url.indexOf('/lab') %>"></nav-menu>-->
 <% } else { %>
+<%
+const menuCounts = typeof filter !== 'undefined' && filter.count && filter.status ? {
+    rectification: filter.count[filter.status.rectification],
+    reply: filter.count[filter.status.reply],
+} : (typeof inspectionMenuCounts !== 'undefined' && inspectionMenuCounts ? inspectionMenuCounts : {});
+%>
 <nav-menu title="巡检概况" url="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/inspection/overview" ml="1" active="<%= ctx.url.indexOf('/inspection/overview') >= 0 ? 1 : -1 %>" icon="fa-pie-chart"></nav-menu>
 <nav-menu title="质量巡检" url="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/inspection" ml="3" active="<%= ctx.url.includes('/inspection') && !ctx.url.includes('/overview') && !ctx.url.includes('/rectification') && !ctx.url.includes('/approval') ? 1 : -1 %>" ></nav-menu>
-<nav-menu title="巡检整改" url="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/inspection/rectification" ml="3" active="<%= ctx.url.indexOf('/inspection/rectification') >= 0 ? 1 : -1 %>" ></nav-menu>
-<nav-menu title="巡检批复" url="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/inspection/approval" ml="3" active="<%= ctx.url.indexOf('/inspection/approval') >= 0 ? 1 : -1 %>" ></nav-menu>
+<div class="nav-box quality-inspection-menu">
+    <ul class="nav-list list-unstyled">
+        <li class="<%= ctx.url.includes('/inspection/rectification') ? 'active' : '' %>">
+            <a href="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/inspection/rectification"><span class="ml-3">巡检整改</span><span class="badge badge-warning quality-inspection-menu-count"><%= menuCounts.rectification === undefined || menuCounts.rectification === null ? '—' : menuCounts.rectification %></span></a>
+        </li>
+    </ul>
+</div>
+<div class="nav-box quality-inspection-menu">
+    <ul class="nav-list list-unstyled">
+        <li class="<%= ctx.url.includes('/inspection/approval') ? 'active' : '' %>">
+            <a href="/sp/<%- ctx.subProject.id %>/quality/tender/<%= ctx.tender.id %>/inspection/approval"><span class="ml-3">巡检批复</span><span class="badge badge-warning quality-inspection-menu-count"><%= menuCounts.reply === undefined || menuCounts.reply === null ? '—' : menuCounts.reply %></span></a>
+        </li>
+    </ul>
+</div>
 <% } %>

+ 4 - 4
app/view/safe/inspection.ejs

@@ -63,13 +63,13 @@
     <div class="content-wrap">
         <div class="c-body">
             <div class="sjs-height-0">
-                <table class="table table-bordered">
+                <table class="table table-bordered safe-inspection-list-table">
                     <thead class="text-center">
                     <tr>
-                        <th width="13%" id="sort_change">编号</th>
+                        <th width="15%" id="sort_change">编号</th>
 <!--                        <th width="5%">部位</th>-->
-                        <th width="10%">检查项目</th><th width="18%">检查情况</th><th width="7%">是否整改</th><th width="13%">处理要求及措施</th>
-                        <th width="10%">检查日期</th><th width="7%">附件</th><th width="13%">状态</th>
+                        <th width="20%">检查项目</th><th width="15%">检查情况</th><th width="7%">是否整改</th><th width="15%">处理要求及措施</th>
+                        <th width="10%">检查日期</th><th width="5%">附件</th><th width="13%">状态</th>
                     </tr>
                     </thead>
                     <tbody id="changeList">

+ 13 - 15
app/view/safe/inspection_flow.ejs

@@ -15,7 +15,7 @@
                 <% [1, 0].forEach(value => { %>
                     <label class="mr-3 mb-0"><input type="radio" name="safe-need-rectification" value="<%= value %>" <%= inspection.need_rectification === value ? 'checked' : '' %> <%= inspection.readOnly ? 'disabled' : '' %>> <%= value ? '是' : '否' %></label>
                 <% }); %>
-                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,所有整改人完成后进入批复。</small>
+                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,整改人按顺序通过后进入批复。</small>
             </div>
         </div>
     <% } else { %>
@@ -30,7 +30,7 @@
                         <% const people = phase.auditors.map((audit, index) => ({
                             name: audit.name,
                             icon: phase.stage === 3 && index === phase.auditors.length - 1 ? 'fa-stop-circle' : 'fa-chevron-circle-down',
-                            title: phase.stage === 2 ? '整改人(同时处理)' : audit.audit_order + '审',
+                            title: phase.stage === 2 ? '第' + audit.audit_order + '位整改人' : audit.audit_order + '审',
                             placeholder: false,
                         }));
                         if (phase.stage === 1) people.unshift({ name: inspection.user.name, icon: 'fa-play-circle fa-rotate-90', title: '上报人', placeholder: false });
@@ -89,7 +89,7 @@
                         <% if (showTimeline) { %>
                             <% if (phase.stage === 2 && phase.auditors.length) { %>
                                 <div class="flow-stage-progress d-flex justify-content-between flex-wrap text-muted">
-                                    <span>同时整改</span><span>已完成 <%= inspection.rectificationDone %>/<%= inspection.rectificationTotal %></span>
+                                    <span>按顺序整改</span><span>已通过 <%= inspection.rectificationDone %>/<%= inspection.rectificationTotal %></span>
                                 </div>
                             <% } %>
                             <ul class="timeline-list list-unstyled flow-timeline">
@@ -105,7 +105,7 @@
                                         <div class="timeline-item-content">
                                             <div class="d-flex justify-content-between py-1">
                                                 <span class="text-black-50"><%= node.label %></span>
-                                                <span class="text-<%= tone %>"><%= node.origin ? '上报审批' : phase.stage !== 2 && node.status === 3 ? '审批通过' : flowStatusText[node.status] %></span>
+                                                <span class="text-<%= tone %>"><%= node.origin ? '上报审批' : node.status === 3 ? (phase.stage === 2 ? '整改通过' : '审批通过') : phase.stage === 2 && [5, 6].includes(node.status) ? '整改退回' : flowStatusText[node.status] %></span>
                                             </div>
                                             <div class="card">
                                                 <div class="card-body px-3 py-2">
@@ -120,11 +120,7 @@
                                                                     <span class="ml-auto pl-2 text-<%= personTone %>"><% if (!node.origin && node.auditors.length > 1) { %><small class="mr-1"><%= flowStatusText[audit.status] %></small><% } %><i class="fa <%= node.origin || audit.status === 3 ? 'fa-check-circle' : [2, 12].includes(audit.status) ? 'fa-commenting' : [5, 6].includes(audit.status) ? 'fa-reply' : audit.status === 4 ? 'fa-stop-circle' : 'fa-circle-o' %>" aria-hidden="true"></i></span>
                                                                 <% } %>
                                                             </div>
-                                                            <% if (phase.stage === 2) { %>
-                                                                <% if (audit.rectification_item) { %><div class="flow-rectification-result mt-2"><%= audit.rectification_item %></div><% } %>
-                                                                <% if (audit.rectification_date) { %><small class="d-block text-muted mt-1">整改日期:<%= moment(audit.rectification_date).format('YYYY-MM-DD') %></small><% } %>
-                                                            <% } %>
-                                                            <% if (phase.stage !== 2 || audit.opinion) { %><div class="bg-light flow-opinion mt-1" aria-label="<%= phase.stage === 2 ? '说明' : '处理意见' %>"><i class="fa fa-commenting-o mr-1" aria-hidden="true"></i><%= audit.opinion || '' %></div><% } %>
+                                                            <div class="bg-light flow-opinion mt-1" aria-label="<%= phase.stage === 2 ? '整改意见' : '处理意见' %>"><i class="fa fa-commenting-o mr-1" aria-hidden="true"></i><%= audit.opinion || (phase.stage === 2 ? '未填写整改意见' : '') %></div>
                                                         </div>
                                                     <% }); %>
                                                 </div>
@@ -132,7 +128,7 @@
                                         </div>
                                     </li>
                                 <% }); %>
-                                <% if (!phase.nodes.length) { %><li class="flow-timeline-empty text-muted py-2"><%= phase.stage === 2 ? '整改人由上报审批人指派,可设置多人同时处理。' : '尚未设置审批人' %></li><% } %>
+                                <% if (!phase.nodes.length) { %><li class="flow-timeline-empty text-muted py-2"><%= phase.stage === 2 ? '整改人由上报审批人按先后顺序指派,依次处理。' : '尚未设置审批人' %></li><% } %>
                             </ul>
                         <% } else { %>
                         <div class="card">
@@ -141,7 +137,7 @@
                                 <% if (phase.stage === 2 && phase.auditors.length) { %><span class="float-right">第 <%= inspection.stage_round %> 轮 · 已完成 <%= inspection.rectificationDone %>/<%= inspection.rectificationTotal %></span><% } %>
                             </div>
                             <ul class="list-group list-group-flush">
-                                <% if (!phase.auditors.length) { %><li class="list-group-item text-muted"><%= phase.stage === 2 ? '整改人由上报审批人指派,可设置多人同时处理。' : '请设置' + phase.name + '审批人' %></li><% } %>
+                                <% if (!phase.auditors.length) { %><li class="list-group-item text-muted"><%= phase.stage === 2 ? '整改人由上报审批人按先后顺序指派,依次处理。' : '请设置' + phase.name + '审批人' %></li><% } %>
                                 <% phase.auditors.forEach(audit => { %>
                                     <li class="list-group-item">
                                         <div class="d-flex justify-content-between">
@@ -160,14 +156,16 @@
                                                 <label class="d-block">说明<textarea class="form-control form-control-sm flow-rectification-opinion" rows="2" maxlength="1000"><%= audit.opinion || '' %></textarea></label>
                                                 <% if (mobileFlow) { %>
                                                     <button type="button" class="btn btn-outline-primary btn-sm flow-save-rectification" data-id="<%= audit.id %>">保存</button>
-                                                    <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= audit.id %>">提交整改完成</button>
+                                                    <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= audit.id %>">提交整改</button>
                                                 <% } %>
                                             </div>
-                                        <% } else if (phase.stage === 2 && (audit.rectification_item || audit.rectification_date)) { %>
+                                        <% } else if (mobileFlow && phase.stage === 2 && (audit.rectification_item || audit.rectification_date)) { %>
                                             <div class="mt-2"><%= audit.rectification_item %></div>
                                             <small class="text-muted">整改日期:<%= audit.rectification_date ? moment(audit.rectification_date).format('YYYY-MM-DD') : '' %></small>
                                         <% } %>
-                                        <% if (audit.opinion && !(phase.stage === 2 && inspection.rectificationPower && audit.aid === ctx.session.sessionUser.accountId && audit.status === 12)) { %><p class="mb-0 mt-2"><%= audit.opinion %></p><% } %>
+                                        <% if (!mobileFlow && phase.stage === 2) { %>
+                                            <div class="bg-light flow-opinion mt-1" aria-label="整改意见"><i class="fa fa-commenting-o mr-1" aria-hidden="true"></i><%= audit.opinion || '未填写整改意见' %></div>
+                                        <% } else if (audit.opinion && !(phase.stage === 2 && inspection.rectificationPower && audit.aid === ctx.session.sessionUser.accountId && audit.status === 12)) { %><p class="mb-0 mt-2"><%= audit.opinion %></p><% } %>
                                         <% if (mobileFlow && phase.stage !== 2 && inspection.shenpiPower && inspection.flow_stage === phase.stage && audit.aid === ctx.session.sessionUser.accountId && audit.status === 2) { %>
                                             <div class="mt-2">
                                                 <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-check" data-result="3" data-id="<%= audit.id %>"><%= phase.stage === 1 ? '审批' : phase.name %>通过</button>
@@ -175,7 +173,7 @@
                                                     <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-check" data-result="5" data-id="<%= audit.id %>">退回上报人</button>
                                                     <% if (phase.auditors.some(x => x.audit_order < audit.audit_order)) { %><button type="button" class="btn btn-outline-warning btn-sm flow-action" data-action="flow-check" data-result="6" data-id="<%= audit.id %>">退回上一审批节点</button><% } %>
                                                 <% } else { %>
-                                                    <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-check" data-result="5" data-id="<%= audit.id %>">退回重新整改</button>
+                                                    <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-check" data-result="5" data-id="<%= audit.id %>">整改退回</button>
                                                 <% } %>
                                                 <button type="button" class="btn btn-outline-danger btn-sm flow-action" data-action="flow-check" data-result="4" data-id="<%= audit.id %>">关闭巡检</button>
                                             </div>

+ 28 - 10
app/view/safe/inspection_information.ejs

@@ -10,7 +10,7 @@
             </div>
             <div class="ml-auto">
                 <% if (!inspection.readOnly) { %>
-                <a href="#del" data-toggle="modal" data-target="#del" class="btn btn-outline-danger btn-sm mr-5">删除巡检</a>
+                <a href="#del" data-toggle="modal" data-target="#del" class="btn btn-outline-danger btn-sm mr-1">删除巡检</a>
                 <% } %>
                 <% if (inspection.flow_version === 1) { %>
                     <% if (inspection.status !== auditConst.status.uncheck && inspection.status !== auditConst.status.rectification && !([auditConst.stage.report, auditConst.stage.reply].includes(inspection.flow_stage) && [auditConst.status.checking, auditConst.status.checkNoPre].includes(inspection.status))) { %>
@@ -20,11 +20,11 @@
                         <button type="button" class="btn btn-success btn-sm flow-action" data-action="start-inspection">提交上报</button>
                     <% } else if (inspection.flowCurrentAudit && inspection.shenpiPower) { %>
                         <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-check" data-result="3" data-id="<%= inspection.flowCurrentAudit.id %>"><%= inspection.flow_stage === 1 ? '审批' : '批复' %>通过</button>
-                        <button type="button" class="btn btn-warning btn-sm flow-action" data-action="<%= inspection.flow_stage === 1 ? 'flow-return' : 'flow-check' %>" data-result="5" data-id="<%= inspection.flowCurrentAudit.id %>"><%= inspection.flow_stage === 1 ? '审批退回' : '退回重新整改' %></button>
+                        <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-return" data-id="<%= inspection.flowCurrentAudit.id %>">审批退回</button>
                         <button type="button" class="btn btn-danger btn-sm flow-action" data-action="flow-check" data-result="4" data-id="<%= inspection.flowCurrentAudit.id %>">关闭</button>
                     <% } else if (inspection.flowCurrentAudit && inspection.rectificationPower) { %>
-                        <button type="button" class="btn btn-outline-primary btn-sm flow-save-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">保存整改</button>
-                        <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">提交整改完成</button>
+                        <button type="button" class="btn btn-warning btn-sm flow-action" data-action="flow-return-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">整改退回</button>
+                        <button type="button" class="btn btn-success btn-sm flow-action" data-action="flow-complete-rectification" data-id="<%= inspection.flowCurrentAudit.id %>">整改通过</button>
                     <% } %>
                 <% } else { %>
                 <% if (inspection.status === auditConst.status.uncheck) { %>
@@ -85,7 +85,7 @@
                                 <% [1, 0].forEach(value => { %>
                                     <label class="mr-3 mb-0"><input type="radio" name="safe-need-rectification" value="<%= value %>" <%= inspection.need_rectification === value ? 'checked' : '' %> <%= inspection.readOnly ? 'disabled' : '' %>> <%= value ? '是' : '否' %></label>
                                 <% }); %>
-                                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,所有整改人完成后进入批复。</small>
+                                <small class="text-warning d-block mt-2">选择“是”时,上报结束后进入整改,整改人按顺序通过后进入批复。</small>
                             </td>
                         </tr>
                         <% } %>
@@ -158,9 +158,11 @@
                         </tbody>
                     </table>
                     <% } %>
-                    <% if (inspection.flow_version === 1 && inspection.rectificationPower && inspection.flowCurrentAudit && inspection.flowCurrentAudit.status === auditConst.status.rectification) { %>
-                    <% const rectificationAudit = inspection.flowCurrentAudit; %>
-                    <table class="table table-bordered safe-inspection-rectification flow-rectification" data-id="<%= rectificationAudit.id %>">
+                    <% if (inspection.flow_version === 1 && ([auditConst.stage.rectification, auditConst.stage.reply].includes(inspection.flow_stage) || inspection.rectification_uid || inspection.rectification_item || inspection.rectification_date || (inspection.flowRectificationReturns && inspection.flowRectificationReturns.length))) { %>
+                    <% const rectificationAudit = inspection.flowCurrentAudit;
+                       const rectification = inspection.flowRectification;
+                       const canEditRectification = inspection.rectificationPower && inspection.flow_stage === auditConst.stage.rectification && rectificationAudit && rectificationAudit.status === auditConst.status.rectification; %>
+                    <table class="table table-bordered safe-inspection-rectification<%= canEditRectification ? ' flow-rectification' : '' %>"<% if (canEditRectification) { %> data-id="<%= rectificationAudit.id %>"<% } %>>
                         <colgroup><col class="rectification-label"><col></colgroup>
                         <thead>
                         <tr><th colspan="2" class="text-center">整改单</th></tr>
@@ -169,16 +171,32 @@
                         <tr>
                             <th scope="row" class="align-middle">整改情况</th>
                             <td>
-                                <textarea class="form-control form-control-sm flow-rectification-item" rows="2" maxlength="1000" aria-label="整改情况"><%= rectificationAudit.rectification_item || '' %></textarea>
+                                <% if (canEditRectification) { %>
+                                <textarea class="form-control form-control-sm flow-rectification-item" rows="2" maxlength="1000" aria-label="整改情况"><%= rectification.rectification_item %></textarea>
                                 <textarea class="flow-rectification-opinion" hidden><%= rectificationAudit.opinion || '' %></textarea>
+                                <% } else { %>
+                                <div class="rectification-content"><%= rectification.rectification_item || '—' %></div>
+                                <% } %>
                             </td>
                         </tr>
                         <tr>
                             <th scope="row" class="align-middle">整改日期</th>
-                            <td><input class="form-control form-control-sm flow-rectification-date" type="date" aria-label="整改日期" value="<%= rectificationAudit.rectification_date ? moment(rectificationAudit.rectification_date).format('YYYY-MM-DD') : '' %>"></td>
+                            <td>
+                                <% if (canEditRectification) { %>
+                                <input class="form-control form-control-sm flow-rectification-date" type="date" aria-label="整改日期" value="<%= rectification.rectification_date %>">
+                                <% } else { %>
+                                <%= rectification.rectification_date || '—' %>
+                                <% } %>
+                            </td>
                         </tr>
                         </tbody>
                     </table>
+                    <% if (canEditRectification) { %>
+                    <div class="safe-inspection-rectification-save small text-right mb-3" role="status" aria-live="polite">
+                        <span class="flow-save-status text-muted">已保存</span>
+                        <button type="button" class="btn btn-link btn-sm p-0 ml-2 d-none flow-save-retry">重试</button>
+                    </div>
+                    <% } %>
                     <% } %>
                     <table class="table table-bordered mt-3">
                         <thead>

+ 17 - 7
app/view/safe/inspection_information_modal.ejs

@@ -255,25 +255,35 @@
             <div class="modal-header"><h5 class="modal-title" id="safe-flow-title">确认操作</h5></div>
             <div class="modal-body">
                 <p id="safe-flow-message"></p>
-                <% if (inspection.shenpiPower && inspection.flow_stage === 1 && inspection.flowCurrentAudit) { %>
+                <% if (inspection.shenpiPower && [1, 3].includes(inspection.flow_stage) && inspection.flowCurrentAudit) { %>
                 <div class="alert alert-warning d-none" id="safe-flow-return-group">
                     <div class="form-check form-check-inline">
                         <input class="form-check-input" type="radio" name="safe-flow-return-type" id="safe-flow-return-origin" value="5">
-                        <label class="form-check-label" for="safe-flow-return-origin">退回原报</label>
+                        <label class="form-check-label" for="safe-flow-return-origin">退回原报<% if (inspection.flow_stage === 3) { %>:<%= inspection.user.name %><% } %></label>
                     </div>
                     <% if (inspection.flowCanReturnPrevious) {
-                        const report = inspection.flowStages.find(phase => phase.stage === 1).auditors;
-                        const earlier = report.filter(audit => audit.audit_order < inspection.flowCurrentAudit.audit_order);
-                        const previousOrder = Math.max(...earlier.map(audit => audit.audit_order));
-                        const previousNames = report.filter(audit => audit.audit_order === previousOrder).map(audit => audit.name).join('、');
+                        let previousNames;
+                        let previousLabel = '退回上一步骤';
+                        if (inspection.flow_stage === 3) {
+                            previousNames = inspection.flowReplyReturnTarget.name;
+                            previousLabel = inspection.flowReplyReturnTarget.flow_stage === 2 ? '退回上一整改人' : '退回上一审批人';
+                        } else {
+                            const report = inspection.flowStages.find(phase => phase.stage === 1).auditors;
+                            const earlier = report.filter(audit => audit.audit_order < inspection.flowCurrentAudit.audit_order);
+                            const previousOrder = Math.max(...earlier.map(audit => audit.audit_order));
+                            previousNames = report.filter(audit => audit.audit_order === previousOrder).map(audit => audit.name).join('、');
+                        }
                     %>
                     <div class="form-check form-check-inline">
                         <input class="form-check-input" type="radio" name="safe-flow-return-type" id="safe-flow-return-previous" value="6">
-                        <label class="form-check-label" for="safe-flow-return-previous">退回上一步骤:<%= previousNames %></label>
+                        <label class="form-check-label" for="safe-flow-return-previous"><%= previousLabel %>:<%= previousNames %></label>
                     </div>
                     <% } %>
                 </div>
                 <% } %>
+                <% if (inspection.flowRectificationReturnTarget) { %>
+                <div class="alert alert-warning d-none" id="safe-flow-rectification-return">退回上一审批人:<%= inspection.flowRectificationReturnTarget.name %></div>
+                <% } %>
                 <div id="safe-flow-opinion-group"><label for="safe-flow-opinion">处理意见</label><textarea id="safe-flow-opinion" class="form-control" rows="3" maxlength="1000"></textarea></div>
             </div>
             <div class="modal-footer">

+ 3 - 3
app/view/safe/inspection_modal.ejs

@@ -24,7 +24,7 @@
     </div>
 </div>
 <!--附件-->
-<div class="modal fade" id="file" data-backdrop="static">
+<div class="modal fade safe-inspection-attachments" id="file" data-backdrop="static">
     <div class="modal-dialog" role="document">
         <div class="modal-content">
             <div class="modal-header">
@@ -33,8 +33,8 @@
             <div class="modal-body">
                 <div class="modal-height-500" style="overflow: auto;">
                     <table class="table table-sm table-bordered">
-                        <thead>
-                        <tr><th>文件名</th><th>上传人</th><th>上传时间</th><th>操作</th></tr>
+                        <thead class="text-center">
+                        <tr><th width="45%">文件名</th><th width="15%">上传人</th><th width="25%">上传时间</th><th width="15%">操作</th></tr>
                         </thead>
                         <tbody id="file-content"></tbody>
                     </table>

+ 20 - 2
app/view/safe/sub_memu_list.ejs

@@ -1,8 +1,26 @@
 <nav-menu title="返回" url="/sp/<%- ctx.subProject.id %>/safe<%- ctx.url.indexOf('/information') > -1 ? ('/tender/' + ctx.tender.id + '/inspection') : (ctx.url.includes('/inspection') ? '/inspection' : '') %>" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
 <% if (!ctx.url.includes('/inspection')) { %>
 <% } else { %>
+<%
+const menuCounts = typeof filter !== 'undefined' && filter.count && filter.status ? {
+    rectification: filter.count[filter.status.rectification],
+    reply: filter.count[filter.status.reply],
+} : (typeof inspectionMenuCounts !== 'undefined' && inspectionMenuCounts ? inspectionMenuCounts : {});
+%>
 <nav-menu title="巡检概况" url="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection/overview" ml="1" active="<%= ctx.url.indexOf('/inspection/overview') >= 0 ? 1 : -1 %>" icon="fa-pie-chart"></nav-menu>
 <nav-menu title="安全巡检" url="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection" ml="3" active="<%= ctx.url.includes('/inspection') && !ctx.url.includes('/overview') && !ctx.url.includes('/rectification') && !ctx.url.includes('/approval') ? 1 : -1 %>" ></nav-menu>
-<nav-menu title="巡检整改" url="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection/rectification" ml="3" active="<%= ctx.url.indexOf('/inspection/rectification') >= 0 ? 1 : -1 %>" ></nav-menu>
-<nav-menu title="巡检批复" url="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection/approval" ml="3" active="<%= ctx.url.indexOf('/inspection/approval') >= 0 ? 1 : -1 %>" ></nav-menu>
+<div class="nav-box safe-inspection-menu">
+    <ul class="nav-list list-unstyled">
+        <li class="<%= ctx.url.includes('/inspection/rectification') ? 'active' : '' %>">
+            <a href="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection/rectification"><span class="ml-3">巡检整改</span><span class="badge badge-warning safe-inspection-menu-count"><%= menuCounts.rectification === undefined || menuCounts.rectification === null ? '—' : menuCounts.rectification %></span></a>
+        </li>
+    </ul>
+</div>
+<div class="nav-box safe-inspection-menu">
+    <ul class="nav-list list-unstyled">
+        <li class="<%= ctx.url.includes('/inspection/approval') ? 'active' : '' %>">
+            <a href="/sp/<%- ctx.subProject.id %>/safe/tender/<%= ctx.tender.id %>/inspection/approval"><span class="ml-3">巡检批复</span><span class="badge badge-warning safe-inspection-menu-count"><%= menuCounts.reply === undefined || menuCounts.reply === null ? '—' : menuCounts.reply %></span></a>
+        </li>
+    </ul>
+</div>
 <% } %>