Bläddra i källkod

Merge branch 'dev' of http://192.168.1.41:3000/maixinrong/Calculation into dev

Tony Kang 2 veckor sedan
förälder
incheckning
9e7cd365d4
43 ändrade filer med 3842 tillägg och 134 borttagningar
  1. 29 20
      app/controller/file_controller.js
  2. 14 2
      app/lib/bills_pos_convert.js
  3. 1 1
      app/lib/gcl_gather.js
  4. 2 0
      app/lib/rm/change.js
  5. 1 1
      app/public/js/change_information.js
  6. 10 6
      app/public/js/change_revise.js
  7. 33 7
      app/public/js/div_resizer.js
  8. 81 67
      app/public/js/file_detail.js
  9. 958 0
      app/public/js/quality_info.js
  10. 64 0
      app/public/js/quality_lab.js
  11. 610 0
      app/public/js/quality_rule.js
  12. 58 0
      app/public/js/quality_tender.js
  13. 153 0
      app/public/js/shares/tender_permission.js
  14. 5 5
      app/service/file.js
  15. 1 0
      app/service/filing.js
  16. 1 1
      app/service/filing_template.js
  17. 1 1
      app/service/jpc_report.js
  18. 522 0
      app/service/quality.js
  19. 92 0
      app/service/quality_file.js
  20. 71 0
      app/service/quality_gongxu.js
  21. 182 0
      app/service/quality_rule.js
  22. 72 0
      app/service/quality_yinbi.js
  23. 8 8
      app/service/stage_pay.js
  24. 2 2
      app/service/stage_stash.js
  25. 4 2
      app/service/tender.js
  26. 14 5
      app/service/tender_info.js
  27. 162 0
      app/service/tender_permission.js
  28. 4 4
      app/view/financial/pay_stage_modal.ejs
  29. 13 0
      app/view/quality/flaw.ejs
  30. 0 0
      app/view/quality/flaw_modal.ejs
  31. 106 0
      app/view/quality/info.ejs
  32. 143 0
      app/view/quality/info_modal.ejs
  33. 13 0
      app/view/quality/lab.ejs
  34. 79 0
      app/view/quality/rule.ejs
  35. 102 0
      app/view/quality/rule_modal.ejs
  36. 14 0
      app/view/quality/sub_memu.ejs
  37. 4 0
      app/view/quality/sub_memu_list.ejs
  38. 16 0
      app/view/quality/sub_mini_menu.ejs
  39. 34 0
      app/view/quality/tender.ejs
  40. 1 0
      app/view/quality/tender_modal.ejs
  41. 68 0
      app/view/shares/tender_permission_modal.ejs
  42. 2 2
      app/view/stage/bwtz.ejs
  43. 92 0
      config/web.js

+ 29 - 20
app/controller/file_controller.js

@@ -309,23 +309,21 @@ module.exports = app => {
                 const accountInfo = await ctx.service.projectAccount.getDataById(ctx.session.sessionUser.accountId);
                 const userPermission = accountInfo !== undefined && accountInfo.permission !== ''
                     ? JSON.parse(accountInfo.permission) : null;
-                const tenderList = await ctx.service.tender.getList('', userPermission, ctx.session.sessionUser.is_admin);
-
-                const rela_tender = await ctx.subProject.rela_tender.split(',');
-                const result = tenderList.filter(x => { return rela_tender.indexOf(x.id + '') >= 0});
-                for (const r of result) {
+                const tenders = await ctx.service.tender.getList('', userPermission, ctx.session.sessionUser.is_admin);
+                for (const r of tenders) {
                     r.advance = await ctx.service.advance.getAllDataByCondition({ columns: ['id', 'order', 'type'], where: { tid: r.id }});
                     r.advance.forEach(a => {
                         const type = advanceConst.typeCol.find(x => { return x.type === a.type });
                         if (type) a.type_str = type.name;
                     });
                     r.stage = await ctx.service.stage.getAllDataByCondition({ columns: ['id', 'order'], where: { tid: r.id, status: auditConst.stage.status.checked } });
-                    r.change = await ctx.service.change.getAllDataByCondition({ columns: ['cid', 'code'], where: { tid: r.id, status: auditConst.flow.status.checked } });
+                    r.change = await ctx.service.change.getAllDataByCondition({ columns: ['cid', 'code'], where: { tid: r.id, status: auditConst.flow.status.checked }, orders: [['in_time', 'asc']] });
                     r.change_apply = await ctx.service.changeApply.getAllDataByCondition({ columns: ['id', 'code'], where: { tid: r.id, status: auditConst.flow.status.checked } });
                     r.change_plan = await ctx.service.changePlan.getAllDataByCondition({ columns: ['id', 'code'], where: { tid: r.id, status: auditConst.flow.status.checked } });
                     r.change_project = await ctx.service.changeProject.getAllDataByCondition({ columns: ['id', 'code'], where: { tid: r.id, status: auditConst.flow.status.checked } });
                 }
-                ctx.body = {err: 0, msg: '', data: result };
+                const category = await this.ctx.service.category.getAllCategory(ctx.subProject);
+                ctx.body = {err: 0, msg: '', data: { category, tenders, selfCategoryLevel: this.ctx.subProject.permission.self_category_level} };
             } catch (error) {
                 ctx.helper.log(error);
                 ctx.body = this.ajaxErrorBody(error, '加载标段信息失败');
@@ -340,16 +338,10 @@ module.exports = app => {
             const stage = await this.ctx.service.stage.getDataById(data.stage);
             switch (data.sub_type) {
                 case 'att':
-                    return await this.ctx.service.stageAtt.getAllDataByCondition({ where: { tid: data.tender_id, sid: stage.order }, order: [['id', 'desc']]});
+                    return await this.ctx.service.stageAtt.getAllDataByCondition({ where: { tid: data.tender_id, sid: stage.order }, orders: [['id', 'desc']]});
                 case 'dealPay':
-                    const dpFiles = [];
-                    await this.ctx.service.stage.doCheckStage(stage, this.ctx.session.sessionUser.is_admin);
-                    const stagePays = await this.ctx.service.stagePay.getAllDataByCondition({ where: { sid: stage.id, stimes: stage.curTimes, sorder: stage.curOrder } });
-                    stagePays.forEach(x => {
-                        x.attachment = x.attachment ? JSON.parse(x.attachment) : [];
-                        if (x.attachment.length > 0) imFiles.push(...x.attachment);
-                    });
-                    return dpFiles;
+                    const payAtt = await this.ctx.service.payAtt.getAllDataByCondition({ where: { sid: stage.id}, orders: [['id', 'desc']] });
+                    return payAtt;
                 case 'stageIm':
                     const imFiles = [];
                     const stageIm = await this.ctx.service.stageDetailAtt.getAllDataByCondition({ where: { sid: stage.id} });
@@ -378,17 +370,35 @@ module.exports = app => {
         }
         async _loadChangePlanAtt(data) {
             if (!data.selectId) throw '参数错误';
-            const result = await this.ctx.service.changeAtt.getAllDataByCondition({ where: { cpid: data.selectId }, order: [['id', 'desc']]});
+            const self = this;
+            const result = await this.ctx.service.changePlanAtt.getAllDataByCondition({ where: { cpid: data.selectId }, order: [['id', 'desc']]});
+            result.forEach(x => {
+                const info = path.parse(x.filename);
+                x.filename = info.name;
+                x.filesize = self.ctx.helper.sizeToBytes(x.filesize);
+            });
             return result;
         }
         async _loadChangeProjectAtt(data) {
             if (!data.selectId) throw '参数错误';
-            const result = await this.ctx.service.changeAtt.getAllDataByCondition({ where: { cpid: data.selectId }, order: [['id', 'desc']]});
+            const self = this;
+            const result = await this.ctx.service.changeProjectAtt.getAllDataByCondition({ where: { cpid: data.selectId }, order: [['id', 'desc']]});
+            result.forEach(x => {
+                const info = path.parse(x.filename);
+                x.filename = info.name;
+                x.filesize = self.ctx.helper.sizeToBytes(x.filesize);
+            });
             return result;
         }
         async _loadChangeApplyAtt(data) {
             if (!data.selectId) throw '参数错误';
-            const result = await this.ctx.service.changeAtt.getAllDataByCondition({ where: { caid: data.selectId }, order: [['id', 'desc']]});
+            const self = this;
+            const result = await this.ctx.service.changeApplyAtt.getAllDataByCondition({ where: { caid: data.selectId }, order: [['id', 'desc']]});
+            result.forEach(x => {
+                const info = path.parse(x.filename);
+                x.filename = info.name;
+                x.filesize = self.ctx.helper.sizeToBytes(x.filesize);
+            });
             return result;
         }
         async loadRelaFiles(ctx) {
@@ -498,7 +508,6 @@ module.exports = app => {
             try {
                 const id = ctx.query.id;
                 await ctx.service.filingTemplateList.delete(id);
-                console.log(ctx.request.headers.referer, id);
                 if (ctx.request.headers.referer.indexOf(id) > 0) {
                     ctx.redirect('/file/template');
                 } else {

+ 14 - 2
app/lib/bills_pos_convert.js

@@ -102,6 +102,17 @@ class BillsPosConvert {
             }
         }
     }
+    _calcPreContractTp(node, data) {
+        const info = this.ctx.tender.info;
+        const org_price = node.org_price || node.unit_price;
+        if (info.calc_type === 'tp') {
+            const activeQty = this.ctx.helper.add(data.quantity, data.pre_qc_minus_qty);
+            const orgTotalPrice = this.ctx.helper.mul(data.quantity, org_price, info.decimal.tp);
+            return this.ctx.helper.mul(this.ctx.helper.div(data.pre_contract_qty, activeQty), orgTotalPrice, info.decimal.tp);
+        } else if (info.calc_type === 'up') {
+            return this.ctx.helper.mul(org_price, data.pre_contract_qty, info.decimal.tp);
+        }
+    }
     // v2
     _loadPosCalcFields(node, data) {
         const tpDecimal = this.ctx.tender.info.decimal.tp;
@@ -116,8 +127,9 @@ class BillsPosConvert {
         node.qc_tp = this.ctx.helper.add(node.qc_tp, this.ctx.helper.mul(node.unit_price, data.qc_qty, tpDecimal));
         node.gather_qty = this.ctx.helper.add(node.gather_qty, data.gather_qty);
 
+        data.pre_contract_tp = this._calcPreContractTp(node, data);
         node.pre_contract_qty = this.ctx.helper.add(node.pre_contract_qty, data.pre_contract_qty);
-        node.pre_contract_tp = this.ctx.helper.add(node.pre_contract_tp, this.ctx.helper.mul(org_price, data.pre_contract_qty, tpDecimal));
+        node.pre_contract_tp = this.ctx.helper.add(node.pre_contract_tp, data.pre_contract_tp);
         node.pre_qc_qty = this.ctx.helper.add(node.pre_qc_qty, data.pre_qc_qty);
         node.pre_qc_tp = this.ctx.helper.add(node.pre_qc_tp, this.ctx.helper.mul(org_price, data.pre_qc_qty, tpDecimal));
         node.pre_gather_qty = this.ctx.helper.add(node.pre_gather_qty, data.pre_gather_qty);
@@ -279,7 +291,7 @@ class BillsPosConvert {
 
         const priceDiff = child.org_price ? this.ctx.helper.sub(child.unit_price, child.org_price) : 0;
         if (priceDiff && (child.pre_contract_qty || child.pre_qc_qty)) {
-            child.contract_pc_tp = this.ctx.helper.sub(this.ctx.helper.mul(child.unit_price, child.pre_contract_qty, this.ctx.tender.info.decimal.tp), child.pre_contract_tp);
+            if (this.ctx.tender.info.calc_type === 'up') child.contract_pc_tp = this.ctx.helper.sub(this.ctx.helper.mul(child.unit_price, child.pre_contract_qty, this.ctx.tender.info.decimal.tp), child.pre_contract_tp);
             child.qc_pc_tp = this.ctx.helper.sub(this.ctx.helper.mul(child.unit_price, child.pre_qc_qty, this.ctx.tender.info.decimal.tp), child.pre_qc_tp);
             child.pc_tp = this.ctx.helper.add(child.contract_pc_tp, child.qc_pc_tp);
             child.gather_tp = this.ctx.helper.add(child.gather_tp, child.pc_tp);

+ 1 - 1
app/lib/gcl_gather.js

@@ -29,7 +29,7 @@ const gclGatherModel = class {
             order: 'order',
             level: 'level',
             rootId: -1,
-            keys: ['id', 'tender_id', 'ledger_id']
+            keys: ['id', 'tender_id', 'ledger_id'],
         });
         this.pos = new Ledger.pos({
             id: 'id', ledgerId: 'lid', order: 'order'

+ 2 - 0
app/lib/rm/change.js

@@ -166,6 +166,8 @@ class rptMemChange extends RptMemBase {
                 return [this.ctx.tender.data];
             case 'mem_tender_info':
                 return [this.ctx.tender.info];
+            case 'mem_deal_bills':
+                return this.ctx.service.dealBills.getAllDataByCondition({ where: { tender_id: this.ctx.tender.id }, orders: [['order', 'asc']] });
             case 'mem_change_ledger_bills':
                 return this.getChangeLedgerBillsData(fields);
             case 'mem_change_ledger_bills_filter':

+ 1 - 1
app/public/js/change_information.js

@@ -6393,7 +6393,7 @@ function makePushBwmx(clinfo, listinfo, removeList, updateList) {
                         oneUpdate[key] = leafInfo[key];
                         clinfo[key] = leafInfo[key];
                         needUpdate = true;
-                    } else if (leafInfo[key] === undefined && leafInfo.jldy !== clinfo[key]) {
+                    } else if (leafInfo[key] === undefined && leafInfo.jldy !== (clinfo[key] || null)) {
                         oneUpdate[key] = leafInfo.jldy;
                         clinfo[key] = leafInfo.jldy;
                         needUpdate = true;

+ 10 - 6
app/public/js/change_revise.js

@@ -205,6 +205,7 @@ $(document).ready(() => {
     if (thousandth) sjsSettingObj.setTpThousandthFormat(billsSpreadSetting);
     sjsSettingObj.setNodeTypeCol(billsSpreadSetting.cols, [{field: 'node_type'}]);
     SpreadJsObj.initSpreadSettingEvents(billsSpreadSetting, billsCol);
+    if (billsSpreadSetting.frozenColCount) billsSpreadSetting.frozenColCount = billsSpreadSetting.frozenColCount + 1;
     SpreadJsObj.initSheet(billsSheet, billsSpreadSetting);
     const posSpread = SpreadJsObj.createNewSpread($('#pos-spread')[0]);
     const posSheet = posSpread.getActiveSheet();
@@ -935,14 +936,16 @@ $(document).ready(() => {
                         if (cInfo.camount === 0 && cInfo.camount_expr === '') {
                             if (changeOrder && _.findIndex(oldChangeList, {id: cInfo.id}) !== -1) {
                                 toastr.warning('插入台账清单功能下无法从这移除已勾选清单');
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
                                 return
                             }
                             if (_.find(changeUsedData, {cbid: cInfo.id})) {
                                 toastr.warning('清单计量单元已被使用,无法取消勾选');
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
                                 return
                             } else if (checkIsSettle(cInfo)) {
                                 toastr.warning('清单计量单元已结算,无法取消勾选');
-                                info.sheet.setValue(info.row, info.col, 1);
+                                SpreadJsObj.reLoadRowData(info.sheet, info.row);
                                 return
                             }
                             postData('/tender/' + window.location.pathname.split('/')[2] + '/change/' + window.location.pathname.split('/')[4] + '/information/save', {type: 'del-change-list', ids: [cInfo.id] }, function (result) {
@@ -1561,7 +1564,6 @@ $(document).ready(() => {
                         return
                     } else if (checkIsSettle(cInfo)) {
                         toastr.warning('清单计量单元已结算,无法取消勾选');
-                        info.sheet.setValue(info.row, info.col, 1);
                         return
                     }
                 }
@@ -2850,14 +2852,16 @@ $(document).ready(() => {
                     if (cInfo.camount === 0 && cInfo.camount_expr === '') {
                         if (changeOrder && _.findIndex(oldChangeList, {id: cInfo.id}) !== -1) {
                             toastr.warning('插入台账清单功能下无法从这移除已勾选清单');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return
                         }
                         if (_.find(changeUsedData, {cbid: cInfo.id})) {
                             toastr.warning('清单计量单元已被使用,无法取消勾选');
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return
                         } else if (checkIsSettle(cInfo)) {
                             toastr.warning('清单计量单元已结算,无法取消勾选');
-                            info.sheet.setValue(info.row, info.col, 1);
+                            SpreadJsObj.reLoadRowData(info.sheet, info.row);
                             return
                         }
                         postData('/tender/' + window.location.pathname.split('/')[2] + '/change/' + window.location.pathname.split('/')[4] + '/information/save', {type: 'del-change-list', ids: [cInfo.id] }, function (result) {
@@ -3098,16 +3102,16 @@ $(document).ready(() => {
                 for (const cInfo of camountDatas) {
                     if (changeOrder && _.findIndex(oldChangeList, {id: cInfo.id}) !== -1) {
                         toastr.warning('插入台账清单功能下无法从这移除已勾选清单');
-                        info.sheet.setValue(info.row, info.col, 1);
+                        posSpreadObj.loadCurPosData();
                         return
                     }
                     if (_.find(changeUsedData, {cbid: cInfo.id})) {
                         toastr.warning('该计量单元已被使用,无法取消变更');
-                        info.sheet.setValue(info.row, info.col, 1);
+                        posSpreadObj.loadCurPosData();
                         return
                     } else if (checkIsSettle(cInfo)) {
                         toastr.warning('该计量单元已结算,无法取消变更');
-                        info.sheet.setValue(info.row, info.col, 1);
+                        posSpreadObj.loadCurPosData();
                         return
                     }
                 }

+ 33 - 7
app/public/js/div_resizer.js

@@ -36,6 +36,24 @@
         //     obj.css('width', '1%').css('height', '100%').css('resize', 'horizontal').css('cursor', 'w-resize').css('float', 'left');
         // }
         // 根据localStorage初始化
+
+        const refreshDivSize = function() {
+            const rType = obj.attr('r-type'), aType = obj.attr('a-type');
+            const div1 = $(obj.attr('div1')), div2 = $(obj.attr('div2'));
+            const parent = div1.parent();
+            if (aType !== 'percent') {
+                orgSize1 = div1[rType]();
+                orgSize2 = div2[rType]();
+                const parentSize = parent[rType]() * 0.99;
+                const size1 = Math.floor(orgSize1 / (orgSize1 + orgSize2) * parentSize);
+                const size2 = Math.floor(parentSize - size1);
+
+                div1[rType](size1);
+                div2[rType](size2);
+            }
+            if (setting.callback) { setting.callback(); }
+        };
+
         if (obj.attr('store-id')) {
             const rType = obj.attr('r-type'), version = obj.attr('store-version') ? ('-'+obj.attr('store-version')) : '' ;
             const objSize1 = getLocalCache('v-resize-1-' + obj.attr('store-id') + version);
@@ -46,6 +64,7 @@
             if (objSize2) {
                 $(obj.attr('div2')).css(rType, objSize2);
             }
+            refreshDivSize();
             if (setting.callback) { setting.callback(); }
         }
 
@@ -73,12 +92,12 @@
                 if(Math.abs(mouseMoveCount) >= 5){
                     if (aType === 'percent') {
                         const min = obj.attr('min') ? obj.attr('min') : 10;
-                        const max = 100 - min;
+                        const max = 99 - min;
 
                         const percent1 = Math.min(Math.max((orgSize1 + moveSize) / (orgSize1 + orgSize2) * 100, min), max);
-                        $(obj.attr('div1')).css(rType, percent1 + '%');
+                        $(obj.attr('div1')).css(rType, percent1 + '%').attr('dr-' + rType, percent1+'%');
                         const percent2 = Math.min(Math.max((orgSize2 - moveSize) / (orgSize1 + orgSize2) * 100, min), max);
-                        $(obj.attr('div2')).css(rType, percent2 + '%');
+                        $(obj.attr('div2')).css(rType, percent2 + '%').attr('dr-' + rType, percent2+'%');
                     } else {
                         const min = obj.attr('min') ? obj.attr('min') : parseInt(((orgSize1 + orgSize2) / 10).toFixed(0));
                         const max = orgSize1 + orgSize2 - min;
@@ -91,7 +110,7 @@
                         $(obj.attr('div2'))[rType](newSize2);
                     }
 
-                    if(setting.callback) { setting.callback(); }
+                    if(setting.callback) setting.callback();
                     mouseMoveCount = 0;
                 }
             }
@@ -99,12 +118,19 @@
         $('body').mouseup(function () {
             if (drag) {
                 drag = false;
-                const rType = obj.attr('r-type');
+                const rType = obj.attr('r-type'), aType = obj.attr('a-type');
                 const localId = obj.attr('store-id'), version = obj.attr('store-version') ? ('-'+obj.attr('store-version')) : '' ;
                 const div1 = $(obj.attr('div1')), div2 = $(obj.attr('div2'));
-                setLocalCache('v-resize-1-' + localId + version, div1[rType]());
-                setLocalCache('v-resize-2-' + localId + version, div2[rType]());
+                if (aType === 'percent') {
+                    setLocalCache('v-resize-1-' + localId + version, div1.attr('dr-' + rType));
+                    setLocalCache('v-resize-2-' + localId + version, div2.attr('dr-' + rType));
+                } else {
+                    setLocalCache('v-resize-1-' + localId + version, div1[rType]());
+                    setLocalCache('v-resize-2-' + localId + version, div2[rType]());
+                }
             }
         });
+
+        return { refreshDivSize };
     }
 })(jQuery);

+ 81 - 67
app/public/js/file_detail.js

@@ -340,9 +340,9 @@ $(document).ready(function() {
             postData('file/rela', { filing_id: this.curFiling.id, files: files }, async function(data) {
                 filingObj.curFiling.source_node.files.unshift(...data.files);
                 filingObj.updateFilingFileCount(filingObj.curFiling, data.filing.file_count);
+                filingObj.refreshPages();
                 filingObj.refreshFilesTable();
                 filingObj.refreshFileCountHint();
-                filingObj.refreshPages();
                 if (callback) callback();
             });
         }
@@ -990,31 +990,40 @@ $(document).ready(function() {
         clearFileSelect() {
             if (!this.tenderTree) return;
 
-            const nodes = this.tenderTree.getNodes();
-            nodes.forEach(node => {
-                const x = node.source_node;
-                x.selectFiles = [];
-                if (x.att) x.att.forEach(la => { la.checked = false });
-                if (x.advance) {
-                    x.advance.forEach(a => {
-                        if (a.files) a.files.forEach(aa => { aa.checked = false });
-                    });
-                }
-                if (x.stage) {
-                    x.stage.forEach(s => {
-                        if (s.att) s.att.forEach(sa => { sa.checked = false });
-                    })
-                }
-            });
+            const tenderTree = this.tenderTree;
+            const recursiveFun = function(nodes) {
+                if (!nodes || nodes.length === 0) return;
+
+                nodes.forEach(node => {
+                    const x = node.source_node;
+                    if (x.tid) {
+                        x.selectFiles = [];
+                        if (x.att) x.att.forEach(la => { la.checked = false });
+                        if (x.advance) {
+                            x.advance.forEach(a => {
+                                if (a.files) a.files.forEach(aa => { aa.checked = false });
+                            });
+                        }
+                        if (x.stage) {
+                            x.stage.forEach(s => {
+                                if (s.att) s.att.forEach(sa => { sa.checked = false });
+                            })
+                        }
+                    } else {
+                        recursiveFun(tenderTree.getNodesByParam('tree_pid', node.id, node));
+                    }
+                });
+            };
+            recursiveFun(this.tenderTree.getNodes());
         }
         refreshSelectHint(){
-            if (this.curTender) {
+            if (this.curTender && this.curTender.source_node.tid) {
                 $('#cur-tender-hint').html(`当前标段,已选${this.curTender.source_node.selectFiles.length}文件`);
             } else {
                 $('#cur-tender-hint').html('');
             }
-            const nodes = this.tenderTree.getNodes();
-            const selectTenders = nodes.filter(x => { return x.source_node.selectFiles.length > 0; });
+            const nodes = this.tenderTree.transformToArray(this.tenderTree.getNodes());
+            const selectTenders = nodes.filter(x => { return x.tid && x.source_node.selectFiles.length > 0; });
             if (selectTenders.length > 0) {
                 const count = selectTenders.reduce((rst, x) => { return rst + x.source_node.selectFiles.length; }, 0);
                 $('#rela-file-hint').html(`已选择${selectTenders.length}个标段,共${count}个文件`);
@@ -1038,11 +1047,17 @@ $(document).ready(function() {
         async showRelaFile(){
             $('#rela-filing-hint').html(`当前目录:${filingObj.getCurFilingFullPath()}`);
             if (!this.tenderTree) {
-                const tenders = await postDataAsync('file/rela/tender', {});
-                const sortNodes = tenders.map(x => {
-                    return { id: x.id, tree_pid: -1, name: x.name, source_node: x };
+                const data = await postDataAsync('file/rela/tender', {});
+                const tenderPathTree = Tender2Tree.convert(data.category, data.tenders, null, null, function(node, source) {
+                    _.assign(node, source);
+                }, data.selfCategoryLevel);
+                const sortNodes = tenderPathTree.nodes.map(x => {
+                    const data = { id: x.tmt_id, tree_pid: x.tmt_pid, name: x.name, source_node: x };
+                    if (x.tid) data.icon = '/public/css/ztree/img/diy/11.png';
+                    return data;
                 });
                 this.tenderTree = this.filingTree = $.fn.zTree.init($('#rela-tender'), this.treeSetting, sortNodes);
+                this.tenderTree.expandAll(true);
             }
             this.clearFileSelect();
             this.refreshSelectHint();
@@ -1082,7 +1097,7 @@ $(document).ready(function() {
         }
         refreshTenderFileSubType() {
             const type = this.tenderFileType.find(x => { return x.value === this.rfType.type});
-            if (type.subType && type.subType.length > 0) {
+            if (type && type.subType && type.subType.length > 0) {
                 this.rfType.sub_type = type.subType[0].value;
                 const html= [];
                 for (const tfst of type.subType) {
@@ -1102,18 +1117,6 @@ $(document).ready(function() {
         }
         refreshSelects(tender) {
             this.tenderFileType = [];
-            this.tenderFileType.push({ value: 'ledger', text: '台账附件' });
-            if (tender.stage && tender.stage.length > 0) {
-                const stages = tender.stage.map(x => { return {value: x.id, text: `第${x.order}期`}; });
-                this.tenderFileType.push({
-                    value: 'stage', text: '计量期',
-                    subType: [
-                        { value: 'att', text: '计量附件', stage: JSON.parse(JSON.stringify(stages)) },
-                        { value: 'dealPay', text: '合同支付', stage: JSON.parse(JSON.stringify(stages)) },
-                        { value: 'stageIm', text: '中间计量', stage: JSON.parse(JSON.stringify(stages)) },
-                    ],
-                });
-            }
             if (tender.advance && tender.advance.length > 0) {
                 const advanceType = [];
                 tender.advance.forEach(x => {
@@ -1128,6 +1131,18 @@ $(document).ready(function() {
                     value: 'advance', text: '预付款', subType: advanceType
                 });
             }
+            if (tender.tid) this.tenderFileType.push({ value: 'ledger', text: '台账附件' });
+            if (tender.stage && tender.stage.length > 0) {
+                const stages = tender.stage.map(x => { return {value: x.id, text: `第${x.order}期`}; });
+                this.tenderFileType.push({
+                    value: 'stage', text: '计量期',
+                    subType: [
+                        { value: 'att', text: '计量附件', stage: JSON.parse(JSON.stringify(stages)) },
+                        { value: 'dealPay', text: '合同支付', stage: JSON.parse(JSON.stringify(stages)) },
+                        { value: 'stageIm', text: '中间计量', stage: JSON.parse(JSON.stringify(stages)) },
+                    ],
+                });
+            }
             if (tender.change && tender.change.length > 0) {
                 const selects = [];
                 tender.change.forEach(x => {
@@ -1138,25 +1153,25 @@ $(document).ready(function() {
             if (tender.change_plan && tender.change_plan.length > 0) {
                 const selects = [];
                 tender.change_plan.forEach(x => {
-                    selects.push({ value: x.cpid, text: x.code })
+                    selects.push({ value: x.id, text: x.code })
                 });
                 this.tenderFileType.push({ value: 'change_plan', text: '变更方案', select: selects });
             }
-            if (tender.change_project && tender.change_project.length > 0) {
-                const selects = [];
-                tender.change_project.forEach(x => {
-                    selects.push({ value: x.cid, text: x.code })
-                });
-                this.tenderFileType.push({ value: 'change_project', text: '变更立项', select: selects });
-            }
             if (tender.change_apply && tender.change_apply.length > 0) {
                 const selects = [];
                 tender.change_apply.forEach(x => {
-                    selects.push({ value: x.cid, text: x.code })
+                    selects.push({ value: x.id, text: x.code })
                 });
                 this.tenderFileType.push({ value: 'change_apply', text: '变更申请', select: selects });
             }
-            this.rfType = { type: this.tenderFileType[0].value };
+            if (tender.change_project && tender.change_project.length > 0) {
+                const selects = [];
+                tender.change_project.forEach(x => {
+                    selects.push({ value: x.id, text: x.code })
+                });
+                this.tenderFileType.push({ value: 'change_project', text: '变更立项', select: selects });
+            }
+            this.rfType = { type: tender.tid ? this.tenderFileType[0].value : '' };
             this.refreshTenderFileType();
             this.refreshTenderFileSubType();
             this.refreshTenderFileStage();
@@ -1214,7 +1229,7 @@ $(document).ready(function() {
             });
         }
         async _loadRelaFiles(rfType) {
-            return await postDataAsync('file/rela/files', { tender_id: this.curTender.id, ...rfType });
+            return await postDataAsync('file/rela/files', { tender_id: this.curTender.source_node.tid, ...rfType });
         }
         async _loadLedgerFile() {
             if (!this.curTender.source_node.att) this.curTender.source_node.att = await this._loadRelaFiles(this.rfType);
@@ -1259,7 +1274,7 @@ $(document).ready(function() {
         async _loadChangePlanFile() {
             const rfType = this.rfType;
             const change = this.curTender.source_node.change_plan.find(x => {
-                return x.id === rfType.selectId;
+                return x.id == rfType.selectId;
             });
             if (!change) {
                 this.curFiles = [];
@@ -1271,7 +1286,7 @@ $(document).ready(function() {
         async _loadChangeProjectFile() {
             const rfType = this.rfType;
             const change = this.curTender.source_node.change_project.find(x => {
-                return x.id === rfType.selectId;
+                return x.id == rfType.selectId;
             });
             if (!change) {
                 this.curFiles = [];
@@ -1283,7 +1298,7 @@ $(document).ready(function() {
         async _loadChangeApplyFile() {
             const rfType = this.rfType;
             const change = this.curTender.source_node.change_apply.find(x => {
-                return x.id === rfType.selectId;
+                return x.id == rfType.selectId;
             });
             if (!change) {
                 this.curFiles = [];
@@ -1301,6 +1316,7 @@ $(document).ready(function() {
                 case 'change_plan': await this._loadChangePlanFile(); break;
                 case 'change_project': await this._loadChangeProjectFile(); break;
                 case 'change_apply': await this._loadChangeApplyFile(); break;
+                default: this.curFiles = [];
             }
             this.initFilesId(this.curFiles);
         }
@@ -1313,9 +1329,9 @@ $(document).ready(function() {
         }
         getSelectRelaFile() {
             const data = [];
-            const nodes = this.tenderTree.getNodes();
+            const nodes = this.tenderTree.transformToArray(this.tenderTree.getNodes());
             nodes.forEach(node => {
-                if (node.source_node.selectFiles.length === 0) return;
+                if (!node.source_node.selectFiles || node.source_node.selectFiles.length === 0) return;
 
                 node.source_node.selectFiles.forEach(x => {
                     data.push({
@@ -1343,7 +1359,7 @@ $(document).ready(function() {
                 cols: [
                     { title: '名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 200, formatter: '@', cellType: 'tree'},
                     { title: '选择', colSpan: '1', rowSpan: '1', field: 'check', hAlign: 1, width: 45, cellType: 'checkbox' },
-                    { title: '归档单位', colSpan: '1', rowSpan: '1', field: 'file_company', hAlign: 0, width: 80, formatter: '@' },
+                    { title: '归档单位', colSpan: '1', rowSpan: '1', field: 'file_company', hAlign: 0, width: 80, formatter: '@', cellType: 'autoTip' },
                 ],
                 emptyRows: 0,
                 headRows: 1,
@@ -1656,6 +1672,18 @@ $(document).ready(function() {
         spread: '#permission-spread',
     });
 
+    $.divResizer({
+        select: '#file-right-spr',
+        callback: function() {
+            if (fileReference) fileReference.spread.refresh();
+        },
+    });
+    $.divResizer({
+        select: '#right-spr',
+        callback: function() {
+            if (fileReference) fileReference.spread.refresh();
+        },
+    });
     class FileSearch {
         constructor() {
             this.searchResult = [];
@@ -1734,12 +1762,6 @@ $(document).ready(function() {
         }
         initSearch() {
             const self = this;
-            $.divResizer({
-                select: '#right-spr',
-                callback: function() {
-                    if (fileReference) fileReference.spread.refresh();
-                },
-            });
             $('input', '#search').bind('keydown', function (e) {
                 if (e.keyCode == 13) self.search();
             });
@@ -1885,12 +1907,4 @@ $(document).ready(function() {
             }, 100);
         });
     })('a[name=showLevel]');
-
-
-    $.divResizer({
-        select: '#file-right-spr',
-        callback: function() {
-            if (fileReference) fileReference.spread.refresh();
-        },
-    });
 });

+ 958 - 0
app/public/js/quality_info.js

@@ -0,0 +1,958 @@
+$(document).ready(() => {
+    autoFlashHeight();
+    const xmjSpread = SpreadJsObj.createNewSpread($('#xmj-spread')[0]);
+    const xmjSheet = xmjSpread.getActiveSheet();
+    const xmjSpreadSetting = {
+        cols: [
+            { title: '工程编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 135, formatter: '@', cellType: 'tree' },
+            { title: '工程名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 195, formatter: '@' },
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [32],
+        headColWidth: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    SpreadJsObj.initSheet(xmjSheet, xmjSpreadSetting);
+    const xmjTree = createNewPathTree('gather', { id: 'ledger_id', pid: 'ledger_pid', order: 'order', level: 'level', rootId: -1 });
+
+    postData('load', { filter: 'xmj;pos1', spec: { loadStatus: 1 } }, function(result) {
+        result.xmj.forEach(x => { x.rela_type = 'xmj'; });
+        xmjTree.loadDatas(result.xmj);
+        const posIndex = {};
+        for (const p of result.pos1) {
+            if (!posIndex[p.lid]) posIndex[p.lid] = [];
+            posIndex[p.lid].push(p);
+        }
+        for (const pi in posIndex) {
+            const xmj = xmjTree.nodes.find(x => { return x.id === pi; });
+            if (!xmj || (xmj.children && xmj.children.length > 0)) continue;
+
+            const posRange = posIndex[pi];
+            posRange.sort((a, b) => { return a.order - b.order; });
+            for (const p of posRange) {
+                if (p.b_code) continue;
+                xmjTree.addNode({ id: p.id, code: p.code, name: p.name, rela_type: 'pos' }, xmj);
+            }
+        }
+        xmjTree.sortTreeNode(false);
+        SpreadJsObj.loadSheetData(xmjSheet, SpreadJsObj.DataType.Tree, xmjTree);
+    });
+
+    class BaseBlock {
+        constructor(qualityObj, objKey, blockType) {
+            this.qualityObj = qualityObj;
+            this.objKey = objKey;
+            this.obj = $(`#${objKey}`);
+            this.blockType = blockType;
+            this.saveUrl = 'save/' + this.blockType;
+        }
+        getFilterData() {
+            if (!this.node) return null;
+            const result = { rela_type: this.node.rela_type, rela_id: this.node.id };
+            if (this.node.quality) result.quality_id = this.node.quality.id;
+            return result;
+        }
+        getBwName(){
+            if (!this.node) return '';
+            const parents = xmjTree.getAllParents(this.node);
+            parents.shift();
+            parents.push(this.node);
+            return parents.map(x => { return x.name; }).join('/');
+        }
+        getFileHtml(file) {
+            if (!file) return '';
+
+            const html = [];
+            html.push('<tr>');
+            html.push(`<td class="text-center"><input fid="${file.id}" type="checkbox"></td>`);
+            const qaMark = file.spec_type === 'qa' ? '<span class="badge badge-pill badge-success p-1 mr-2">质</span>' : '';
+            html.push(`<td>${file.filename} ${qaMark}</td>`);
+            html.push(`<td class="text-center">${file.user_name}</td>`);
+            html.push(`<td class="text-center">${moment(file.create_time).format('YYYY-MM-DD')}</td>`);
+            // const editHtml = file.canEdit ? `<a href="javascript: void(0);" class="mr-1" name="edit-file" fid="${file.id}"><i class="fa fa-pencil fa-fw"></i></a>` : '';
+            const editHtml = '';
+            const viewHtml = file.viewpath ? `<a href="${file.viewpath}" class="mr-1" target="_blank"><i class="fa fa-eye fa-fw"></i></a>` : '';
+            const downHtml = `<a href="javascript: void(0);" onclick="AliOss.downloadFile('${file.filepath}', '${file.filename + file.fileext}')" class="mr-1"><i class="fa fa-download fa-fw"></i></a>`;
+            const delHtml = file.canEdit || canDelete
+                ? `<a href="javascript: void(0);" class="mr-1 text-danger" name="del-file" fid="${file.id}"><i class="fa fa-trash-o fa-fw"></i></a>`
+                : ''; //'<a href="javascript: void(0);" class="mr-1 text-muted"><i class="fa fa-trash-o fa-fw"></i></a>';
+            html.push(`<td class="text-center">${editHtml}${viewHtml}${downHtml}${delHtml}</td>`);
+            html.push('</tr>');
+            return html.join('');
+        }
+        getFilesTableHtml(files, key) {
+            if (!files) return '';
+
+            const html = [];
+            html.push('<table class="table table-bordered"><thead><tr class="text-center"><th width="60px">选择</th><th width="">名称</th><th width="150px">上传人</th><th width="150px">上传时间</th><th width="100px">操作</th></tr></thead>');
+            html.push(`<tbody id="${key}">`);
+            for (const f of files) {
+                html.push(this.getFileHtml(f));
+            }
+            html.push('<tbody>');
+            html.push('</table>');
+            return html.join('');
+        }
+        async uploadFiles(files, blockId = '', specType = ''){
+            const formData = new FormData();
+            formData.append('quality_id', this.node.quality.id);
+            formData.append('block_type', this.blockType);
+            formData.append('block_id', blockId);
+            formData.append('spec_type', specType);
+            let count = 0;
+            for (const file of files) {
+                if (file === undefined) {
+                    toastr.error('未选择上传文件。');
+                    return false;
+                }
+                if (file.size > 50 * 1024 * 1024) {
+                    toastr.error('上传文件大小超过50MB。');
+                    return false;
+                }
+                const fileext = '.' + file.name.toLowerCase().split('.').splice(-1)[0];
+                if (whiteList.indexOf(fileext) === -1) {
+                    toastr.error('仅支持office文档、图片、压缩包格式,请勿上传' + fileext + '格式文件。');
+                    return false;
+                }
+                formData.append('size', file.size);
+                formData.append('file[]', file);
+                count++;
+            }
+            if (count === 0) {
+                toastr.warning('没有可上传的文件');
+                return false;
+            }
+            return await postDataWithFileAsync('file/upload', formData);
+        }
+        delFiles(files, callback) {
+            postData('file/del', { del: files}, function(result) {
+                callback(result);
+            });
+        }
+        selectUploadFiles(setting){
+            if (!setting) return;
+
+            $('#upload-file').val('');
+            $('#add-file-ok').off('click');
+            $('#add-file').modal('show');
+            $('.spec-type-detail').hide();
+            if (setting.specType) {
+                $('.spec-type').show();
+                for (const st of setting.specType) {
+                    $(`.spec-type-${st}`).show();
+                }
+            } else {
+                $('.spec-type').hide();
+            }
+            $('#add-file-ok').on("click", function () {
+                const specType = setting.specType ? $('[name=specType]:checked').val() : '';
+                setting.select(document.getElementById('upload-file').files, specType);
+                $('#add-file').modal('hide');
+            });
+        }
+        getControlHtml() {
+            throw '请在子类中定义';
+        }
+        getContentHtml() {
+            throw '请在子类中定义';
+        }
+        showQuality() {
+            const html = [];
+            html.push('<div class="title-main d-flex justify-content-between sjs-bar">', this.getControlHtml(), '</div>');
+            html.push('<div class="sjs-quality scroll-y">', this.getContentHtml(), '</div>');
+            html.push('</div>');
+            this.obj.html(html.join(''));
+        }
+        showSum() {
+            this.obj.html('');
+        }
+        show(node) {
+            this.node = node;
+            this.refresh();
+        }
+        refresh(data){
+            if (data) this.node.quality[this.blockType] = data;
+            if (!this.node.children || this.node.children.length === 0) {
+                this.showQuality();
+            } else {
+                this.showSum();
+            }
+        }
+    }
+    class Kaigong extends BaseBlock {
+        constructor(objKey, qualityObj) {
+            super(qualityObj, objKey, 'kaigong');
+            this.filesTable = objKey + '_files';
+            this.yyFilesTable = objKey + '_yy_files';
+            this.fileCountKey = objKey + '_filecount';
+
+            const self = this;
+            $('#add-kaigong-ok').click(function() {
+                const data = self.getFilterData();
+                data.add = { name: $('[name=kaigong-name]').val(), date: $('[name=kaigong-date]').val() };
+                postData(self.saveUrl, data, function(result) {
+                    if (self.node.quality) {
+                        self.node.quality.kaigong = result.kaigong
+                    } else {
+                        self.node.quality = result;
+                    }
+                    self.showQuality();
+                    self.qualityObj.jiaogong.refresh(result.jiaogong);
+                    self.qualityObj.pingding.refresh(result.pingding);
+                    $('#add-kaigong').modal('hide');
+                    autoFlashHeight();
+                });
+            });
+            $('body').on('click', '#del-kaigong', function() {
+                const data = self.getFilterData();
+                data.del = data.quality_id;
+                postData(self.saveUrl, data, function(result) {
+                    delete self.node.quality.kaigong;
+                    self.showQuality();
+                    self.qualityObj.jiaogong.refresh(result.jiaogong);
+                    self.qualityObj.pingding.refresh(result.pingding);
+                    autoFlashHeight();
+                })
+            });
+
+            $('body').on('click', '[name=upload-kg]', function() {
+                self.selectUploadFiles({
+                    select: async function(files) {
+                        const result = await self.uploadFiles(files);
+                        self.node.quality.kaigong.files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.filesTable}`).append(self.getFileHtml(r));
+                        }
+                        self.refreshFileCountHtml();
+                        self.qualityObj.refreshFileCountHtml();
+                        autoFlashHeight();
+                    }
+                });
+            });
+            $('body').on('click', `#${this.filesTable} [name=del-file]`, function() {
+                const del = [this.getAttribute('fid')];
+                self.delFiles(del, function(result) {
+                    for (const r of result) {
+                        $(`input[fid=${r}]`).parent().parent().remove();
+                        const fi = self.node.quality.kaigong.files.findIndex(x => { return x.id === r; });
+                        if (fi >= 0) self.node.quality.kaigong.files.splice(fi, 1);
+                    }
+                    self.refreshFileCountHtml();
+                    self.qualityObj.refreshFileCountHtml();
+                    autoFlashHeight();
+                });
+            });
+        }
+        _getAddHtml() {
+            if (!permission.add) return '';
+            return !this.node.quality || !this.node.quality.kaigong ? '<a href="#add-kaigong" data-toggle="modal" data-target="#add-kaigong" class="btn btn-primary btn-sm" >新增开工</a>' : '';
+        }
+        getControlHtml() {
+            const html = [];
+            html.push('<div>', this._getAddHtml(), '</div>');
+            return html.join('');
+        }
+        _getCardHtml() {
+            if (!this.node.quality.kaigong) return '';
+
+            const html = [];
+            html.push('<div class="card mt-2">');
+            html.push('<div class="card-header d-flex justify-content-between">', this.getBwName(),
+                `<a href="javascript: void(0)" class="text-danger" id="del-kaigong"><i class="fa fa-trash-o fa-fw"></i></a>`, '</div>');
+            html.push(`<div class="card-body pt-2"> <div class="my-2"><table class="col-12"><tbody><tr><td width="33%">计划开工日期:${this.node.quality.kaigong.kaigong_date}</td><td width="33%">资料个数:<span id="${this.fileCountKey}">${this.node.quality.kaigong.files.length}</span></td></tr></tbody></table></div>`);
+            html.push('<hr/>');
+            html.push('<nav class="nav nav-tabs"><a class="nav-link nav-item active" data-toggle="tab" href="#kg_files">开工资料</a><a class="nav-link nav-item" data-toggle="tab" href="#yy_files" style="display: none;">引用试验资料</a>',
+                '<div class="ml-auto"><a href="javascript: void(0);" name="upload-kg" class="btn btn-outline-primary btn-sm">上传开工资料</a><a href="#" data-toggle="modal" data-target="#upload-kg" class="btn btn-outline-primary btn-sm ml-1" style="display: none;">引用试验资料</a></div>', '</nav>');
+            html.push('<div class="tab-content my-2">');
+            html.push('<div class="tab-pane fade show active" id="kg_files" role="tabpanel" aria-labelledby="home-tab">', this.getFilesTableHtml(this.node.quality.kaigong.files, this.filesTable), '</div>');
+            html.push('<div class="tab-pane fade show" id="yy_files" role="tabpanel" aria-labelledby="home-tab">', this.getFilesTableHtml(this.node.quality.kaigong.qa_files, this.yyFilesTable), '</div>');
+            html.push('</div>');
+            html.push('</div>');
+            return html.join('');
+        }
+        getContentHtml() {
+            const html = [];
+            if (this.node.quality && this.node.quality.kaigong) html.push(this._getCardHtml());
+            return html.join('');
+        }
+        refreshFileCountHtml() {
+            $(`#${this.fileCountKey}`).html(this.node.quality.kaigong.files.length);
+        }
+    }
+    class Gongxu extends BaseBlock {
+        constructor (objKey, qualityObj) {
+            super(qualityObj, objKey, 'gongxu');
+            this.fileTablesPre = objKey + 'Files';
+            this.fileCountPre = objKey + 'Filecount';
+            this.pingding = this.qualityObj.pingding;
+            const self = this;
+
+            $('#add-gongxu-ok').click(function() {
+                const name = $('[name=gongxu-name]').val();
+                if (!name) return;
+                if (name.length > 100) {
+                    $('[name=gongxu-name]').addClass('is-invalid');
+                    $('#gongxu-name-hint').show();
+                    return;
+                } else {
+                    $('[name=gongxu-name]').removeClass('is-invalid');
+                    $('#gongxu-name-hint').hide();
+                }
+
+                const data = self.getFilterData();
+                const gxid = $('#gongxu-id').val();
+                if (gxid) {
+                    data.update = { id: gxid, name };
+                } else {
+                    data.add = { name };
+                }
+                postData(self.saveUrl, data, function(result) {
+                    if (data.add) {
+                        if (self.node.quality) {
+                            self.node.quality.gongxu = result.gongxu
+                        } else {
+                            self.node.quality = result;
+                        }
+                        self.qualityObj.jiaogong.refresh(result.jiaogong);
+                        self.qualityObj.pingding.refresh(result.pingding);
+                        self.showQuality();
+                    } else {
+                        const gx = self.node.quality.gongxu.list.find(x => { return x.id === gxid; });
+                        gx.name = name;
+                        $('[name=gx-name]', `.gongxu-card[gxid=${gxid}]`).html(name);
+                    }
+                    $('#add-gongxu').modal('hide');
+                    autoFlashHeight();
+                });
+            });
+            $('body').on('click', '[name=add-gx]', function() {
+                $('[name=gongxu-name]').val('');
+                $('#gongxu-id').val('');
+                $('#add-gongxu').modal('show');
+            });
+            $('body').on('click', '[name=del-gx]', function() {
+                const gxid = this.getAttribute('gxid');
+                const data = self.getFilterData();
+                data.del = gxid;
+                postData(self.saveUrl, data, function(result) {
+                    const gxIndex = self.node.quality.gongxu.list.findIndex(x => { return x.id === gxid; });
+                    self.node.quality.gongxu.list.splice(gxIndex, 1);
+                    self.qualityObj.jiaogong.refresh(result.jiaogong);
+                    self.qualityObj.pingding.refresh(result.pingding);
+                    $(`.gongxu-card[gxid=${gxid}]`).remove();
+                    autoFlashHeight();
+                });
+            });
+            $('body').on('click', '[name=edit-gx]', function() {
+                const gxid = this.getAttribute('gxid');
+                const gx = self.node.quality.gongxu.list.find(x =>{ return x.id === gxid; });
+                $('[name=gongxu-name]').val(gx.name);
+                $('#gongxu-id').val(gx.id);
+                $('#add-gongxu').modal('show');
+            });
+
+            $('body').on('click', '[name=upload-gx]', function() {
+                const gxid = this.getAttribute('gxid');
+                self.selectUploadFiles({
+                    specType: ['qa'],
+                    select: async function(files, specType) {
+                        const gx = self.node.quality.gongxu.list.find(x => { return x.id === gxid });
+                        const result = await self.uploadFiles(files, gxid, specType);
+                        gx.files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.fileTablesPre}-${gx.id}`).append(self.getFileHtml(r));
+                        }
+                        self.refreshFileCountHtml(gx);
+                        if (specType === 'qa') {
+                            self.pingding.addQaFiles(result);
+                        }
+                        self.qualityObj.refreshFileCountHtml();
+                        autoFlashHeight();
+                    }
+                });
+            });
+            $('body').on('click', '.gongxu-card [name=del-file]', function(){
+                const gxid = $(this).closest('.gongxu-card').attr('gxid');
+                const gx = self.node.quality.gongxu.list.find(x => { return x.id === gxid; });
+                const del = [this.getAttribute('fid')];
+                self.delFiles(del, function(result) {
+                    const pdRemoveFiles = [];
+                    for (const r of result) {
+                        $(`input[fid=${r}]`).parent().parent().remove();
+                        const fi = gx.files.findIndex(x => { return x.id === r; });
+                        const file = gx.files[fi];
+                        if (file.spec_type === 'qa') pdRemoveFiles.push(file);
+                        if (fi >= 0) gx.files.splice(fi, 1);
+                    }
+                    if (pdRemoveFiles.length > 0) self.pingding.removeQaFiles(pdRemoveFiles);
+                    self.refreshFileCountHtml(gx);
+                    self.qualityObj.refreshFileCountHtml();
+                    autoFlashHeight();
+                });
+            });
+        }
+        _getAddHtml() {
+            return permission.add ? '<a href="javascript: void(0)" name="add-gx" class="btn btn-primary btn-sm" >新增工序</a>' : '';
+        }
+        getControlHtml() {
+            const html = [];
+            html.push('<div>', this._getAddHtml(), '</div>');
+            return html.join('');
+        }
+        _getCardHtml(gx) {
+            if (!gx) return '';
+            const html = [];
+            html.push(`<div class="card mt-2 gongxu-card" gxid="${gx.id}">`);
+            html.push('<div class="card-header d-flex justify-content-between">');
+            const editHtml = gx.canEdit ? `<a href="javascript: void(0);" class="ml-1" name="edit-gx" gxid="${gx.id}"><i class="fa fa-pencil fa-fw"></i></a>` : '';
+            const delHtml = gx.canEdit ? `<a href="javascript: void(0);" class="ml-1 text-danger" name="del-gx" gxid="${gx.id}"><i class="fa fa-trash-o fa-fw"></i></a>` : '';
+            html.push(`<span style="width: 40%"><span name="gx-name">${gx.name}</span>${editHtml}${delHtml}<a href="javascript: void(0)" class="btn btn-sm btn-light text-primary" gxid="${gx.id}" name="upload-gx">上传文件</a></span>`);
+            html.push(`<div class="" style="width:30%">资料个数:<span id="${this.fileCountPre}-${gx.id}">${gx.files.length}</span></div>`);
+            html.push('<div style="width:30%"></div></div>');
+
+            html.push('<div class="card-body pt-2">', this.getFilesTableHtml(gx.files, this.fileTablesPre + '-' + gx.id),'</div>');
+            html.push('</div>');
+            return html.join('');
+        }
+        getContentHtml() {
+            const html = [];
+            if (this.node.quality && this.node.quality.gongxu) {
+                for (const gx of this.node.quality.gongxu.list) {
+                    html.push(this._getCardHtml(gx));
+                }
+            }
+            return html.join('');
+        }
+        refreshFileCountHtml(gx) {
+            $(`#${this.fileCountPre}-${gx.id}`).html(gx.files.length);
+        }
+    }
+    class Pingding extends BaseBlock {
+        constructor(objKey, qualityObj) {
+            super(qualityObj, objKey, 'pingding');
+            this.filesTable = objKey + '_files';
+            this.sourceFilesTable = objKey + '_source_files';
+
+            const self = this;
+            $('body').on('click', '[name=upload-pd]', function() {
+                self.selectUploadFiles({
+                    select: async function(files) {
+                        const result = await self.uploadFiles(files);
+                        self.node.quality.pingding.files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.filesTable}`).append(self.getFileHtml(r));
+                        }
+                        autoFlashHeight();
+                        self.qualityObj.refreshFileCountHtml();
+                    }
+                });
+            });
+            $('body').on('click', `#${this.filesTable} [name=del-file]`, function() {
+                const del = [this.getAttribute('fid')];
+                self.delFiles(del, function(result) {
+                    for (const r of result) {
+                        $(`input[fid=${r}]`).parent().parent().remove();
+                        const fi = self.node.quality.pingding.files.findIndex(x => { return x.id === r; });
+                        if (fi >= 0) self.node.quality.pingding.files.splice(fi, 1);
+                    }
+                    self.qualityObj.refreshFileCountHtml();
+                    autoFlashHeight();
+                });
+            });
+            $('body').on('click', '[name=upload-pd-source]', function() {
+                self.selectUploadFiles({
+                    select: async function(files) {
+                        const result = await self.uploadFiles(files, '', 'add');
+                        self.node.quality.pingding.files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.sourceFilesTable}`).append(self.getFileHtml(r));
+                        }
+                        self.qualityObj.refreshFileCountHtml();
+                        autoFlashHeight();
+                    }
+                });
+            });
+            $('body').on('click', `#${this.sourceFilesTable} [name=del-file]`, function() {
+                const del = [this.getAttribute('fid')];
+                self.delFiles(del, function(result) {
+                    for (const r of result) {
+                        $(`input[fid=${r}]`).parent().parent().remove();
+                        const fi = self.node.quality.pingding.source_files.findIndex(x => { return x.id === r; });
+                        if (fi >= 0) self.node.quality.pingding.source_files.splice(fi, 1);
+                    }
+                    self.qualityObj.refreshFileCountHtml();
+                    autoFlashHeight();
+                });
+            });
+        }
+        getControlHtml() {
+            const html = [];
+            return html.join('');
+        }
+        _getCardHtml() {
+            if (!this.node.quality.pingding) return '';
+
+            const html = [];
+            html.push('<div class="card mt-2">');
+            html.push('<div class="card-header d-flex justify-content-between">', this.getBwName(), '<div><a href="javascript: void(0)" class="btn btn-sm btn-light text-primary" name="upload-pd">上传文件</a></div>', '</div>');
+            html.push('<div class="card-body pt-2">');
+            html.push('<div class="mt-2">', this.getFilesTableHtml(this.node.quality.pingding.files, this.filesTable), '</div>');
+            html.push('<nav class="nav nav-tabs mt-4"><a class="nav-link nav-item active" data-toggle="tab" href="#pd-source">评定资料来源</a>',
+                '<div class="ml-auto"><a href="javascript: void(0)" name="upload-pd-source" class="btn btn-outline-primary btn-sm">补充资料</a></div></nav>');
+            html.push('<div class="tab-content my-2">');
+            html.push('<div class="tab-pane fade show active" id="pd_source" role="tabpanel" aria-labelledby="home-tab">', this.getFilesTableHtml(this.node.quality.pingding.source_files, this.sourceFilesTable), '</div>');
+            html.push('</div>');
+            html.push('</div>');
+            html.push('</div>');
+            return html.join('');
+        }
+        getContentHtml() {
+            const html = [];
+            if (this.node.quality && this.node.quality.pingding) html.push(this._getCardHtml());
+            return html.join('');
+        }
+        addQaFiles(files) {
+            for (const f of files) {
+                const qaF = JSON.parse(JSON.stringify(f));
+                qaF.canEdit = false;
+                this.node.quality.pingding.files.push(qaF);
+                $(`#${this.sourceFilesTable}`).append(this.getFileHtml(qaF));
+            }
+        }
+        removeQaFiles(files) {
+            for (const f of files) {
+                const qaFi = this.node.quality.pingding.source_files.findIndex(x => { return x.id === f.id; });
+                $(`input[fid=${f.id}]`, this.sourceFilesTable).parent().parent().remove();
+                this.node.quality.pingding.source_files.splice(qaFi, 1);
+            }
+        }
+    }
+    class Jiaogong extends BaseBlock {
+        constructor(objKey, qualityObj) {
+            super(qualityObj, objKey, 'jiaogong');
+            this.filesTable = objKey + '_files';
+
+            const self = this;
+            $('body').on('click', '[name=upload-jg]', function() {
+                self.selectUploadFiles({
+                    select: async function(files) {
+                        const result = await self.uploadFiles(files);
+                        self.node.quality.jiaogong.files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.filesTable}`).append(self.getFileHtml(r));
+                        }
+                        self.qualityObj.refreshFileCountHtml();
+                        autoFlashHeight();
+                    }
+                });
+            });
+            $('body').on('click', `#${this.filesTable} [name=del-file]`, function() {
+                const del = [this.getAttribute('fid')];
+                self.delFiles(del, function(result) {
+                    for (const r of result) {
+                        $(`input[fid=${r}]`).parent().parent().remove();
+                        const fi = self.node.quality.jiaogong.files.findIndex(x => { return x.id === r; });
+                        if (fi >= 0) self.node.quality.jiaogong.files.splice(fi, 1);
+                    }
+                    self.qualityObj.refreshFileCountHtml();
+                    autoFlashHeight();
+                });
+            });
+        }
+        getControlHtml() {
+            const html = [];
+            return html.join('');
+        }
+        _getCardHtml() {
+            if (!this.node.quality.jiaogong) return '';
+
+            const html = [];
+            html.push('<div class="card mt-2">');
+            html.push('<div class="card-header d-flex justify-content-between">', this.getBwName(), '</div>');
+            html.push('<div class="card-body pt-2">');
+            html.push('<div class="card">', '<div class="card-header d-flex justify-content-between">', '交工工序', '<div><a href="javascript: void(0)" class="btn btn-sm btn-light text-primary" name="upload-jg">上传文件</a></div>', '</div>');
+            html.push('<div class="card-body pt-2">', this.getFilesTableHtml(this.node.quality.jiaogong.files, this.filesTable), '</div>');
+            html.push('</div>');
+            html.push('</div>');
+            return html.join('');
+        }
+        getContentHtml() {
+            const html = [];
+            if (this.node.quality && this.node.quality.jiaogong) html.push(this._getCardHtml());
+            return html.join('');
+        }
+    }
+    class Yinbi extends BaseBlock {
+        constructor (objKey, qualityObj) {
+            super(qualityObj, objKey, 'yinbi');
+            this.beforeFilesPre = objKey + 'BeforeFiles';
+            this.afterFilesPre = objKey + 'AfterFiles';
+            const self = this;
+
+            $('#add-yinbi-ok').click(function() {
+                const name = $('[name=yinbi-name]').val();
+                if (!name) {
+                    toastr.warning('请输入名称');
+                    return;
+                }
+                if (name.length > 100) {
+                    toastr.warning('名称超过100,请缩短');
+                    return;
+                }
+                const gongxu_id = $('[name=yinbi-gongxu-id]').val();
+                const gcbw = $('[name=yinbi-gcbw]').val();
+                const content = $('[name=yinbi-content]').val();
+                if (gcbw.length > 1000) {
+                    toastr.warning('工程部位超过1000,请缩短');
+                    return;
+                }
+                if (content.length > 1000) {
+                    toastr.warning('隐蔽工程说明超过1000,请缩短');
+                    return;
+                }
+
+                const data = self.getFilterData();
+                const ybid = $('#yinbi-id').val();
+                if (ybid) {
+                    data.update = { id: ybid, name, gongxu_id, gcbw, content };
+                } else {
+                    data.add = { name, gongxu_id, gcbw, content };
+                }
+                postData(self.saveUrl, data, function(result) {
+                    if (data.add) {
+                        if (self.node.quality) {
+                            self.node.quality.yinbi = result.yinbi
+                        } else {
+                            self.node.quality = result;
+                        }
+                        self.showQuality();
+                    } else {
+                        const yb = self.node.quality.yinbi.list.find(x => { return x.id === ybid; });
+                        yb.name = name;
+                        yb.gongxu_id = gongxu_id;
+                        yb.gcbw = gcbw;
+                        yb.content = content;
+                        self.afterEdit(yb);
+                    }
+                    $('#add-yinbi').modal('hide');
+                    autoFlashHeight();
+                });
+            });
+            $('body').on('click', '[name=add-yb]', function() {
+                $('[name=yinbi-name]').val('');
+                $('[name=yinbi-gongxu-id]').html(self._getSelectGongxuHtml());
+                $('[name=yinbi-gongxu-id]').val('');
+                $('[name=yinbi-gcbw]').val('');
+                $('[name=yinbi-content]').val('');
+                $('#yinbi-id').val('');
+                $('#add-yinbi').modal('show');
+            });
+            $('body').on('click', '[name=del-yb]', function() {
+                const ybid = this.getAttribute('ybid');
+                const data = self.getFilterData();
+                data.del = ybid;
+                postData(self.saveUrl, data, function(result) {
+                    const ybIndex = self.node.quality.yinbi.list.findIndex(x => { return x.id === ybid; });
+                    self.node.quality.yinbi.list.splice(ybIndex, 1);
+                    $(`.yinbi-card[ybid=${ybid}]`).remove();
+                    autoFlashHeight();
+                });
+            });
+            $('body').on('click', '[name=edit-yb]', function() {
+                const ybid = this.getAttribute('ybid');
+                const yb = self.node.quality.yinbi.list.find(x =>{ return x.id === ybid; });
+                $('[name=yinbi-name]').val(yb.name);
+                $('[name=yinbi-gongxu-id]').html(self._getSelectGongxuHtml());
+                $('[name=yinbi-gongxu-id]').val(yb.gongxu_id);
+                $('[name=yinbi-gcbw]').val(yb.gcbw);
+                $('[name=yinbi-content]').val(yb.content);
+                $('#yinbi-id').val(yb.id);
+                $('#add-yinbi').modal('show');
+            });
+
+            $('body').on('click', '[name=upload-yb-before]', function() {
+                const ybid = $(this).closest('.yinbi-card').attr('ybid');
+                self.selectUploadFiles({
+                    select: async function(files, specType) {
+                        const yb = self.node.quality.yinbi.list.find(x => { return x.id === ybid });
+                        const result = await self.uploadFiles(files, ybid, 'before');
+                        yb.before_files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.beforeFilesPre}-${yb.id}`).append(self.getFileHtml(r));
+                        }
+                        self.qualityObj.refreshFileCountHtml();
+                        autoFlashHeight();
+                    }
+                });
+            });
+            $('body').on('click', '[name=upload-yb-after]', function() {
+                const ybid = $(this).closest('.yinbi-card').attr('ybid');
+                self.selectUploadFiles({
+                    select: async function(files, specType) {
+                        const yb = self.node.quality.yinbi.list.find(x => { return x.id === ybid });
+                        const result = await self.uploadFiles(files, ybid, 'after');
+                        yb.after_files.push(...result);
+                        for (const r of result) {
+                            $(`#${self.afterFilesPre}-${yb.id}`).append(self.getFileHtml(r));
+                        }
+                        self.qualityObj.refreshFileCountHtml();
+                        autoFlashHeight();
+                    }
+                });
+            });
+            $('body').on('click', `#${self.objKey} [name=del-file]`, function(){
+                const tableKey = $(this).closest('tbody').attr('id');
+                const ybid = $(this).closest('.yinbi-card').attr('ybid');
+                const yb = self.node.quality.yinbi.list.find(x => { return x.id === ybid; });
+                const del = [this.getAttribute('fid')];
+                self.delFiles(del, function(result) {
+                    for (const r of result) {
+                        $(`input[fid=${r}]`).parent().parent().remove();
+                        if (tableKey.indexOf(self.beforeFilesPre) === 0) {
+                            const fi = yb.before_files.findIndex(x => { return x.id === r; });
+                            if (fi >= 0) yb.before_files.splice(fi, 1);
+                        } else if (tableKey.indexOf(self.afterFilesPre) === 0) {
+                            const fi = yb.after_files.findIndex(x => { return x.id === r; });
+                            if (fi >= 0) yb.after_files.splice(fi, 1);
+                        }
+                    }
+                    self.qualityObj.refreshFileCountHtml();
+                    autoFlashHeight();
+                });
+            });
+        }
+        _getSelectGongxuHtml(){
+            const html = ['<option value=""></option>'];
+            this.node.quality.gongxu.list.forEach(x => { html.push(`<option value="${x.id}">${x.name}</option>`)});
+            return html.join('');
+        }
+        _getAddHtml() {
+            return permission.add ? '<a href="javascript: void(0)" name="add-yb" class="btn btn-primary btn-sm" >新增隐蔽工程</a>' : '';
+        }
+        getControlHtml() {
+            const html = [];
+            html.push('<div>', this._getAddHtml(), '</div>');
+            return html.join('');
+        }
+        _getCardHeaderHtml(yb){
+            const html = [];
+            const editHtml = yb.canEdit ? `<a href="javascript: void(0);" class="ml-1" name="edit-yb" ybid="${yb.id}"><i class="fa fa-pencil fa-fw"></i></a>` : '';
+            const delHtml = yb.canEdit ? `<a href="javascript: void(0);" class="ml-1 text-danger" name="del-yb" ybid="${yb.id}"><i class="fa fa-trash-o fa-fw"></i></a>` : '';
+            html.push(`<div class="d-inline-block col-6 pl-0"><span name="yb-name">${yb.name}</span>${editHtml}${delHtml}</div>`);
+            const gongxu = yb.gongxu_id ? this.node.quality.gongxu.list.find(x => { return x.id === yb.gongxu_id; }) : null;
+            html.push(`<div class="d-inline-block col-4">${gongxu ? gongxu.name : ''}</div>`);
+            html.push(`<div class="d-inline-block pull-right"><span class="mr-3">创建日期:${moment(yb.create_time).format('YYYY-MM-DD')}</span></div>`);
+            return html.join('');
+        }
+        _getCardHtml(yb) {
+            if (!yb) return '';
+            const html = [];
+            html.push(`<div class="card mt-2 yinbi-card" ybid="${yb.id}">`);
+            html.push('<div class="card-header d-flex justify-content-between yinbi-card-header">', this._getCardHeaderHtml(yb), '</div>');
+            html.push('<div class="card-body pt-2">');
+            html.push(`<div class="mb-2"><span class="yinbi-gcbw">工程部位及桩号:${yb.gcbw || ''}</span></div>`);
+            html.push(`<div class="mb-2"><span class="yinbi-content">隐蔽工程说明:${yb.content || ''}</span></div>`);
+            html.push('<div class="card mt-2">', '<div class="card-header d-flex justify-content-between">', '施工前:', '<div><a href="javascript: void(0)" class="btn btn-sm btn-light text-primary" name="upload-yb-before">上传文件</a></div>', '</div>');
+            html.push('<div class="card-body pt-2">', this.getFilesTableHtml(yb.before_files, this.beforeFilesPre + '-' + yb.id ), '</div>');
+            html.push('</div>');
+            html.push('<div class="card mt-2">', '<div class="card-header d-flex justify-content-between">', '施工后:', '<div><a href="javascript: void(0)" class="btn btn-sm btn-light text-primary" name="upload-yb-after">上传文件</a></div>', '</div>');
+            html.push('<div class="card-body pt-2">', this.getFilesTableHtml(yb.after_files, this.afterFilesPre + '-' + yb.id ), '</div>');
+            html.push('</div>');
+            html.push('</div>');
+            html.push('</div>');
+            return html.join('');
+        }
+        getContentHtml() {
+            const html = [];
+            if (this.node.quality && this.node.quality.yinbi) {
+                for (const yb of this.node.quality.yinbi.list) {
+                    html.push(this._getCardHtml(yb));
+                }
+            }
+            return html.join('');
+        }
+        afterEdit(yb) {
+           $(`.yinbi-card[ybid=${yb.id}] .yinbi-card-header`).html(this._getCardHeaderHtml(yb));
+            $(`.yinbi-card[ybid=${yb.id}] .yinbi-gcbw`).html(`工程部位及桩号:${yb.gcbw || ''}`);
+            $(`.yinbi-card[ybid=${yb.id}] .yinbi-content`).html(`隐蔽工程说明:${yb.content || ''}`);
+        }
+    }
+
+    class Quality {
+        constructor () {
+            this.kaigong = new Kaigong('kaigong', this);
+            this.pingding = new Pingding('pingding', this);
+            this.gongxu = new Gongxu('gongxu', this);
+            this.jiaogong = new Jiaogong('jiaogong', this);
+            this.yinbi = new Yinbi('yinbi', this);
+        }
+        getStatusText(type, status) {
+            const def = thirdParty[type].find(function (x) {
+                return x.value === status;
+            });
+            return def ? def.name : '';
+        }
+        getGxbyText(data) {
+            return this.getStatusText('gxby', data.gxby_status);
+        }
+        getDaglText(data) {
+            return this.getStatusText('dagl', data.dagl_status);
+        }
+        getFileCount() {
+            if (!this.node || !this.node.quality) return 0;
+
+            let result = this.node.quality.kaigong ? this.node.quality.kaigong.files.length : 0;
+            this.node.quality.gongxu.list.forEach(gx => { result = result + gx.files.length; });
+            if (this.node.quality.pingding) {
+                result = result + this.node.quality.pingding.files.length + this.node.quality.pingding.source_files.filter(x => {return x.spec_type !== 'qa'; }).length;
+            }
+            if (this.node.quality.jiaogong) result = result + this.node.quality.jiaogong.files.length;
+            return result;
+        }
+        getYinbiFileCount() {
+            if (!this.node || !this.node.quality) return 0;
+
+            let result = 0;
+            this.node.quality.yinbi.list.forEach(x => {
+                result = result + x.before_files.length + x.after_files.length;
+            });
+            return result;
+        }
+        refreshFileCountHtml() {
+            const fileCount = this.getFileCount();
+            const yinbiFileCount = this.getYinbiFileCount();
+            $('#file-count').html('合计:' + (fileCount + yinbiFileCount));
+            $('#file-count-wo-yinbi').html('不含隐蔽:' + (fileCount));
+            $('#file-count-yinbi').html('隐蔽工程:' + (yinbiFileCount));
+        }
+        refreshGaikuang() {
+            let gxbyText = this.getGxbyText(this.node);
+            if (this.node.gxby_date) gxbyText = gxbyText + `(${moment(this.node.gxby_date).format('YYYY-MM-DD')})`;
+            if (gxbyText) {
+                $('#gxby-info').html('完工:' + gxbyText).show();
+            } else {
+                $('#gxby-info').hide();
+            }
+            const daglText = this.getDaglText(this.node);
+            if (daglText) {
+                $('#dagl-info').html('资料:' + daglText).show();
+            } else {
+                $('#dagl-info').hide();
+            }
+            this.refreshFileCountHtml();
+        }
+        refreshQuality() {
+            this.refreshGaikuang();
+            this.kaigong.show(this.node);
+            this.gongxu.show(this.node);
+            this.pingding.show(this.node);
+            this.jiaogong.show(this.node);
+            this.yinbi.show(this.node);
+            autoFlashHeight();
+        }
+        async showQuality(node, force = 1) {
+            this.node = node;
+            this.isChild = !this.node.children || this.node.children.length === 0;
+            if (!this.isChild) {
+                $('#quality-detail').hide();
+                return;
+            } else {
+                $('#quality-detail').show();
+            }
+            if (!node.quality || force) {
+                node.quality = (await postDataAsync('load', { filter: 'detail', id: node.id })).detail;
+                if (node.quality) {
+                    node.gxby_status = node.quality.gxby_status;
+                    node.gxby_date = node.quality.gxby_date;
+                    node.dagl_status = node.quality.dagl_status;
+                }
+            }
+            this.refreshQuality();
+        }
+    }
+    const qualityObj = new Quality();
+
+    xmjSpread.bind(spreadNS.Events.SelectionChanged, function(e, info) {
+        if (!info.oldSelections || !info.oldSelections[0] || info.newSelections[0].row !== info.oldSelections[0].row) {
+            qualityObj.showQuality(SpreadJsObj.getSelectObject(info.sheet));
+        }
+    });
+    $('#reload-quality').click(function() {
+        qualityObj.showQuality(SpreadJsObj.getSelectObject(xmjSheet), true);
+    });
+    $.contextMenu({
+        selector: '#xmj-spread',
+        build: function ($trigger, e) {
+            const target = SpreadJsObj.safeRightClickSelection($trigger, e, xmjSpread);
+            return target.hitTestType === spreadNS.SheetArea.viewport || target.hitTestType === spreadNS.SheetArea.rowHeader;
+        },
+        items: {
+            pushInc: {
+                name: '推送状态',
+                callback: function (key, opt, menu, e) {
+                    const select = [];
+                    const node = SpreadJsObj.getSelectObject(xmjSheet);
+                    if (node.children && node.children.length > 0) {
+                        const posterity = xmjTree.getPosterity(node);
+                        for (const p of posterity) {
+                            if (!node.children || node.children.length === 0) select.push(node.id);
+                        }
+                    } else {
+                        select.push(node.id);
+                    }
+                    postData('push', {push_type: 'inc', select}, function(result) {
+                        if (!result) {
+                            toastr.warning('暂无状态推送');
+                        } else {
+                            toastr.success(`成功推送${result}条状态`);
+                        }
+                    })
+                },
+            },
+            pushAll: {
+                name: '推送全部状态',
+                callback: function (key, opt, menu, e) {
+                    postData('push', {push_type: 'all'}, function(result) {
+                        if (!result) {
+                            toastr.warning('暂无状态推送');
+                        } else {
+                            toastr.success('推送成功');
+                        }
+                    });
+                },
+            },
+        }
+    });
+
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            const tag = $(this).attr('tag');
+            setTimeout(() => {
+                showWaitingView();
+                const tree = sheet.zh_tree;
+                if (!tree) return;
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                    case "5":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', xmjSheet);
+    const xmjSearch = $.posSearch({selector: '#xmj-search', searchSpread: xmjSpread, hint: '请输入 编号/名称 查询', specClass: 'mt-1'});
+});

+ 64 - 0
app/public/js/quality_lab.js

@@ -0,0 +1,64 @@
+$(document).ready(() => {
+    autoFlashHeight();
+    const labSpread = SpreadJsObj.createNewSpread($('#lab-spread')[0]);
+    const labSheet = labSpread.getActiveSheet();
+    const labSpreadSetting = {
+        cols: [
+            { title: '分类编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 80, formatter: '@', cellType: 'tree' },
+            { title: '分类名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 185, formatter: '@' },
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [32],
+        headColWidth: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+    };
+    SpreadJsObj.initSheet(labSheet, labSpreadSetting);
+    const labTree = createNewPathTree('base', { id: 'tree_id', pid: 'tree_pid', order: 'order', level: 'level', rootId: -1 });
+
+    postData('load', { filter: 'lab' }, function(result) {
+        labTree.loadDatas(result.lab);
+        SpreadJsObj.loadSheetData(labSheet, SpreadJsObj.DataType.Tree, labTree);
+    });
+
+    const labSpreadObj = {
+        selectionChanged: function(e, info) {
+            if (!info.oldSelections || !info.oldSelections[0] || info.newSelections[0].row !== info.oldSelections[0].row) {
+                lab
+            }
+        }
+    };
+
+    labSpread.bind(spreadNS.Events.SelectionChanged, labSpreadObj.selectionChanged);
+
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            const tag = $(this).attr('tag');
+            setTimeout(() => {
+                showWaitingView();
+                const tree = sheet.zh_tree;
+                if (!tree) return;
+                switch (tag) {
+                    case "1":
+                    case "2":
+                    case "3":
+                    case "4":
+                    case "5":
+                        tree.expandByLevel(parseInt(tag));
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                    case "last":
+                        tree.expandByCustom(() => { return true; });
+                        SpreadJsObj.refreshTreeRowVisible(sheet);
+                        break;
+                }
+                closeWaitingView();
+            }, 100);
+        });
+    })('a[name=showLevel]', labSheet);
+    const labSearch = $.posSearch({selector: '#lab-search', searchSpread: labSpread, hint: '请输入 编号/名称 查询', specClass: 'mt-1', searchFields: ['name'] });
+});

+ 610 - 0
app/public/js/quality_rule.js

@@ -0,0 +1,610 @@
+'use strict';
+$(() => {
+    autoFlashHeight();
+
+    const StatusConst = (function(){
+        const result = [
+            { field: 'gxby_status', title: '工序报验', type: 'enum', range: thirdParty.gxby},
+            // { field: 'gxby_date', title: '完工日期', type: 'time'},
+            { field: 'dagl_status', title: '档案管理', type: 'enum', range: thirdParty.dagl},
+        ];
+        return result;
+    })();
+    const CheckConst = [
+        { key: 'kaigong', name: '开工', checks: [{ key: 'file_count', name: '资料个数', operate: [{ key: '=', name: '=' }, { key: '>', name: '>'}] }, { key: 'file_name', name: '资料名称', multi: 1, operate: [{ key: '=', name: '=' }, { key: 'has', name: '包含'}] }] },
+        { key: 'gongxu', name: '工序', checks: [{ key: 'gongxu_name', name: '工序名称',  multi: 1, operate: [{ key: '=', name: '=' }, { key: 'has', name: '包含'}] }, { key: 'file_count', name: '资料个数', operate: [{ key: '=', name: '=' }, { key: '>', name: '>'}] }, { key: 'file_name', name: '资料名称', multi: 1, operate: [{ key: '=', name: '=' }, { key: 'has', name: '包含'}] }] },
+        { key: 'jiaogong', name: '中间交工', checks: [{ key: 'file_count', name: '资料个数', operate: [{ key: '=', name: '=' }, { key: '>', name: '>'}] }, { key: 'file_name', name: '资料名称', multi: 1, operate: [{ key: '=', name: '=' }, { key: 'has', name: '包含'}] }] },
+        { key: 'pingding', name: '评定', checks: [{ key: 'file_count', name: '资料个数', operate: [{ key: '=', name: '=' }, { key: '>', name: '>'}] }, { key: 'file_name', name: '资料名称', multi: 1, operate: [{ key: '=', name: '=' }, { key: 'has', name: '包含'}] }] },
+        { key: 'all', name: '全部资料', checks: [{ key: 'file_count', name: '资料个数', operate: [{ key: '=', name: '=' }, { key: '>', name: '>'}] }, { key: 'file_name', name: '资料名称', multi: 1, operate: [{ key: '=', name: '=' }, { key: 'has', name: '包含'}]}] },
+    ];
+
+    const groupObj = (function(list){
+        const groups = list;
+        let curGroup;
+
+        const getGroups = function() {
+            return groups;
+        };
+        const getConditionHtml = function(condition) {
+            const html = [];
+            for (const check of condition) {
+                if (html.length > 0) html.push('</br>');
+                html.push(`[${check.blockName}]${check.fieldName} ${check.operateName} ${check.value}${check.multi ? '(多)' : ''}`);
+            }
+            return html.join('');
+        };
+        const getPushStatusHtml = function(pushStatus) {
+            const html = [];
+            for (const ps of pushStatus) {
+                if (html.length > 0) html.push('</br>');
+                const fieldConst = StatusConst.find(x => { return x.field === ps.field; });
+                if (!fieldConst) continue;
+
+                switch(ps.field) {
+                    case 'gxby_status':
+                    case 'dagl_status':
+                        const valueConst = fieldConst.range.find(x => { return x.value === ps.value; });
+                        if (valueConst) {
+                            html.push(`${ps.fieldName} = ${valueConst.name}`);
+                        } else {
+                            html.push(`${ps.fieldName} = ${ps.value}(该值状态已删除)`);
+                        }
+                        break;
+                    case 'gxby_date':
+                        html.push(`${ps.fieldName} = ${ps.alias}`);
+                        break;
+                }
+            }
+            return html.join('');
+        };
+        const getRuleHtml = function(rule) {
+            const html = [];
+            html.push('<td>', rule.name ,'</td>');
+            html.push(`<td>`, getConditionHtml(rule.condition), '</td>');
+            html.push(`<td>`, getPushStatusHtml(rule.push_status), '</td>');
+            html.push(`<td><a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="editRule"><i class="fa fa-pencil fa-fw"></i></a>
+                <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delRule"><i class="fa fa-trash-o fa-fw text-danger"></i></a></td>`);
+            return html.join('');
+        };
+        const loadGroupRules = function(group) {
+            const html = [];
+            for (const rule of group.rules) {
+                html.push(`<tr class="text-center" ruleId = "${rule.id}">`, getRuleHtml(rule), '</tr>');
+            }
+            $('#ruleOptions').html(html.join(''));
+        };
+        const setCurGroup = function(group) {
+            curGroup = group;
+            if (!group) return;
+            loadGroupRules(group);
+            $('dd[groupId]').removeClass('bg-warning');
+            $(`dd[groupId=${curGroup.group_id}]`).addClass('bg-warning');
+        };
+        const getCurGroup = function() {
+            return curGroup;
+        };
+        const getGroupCaptionHtml = function(group) {
+            return `<div class="d-flex justify-content-between align-items-center table-file" groupId="${group.group_id}"><div>${group.group_name}</div>` +
+                '    <div class="btn-group-table" style="display: none;">\n' +
+                '    <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameGroup"><i class="fa fa-pencil fa-fw"></i></a>\n' +
+                '    <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delGroup"><i class="fa fa-trash-o fa-fw text-danger"></i></a>\n' +
+                '</div></div>';
+        };
+        const getGroupHtml = function(group) {
+            const html = [];
+            html.push(`<dd class="list-group-item" groupId="${group.group_id}">`, getGroupCaptionHtml(group), '</dd>');
+            return html.join('');
+        };
+
+        const addGroup = function() {
+            postData(window.location.pathname + '/save', { group: {add: 1} }, function(result) {
+                groups.push(result.add);
+                $('#group-list').append(getGroupHtml(result.add));
+            });
+        };
+        const renameGroup = function(group_id, group_name) {
+            postData(window.location.pathname + '/save', { group: { update: { group_id, group_name } } }, function(result){
+                const group = groups.find(x => { return x.group_id === result.update.group_id; });
+                group.group_name = result.update.group_name;
+                group.rules.forEach(x => { x.group_name === group.group_name; });
+                $(`dd[groupId=${group.group_id}]`).html(getGroupCaptionHtml(group));
+            });
+        };
+        const delGroup = function(group_id){
+            postData(window.location.pathname + '/save', { group: {del: group_id} }, function(result) {
+                $(`dd[groupId=${result.del}]`).remove();
+                const groupIndex = groups.findIndex(x => { return x.group_id === group_id; });
+                groups.splice(groupIndex, 1);
+            });
+        };
+
+        const addRule = function(rule) {
+            const rules = rule instanceof Array ? rule : [rule];
+            const group = groups.find(x => { return x.group_id === rules[0].group_id; });
+            group.rules.push(...rules);
+            loadGroupRules(curGroup);
+        };
+        const updateRule = function(rule) {
+            const group = groups.find(x => { return x.group_id === rule.group_id; });
+            const orgRule = group.rules.find(x => { return x.id === rule.id; });
+            if (!orgRule) return;
+            _.assignIn(orgRule, rule);
+            loadGroupRules(curGroup);
+        };
+        const delRule = function (rule) {
+            const group = groups.find(x => { return x.group_id === rule.group_id; });
+            const index = group.rules.findIndex(x => { return x.id === rule.id; });
+            group.rules.splice(index, 1);
+            loadGroupRules(curGroup);
+        };
+
+        if (groups.length > 0) setCurGroup(groups[0]);
+        return {  getGroups, setCurGroup, getCurGroup, addGroup, delGroup, renameGroup, addRule, updateRule, delRule, getConditionHtml, getPushStatusHtml }
+    })(ruleGroups);
+
+    const ruleSaveObj = (function(){
+        let group_id, group_name;
+        const addCheckHtml = function(check) {
+            const html = [];
+            html.push('<tr>', `<td condition-info="${check.blockName}&^&${check.blockKey}&^&${check.fieldName}&^&${check.fieldKey}&^&${check.operateName}&^&${check.operateKey}&^&${check.value}&^&${check.multi ? 1 : 0}">[${check.blockName}]${check.fieldName} ${check.operateName} ${check.value}${check.multi ? '(多)' : ''}</td>`,
+                '<td><a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="condition-del"><i class="fa fa-trash-o fa-fw text-danger"></i></a></td>', '</tr>');
+            $('#condition-list').append(html.join(''));
+        };
+        const addStatusHtml = function(status) {
+            const html = [];
+            html.push('<tr>');
+            const selectStatus = StatusConst.find(x => { return x.field === status.field; });
+            const value = selectStatus.range.find(x => { return x.value === status.value; });
+            html.push(`<td status-info="${status.fieldName}&^&${status.field}&^&${status.value}"> ${status.fieldName} = ${value ? value.name : status.value}</td>`);
+            html.push('<td><a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="status-del"><i class="fa fa-trash-o fa-fw text-danger"></i></a></td>');
+            html.push('</tr>');
+            $('#status-list').append(html.join(''));
+        };
+        const getConditionBlockHtml = function(blocks) {
+            const html = [];
+            for (const d of blocks) {
+                html.push(`<option value="${d.key}">${d.name}</option>`);
+            }
+            $('#condition-block').html(html.join(''));
+            getConditionFieldHtml(blocks[0]);
+        };
+        const getConditionFieldHtml = function(block) {
+            const html = [];
+            for (const d of block.checks) {
+                html.push(`<option value="${d.key}">${d.name}</option>`);
+            }
+            $('#condition-field').html(html.join(''));
+            getConditionOperateHtml(block.checks[0]);
+            showMulti(block.checks[0]);
+        };
+        const getConditionOperateHtml = function(check) {
+            const html = [];
+            for (const d of check.operate) {
+                html.push(`<option value="${d.key}">${d.name}</option>`);
+            }
+            $('#condition-operate').html(html.join(''));
+        };
+        const showMulti = function (check) {
+            if (check.multi) {
+                $('#multi-set').show();
+            } else {
+                $('#multi-set').hide();
+                $('#condition-multi')[0].checked = false;
+            }
+        };
+        const getStatusFieldHtml = function(fields) {
+            const html = [];
+            for (const d of fields) {
+                html.push(`<option value="${d.field}">${d.title}</option>`);
+            }
+            $('#status-field').html(html.join(''));
+            getStatusValueHtml(fields[0]);
+        };
+        const getStatusValueHtml = function(status) {
+            const html = [];
+            for (const d of status.range) {
+                html.push(`<option value="${d.value}">${d.name}</option>`);
+            }
+            $('#status-value').html(html.join(''));
+        };
+        const initModal = function() {
+            getConditionBlockHtml(CheckConst);
+            getStatusFieldHtml(StatusConst);
+        };
+        initModal();
+
+        $('#condition-add').click(() => {
+            const blockValue = $('#condition-block').val();
+            const fieldValue = $('#condition-field').val();
+            const operateValue = $('#condition-operate').val();
+            const checkValue = $('#condition-value').val();
+
+            const block = CheckConst.find(x => { return x.key === blockValue; });
+            if (!block) {
+                toastr.warning('未知判断模块');
+                return;
+            }
+            const check = block.checks.find(x => { return x.key === fieldValue; });
+            if (!check) {
+                toastr.warning('未知判断条件');
+                return;
+            }
+            if (!checkValue) {
+                toastr.warning('请输入判断值');
+                return;
+            }
+            const operate = check.operate.find(x => { return x.key === operateValue; });
+            if (!operate) {
+                toastr.warning('未知判断符');
+                return;
+            }
+            const data = { blockKey: blockValue, blockName: block.name, fieldKey: fieldValue, fieldName: check.name, operateKey: operateValue, operateName: operate.name, value: checkValue };
+            if (check.type === 'enum') {
+                data.value = _.toInteger(data.value);
+                if (_.isNil(data.value) || data.value < 0) {
+                    toastr.warning('判断值仅限0、正整数');
+                    return;
+                }
+            }
+            if (check.multi) {
+                data.multi = $('#condition-multi')[0].checked;
+            }
+            addCheckHtml(data);
+        });
+        $('#status-add').click(() => {
+            const fieldValue = $('#status-field').val();
+            const statusValue = parseInt($('#status-value').val());
+
+            const field = StatusConst.find(x => { return x.field === fieldValue; });
+            const status = field.range.find(x => { return x.value === statusValue; });
+            if (!field || !status) {
+                toastr.warning('未知判断条件');
+                return;
+            }
+            const data = { fieldName: field.title, field: fieldValue, value: statusValue };
+            addStatusHtml(data);
+        });
+
+        $('body').on('click', 'a[name=condition-del]', function() {
+            $(this).parent().parent().remove();
+        });
+        $('body').on('click', 'a[name=status-del]', function() {
+            $(this).parent().parent().remove();
+        });
+        $('#condition-block').click(function() {
+            const value = this.value;
+            const block = CheckConst.find(x => { return x.key === value; });
+            getConditionFieldHtml(block);
+        });
+        $('#condition-field').click(function() {
+            const blockValue = $('#condition-block').val();
+            const block = CheckConst.find(x => { return x.key === blockValue; });
+            const fieldValue = this.value;
+            const check = block.checks.find(x => { return x.key === fieldValue; });
+            getConditionOperateHtml(check);
+            showMulti(check);
+        });
+        $('#status-field').click(function() {
+            const field = this.value;
+            const selectStatus = StatusConst.find(x => { return x.field === field; });
+            getStatusValueHtml(selectStatus);
+        });
+
+        const showRuleModal = function(rule, group) {
+            initModal();
+            group_id = rule ? rule.group_id : group.group_id;
+            group_name = rule ? rule.group_name : group.group_name;
+            $('#rule-id').val(rule ? rule.id : '');
+            $('#rule-name').val(rule ? rule.name : '');
+            $('#loc-list').html('');
+            $('#condition-list').html('');
+            $('#status-list').html('');
+            if (rule) {
+                for (const c of rule.condition) {
+                    addCheckHtml(c);
+                }
+                for (const s of rule.push_status) {
+                    addStatusHtml(s);
+                }
+            }
+            $('#save-rule').modal('show');
+        };
+
+        const getRuleData = function() {
+            const data = {};
+            data.name = $('#rule-name').val();
+            if (data.name.length > 20) {
+                toastr.warning('名称过长,请再精简');
+                return;
+            }
+            try {
+                data.condition = [];
+                const clist = $('td[condition-info]');
+                for (const c of clist) {
+                    const info = c.getAttribute('condition-info').split('&^&');
+                    data.condition.push({
+                        blockName: info[0],
+                        blockKey: info[1],
+                        fieldName: info[2],
+                        fieldKey: info[3],
+                        operateName: info[4],
+                        operateKey: info[5],
+                        value: info[6],
+                        multi: info[7] ? parseInt(info[7]) : 0,
+                    });
+                }
+            } catch(err) {
+                toastr.warning('判断条件错误');
+                return;
+            }
+            try {
+                data.push_status = [];
+                const slist = $('td[status-info]');
+                for (const s of slist) {
+                    const info = s.getAttribute('status-info').split('&^&');
+                    data.push_status.push({
+                        fieldName: info[0],
+                        field: info[1],
+                        value: parseInt(info[2]),
+                    });
+                }
+            } catch(err) {
+                toastr.warning('更新状态错误');
+                return;
+            }
+            const id = $('#rule-id').val();
+            if (id) {
+                data.id = id;
+            } else {
+                data.group_id = group_id;
+                data.group_name = group_name;
+            }
+            return data;
+        };
+
+        return { show: showRuleModal, data: getRuleData, }
+    })();
+    $('#add-rule').click(() => {
+        ruleSaveObj.show(null, groupObj.getCurGroup());
+    });
+    $('body').on('click', 'a[name=editRule]', function() {
+        const ruleId = $(this).parent().parent().attr('ruleId');
+        const curGroup = groupObj.getCurGroup();
+        if (!curGroup) return;
+        const rule = curGroup.rules.find(x => { return x.id == ruleId; });
+        ruleSaveObj.show(rule, curGroup);
+    });
+    $('body').on('click', 'a[name=delRule]', function() {
+        const ruleId = $(this).parent().parent().attr('ruleId');
+        const curGroup = groupObj.getCurGroup();
+        if (!curGroup) return;
+        const rule = curGroup.rules.find(x => { return x.id == ruleId; });
+        if (curGroup.rules.length === 1) {
+            toastr.warning('当前配置,仅剩最后一个规则,如需删除,请直接删除规则组');
+            return;
+        }
+
+        postData(window.location.pathname + '/save', { rule: { del: rule } }, function (result) {
+            if (result.del) groupObj.delRule(result.del);
+        });
+    });
+
+    $('#save-rule-ok').click(function() {
+        const loData = ruleSaveObj.data();
+        const updateData = {};
+        if (loData.id) {
+            updateData.update = loData;
+        } else {
+            updateData.add = loData;
+        }
+        postData(window.location.pathname + '/save', { rule: updateData }, function (result) {
+            if (result.add) groupObj.addRule(result.add);
+            if (result.update) groupObj.updateRule(result.update);
+            $('#save-rule').modal('hide');
+        })
+    });
+
+    $('body').on('click', '.table-file', function(e) {
+        if (this.getAttribute('renaming') === '1') return;
+        if (e.target.tagName === 'A' || e.target.tagName === 'I' || e.target.tagName === 'INPUT') return;
+        const groupId = this.getAttribute('groupId');
+        const group = ruleGroups.find(x => { return x.group_id === groupId; });
+        groupObj.setCurGroup(group);
+    });
+    $('body').on('mouseenter', ".table-file", function(){
+        $(this).children(".btn-group-table").css("display","block");
+    });
+    $('body').on('mouseleave', ".table-file", function(){
+        $(this).children(".btn-group-table").css("display","none");
+    });
+
+    $('body').on('click', 'a[name=renameGroup]', function(e){
+        $(this).parents('.table-file').attr('renaming', '1');
+        $(`#${this.getAttribute('aria-describedby')}`).remove();
+        const groupId = $(this).parents('.table-file').attr('groupId');
+        const group = ruleGroups.find(x => { return x.group_id === groupId; });
+        if (!group) return;
+
+        const html = [];
+        html.push(`<div><input type="text" class="form-control form-control-sm" style="width: 160px" value="${group.group_name}"/></div>`);
+        html.push('<div class="btn-group-table" style="display: none;">',
+            `<a href="javascript: void(0)" name="renameOk" class="mr-1"><i class="fa fa-check fa-fw"></i></a>`,
+            `<a href="javascript: void(0)" class="mr-1" name="renameCancel"><i class="fa fa-remove fa-fw text-danger"></i></a>`, '</div>');
+        $(`.table-file[groupId=${groupId}]`).html(html.join(''));
+        e.stopPropagation();
+    });
+    $('body').on('click', 'a[name=renameOk]', function(){
+        const groupId = $(this).parents('.table-file').attr('groupId');
+        const newName = $(this).parents('.table-file').find('input').val();
+        groupObj.renameGroup(groupId, newName);
+        $(this).parents('.table-file').attr('renaming', '0');
+    });
+    $('body').on('click', 'a[name=renameCancel]', function() {
+        $(this).parents('.table-file').attr('renaming', '0');
+        const groupId = $(this).parents('.table-file').attr('groupId');
+        const group = ruleGroups.find(x => { return x.group_id === groupId; });
+        if (!group) return;
+
+        const html = [];
+        html.push(`<div>${group.group_name}</div>`);
+        html.push('<div class="btn-group-table" style="display: none;">',
+            '<a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameGroup"><i class="fa fa-pencil fa-fw"></i></a>',
+            '<a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delGroup"><i class="fa fa-trash-o fa-fw text-danger"></i></a>',
+            '</div>');
+        $(`.table-file[groupId=${groupId}]`).html(html.join(''));
+    });
+    $('body').on('click', 'a[name=delGroup]', function(e){
+        e.stopPropagation();
+        const groupId = $(this).parents('.table-file').attr('groupId');
+        groupObj.delGroup(groupId);
+    });
+    $('#addGroup').click(function() {
+        groupObj.addGroup();
+    });
+
+    const loadCopyRuleList= function(groupId) {
+        const group = groupObj.getGroups().find(x => { return x.group_id === groupId; });
+        const ruleHtml = group.rules.map(rule => {
+            return `<tr class="text-center"><td><input type="checkbox" name="copy-rule-id" ruleId="${rule.id}"></td><td>${rule.name}</td><td>${groupObj.getConditionHtml(rule.condition)}</td><td>${groupObj.getPushStatusHtml(rule.push_status)}</td></tr>`
+        });
+        $('#copy-rule-list').html(ruleHtml);
+    };
+    $('#copy-rule').on('show.bs.modal', function() {
+        const groups = groupObj.getGroups();
+        const groupHtml = groups.map(x => { return `<option value="${x.group_id}">${x.group_name}</option>`});
+        $('#select-group').html(groupHtml.join(''));
+        loadCopyRuleList(groups[0].group_id);
+    });
+    $('#select-group').click(function() {
+        loadCopyRuleList(this.value);
+    });
+    $('#copy-rule-ok').click(function() {
+        const selects = $('input:checked', '#copy-rule');
+        if (selects.length === 0) {
+            toastr.warning('请选择要拷贝的规则');
+            return;
+        }
+        const copy = selects.map(function() { return this.getAttribute('ruleId'); }).get().join(',');
+        const updateData = { rule: { copy, group_id: groupObj.getCurGroup().group_id } };
+        postData(window.location.pathname + '/save', updateData, function (result) {
+            if (result.add) groupObj.addRule(result.add);
+            $('#copy-rule').modal('hide');
+        })
+    });
+
+    const getSelectGroup = function() {
+        const selectGroup = [{ value: '', text: '' }];
+        const groups = groupObj.getGroups();
+        for (const g of groups) {
+            selectGroup.push({ value: g.group_id, text: g.group_name });
+        }
+        return selectGroup;
+    };
+    const xmjSpread = SpreadJsObj.createNewSpread($('#xmj-spread')[0]);
+    const xmjSheet = xmjSpread.getActiveSheet();
+    $('[role=tab]').click(function() {
+        const items = getSelectGroup();
+        const groupComboCell = SpreadJsObj.CellType.getCustomizeComboCellType(items);
+        xmjTree.datas.forEach(x => {
+            if (!x) return;
+            const index = items.findIndex( i => { return i.value === x.group_id; } );
+            if (index < 0) x.group_id = '';
+        });
+        xmjSheet.getRange(-1, 2, -1, 1).cellType(groupComboCell);
+        SpreadJsObj.reloadColData(xmjSheet, 2);
+        if (xmjSpread.shown) return;
+        setTimeout(() => {
+            xmjSpread.refresh();
+            xmjSpread.shown = true;
+        }, 100);
+    });
+    const xmjSpreadSetting = {
+        cols: [
+            { title: '工程编号', colSpan: '1', rowSpan: '1', field: 'code', hAlign: 0, width: 240, formatter: '@', cellType: 'tree', readOnly: true, },
+            { title: '工程名称', colSpan: '1', rowSpan: '1', field: 'name', hAlign: 0, width: 350, formatter: '@', readOnly: true, },
+            { title: '规则组', colSpan: '1', rowSpan: '1', field: 'group_id', hAlign: 0, width: 200, formatter: '@', cellType: 'customizeCombo', comboItems: getSelectGroup(), },
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [32],
+        headColWidth: [32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+    };
+    sjsSettingObj.setFxTreeStyle(xmjSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    SpreadJsObj.initSheet(xmjSheet, xmjSpreadSetting);
+    const xmjTree = createNewPathTree('gather', { id: 'ledger_id', pid: 'ledger_pid', order: 'order', level: 'level', rootId: -1 });
+
+    let tenderId;
+    const loadTenderData = function(tid) {
+        tenderId = tid;
+        postData(`/quality/tender/${tid}/load`, {filter: 'xmj;pos1;quality'}, function(result) {
+            const items = getSelectGroup();
+            result.quality.forEach(q => {
+                const index = items.findIndex( i => { return i.value === q.group_id; } );
+                if (index < 0) return;
+                if (q.rela_type === 'xmj') {
+                    const x = result.xmj.find(x => { return x.id === q.rela_id; });
+                    if (x) x.group_id = q.group_id;
+                } else if (q.rela_type === 'pos') {
+                    const x = result.pos1.find(x => { return x.id === q.rela_id; });
+                    if (x) x.group_id = q.group_id;
+                }
+            });
+
+            xmjTree.loadDatas(result.xmj);
+            xmjTree.nodes.forEach(x => { x.rela_type = 'xmj'; });
+            const posIndex = {};
+            for (const p of result.pos1) {
+                if (!posIndex[p.lid]) posIndex[p.lid] = [];
+                posIndex[p.lid].push(p);
+            }
+            for (const pi in posIndex) {
+                const xmj = xmjTree.nodes.find(x => { return x.id === pi; });
+                if (!xmj || (xmj.children && xmj.children.length > 0)) continue;
+
+                const posRange = posIndex[pi];
+                posRange.sort((a, b) => { return a.order - b.order; });
+                for (const p of posRange) {
+                    if (p.b_code) continue;
+                    xmjTree.addNode({ id: p.id, tender_id: p.tid, code: p.code, name: p.name, group_id: p.group_id, rela_type: 'pos' }, xmj);
+                }
+            }
+            xmjTree.sortTreeNode(false);
+            SpreadJsObj.loadSheetData(xmjSheet, SpreadJsObj.DataType.Tree, xmjTree);
+        });
+    };
+    $('body').on('click', 'tr[tid]', function() {
+        if (!$(this).hasClass('table-active')) {
+            $('tr[tid].table-active').removeClass('table-active');
+            $(this).addClass('table-active');
+            if (!xmjSpread.shown) {
+                setTimeout(() => {
+                    loadTenderData($('.table-active').attr('tid'));
+                }, 1000);
+            } else {
+                loadTenderData($('.table-active').attr('tid'));
+            }
+        }
+    });
+
+    xmjSpread.bind(spreadNS.Events.EditEnded, function (e, info) {
+        if (!info.sheet.zh_setting) return;
+
+        const col = info.sheet.zh_setting.cols[info.col];
+        if (col.field !== 'group_id') return;
+
+        const node = SpreadJsObj.getSelectObject(info.sheet);
+        const updateData = { rela_type: node.rela_type, rela_id: node.id, group_id: info.editingText || '' };
+        if (updateData.group_id === node.group_id || (!updateData.group_id && !node.group_id)) return;
+
+        // 更新至服务器
+        postData(`/quality/tender/${node.tender_id}/rule/save`, { quality: updateData }, function (result) {
+            node.group_id = result.group_id;
+            SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+        }, function() {
+            SpreadJsObj.reLoadRowData(info.sheet, info.row, 1);
+        });
+    });
+});

+ 58 - 0
app/public/js/quality_tender.js

@@ -0,0 +1,58 @@
+'use strict';
+
+const tenderListSpec = (function(){
+    function getTenderNodeHtml(node, arr, pid) {
+        const html = [];
+        html.push('<tr pid="' + pid + '">');
+        // 名称
+        html.push('<td style="min-width: 200px" class="in-' + node.level + '">');
+        if (node.cid) {
+            html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" cid="'+ node.sort_id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
+        } else {
+            html.push('<span class="text-muted mr-2">');
+            html.push(arr.indexOf(node) === arr.length - 1 ? '└' : '├');
+            html.push('</span>');
+            //html.push('<a href="/tender/' + node.id + '">', node[c.field], '</a>');
+            html.push(`<a href="/quality/tender/${node.id}/info" name="name" style="min-width: 200px;word-break:break-all;" id="${node.id}">${node.name}</a>`);
+        }
+        html.push('</td>');
+
+        // 创建时间
+        html.push('<td style="width: 8%" class="text-center">');
+        html.push(node.create_time ? moment(node.create_time).format('YYYY-MM-DD HH:mm:ss') : '');
+        html.push('</td>');
+        // 设置
+        if (is_admin) {
+            html.push('<td style="width: 10%" class="text-center">');
+            if (!node.cid) {
+                html.push(`<a href="javascript:void(0);" data-toggle="modal" data-tid="${node.id}" class="btn btn-sm btn-outline-primary member-manage"> 成员管理 </a>`);
+            }
+            html.push('</td>');
+        }
+        html.push('</tr>');
+        return html.join('');
+    }
+    function getTenderTreeHeaderHtml() {
+        const html = [];
+        const left = $('#sub-menu').css('display') === 'none' ? 56 : 176;
+        html.push('<table class="table table-hover table-bordered" id="progress-table">');
+        html.push('<thead style="position: sticky;left:'+ left +'px;top: 0;" class="text-center">', '<tr>');
+        html.push('<th style="min-width: 50%">',  '标段名称',  tenderListOrder.getOrderButton('name'), '</th>');
+        html.push('<th style="width: 15%">', '创建时间',  tenderListOrder.getOrderButton('create_time'), '</th>');
+        if (is_admin) {
+            html.push('<th style="width: 15%">', '操作', '</th>');
+        }
+        html.push('</tr>');
+        html.push('</thead>');
+        return html.join('');
+    }
+    return { getTenderNodeHtml, getTenderTreeHeaderHtml }
+})();
+
+$(document).ready(() => {
+    const memberPermission = MemberPermission();
+    $('.member-manage').click(function(){
+        const tid = this.getAttribute('data-tid');
+        memberPermission.show({ data: { tid }, loadUrl: '/quality/member', saveUrl: '/quality/memberSave'});
+    });
+});

+ 153 - 0
app/public/js/shares/tender_permission.js

@@ -0,0 +1,153 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2025/7/18
+ * @version
+ */
+
+const MemberPermission = function() {
+    let setting;
+    // 搜索&展开收起
+    let timer = null;
+    let oldSearchVal = null;
+    $('#member-search').bind('input propertychange', function(e) {
+        oldSearchVal = e.target.value;
+        timer && clearTimeout(timer);
+        timer = setTimeout(() => {
+            const newVal = $('#member-search').val();
+            let html = '';
+            if (newVal && newVal === oldSearchVal) {
+                accountList
+                    .filter(item => item && (item.name.indexOf(newVal) !== -1 || (item.mobile && item.mobile.indexOf(newVal) !== -1)))
+                    .forEach(item => {
+                        html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                        <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                class="ml-auto">${item.mobile || ''}</span></p>
+                        <span class="text-muted">${item.role || ''}</span></dd>`
+                    });
+                $('.book-list').empty();
+                $('.book-list').append(html);
+            } else {
+                if (!$('.acc-btn').length) {
+                    accountGroup.forEach((group, idx) => {
+                        if (!group) return;
+                        html += `<dt><a href="javascript: void(0);" class="acc-btn" data-groupid="${idx}" data-type="hide"><i class="fa fa-plus-square"></i>
+                        </a> ${group.groupName}</dt>
+                        <div class="dd-content" data-toggleid="${idx}">`;
+                        group.groupList.forEach(item => {
+                            html += `<dd class="border-bottom p-2 mb-0 " data-id="${item.id}" >
+                                    <p class="mb-0 d-flex"><span class="text-primary">${item.name}</span><span
+                                            class="ml-auto">${item.mobile || ''}</span></p>
+                                    <span class="text-muted">${item.role || ''}</span>
+                                </dd>`
+                        });
+                        html += '</div>';
+                    });
+                    $('.book-list').empty();
+                    $('.book-list').append(html);
+                }
+            }
+        }, 400);
+    });
+    $('.book-list').on('click', 'dt', function () {
+        const idx = $(this).find('.acc-btn').attr('data-groupid')
+        const type = $(this).find('.acc-btn').attr('data-type')
+        if (type === 'hide') {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).show(() => {
+                $(this).children().find('i').removeClass('fa-plus-square').addClass('fa-minus-square-o')
+                $(this).find('.acc-btn').attr('data-type', 'show')
+
+            })
+        } else {
+            $(this).parent().find(`div[data-toggleid="${idx}"]`).hide(() => {
+                $(this).children().find('i').removeClass('fa-minus-square-o').addClass('fa-plus-square')
+                $(this).find('.acc-btn').attr('data-type', 'hide')
+            })
+        }
+        return false
+    });
+    // 添加
+    $('dl').on('click', 'dd', function () {
+        const auditorId = parseInt($(this).data('id'));
+        const user = accountList.find(x => { return x.id === auditorId; });
+        const check = $(`tr[uid=${auditorId}]`, '#member-list');
+        if (check.length > 0) {
+            toastr.error('请勿重复添加成员');
+            return;
+        }
+        $('#member-list').append(getUserPermissionHtml({ uid: user.id, name: user.name, role: user.role }));
+    });
+
+
+    const getUserPermissionHtml = function(user) {
+        const html = [];
+        html.push(`<tr uid="${user.uid}">`);
+        html.push(`<td>${user.name}</td>`, `<td>${user.role}</td>`);
+        for (const block of permissionBlock) {
+            for (const p of block.permission) {
+                if (p.isDefault) continue;
+                const checked = user[block.key] ? (user[block.key].indexOf(p.value) >= 0 ? 'checked' : '') : '';
+                html.push(`<td class="text-center"><input type="checkbox" data-block="${block.key}" data-value="${p.value}" ${checked}></td>`);
+            }
+        }
+        html.push(`<td><a href="javascript: void(0)" class="btn btn-outline-danger btn-sm ml-1" name="del-member">移除</a></td>`);
+        html.push('</tr>');
+        return html.join('');
+    };
+    const getPermissionHtml = function(member) {
+        const html = [];
+        for (const m of member) {
+            html.push(getUserPermissionHtml(m));
+        }
+        return html.join('');
+    };
+    const loadMemberPermission = async function() {
+        const data = JSON.parse(JSON.stringify(setting.data));
+        data.parts = permissionBlock.map(x => { return x.key; });
+        const result = await postDataAsync(setting.loadUrl, data );
+        $('#member-list').html(getPermissionHtml(result));
+    };
+
+    const show = async function(info) {
+        setting = info;
+        await loadMemberPermission();
+        $('#member').modal('show');
+    };
+
+    $('#member').on('click', 'a[name="del-member"]', function () {
+        $(this).parent().parent().remove();
+    });
+
+    const getMemberPermission = function() {
+        const result = [];
+        const trs = $('tr', '#member-list');
+        for (const tr of trs) {
+            const user = { uid: tr.getAttribute('uid') };
+            for (const block of permissionBlock) {
+                const defaultPermission = block.permission.find(x => { return x.isDefault; });
+                const permission = [];
+                if (defaultPermission) permission.unshift(defaultPermission.value + '');
+                const checkedPermission = $(`[data-block=${block.key}]:checked`, tr);
+                for (const cp of checkedPermission) {
+                    permission.push(cp.getAttribute('data-value'));
+                }
+                user[block.key] = permission;
+            }
+            result.push(user);
+        }
+        return result;
+    };
+
+    $('#member-ok').click(function() {
+        const data = JSON.parse(JSON.stringify(setting.data));
+        data.member = getMemberPermission();
+        data.permissionBlock = permissionBlock.map(x => { return x.key;});
+        postData(setting.saveUrl, data, function() {
+            $('#member').modal('hide');
+        });
+    });
+    return { show }
+};

+ 5 - 5
app/service/file.js

@@ -11,7 +11,7 @@
 const path = require('path');
 
 module.exports = app => {
-    class Filing extends app.BaseService {
+    class File extends app.BaseService {
 
         /**
          * 构造函数
@@ -82,7 +82,7 @@ module.exports = app => {
             const filing = await this.ctx.service.filing.getDataById(fileDatas[0].filing_id);
             if (this.ctx.subProject.permission.file_permission.indexOf(this.ctx.service.subProjPermission.PermissionConst.file.editfile.value) < 0) {
                 for (const file of fileDatas) {
-                    if (file.user_id !== this.ctx.session.sessionUser.accountId) throw '无权删除文件';
+                    if (file.user_id !== this.ctx.session.sessionUser.accountId && this.ctx.subProject.permission.file_permission.indexOf(this.ctx.service.subProjPermission.PermissionConst.file.editfile.value) < 0) throw '无权删除文件';
                 }
             }
             const result = {};
@@ -132,7 +132,7 @@ module.exports = app => {
         async saveFile(id, filename){
             const file = await this.getDataById(id);
             if (!file) throw '文件不存在';
-            if (file.user_id !== this.ctx.session.sessionUser.accountId) throw '您无权编辑该文件';
+            if (file.user_id !== this.ctx.session.sessionUser.accountId && this.ctx.subProject.permission.file_permission.indexOf(this.ctx.service.subProjPermission.PermissionConst.file.editfile.value) < 0) throw '您无权编辑该文件';
 
             const info = path.parse(filename);
             const updateData = { id, filename: info.name, fileext: info.ext};
@@ -143,7 +143,7 @@ module.exports = app => {
         async moveFile(id, filing_id) {
             const file = await this.getDataById(id);
             if (!file) throw '文件不存在';
-            if (file.user_id !== this.ctx.session.sessionUser.accountId) throw '您无权编辑该文件';
+            if (file.user_id !== this.ctx.session.sessionUser.accountId && this.ctx.subProject.permission.file_permission.indexOf(this.ctx.service.subProjPermission.PermissionConst.file.editfile.value) < 0) throw '您无权编辑该文件';
             const orgFiling = await this.ctx.service.filing.getDataById(file.filing_id);
             const filing = await this.ctx.service.filing.getDataById(filing_id);
             if (!filing) throw '目标分类不存在';
@@ -173,5 +173,5 @@ module.exports = app => {
         }
     }
 
-    return Filing;
+    return File;
 };

+ 1 - 0
app/service/filing.js

@@ -229,6 +229,7 @@ module.exports = app => {
             try {
                 await conn.updateRows(this.tableName, delData);
                 if (updateData.length > 0) conn.updateRows(this.tableName, updateData);
+                await conn.update(this.ctx.service.file.tableName, { is_deleted: 1}, { where: {filing_id: delData.map(x => { return x.id; })} });
 
                 await conn.commit();
                 return { delete: delData.map(x => { return x.id }), update: updateData };

+ 1 - 1
app/service/filing_template.js

@@ -256,7 +256,7 @@ module.exports = app => {
                 insertData.push({
                     id: this.uuid.v4(), temp_id: templateId, add_user_id: this.ctx.session.sessionUser.accountId,
                     tree_pid: parent ? parent.id : rootId, tree_level: parent ? parent.tree_level + 1 : 1, tree_order: d.tree_order,
-                    name: d.name, file_company: node.file_company, tips: d.tips, filing_type: d.filing_type, is_fixed: parent ? d.is_fixed : 1, org_id: d.id,
+                    name: d.name, file_company: d.file_company, tips: d.tips, filing_type: d.filing_type, is_fixed: parent ? d.is_fixed : 1, org_id: d.id,
                 });
             }
             insertData.forEach(x => { delete x.org_id; });

+ 1 - 1
app/service/jpc_report.js

@@ -216,7 +216,7 @@ module.exports = app => {
             let params = null;
             if (ctx.params) {
                 params = {};
-                params.tender_id = ctx.params.id;
+                params.tender_id = ctx.params.pid;
                 params.detail_id = ctx.params.did;
             }
             const rptDataUtil = new rptDataExtractor();

+ 522 - 0
app/service/quality.js

@@ -0,0 +1,522 @@
+'use strict';
+
+/**
+ * 质量管理 - 工程质量
+ *
+ * @author Mai
+ * @date 2024/7/22
+ * @version
+ */
+
+class RuleCheck {
+    sortCondition(condition) {
+        const fieldKey = ['gongxu_name', 'yinbi_name', 'file_count', 'file_name'];
+        condition.forEach(c => { c.fieldKeyIndex = fieldKey.indexOf(c.fieldKey); });
+        condition.sort((a, b) => { return a.fieldKeyIndex - b.fieldKeyIndex; });
+    }
+    fileCountCheck(files, check) {
+        if (check.operateKey === '>') return files.length > parseInt(check.value);
+        if (check.operateKey === '=') return files.length === parseInt(check.value);
+    }
+    fileNameCheck(files, check) {
+        const matchName = check.multi ? check.value.split(';') : [check.value];
+        if (check.operateKey === '=') return files.findIndex(f => { return matchName.indexOf(f.filename) >= 0; }) >= 0;
+        if (check.operateKey === 'has') return files.findIndex(f => {
+            return matchName.findIndex(x => { return f.filename.indexOf(x) >= 0; }) >= 0;
+        }) >= 0;
+    }
+    getAllFiles() {
+        const files = [];
+        if (this.quality.kaigong) files.push(...this.quality.kaigong.files);
+        if (this.quality.gongxu) {
+            for (const gx of this.quality.gongxu.list) {
+                files.push(...gx.files);
+            }
+        }
+        if (this.quality.pingding) {
+            files.push(...this.quality.pingding.files);
+            this.quality.pingding.source_files.forEach(sf => {
+                if (sf.spec_type === 'add') files.push(sf);
+            });
+        }
+        if (this.quality.jiaogong) files.push(...this.quality.jiaogong.files);
+        // if (this.quality.yinbi) {
+        //     for (const yb of this.quality.yinbi.list) {
+        //         files.push(...yb.files);
+        //     }
+        // }
+        return files;
+    }
+    kaigongCheck(check) {
+        if (!this.quality.kaigong) return false;
+        switch (check.fieldKey) {
+            case 'file_count':
+                return this.fileCountCheck(this.quality.kaigong.files, check);
+            case 'file_name':
+                return this.fileNameCheck(this.quality.kaigong.files, check);
+            default:
+                return false;
+        }
+    }
+    GongxuNameCheck(check) {
+        const matchName = check.multi ? check.value.split(';') : [check.value];
+        if (check.operateKey === '=') {
+            this.matchGongxu = this.quality.gongxu.list.filter(f => { return matchName.indexOf(f.name) >= 0; });
+        }
+        if (check.operateKey === 'has') {
+            this.matchGongxu = this.quality.gongxu.list.filter(f => {
+                return matchName.findIndex(x => { return f.name.indexOf(x) >= 0; }) >= 0;
+            });
+        }
+        return this.matchGongxu.length > 0;
+    }
+    getGongxuFiles() {
+        const files = [];
+        const gongxuList = this.matchGongxu || this.quality.gongxu.list;
+        for (const gx of gongxuList) {
+            files.push(...gx.files);
+        }
+        return files;
+    }
+    gongxuCheck(check) {
+        switch (check.fieldKey) {
+            case 'gongxu_name':
+                return this.GongxuNameCheck(check);
+            case 'file_count':
+                return this.fileCountCheck(this.getGongxuFiles(), check);
+            case 'file_name':
+                return this.fileNameCheck(this.getGongxuFiles(), check);
+            default:
+                return false;
+        }
+    }
+    pingdingCheck(check) {
+        if (!this.quality.pingding) return false;
+        const files = [...this.quality.pingding.files, ...this.quality.pingding.source_files];
+        switch (check.fieldKey) {
+            case 'file_count':
+                return this.fileCountCheck(files, check);
+            case 'file_name':
+                return this.fileNameCheck(files, check);
+            default:
+                return false;
+        }
+    }
+    jiaogongCheck(check) {
+        if (!this.quality.jiaogong) return false;
+        switch (check.fieldKey) {
+            case 'file_count':
+                return this.fileCountCheck(this.quality.jiaogong.files, check);
+            case 'file_name':
+                return this.fileNameCheck(this.quality.jiaogong.files, check);
+            default:
+                return false;
+        }
+    }
+    YinbiNameCheck(check) {
+        const matchName = check.multi ? check.value.split(';') : [check.value];
+        if (check.operateKey === '=') {
+            this.matchYinbi = this.quality.yinbi.list.filter(f => { return matchName.indexOf(f.name) >= 0; });
+        }
+        if (check.operateKey === '>') {
+            this.matchYinbi = this.quality.yinbi.list.filter(f => {
+                return matchName.findIndex(x => { return f.name.indexOf(x) >= 0; }) >= 0;
+            });
+        }
+        return this.matchYinbi.length > 0;
+    }
+    getYinbiFiles() {
+        const files = [];
+        const yinbiList = this.matchYinbi || this.quality.yinbi.list;
+        for (const yb of yinbiList) {
+            files.push(...yb.files);
+        }
+        return files;
+    }
+    yinbiCheck(check) {
+        switch (check.fieldKey) {
+            case 'gongxu_name':
+                return this.YinbiNameCheck(check);
+            case 'file_count':
+                return this.fileCountCheck(this.getYinbiFiles(), check);
+            case 'file_name':
+                return this.fileNameCheck(this.getYinbiFiles(), check);
+            default:
+                return false;
+        }
+    }
+    allFilesCheck(check) {
+        const allFiles = this.getAllFiles();
+        switch (check.fieldKey) {
+            case 'file_count':
+                return this.fileCountCheck(allFiles, check);
+            case 'file_name':
+                return this.fileNameCheck(allFiles, check);
+            default:
+                return false;
+        }
+    }
+    conditionCheck(check) {
+        switch (check.blockKey) {
+            case 'kaigong':
+                return this.kaigongCheck(check);
+            case 'gongxu':
+                return this.gongxuCheck(check);
+            case 'pingding':
+                return this.pingdingCheck(check);
+            case 'jiaogong':
+                return this.jiaogongCheck(check);
+            case 'yinbi':
+                return this.yinbiCheck(check);
+            case 'all':
+                return this.allFilesCheck(check);
+            default:
+                return false;
+        }
+    }
+    calcQualityStatus(quality, group) {
+        this.quality = quality;
+        this.group = group;
+
+        for (const r of group.rules) {
+            if (r.condition.length === 0) return false;
+            if (r.condition.length > 1) this.sortCondition(r.condition);
+        }
+        const match = group.rules.filter(r => {
+            this.matchGongxu = null;
+            this.matchYinbi = null;
+            for (const check of r.condition) {
+                if (!this.conditionCheck(check)) {
+                    return false;
+                }
+            }
+            return true;
+        });
+        if (match.length === 0) return null;
+        const data = {};
+        for (const m of match) {
+            for (const s of m.push_status) {
+                if (!data[s.field] || s.value > data[s.field]) {
+                    data[s.field] = s.value;
+                }
+            }
+        }
+        return data;
+    }
+}
+
+module.exports = app => {
+
+    class Quality extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'quality';
+            this.editableColumns = ['gxby_status', 'gxby_date', 'gxby_limit', 'dagl_status', 'dagl_limit'];
+            this.blockType = {
+                kaigong: { key: 'kaigong', name: '开工' },
+                gongxu: { key: 'gongxu', name: '工序' },
+                jiaogong: { key: 'jiaogong', name: '中间交工' },
+                pingding: { key: 'pingding', name: '评定' },
+                yinbi: { key: 'yinbi', name: '隐蔽工程' },
+                shiyan: { key: 'shiyan', name: '试验检测' },
+                waiwei: { key: 'waiwei', name: '外委检测' },
+                sanfang: { key: 'sanfang', name: '第三方检测' },
+            };
+        }
+
+        async saveQualityManage(tid, data) {
+            const quality = await this.getDataByCondition({ tid, rela_type: data.rela_type, rela_id: data.rela_id, rela_name: data.rela_name });
+            if (quality) {
+                const updateData = { id: quality.id, group_id: data.group_id, update_uid: this.ctx.session.sessionUser.accountId, };
+                await this.db.update(this.tableName, updateData);
+                updateData.rela_id = data.rela_id;
+                updateData.rela_name = data.rela_name;
+                return updateData;
+            } else {
+                const newQuality = {
+                    id: this.uuid.v4(), tid, rela_type: data.rela_type, rela_id: data.rela_id, rela_name: data.rela_name,
+                    create_uid: this.ctx.session.sessionUser.accountId, update_uid: this.ctx.session.sessionUser.accountId,
+                    group_id: data.group_id,
+                };
+                await this.db.insert(this.tableName, newQuality);
+                return newQuality;
+            }
+        }
+
+        async saveQuality(quality, filter, data, conn) {
+            data.update_uid = this.ctx.session.sessionUser.accountId;
+            data.is_used = 1;
+            if (quality) {
+                data.id = quality.id;
+                if (conn) {
+                    await conn.update(this.tableName, data);
+                } else {
+                    await this.db.update(this.tableName, data);
+                }
+                return data;
+            } else {
+                data.id = this.uuid.v4();
+                data.tid = this.ctx.tender.id;
+                data.rela_type = filter.rela_type;
+                data.rela_id = filter.rela_id;
+                data.rela_name = filter.rela_name;
+                data.create_uid = this.ctx.session.sessionUser.accountId;
+                if (conn) {
+                    await conn.insert(this.tableName, data);
+                } else {
+                    await this.db.insert(this.tableName, data);
+                }
+                return data;
+            }
+        }
+
+        async getQuality(filter) {
+            if (filter.quality_id) return await this.getDataByCondition({ id: filter.quality_id });
+            if (filter.rela_type && filter.rela_id) return await this.getDataByCondition({ tid: filter.tid || this.ctx.tender.id, rela_id: filter.rela_id, rela_name: filter.rela_name });
+            throw '参数错误';
+        }
+
+        async _loadKgData(quality) {
+            if (!quality.kaigong_time) return;
+            quality.kaigong = {};
+            for (const p in quality) {
+                if (p.indexOf('kaigong_') !== 0) continue;
+                quality.kaigong[p] = quality[p];
+                delete quality[p];
+            }
+            const files = await this.ctx.service.qualityFile.getBlockFiles(quality, 'kaigong');
+            quality.kaigong.files = files.filter(f => { return !f.spec_type; });
+            quality.kaigong.qa_files = files.filter(f => { return f.spec_type === 'qa'; });
+            // todo 加载开工审批数据
+        }
+        async _loadGxData(quality) {
+            quality.gongxu = {};
+            // todo 加载工序审批数据
+            quality.gongxu.list = await this.ctx.service.qualityGongxu.getAllDataByCondition({ where: { quality_id: quality.id }, orders:[['create_time', 'asc']] });
+            for (const gx of quality.gongxu.list) {
+                gx.canEdit = gx.user_id === this.ctx.session.sessionUser.accountId;
+                gx.files = await this.ctx.service.qualityFile.getBlockFiles(quality, 'gongxu', gx.id);
+                // todo 加载工序步骤审批数据
+            }
+        }
+        async _loadPdData(quality) {
+            if (!quality.kaigong && quality.gongxu.list.length === 0) return;
+            quality.pingding = {};
+            for (const p in quality) {
+                if (p.indexOf('pingding_') !== 0) continue;
+                quality.kaigong[p] = quality[p];
+                delete quality[p];
+            }
+            const files = await this.ctx.service.qualityFile.getBlockFiles(quality, 'pingding');
+            quality.pingding.files = files.filter(f => { return !f.spec_type; });
+            quality.pingding.source_files = files.filter(f => { return f.spec_type === 'add'; }); // addtional
+            if (quality.gongxu) {
+                for (const gx of quality.gongxu.list) {
+                    const qaFiles = gx.files.filter(f => { return f.spec_type === 'qa'; });
+                    for (const f of qaFiles) {
+                        const qaF = JSON.parse(JSON.stringify(f));
+                        qaF.canEdit = false;
+                        quality.pingding.source_files.push(qaF);
+                    }
+                }
+            }
+            // todo 加载评定审批数据
+        }
+        async _loadJgData(quality) {
+            if (!quality.kaigong && quality.gongxu.list.length === 0) return;
+            quality.jiaogong = {};
+            for (const p in quality) {
+                if (p.indexOf('jiaogong_') !== 0) continue;
+                quality.jiaogong[p] = quality[p];
+                delete quality[p];
+            }
+            quality.jiaogong.files = await this.ctx.service.qualityFile.getBlockFiles(quality, 'jiaogong');
+        }
+        async _loadYbData(quality) {
+            quality.yinbi = {};
+            quality.yinbi.list = await this.ctx.service.qualityYinbi.getAllDataByCondition({ where: { quality_id: quality.id }, orders:[['create_time', 'asc']] });
+            for (const yb of quality.yinbi.list) {
+                yb.canEdit = yb.user_id === this.ctx.session.sessionUser.accountId;
+                const files = await this.ctx.service.qualityFile.getBlockFiles(quality, 'yinbi', yb.id);
+                yb.before_files = files.filter(f => { return f.spec_type === 'before'; });
+                yb.after_files = files.filter(f => { return f.spec_type === 'after'; });
+            }
+        }
+        async loadQualityDetail(quality) {
+            if (!quality) return;
+            await this._loadKgData(quality);
+            await this._loadGxData(quality);
+            await this._loadPdData(quality);
+            await this._loadJgData(quality);
+            await this._loadYbData(quality);
+        }
+
+        async getQualityGroup(quality) {
+            if (quality.group_id) return quality.group_id;
+
+            const ledger = await this.ctx.service.ledger.getDataById(quality.rela_id);
+            const sql = 'SELECT l.id, l.ledger_id, l.level, q.group_id ' +
+                `  FROM ${this.tableName} q LEFT JOIN ${this.ctx.service.ledger.tableName} l ON q.rela_id = l.id ` +
+                `  WHERE q.tid = ${quality.tid} and l.ledger_id in (${ledger.full_path.split('-').join(', ')}) and group_id <> ''` +
+                '  ORDER BY l.level DESC';
+            const allQuality = await this.db.query(sql);
+            return allQuality.length > 0 ? allQuality[0].group_id : '';
+        }
+        async checkQualityStatus(quality) {
+            const groupId = await this.getQualityGroup(quality);
+            if (!groupId) return null;
+            const groupRule = await this.ctx.service.qualityRule.getGroupRule(groupId);
+            if (!groupRule) return null;
+            const ruleCheck = new RuleCheck();
+            return ruleCheck.calcQualityStatus(quality, groupRule);
+        }
+        async checkQualityStatusAndSave(quality) {
+            const data = await this.checkQualityStatus(quality);
+            if (!data) return;
+            for (const field in data) {
+                quality[field] = data[field];
+            }
+            data.id = quality.id;
+            await this.db.update(this.tableName, data);
+        }
+
+        async kaigong(filter, name, date) {
+            const quality = await this.getQuality(filter);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                const kaigongData = {
+                    kaigong_name: name || '开工工序', kaigong_date: date || this.ctx.moment().format('YYYY-MM-DD'),
+                    kaigong_uid: this.ctx.session.sessionUser.accountId, kaigong_time: new Date(),
+                };
+                await this.saveQuality(quality, filter, kaigongData, conn);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+
+        async delKaigong(filter) {
+            const quality = await this.getQuality(filter);
+            if (!quality || !quality.kaigong_uid) throw '该工程未开工';
+            if (quality.kaigong_uid !== this.ctx.session.sessionUser.accountId) throw '开工数据不是您新增的,无权删除';
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await this.saveQuality(quality, filter, { kaigong_name: '', kaigong_date: '', kaigong_uid: 0, kaigong_time: null }, conn);
+                await conn.update(this.ctx.service.qualityFile.tableName, { is_deleted: 1 }, { where: { quality_id: quality.id, block_type: 'kaigong' } });
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+
+        async pushIncStatus(select) {
+            const quality = await this.ctx.service.quality.getAllDataByCondition({ where: { tid: this.ctx.tender.id, rela_id: select, is_used: 1 } });
+            if (quality.length === 0) return 0;
+            const xmjInsert = [], xmjUpdate = [], posInsert = [], posUpdate = [];
+            const xmjQuality = [], posQuality = [];
+            quality.forEach(q => {
+                if (q.rela_type === 'xmj') {
+                    xmjQuality.push(q);
+                } else {
+                    posQuality.push(q);
+                }
+            });
+            const xmjExtra = xmjQuality.length > 0 ? await this.ctx.service.ledgerExtra.getAllDataByCondition({ where: { id: xmjQuality.map(x => { return x.rela_id; }) } }) : [];
+            for (const xq of xmjQuality) {
+                const xe = xmjExtra.find(x => { return x.id === xq.rela_id; });
+                const wbs_url = `/3f/zh/info?pid=${this.ctx.session.sessionProject.id}&tid=${xq.tid}&${xq.rela_type}id=${xq.rela_id}`;
+                if (xe) {
+                    xmjUpdate.push({ id: xe.id, gxby_status: xq.gxby_status, gxby_date: xq.gxby_date, dagl_status: xq.dagl_status, wbs_url });
+                } else {
+                    xmjInsert.push({ id: xq.rela_id, gxby_status: xq.gxby_status, gxby_date: xq.gxby_date, dagl_status: xq.dagl_status, wbs_url });
+                }
+            }
+            const posExtra = posQuality.length > 0 ? await this.ctx.service.posExtra.getAllDataByCondition({ where: { id: posQuality.map(x => { return x.rela_id; }) } }) : [];
+            for (const pq of posQuality) {
+                const pe = posExtra.find(x => { return x.id === pq.rela_id; });
+                const wbs_url = `/3f/zh/info?pid=${this.ctx.session.sessionProject.id}&tid=${pq.tid}&${pq.rela_type}id=${pq.rela_id}`;
+                if (pe) {
+                    posUpdate.push({ id: pe.id, gxby_status: pq.gxby_status, gxby_date: pq.gxby_date, dagl_status: pq.dagl_status, wbs_url });
+                } else {
+                    posInsert.push({ id: pq.rela_id, gxby_status: pq.gxby_status, gxby_date: pq.gxby_date, dagl_status: pq.dagl_status, wbs_url });
+                }
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                if (xmjInsert.length > 0) await conn.insert(this.ctx.service.ledgerExtra.tableName, xmjInsert);
+                if (xmjUpdate.length > 0) await conn.updateRows(this.ctx.service.ledgerExtra.tableName, xmjUpdate);
+                if (posInsert.length > 0) await conn.insert(this.ctx.service.posExtra.tableName, posInsert);
+                if (posUpdate.length > 0) await conn.updateRows(this.ctx.service.posExtra.tableName, posUpdate);
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '推送状态错误';
+            }
+            return xmjInsert.length + xmjUpdate.length + posInsert.length + posUpdate.length;
+        }
+        async pushAllStatus() {
+            const quality = await this.ctx.service.quality.getAllDataByCondition({ where: { tid: this.ctx.tender.id, is_used: 1 } });
+            if (quality.length === 0) return 0;
+            const xmjQuality = [], posQuality = [];
+            quality.forEach(q => {
+                if (q.rela_type === 'xmj') {
+                    xmjQuality.push(q);
+                } else {
+                    posQuality.push(q);
+                }
+            });
+            const xmjInsert = [], xmjUpdate = [], posInsert = [], posUpdate = [];
+            const xmjExtra = await this.ctx.service.ledgerExtra.getAllDataByCondition({ where: { tid: this.ctx.tender.id } });
+            for (const xq of xmjQuality) {
+                const xe = xmjExtra.find(x => { return x.id === xq.rela_id; });
+                const wbs_url = `/3f/zh/info?pid=${this.ctx.session.sessionProject.id}&tid=${xq.tid}&${xq.rela_type}id=${xq.rela_id}`;
+                if (xe) {
+                    xmjUpdate.push({ id: xe.id, gxby_status: xq.gxby_status, gxby_date: xq.gxby_date, dagl_status: xq.dagl_status, wbs_url });
+                } else {
+                    xmjInsert.push({ id: xq.rela_id, tid: this.ctx.tender.id, gxby_status: xq.gxby_status, gxby_date: xq.gxby_date, dagl_status: xq.dagl_status, wbs_url });
+                }
+            }
+            const posExtra = await this.ctx.service.posExtra.getAllDataByCondition({ where: { tid: this.ctx.tender.id } });
+            for (const pq of posQuality) {
+                const pe = posExtra.find(x => { return x.id === pq.rela_id; });
+                const wbs_url = `/3f/zh/info?pid=${this.ctx.session.sessionProject.id}&tid=${pq.tid}&${pq.rela_type}id=${pq.rela_id}`;
+                if (pe) {
+                    posUpdate.push({ id: pe.id, gxby_status: pq.gxby_status, gxby_date: pq.gxby_date, dagl_status: pq.dagl_status, wbs_url });
+                } else {
+                    posInsert.push({ id: pq.rela_id, tid: this.ctx.tender.id, gxby_status: pq.gxby_status, gxby_date: pq.gxby_date, dagl_status: pq.dagl_status, wbs_url });
+                }
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                const defaultData = { gxby_status: -1, gxby_date: null, dagl_status: -1 };
+                await conn.update(this.ctx.service.ledgerExtra.tableName, defaultData, { where: {tid: this.ctx.tender.id } });
+                await conn.update(this.ctx.service.posExtra.tableName, defaultData, { where: {tid: this.ctx.tender.id } });
+
+                if (xmjInsert.length > 0) await conn.insert(this.ctx.service.ledgerExtra.tableName, xmjInsert);
+                if (xmjUpdate.length > 0) await conn.updateRows(this.ctx.service.ledgerExtra.tableName, xmjUpdate);
+                if (posInsert.length > 0) await conn.insert(this.ctx.service.posExtra.tableName, posInsert);
+                if (posUpdate.length > 0) await conn.updateRows(this.ctx.service.posExtra.tableName, posUpdate);
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '推送状态错误';
+            }
+            return xmjInsert.length + xmjUpdate.length + posInsert.length + posUpdate.length;
+        }
+    }
+
+    return Quality;
+};

+ 92 - 0
app/service/quality_file.js

@@ -0,0 +1,92 @@
+'use strict';
+
+/**
+ * 质量管理 - 工程质量
+ *
+ * @author Mai
+ * @date 2024/7/22
+ * @version
+ */
+
+const path = require('path');
+
+module.exports = app => {
+
+    class QualityFile extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'quality_file';
+        }
+
+        analysisFiles(files) {
+            const helper = this.ctx.helper;
+            const userId = this.ctx.session.sessionUser.accountId;
+            const ossPath = this.ctx.app.config.ossUrl;
+            files.forEach(x => {
+                x.viewpath = helper.canPreview(x.fileext) ? ossPath + x.filepath : '';
+                x.filepath = ossPath + x.filepath;
+                x.fileext_str = helper.fileExtStr(x.fileext);
+                x.canEdit = x.user_id === userId;
+            });
+        }
+
+        async getFiles(condition) {
+            condition.orders = [['create_time', 'asc']];
+            const result = await this.getAllDataByCondition(condition);
+            this.analysisFiles(result);
+            return result;
+        }
+
+        async getBlockFiles(quality, blockType, blockId) {
+            if (!quality) throw '参数错误';
+            // const where = { tid: quality.tid, quality_id: quality.id };
+            const where = { quality_id: quality.id, is_deleted: 0 };
+            if (blockType) where.block_type = blockType;
+            if (blockId) where.block_id = blockId;
+            return this.getFiles({ where });
+        }
+
+        async addFiles(user, quality, fileInfo, blockType, blockId = '', specType = '') {
+            const insertData = fileInfo.map(x => {
+                return {
+                    id: this.uuid.v4(), tid: quality.tid, quality_id: quality.id,
+                    rela_type: quality.rela_type, rela_id: quality.rela_id,
+                    block_type: blockType, block_id: blockId, spec_type: specType,
+                    user_id: user.id, user_name: user.name, user_company: user.company, user_role: user.role,
+                    filename: x.filename, fileext: x.fileext, filesize: x.filesize, filepath: x.filepath,
+                };
+            });
+            await this.db.insert(this.tableName, insertData);
+            return await this.getFiles({ where:  { id: insertData.map(x => { return x.id; })} });
+        }
+
+        async delFiles(files) {
+            if (files.length === 0) return;
+
+            const fileDatas = await this.getAllDataByCondition({ where: { id: files } });
+            const updateData = fileDatas.map(x => { return { id: x.id, is_deleted: 1 }; });
+            if (updateData.length > 0) await this.db.updateRows(this.tableName, updateData);
+            return files;
+        }
+
+        async saveFile(id, filename){
+            const file = await this.getDataById(id);
+            if (!file) throw '文件不存在';
+            if (file.user_id !== this.ctx.session.sessionUser.accountId) throw '您无权编辑该文件';
+
+            const info = path.parse(filename);
+            const updateData = { id, filename: info.name, fileext: info.ext};
+            await this.defaultUpdate(updateData);
+            return updateData;
+        }
+    }
+
+    return QualityFile;
+};

+ 71 - 0
app/service/quality_gongxu.js

@@ -0,0 +1,71 @@
+'use strict';
+
+/**
+ * 质量管理 - 工程质量
+ *
+ * @author Mai
+ * @date 2024/7/22
+ * @version
+ */
+
+module.exports = app => {
+
+    class QualityGongxu extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'quality_gongxu';
+        }
+
+        async add(filter, name) {
+            let quality = await this.ctx.service.quality.getQuality(filter);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                if (!quality) {
+                    quality = await this.ctx.service.quality.saveQuality(quality, filter, {}, conn);
+                }
+                const newGongxu = {
+                    id: this.uuid.v4(), tid: quality.tid, quality_id: quality.id, name, user_id: this.ctx.session.sessionUser.accountId,
+                };
+                await conn.insert(this.tableName, newGongxu);
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '新增工序失败';
+            }
+        }
+
+        async update(id, name) {
+            const gongxu = await this.getDataById(id);
+            if (!gongxu) throw '工序不存在';
+
+            await this.db.update(this.tableName, { id, name });
+        }
+
+        async del(id) {
+            const gongxu = await this.getDataById(id);
+            if (!gongxu) throw '工序不存在';
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id });
+                await conn.update(this.ctx.service.qualityFile.tableName, { is_deleted: 1 }, { where: { tid: gongxu.tid, quality_id: gongxu.quality_id, block_type: 'gongxu', block_id: id } });
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '删除工序失败';
+            }
+        }
+    }
+
+    return QualityGongxu;
+};

+ 182 - 0
app/service/quality_rule.js

@@ -0,0 +1,182 @@
+'use strict';
+
+/**
+ * 质量管理 - 状态规则(规则组)
+ *
+ * @author Mai
+ * @date 2024/7/19
+ * @version
+ */
+
+module.exports = app => {
+
+    class QualityRule extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'quality_rule';
+        }
+
+        async getGroupList(pid) {
+            const sql = `SELECT group_id, group_name FROM ${this.tableName} WHERE pid = ? GROUP BY group_id`;
+            return await this.db.query(sql, [pid]);
+        }
+
+        analysisiRule(data) {
+            data.condition = data.condition ? JSON.parse(data.condition) : [];
+            data.push_status = data.push_status ? JSON.parse(data.push_status) : [];
+        }
+
+        analysisRuleGroups(datas) {
+            const result = [];
+            datas.forEach(x => {
+                this.analysisiRule(x);
+                let group = result.find(r => { return x.group_id === r.group_id; });
+                if (!group) {
+                    group = { group_id: x.group_id, group_name: x.group_name, rules: [] };
+                    result.push(group);
+                }
+                group.rules.push(x);
+            });
+            return result;
+        }
+
+        async getRuleGroups(pid) {
+            const result = await this.getAllDataByCondition({
+                columns: ['id', 'group_id', 'group_name', 'name', 'condition', 'push_status', 'memo'],
+                where: { pid },
+            });
+            return this.analysisRuleGroups(result);
+        }
+
+        async getGroupRule(group_id) {
+            const result = await this.getAllDataByCondition({
+                columns: ['id', 'group_id', 'group_name', 'name', 'condition', 'push_status', 'memo'],
+                where: { group_id },
+            });
+            if (result.length === 0) return;
+            result.forEach(x => {
+                this.analysisiRule(x);
+            });
+            return { group_id, group_name: result[0].group_name, rules: result };
+        }
+
+        async getRule(id) {
+            const result = await this.getDataById(id);
+            this.analysisiRule(result);
+            return result;
+        }
+
+        async getRules(ids) {
+            const result = await this.getAllDataByCondition({ where: { id: ids } });
+            for (const r of result) {
+                this.analysisiRule(r);
+            }
+            return result;
+        }
+
+        async _delGroup(id) {
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { group_id: id });
+                // 下面这句update在qa上执行时,无法得到SET `group_id` = '',可能是对空值进行了过滤?
+                // await conn.update(this.ctx.service.quality.tableName, { group_id: '' }, { where: { group_id: id } });
+                await conn.query(`UPDATE ${this.ctx.service.quality.tableName} SET group_id = '' WHERE group_id = ?`, [id]);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                this.ctx.log(err);
+                throw err;
+            }
+            return id;
+        }
+
+        async _saveGroup(id, group_name) {
+            if (id) {
+                await this.db.update(this.tableName, { group_name }, { where: { group_id: id } });
+                return { group_id: id, group_name };
+            }
+
+            const newRule = {
+                group_id: this.uuid.v4(), pid: this.ctx.session.sessionProject.id,
+                user_id: this.ctx.session.sessionUser.accountId,
+                group_name: group_name || '新增规则组', name: '新增规则', condition: '[]', push_status: '[]',
+            };
+            const result = await this.db.insert(this.tableName, newRule);
+            return await this.getGroupRule(newRule.group_id);
+        }
+
+        async saveGroup(data) {
+            const result = {};
+            try {
+                if (data.add) result.add = await this._saveGroup();
+                if (data.del) result.del = await this._delGroup(data.del);
+                if (data.update) result.update = await this._saveGroup(data.update.group_id, data.update.group_name);
+            } catch (err) {
+                this.ctx.log(err);
+                throw err;
+            }
+            return result;
+        }
+
+        async _saveRule(data) {
+            if (data.id) {
+                if (data.name) data.name = data.name;
+                if (data.condition) data.condition = JSON.stringify(data.condition);
+                if (data.push_status) data.push_status = JSON.stringify(data.push_status);
+                await this.db.update(this.tableName, data);
+            } else {
+                data.pid = this.ctx.session.sessionProject.id;
+                data.user_id = this.ctx.session.sessionUser.accountId;
+                data.name = data.name || '新增规则';
+                data.condition = data.condition ? JSON.stringify(data.condition) : '[]';
+                data.push_status = data.push_status ? JSON.stringify(data.push_status) : '[]';
+                const result = await this.db.insert(this.tableName, data);
+                data.id = result.insertId;
+            }
+            return await this.getRule(data.id);
+        }
+
+        async _delRule(data) {
+            await this.deleteById(data.id);
+            return data;
+        }
+
+        async _copyRule(group_id, copy) {
+            const copyId = copy.split(',').map(x => { return parseInt(x); });
+            const rules = await this.getAllDataByCondition({ where: { id: copyId } });
+            const group = await this.getDataByCondition({ group_id });
+            if (!group) throw '您选择的规则组不存在,请刷新页面再试';
+            const insertData = rules.map(rule => {
+                return {
+                    pid: rule.pid, user_id: this.ctx.session.sessionUser.accountId,
+                    group_id: group.group_id, group_name: group.group_name,
+                    name: rule.name, condition: rule.condition, push_status: rule.push_status,
+                };
+            });
+            const result = await this.db.insert(this.tableName, insertData);
+            const ids = [];
+            for (let i = 0; i < insertData.length; i++) {
+                ids.push(result.insertId + i);
+            }
+            return await this.getRules(ids);
+        }
+
+        async saveRule(data) {
+            const result = {};
+            if (data.add) result.add = await this._saveRule(data.add);
+            if (data.del) result.del = await this._delRule(data.del);
+            if (data.update) result.update = await this._saveRule(data.update);
+            if (data.copy) result.add = await this._copyRule(data.group_id, data.copy);
+            return result;
+        }
+    }
+
+    return QualityRule;
+};

+ 72 - 0
app/service/quality_yinbi.js

@@ -0,0 +1,72 @@
+'use strict';
+
+/**
+ * 质量管理 - 工程质量
+ *
+ * @author Mai
+ * @date 2024/7/22
+ * @version
+ */
+
+module.exports = app => {
+
+    class QualityYinbi extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'quality_yinbi';
+        }
+
+        async add(filter, data) {
+            let quality = await this.ctx.service.quality.getQuality(filter);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                if (!quality) {
+                    quality = await this.saveQuality(quality, filter, {}, conn);
+                }
+                const newYinbi = {
+                    id: this.uuid.v4(), tid: quality.tid, quality_id: quality.id, user_id: this.ctx.session.sessionUser.accountId,
+                    name: data.name, gongxu_id: data.gongxu_id || '', gcbw: data.gcbw || '', content: data.content,
+                };
+                await conn.insert(this.tableName, newYinbi);
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '新增工序失败';
+            }
+        }
+
+        async update(id, data) {
+            const yinbi = await this.getDataById(id);
+            if (!yinbi) throw '隐蔽工程不存在';
+            const updateData = { id: yinbi.id, name: data.name, gongxu_id: data.gongxu_id || '', gcbw: data.gcbw || '', content: data.content };
+            await this.db.update(this.tableName, updateData);
+        }
+
+        async del(id) {
+            const yinbi = await this.getDataById(id);
+            if (!yinbi) throw '工序不存在';
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id });
+                await conn.update(this.ctx.service.qualityFile.tableName, { is_deleted: 1 }, { where: { tid: yinbi.tid, quality_id: yinbi.quality_id, block_type: 'yinbi', block_id: id } });
+                await conn.commit();
+            } catch (err) {
+                this.ctx.log(err);
+                await conn.rollback();
+                throw '删除工序失败';
+            }
+        }
+    }
+
+    return QualityYinbi;
+};

+ 8 - 8
app/service/stage_pay.js

@@ -85,14 +85,14 @@ module.exports = app => {
                 throw '初始化期合同支付数据失败';
             }
             let pays = await this.ctx.service.pay.getAllDataByCondition({where: { tid: this.ctx.tender.id } });
-            // 兼容旧项目,无初始化合同支付数据
-            if (!pays || pays.length === 0) {
-                const result = await this.ctx.service.pay.addDefaultPayData(this.ctx.tender.id, transaction);
-                if (!result) {
-                    throw '初始化合同支付数据失败';
-                }
-                pays = await transaction.select(this.ctx.service.pay.tableName, {where: { tid: this.ctx.tender.id } });
-            }
+            // // 兼容旧项目,无初始化合同支付数据
+            // if (!pays || pays.length === 0) {
+            //     const result = await this.ctx.service.pay.addDefaultPayData(this.ctx.tender.id, transaction);
+            //     if (!result) {
+            //         throw '初始化合同支付数据失败';
+            //     }
+            //     pays = await transaction.select(this.ctx.service.pay.tableName, {where: { tid: this.ctx.tender.id } });
+            // }
             const stagePays = [];
             // 获取截止上期数据
             if (stage.preCheckedStage) {

+ 2 - 2
app/service/stage_stash.js

@@ -311,11 +311,11 @@ module.exports = app => {
             const bills = await this.ctx.service.ledger.getAllDataByCondition({ where: { tender_id: stage.tid, is_leaf: true } });
             const extraData = await this.ctx.service.ledgerExtra.getData(stage.tid, ['id', 'is_tp']);
             const settleStatusBills = stage.readySettle ? await this.ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: stage.readySettle.id }}) : [];
-            const preStageBills = stage.preCheckedStage ? await this.ctx.service.stageBillsFinal.getFinalData(stage.tid, this.ctx.stage.preCheckedStage.order) : [];
+            const preStageBills = stage.preCheckedStage ? await this.ctx.service.stageBillsFinal.getFinalData(this.ctx.tender, this.ctx.stage.preCheckedStage.order) : [];
             this.ctx.helper.assignRelaData(bills, [
                 { data: extraData, fields: ['is_tp'], prefix: '', relaId: 'id' },
                 { data: settleStatusBills, fields: ['settle_status'], prefix: '', relaId: 'lid' },
-                { data: preStageBills, field: ['contract_qty', 'contract_tp', 'qc_minus_qty'], prefix: 'pre_', relaId: 'lid'},
+                { data: preStageBills, fields: ['contract_qty', 'contract_tp', 'qc_minus_qty'], prefix: 'pre_', relaId: 'lid'},
             ]);
             const pos = await this.ctx.service.pos.getAllDataByCondition({ where: { tid: stage.tid } });
             const settleStatusPos = stage.readySettle ? await this.ctx.service.settlePos.getAllDataByCondition({ where: { settle_id: stage.readySettle.id }}) : [];

+ 4 - 2
app/service/tender.js

@@ -594,9 +594,11 @@ module.exports = app => {
         }
 
         // 仅要求授权表用tid/uid授权(条件:存在数据行即授权查看,取消授权直接删除行)
+        // 如果授权表是牵扯多个模块的授权,则part是模块权限字段
         // filter目前仅支持['self', 'all'],可随时增加过滤类型
-        async getSpecList(specPermissionService, filter = 'self') {
-            const filterSql = filter === 'all' ? '' : ` AND t.id IN (SELECT tid FROM ${specPermissionService.tableName} WHERE uid = ?) `;
+        async getSpecList(specPermissionService, part = '', filter = 'self') {
+            const partSql = part ? ` AND ${part} <> '' ` : '';
+            const filterSql = filter === 'all' ? '' : ` AND t.id IN (SELECT tid FROM ${specPermissionService.tableName} WHERE uid = ? ${partSql}) `;
             const sql = 'SELECT t.`id`, t.`project_id`, t.`name`, t.`status`, t.`category`, t.`user_id`, t.`create_time`, t.`spid`,' +
                 '    pa.`name` As `user_name`, pa.`role` As `user_role`, pa.`company` As `user_company` ' +
                 `  FROM ${this.tableName} As t Left Join ${this.ctx.service.projectAccount.tableName} As pa ON t.user_id = pa.id` +

+ 14 - 5
app/service/tender_info.js

@@ -262,19 +262,24 @@ module.exports = app => {
                     if (!b) continue;
                     const contract_tp = this.ctx.helper.mul(b.unit_price, sb.contract_qty, decimal.tp);
                     const qc_tp = this.ctx.helper.mul(b.unit_price, sb.qc_qty, decimal.tp);
+                    const positive_qc_tp = this.ctx.helper.mul(b.unit_price, sb.positive_qc_qty, decimal.tp);
+                    const negative_qc_tp = this.ctx.helper.mul(b.unit_price, sb.positive_qc_qty, decimal.tp);
                     const ex_stage_tp1 = this.ctx.helper.mul(b.unit_price, sb.ex_stage_qty1, decimal.tp);
-                    if (contract_tp == sb.contract_tp && qc_tp === sb.qc_tp && ex_stage_tp1 === sb.ex_stage_tp1) continue;
+                    if (contract_tp == sb.contract_tp && qc_tp === sb.qc_tp && ex_stage_tp1 === sb.ex_stage_tp1
+                        && positive_qc_tp === sb.positive_qc_tp && negative_qc_tp === sb.negative_qc_tp) continue;
 
                     if (sb.times === stage.times && sb.order === 0) {
                         updateStageBills.push({
-                            id: sb.id, contract_tp, qc_tp, ex_stage_tp1,
+                            id: sb.id, contract_tp, qc_tp, positive_qc_tp, negative_qc_tp, ex_stage_tp1,
                         });
                     } else {
                         insertStageBills.push({
                             tid: stage.tid, lid: sb.lid, sid: stage.id, said: this.ctx.session.sessionUser.accountId,
                             times: stage.times, order: 0,
                             contract_qty: sb.contract_qty, contract_expr: sb.contract_expr, contract_tp,
-                            qc_qty: sb.qc_qty, qc_tp,
+                            qc_qty: sb.qc_qty, qc_tp, qc_minus_qty: sb.qc_minus_qty,
+                            positive_qc_qty: sb.positive_qc_qty, positive_qc_tp: sb.positive_qc_tp,
+                            negative_qc_qty: sb.negative_qc_qty, negative_qc_tp: sb.negative_qc_tp,
                             ex_stage_qty1: sb.ex_stage_qty1, ex_stage_tp1,
                             postil: sb.postil,
                         });
@@ -306,19 +311,23 @@ module.exports = app => {
                     const end_contract_tp = this.ctx.helper.mul(this.ctx.helper.div(end_contract_qty, activeQty), b.total_price, decimal.tp);
                     const contract_tp = preSb ? this.ctx.helper.sub(end_contract_tp, preSb.contract_tp) : end_contract_tp;
                     const qc_tp = this.ctx.helper.mul(b.unit_price, sb.qc_qty, decimal.tp);
+                    const positive_qc_tp = this.ctx.helper.mul(b.unit_price, sb.positive_qc_qty, decimal.tp);
+                    const negative_qc_tp = this.ctx.helper.mul(b.unit_price, sb.positive_qc_qty, decimal.tp);
                     const ex_stage_tp1 = this.ctx.helper.mul(b.unit_price, sb.ex_stage_qty1, decimal.tp);
                     if (contract_tp == sb.contract_tp && qc_tp === sb.qc_tp && ex_stage_tp1 === sb.ex_stage_tp1) continue;
 
                     if (sb.times === stage.times && sb.order === 0) {
                         updateStageBills.push({
-                            id: sb.id, contract_tp, qc_tp, ex_stage_tp1,
+                            id: sb.id, contract_tp, qc_tp, ex_stage_tp1, positive_qc_tp, negative_qc_tp,
                         });
                     } else {
                         insertStageBills.push({
                             tid: stage.tid, lid: sb.lid, sid: stage.id, said: this.ctx.session.sessionUser.accountId,
                             times: stage.times, order: 0,
                             contract_qty: sb.contract_qty, contract_expr: sb.contract_expr, contract_tp,
-                            qc_qty: sb.qc_qty, qc_tp,
+                            qc_qty: sb.qc_qty, qc_tp, qc_minus_qty: sb.qc_minus_qty,
+                            positive_qc_qty: sb.positive_qc_qty, positive_qc_tp: sb.positive_qc_tp,
+                            negative_qc_qty: sb.negative_qc_qty, negative_qc_tp: sb.negative_qc_tp,
                             ex_stage_qty1: sb.ex_stage_qty1, ex_stage_tp1,
                             postil: sb.postil,
                         });

+ 162 - 0
app/service/tender_permission.js

@@ -0,0 +1,162 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2025/7/17
+ * @version
+ */
+
+module.exports = app => {
+    class tenderPermission extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @param {String} tableName - 表名
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'tender_permission';
+            this.PermissionConst = {
+                quality: {
+                    view: { title: '查看', value: 1, isDefault: 1 },
+                    upload: { title: '上传文件', value: 2 },
+                    add: { title: '新增功能', value: 3 },
+                },
+            };
+            this.PermissionBlock = [
+                { key: 'quality', name: '质量管理', field: 'quality' },
+            ];
+            for (const p of this.PermissionBlock) {
+                if (p.children) {
+                    for (const c of p.children) {
+                        c.permission = [];
+                        const pConst = this.PermissionConst[c.key];
+                        if (!pConst) continue;
+                        for (const prop in pConst) {
+                            c.permission.push({ key: prop, ...pConst[prop]});
+                        }
+                    }
+                } else {
+                    p.permission = [];
+                    const pConst = this.PermissionConst[p.key];
+                    if (!pConst) continue;
+                    for (const prop in pConst) {
+                        p.permission.push({ key: prop, ...pConst[prop]});
+                    }
+                }
+            }
+            this.AdminPermission = {};
+            for (const p in this.PermissionConst) {
+                this.AdminPermission[p] = this.ctx.helper.mapAllSubField(this.PermissionConst[p], 'value');
+            }
+        }
+
+        partPermissionConst(part) {
+            if (!part) return this.PermissionConst;
+
+            const parts = part instanceof Array ? part : [part];
+            const result = {};
+            for (const p of parts) {
+                result[p] = this.PermissionConst[p];
+            }
+            return result;
+        }
+        partPermissionBlock(part) {
+            if (!part) return this.PermissionBlock;
+
+            const parts = part instanceof Array ? part : [part];
+            const result = this.PermissionBlock.filter(x => { return parts.indexOf(x.key) >= 0; });
+            return result;
+        }
+
+        parsePermission(data) {
+            const _ = this.ctx.helper._;
+            const datas = data instanceof Array ? data : [data];
+            datas.forEach(x => {
+                for (const p in this.PermissionConst) {
+                    x[p] = x[p] ? _.map(x[p].split(','), _.toInteger) : [];
+                }
+            });
+        }
+
+        // 权限检查
+        conversePermission(permission) {
+            const result = {};
+            for (const block of this.PermissionBlock) {
+                const per = {};
+                for(const p of block.permission) {
+                    per[p.key] = permission[block.key].indexOf(p.value) >= 0;
+                }
+                result[block.key] = per;
+            }
+            return result;
+        }
+        getAdminPermission() {
+            return this.conversePermission(this.AdminPermission);
+        }
+        async getUserPermission(tid, uid) {
+            const result = await this.getDataByCondition({ tid, uid });
+            this.parsePermission(result);
+            return this.conversePermission(result);
+        }
+
+        // 用户权限编辑
+        async getPartsPermission(tid, parts) {
+            if (!parts || parts.length === 0) return [];
+
+            const partSql = parts.map(x => { return `${x} <> ''`}).join(' OR ');
+            const sql = `SELECT qp.*, pa.name, pa.role FROM ${this.tableName} qp LEFT JOIN ${this.ctx.service.projectAccount.tableName} pa ON qp.uid = pa.id WHERE qp.tid = ? AND (${partSql})`;
+            const result = await this.db.query(sql, [tid]);
+            this.parsePermission(result);
+            return result;
+        }
+
+        async savePermission(tid, member, permissionBlock) {
+            const orgMember = await this.getAllDataByCondition({ where: { tid } });
+            const updateMember = [], insertMember = [];
+            for (const om of orgMember) {
+                const nmi = member.findIndex(x => { return om.uid == x.uid; });
+                if (nmi < 0) {
+                    const um = { id: om.id };
+                    for (const p of permissionBlock) {
+                        um[p] = '';
+                    }
+                    updateMember.push(um);
+                } else {
+                    const nm = member[nmi];
+                    const um = { id: om.id };
+                    for (const p in this.PermissionConst) {
+                        if (nm[p]) um[p] = nm[p].join(',');
+                    }
+                    updateMember.push(um);
+                    member.splice(nmi, 1);
+                }
+            }
+            for (const m of member) {
+                const im = { id: this.uuid.v4(), pid: this.ctx.session.sessionProject.id, tid, uid: m.uid };
+                for (const p in this.PermissionConst) {
+                    if (m[p]) im[p] = m[p].join(',');
+                }
+                insertMember.push(im);
+            }
+            const conn = await this.db.beginTransaction();
+            try {
+                if (updateMember.length > 0) await conn.updateRows(this.tableName, updateMember);
+                if (insertMember.length > 0) await conn.insert(this.tableName, insertMember);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+
+    }
+
+    return tenderPermission;
+};
+

+ 4 - 4
app/view/financial/pay_stage_modal.ejs

@@ -58,14 +58,14 @@
 </div>
 <!--付款账号-->
 <div class="modal fade" id="payaccount" data-backdrop="static">
-    <div class="modal-dialog modal-lg" role="document">
+    <div class="modal-dialog modal-lgx" role="document">
         <div class="modal-content">
             <div class="modal-header">
-                <h5 class="modal-title">付款账号(标段名称)</h5>
+                <h5 class="modal-title">付款账号</h5>
             </div>
             <div class="modal-body">
-                <div class="row" style="min-height: 400px;max-height: 700px;">
-                    <div class="col-5" style="min-height: 400px;max-height: 700px;overflow:auto;">
+                <div class="row" style="min-height: 500px;max-height: 500px;">
+                    <div class="col-5" style="min-height: 500px;max-height: 500px;overflow:auto;">
                         <div class="p-2">标段列表</div>
                         <div id="pay-tender-list"></div>
                     </div>

+ 13 - 0
app/view/quality/flaw.ejs

@@ -0,0 +1,13 @@
+<% include ./sub_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div></div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+    </div>
+</div>

+ 0 - 0
app/view/quality/flaw_modal.ejs


+ 106 - 0
app/view/quality/info.ejs

@@ -0,0 +1,106 @@
+<% include ./sub_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div>
+                <div class="d-inline-block">
+                    <div class="dropdown">
+                        <button class="btn btn-sm btn-light dropdown-toggle text-primary" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                            显示层级
+                        </button>
+                        <div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
+                            <a class="dropdown-item" name="showLevel" tag="1" href="javascript: void(0);">第一层</a>
+                            <a class="dropdown-item" name="showLevel" tag="2" href="javascript: void(0);">第二层</a>
+                            <a class="dropdown-item" name="showLevel" tag="3" href="javascript: void(0);">第三层</a>
+                            <a class="dropdown-item" name="showLevel" tag="4" href="javascript: void(0);">第四层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+                <div class="d-inline-block">
+                    <div id="xmj-search"></div>
+                </div>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-header p-0"></div>
+        <div class="row c-body">
+            <div class="col-3 border-right-1 ml-1">
+                <div class="sjs-height-0" id="xmj-spread">
+                </div>
+            </div>
+            <div class="col mr-3" id="quality-detail" style="display: none;">
+                <nav class="nav nav-tabs my-2 sjs-quality-nav" role="tablist">
+                    <!-- 计划设置节点状态-->
+                    <a class="nav-item nav-link px-3 active" data-toggle="tab" href="#gaikuang" role="tab" aria-selected="true">资料概览</a>
+                    <a class="nav-item nav-link px-3" data-toggle="tab" href="#kaigong" role="tab" aria-selected="false">开工</a>
+                    <a class="nav-item nav-link px-3" data-toggle="tab" href="#gongxu" role="tab" aria-selected="false">工序</a>
+                    <a class="nav-item nav-link px-3" data-toggle="tab" href="#pingding" role="tab" aria-selected="false">评定</a>
+                    <a class="nav-item nav-link px-3" data-toggle="tab" href="#jiaogong" role="tab" aria-selected="false">中间交工</a>
+                    <a class="nav-item nav-link px-3" data-toggle="tab" href="#yinbi" role="tab" aria-selected="false">隐蔽工程</a>
+                    <!--<a class="nav-item nav-link px-3" data-toggle="tab" href="#shiyan" role="tab" aria-selected="false">试验检测</a>-->
+                    <!--<a class="nav-item nav-link px-3" data-toggle="tab" href="#waiwei" role="tab" aria-selected="false">外委检测</a>-->
+                    <!--<a class="nav-item nav-link px-3" data-toggle="tab" href="#sanfang" role="tab" aria-selected="false">第三方检测</a>-->
+                    <!--<a class="nav-item nav-link px-3" data-toggle="tab" href="#shigong" role="tab" aria-selected="false">施工日志</a>-->
+                    <div class="ml-auto">
+                        <div class="d-inline-block">
+                            <a class="btn btn-sm btn-primary mr-2" href="javascript: void(0);" id="reload-quality">刷新节点数据</a>
+                            <!--<a class="btn btn-sm btn-primary mr-2" href="#jlrule" data-toggle="modal" data-target="#jlrule">查看计量条件</a>-->
+                            <!--<a class="btn btn-sm btn-primary mr-2" href="#pushjl" data-toggle="modal" data-target="#pushjl">推送状态到计量</a>-->
+                            <!--<a class="btn btn-sm btn-primary" href="#pushjl-bat" data-toggle="modal" data-target="#pushjl-bat">批量推送状态到计量</a>-->
+                        </div>
+                    </div>
+                </nav>
+                <div class="tab-content">
+                    <!--资料概况 -->
+                    <div id="gaikuang" class="tab-pane active">
+                        <div class="row">
+                            <div class="col-3">
+                                <div class="m-3">
+                                    <h6>节点状态</h6>
+                                    <div class="m-3">
+                                        <div id="gxby-info"></div>
+                                        <div class="mt-2"id="dagl-info"></div>
+                                    </div>
+                                </div>
+                            </div>
+                            <div class="col"></div>
+                        </div>
+                        <div class="row">
+                            <div class="col-3">
+                                <div class="m-3">
+                                <h6>资料个数</h6>
+                                <div class="m-3">
+                                    <div id="file-count" class="mb-2">合计:0</div>
+                                    <div id="file-count-wo-yinbi" class="mb-2">不含隐蔽:0</div>
+                                    <div id="file-count-yinbi">隐蔽工程:0</div>
+                                </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <!--开工 -->
+                    <div id="kaigong" class="tab-pane"></div>
+                    <!--工序 -->
+                    <div id="gongxu" class="tab-pane"></div>
+                    <!--评定 -->
+                    <div id="pingding" class="tab-pane"></div>
+                    <!--交工 -->
+                    <div id="jiaogong" class="tab-pane"></div>
+                    <!--隐蔽工程 -->
+                    <div id="yinbi" class="tab-pane"></div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const whiteList = JSON.parse('<%- JSON.stringify(ctx.app.config.multipart.whitelist) %>');
+    const thirdParty = JSON.parse('<%- JSON.stringify(thirdParty) %>');
+    const permission = JSON.parse('<%- JSON.stringify(ctx.permission.quality) %>');
+    const canDelete = false;
+</script>

+ 143 - 0
app/view/quality/info_modal.ejs

@@ -0,0 +1,143 @@
+<div class="modal fade" id="add-kaigong" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新增开工</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group" style="display: none">
+                    <label>开工名称<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="默认显示“开工工序”" type="text" name="kaigong-name">
+                </div>
+                <div class="form-group form-group-sm">
+                    <label>计划开工日期</label>
+                    <input class="datepicker-here form-control form-control-sm" auto-close="true" autocomplete="off" placeholder="点击选择日期" data-date-format="yyyy-MM-dd" data-language="zh" type="text" name="kaigong-date">
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="add-kaigong-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="add-gongxu" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新增工序</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label>工序名称<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm" placeholder="输入工序名称" type="text" name="gongxu-name">
+                    <div class="invalid-feedback" id="gongxu-name-hint">名称超过100个字,请缩减名称。</div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="gongxu-id">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="add-gongxu-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="add-yinbi" data-backdrop="static" style="display: none;" aria-hidden="true">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新增隐蔽工程</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group row">
+                    <label class="col-sm-2 text-right col-form-label"><b class="text-danger">*</b>名称:</label>
+                    <div class="col-sm-10 pl-0">
+                        <input type="email" class="form-control form-control-sm" placeholder="输入名称" name="yinbi-name">
+                    </div>
+                </div>
+                <div class="form-group row">
+                    <label class="col-sm-2 text-right col-form-label">工序:</label>
+                    <div class="col-sm-10 pl-0">
+                        <select class="form-control form-control-sm" name="yinbi-gongxu-id">
+                        </select>
+                    </div>
+                </div>
+                <div class="form-group row">
+                    <label class="col-sm-2 text-right">工程部位及桩号:</label>
+                    <div class="col-sm-10 pl-0">
+                        <textarea class="form-control form-control-sm" id="" rows="5" name="yinbi-gcbw"></textarea>
+                    </div>
+                </div>
+                <div class="form-group row">
+                    <label class="col-sm-2 text-right">隐蔽工程说明:</label>
+                    <div class="col-sm-10 pl-0">
+                        <textarea class="form-control form-control-sm" id="" rows="5" name="yinbi-content"></textarea>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="yinbi-id">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-sm btn-primary" id="add-yinbi-ok">确定</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="add-file" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">上传文件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group text-danger" style="display: none" id="upload-file-hint">
+                </div>
+                <div class="form-group">
+                    <label for="formGroupExampleInput">单个文件大小限制:50MB,支持<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="rar,zip">压缩包格式</span></label>
+                    <input type="file" class="" id="upload-file" multiple>
+                </div>
+                <div class="form-group spec-type">
+                    <div class="form-row align-items-center mb-2">
+                        <div class="col-auto"><span class="form-check-label">文件类型:</span></div>
+                        <div class="col-auto spec-type-detail spec-type-qa">
+                            <div class="form-check form-check-inline">
+                                <input class="form-check-input" type="checkbox" id="inlineCheckbox1" value="qa" name="specType">
+                                <label class="form-check-label" for="inlineCheckbox1">质检</label>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="add-file-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="add-big-file" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">上传附件</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group text-danger" style="display: none" id="upload-big-file-hint">
+                </div>
+                <div class="form-group">
+                    <label for="formGroupExampleInput">文件大小限制:500MB,支持<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="doc,docx,xls,xlsx,ppt,pptx,pdf">office等文档格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="jpg,png,bmp">图片格式</span>、<span data-toggle="tooltip" data-placement="bottom" title="" data-original-title="rar,zip">压缩包格式</span></label>
+                    <input type="file" class="" id="upload-big-file">
+                </div>
+                <div class="form-group progress">
+                    <div id="upload-big-file-progress" class="progress-bar" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100"></div>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-primary btn-sm" id="add-big-file-stop" style="display: none">暂停</button>
+                <button type="button" class="btn btn-primary btn-sm" id="add-big-file-resume" style="display: none">重新上传</button>
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="add-big-file-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 13 - 0
app/view/quality/lab.ejs

@@ -0,0 +1,13 @@
+<% include ./sub_memu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./sub_mini_menu.ejs %>
+            <div></div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+    </div>
+</div>

+ 79 - 0
app/view/quality/rule.ejs

@@ -0,0 +1,79 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main d-flex">
+            <div> 状态推送规则 </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <nav class="nav nav-tabs m-3" role="tablist">
+                    <a class="nav-item nav-link active" data-toggle="tab" href="#rule-group" role="tab">规则组配置</a>
+                    <a class="nav-item nav-link" data-toggle="tab" href="#ledger" role="tab">节点设置</a>
+                </nav>
+                <div class="m-3">
+                    <div class="tab-content">
+                        <div class="tab-pane active" id="rule-group">
+                            <div class="row">
+                                <div class="col-3">
+                                    <div class="d-flex flex-row mb-2">
+                                        <button class="btn btn-sm btn-light text-primary" id="addGroup"><i class="fa fa-plus" aria-hidden="true"></i> 新增规则组</button>
+                                    </div>
+                                    <div>
+                                        <dl class="list-group" id="group-list">
+                                            <% for (const group of ruleGroups) { %>
+                                            <dd class="list-group-item" groupId="<%- group.group_id %>">
+                                                <div class="d-flex justify-content-between align-items-center table-file" groupId="<%- group.group_id %>">
+                                                    <div><%- group.group_name %>%></div>
+                                                    <div class="btn-group-table" style="display: none;">
+                                                        <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="编辑" name="renameGroup"><i class="fa fa-pencil fa-fw"></i></a>
+                                                        <a href="javascript: void(0);" class="mr-1" data-toggle="tooltip" data-placement="bottom" data-original-title="删除" name="delGroup"><i class="fa fa-trash-o fa-fw text-danger"></i></a>
+                                                    </div>
+                                                </div>
+                                            </dd>
+                                            <% } %>
+                                        </dl>
+                                    </div>
+                                </div>
+                                <div class="col-9">
+                                    <div class="d-flex flex-row mb-2">
+                                        <button class="btn btn-sm btn-light text-primary" id="add-rule"><i class="fa fa-plus" aria-hidden="true"></i> 新增规则</button>
+                                        <button class="btn btn-sm btn-light text-primary" data-toggle="modal" data-target="#copy-rule"><i class="fa fa-paste"></i> 拷贝规则</button>
+                                    </div>
+                                    <div>
+                                        <table class="table table-sm table-bordered">
+                                            <tr class="text-center"><th width="10%">规则名称</th><th width="46%">条件详情</th><th>推送状态</th><th width="10%">操作</th></tr>
+                                            <tbody id="ruleOptions">
+                                            </tbody>
+                                        </table>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                        <div class="tab-pane" id="ledger">
+                            <div class="row">
+                                <div class="col-3">
+                                    <div class="sjs-height-2 scroll-y">
+                                        <table class="table table-sm table-bordered table-hover">
+                                            <tr class="text-center"><th>标段名称</th></tr>
+                                            <% for (const t of tenderList) { %>
+                                            <tr tid="<%- t.id %>"><td><%- t.name %></td></tr>
+                                            <% } %>
+                                        </table>
+                                    </div>
+                                </div>
+                                <div class="col-9">
+                                    <div class="sjs-height-2" id="xmj-spread"></div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const thirdParty = JSON.parse('<%- JSON.stringify(thirdParty) %>');
+    const ruleGroups = JSON.parse('<%- JSON.stringify(ruleGroups) %>');
+</script>

+ 102 - 0
app/view/quality/rule_modal.ejs

@@ -0,0 +1,102 @@
+<div class="modal fade" id="save-rule" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">新增/修改规则</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <label>规则名称</label>
+                    <input class="form-control form-control-sm col-11 ml-3"  placeholder="请使用精简的名称" type="text" id="rule-name">
+                </div>
+                <div class="form-group">
+                    <label>判断条件</label>
+                    <div class="ml-4 mb-2">
+                        <div>
+                            <div class="d-inline-block">
+                                <select class="form-control form-control-sm" id="condition-block" style="width: 150px;">
+                                </select>
+                            </div>
+                            <div class="d-inline-block ml-2">
+                                <select class="form-control form-control-sm" id="condition-field" style="width: 150px;">
+                                </select>
+                            </div>
+                            <div class="d-inline-block ml-2">
+                                <select class="form-control form-control-sm" id="condition-operate" style="width: 80px;">
+                                </select>
+                            </div>
+                            <div class="d-inline-block ml-2">
+                                <button class="btn btn-sm btn-primary" id="condition-add">新增判断</button>
+                            </div>
+                        </div>
+                        <div class="mt-1">
+                            <div>
+                                <input class="form-control form-control-sm col-11"  placeholder="请输入判断值" type="text" id="condition-value">
+                            </div>
+                        </div>
+                        <div class="mt-1" id="multi-set">
+                            <div class="d-inline-block">
+                                <div class="form-check form-check-inline">
+                                    <input class="form-check-input" type="checkbox" id="condition-multi">
+                                    <label class="form-check-label" for="condition-multi">匹配多个<span class="text-warning ml-2">如需匹配多个值,请在判断中使用;(英文分号)来分隔多个值,示例:批复单;记录表</span></label>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <table class="table table-sm table-bordered col-11 ml-3">
+                        <tr class="text-center"><th width="80%">判断条件</th><th width="20%">操作</th></tr>
+                        <tbody id="condition-list"></tbody>
+                    </table>
+                </div>
+                <div class="form-group">
+                    <label>更新状态</label>
+                    <div class="ml-4 mb-2">
+                        <div class="d-inline-block">
+                            <select class="form-control form-control-sm" id="status-field" style="width: 150px;">
+                            </select>
+                        </div>
+                        <div class="d-inline-block ml-2">
+                            <select class="form-control form-control-sm" id="status-value" style="width: 150px;">
+                            </select>
+                        </div>
+                        <div class="d-inline-block ml-2">
+                            <button class="btn btn-sm btn-primary" id="status-add">新增状态</button>
+                        </div>
+                    </div>
+                    <table class="table table-sm table-bordered col-11 ml-3">
+                        <tr class="text-center"><th width="80%">更新状态</th><th width="20%">操作</th></tr>
+                        <tbody id="status-list"></tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="rule-id">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="save-rule-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal fade" id="copy-rule" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">拷贝规则</h5>
+            </div>
+            <div class="modal-body">
+                <div class="form-group">
+                    <select class="form-control form-control-sm col-5 ml-3" id="select-group">
+                    </select>
+                    <table class="table table-sm table-bordered col-11 ml-3 mt-2">
+                        <tr class="text-center"><th>选择</th><th>规则名称</th><th>条件详情</th><th>推送状态</th></tr>
+                        <tbody id="copy-rule-list"></tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-secondary btn-sm" data-dismiss="modal">关闭</button>
+                <button type="button" class="btn btn-primary btn-sm" id="copy-rule-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>

+ 14 - 0
app/view/quality/sub_memu.ejs

@@ -0,0 +1,14 @@
+<div class="panel-sidebar" id="sub-menu">
+    <div class="sidebar-title" data-toggle="tooltip" data-placement="right" data-original-title="<%- ctx.tender.data.name %>">
+        <%- (ctx.tender.data.name.length > 15 ? ctx.tender.data.name.substring(0,15) + '...' : ctx.tender.data.name) %>
+    </div>
+    <div class="scrollbar-auto">
+        <% include ./sub_memu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0)" data-toggle="tooltip" data-placement="top" data-original-title="折叠侧栏" id="to-mini-menu"><i class="fa fa-upload fa-rotate-270"></i></a></div>
+    </div>
+    <script>
+        new Vue({
+            el: '.scrollbar-auto',
+        });
+    </script>
+</div>

+ 4 - 0
app/view/quality/sub_memu_list.ejs

@@ -0,0 +1,4 @@
+<nav-menu title="返回" url="/quality" tclass="text-primary" ml="1" icon="fa-chevron-left"></nav-menu>
+<nav-menu title="工程资料" url="/quality/tender/<%= ctx.tender.id %>/info%>" ml="3" active="<%= ctx.url.indexOf('/info') %>"></nav-menu>
+<!--<nav-menu title="缺陷管理" url="/quality/tender/<%= ctx.tender.id %>/flaw%>" ml="3" active="<%= ctx.url.indexOf('/flaw') %>"></nav-menu>-->
+<!--<nav-menu title="试验报告" url="/quality/tender/<%= ctx.tender.id %>/lab%>" ml="3" active="<%= ctx.url.indexOf('/lab') %>"></nav-menu>-->

+ 16 - 0
app/view/quality/sub_mini_menu.ejs

@@ -0,0 +1,16 @@
+<!--折起的菜单-->
+<div class="min-side" id="sub-mini-menu" style="display: none;">
+    <div id="sub-mini-hint" class="side-switch" data-container="body" data-toggle="popover" data-placement="bottom" data-content="这里打开收起的菜单栏"></div>
+    <div class="side-switch">
+        <i class="fa fa-bars"></i>
+    </div>
+    <div class="side-menu" id="mini-menu-list" style="display: none">
+        <% include ./sub_memu_list.ejs %>
+        <div class="side-fold"><a href="javascript: void(0);" data-toggle="tooltip" data-placement="top" data-original-title="展开侧栏" id="to-menu"><i class="fa fa-upload fa-rotate-90"></i></a></div>
+    </div>
+</div>
+<script>
+    new Vue({
+        el: '.side-menu',
+    });
+</script>

+ 34 - 0
app/view/quality/tender.ejs

@@ -0,0 +1,34 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex justify-content-between">
+            <div>
+                <div class="d-inline-block mr-2">
+                    <button type="button" class="btn btn-sm btn-light dropdown-toggle text-primary" data-toggle="dropdown">展开/收起</button>
+                    <div class="dropdown-menu">
+                        <a class="dropdown-item tree-toggle" href="javascript:void(0);" data-item="open">展开所有</a>
+                        <a class="dropdown-item tree-toggle" href="javascript:void(0);" data-item="hide">收起所有</a>
+                    </div>
+                </div>
+            </div>
+            <div class="ml-auto">
+                <% if (ctx.session.sessionUser.is_admin) { %>
+                <a class="btn btn-sm btn-primary mr-2" href="/quality/rule">设置状态规则</a>
+                <% } %>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="sjs-height-0" style="background-color: #fff">
+            <div class="c-body">
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenderList)) %>'));
+    const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
+    const is_admin = <%- ctx.session.sessionUser.is_admin %>;
+
+    const pid = '<%- ctx.session.sessionProject.id %>';
+    const uphlname = 'user_<%- ctx.session.sessionUser.accountId %>_pro_<% ctx.session.sessionProject.id %>_category_hide_list';
+</script>

+ 1 - 0
app/view/quality/tender_modal.ejs

@@ -0,0 +1 @@
+<% include ../shares/tender_permission_modal.ejs %>

+ 68 - 0
app/view/shares/tender_permission_modal.ejs

@@ -0,0 +1,68 @@
+<!--成员管理-->
+<div class="modal" id="member" data-backdrop="static">
+    <div class="modal-dialog modal-lg" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">成员管理</h5>
+            </div>
+            <div class="modal-body" style="overflow-y: auto; height: 480px">
+                <div class="dropdown">
+                    <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                        添加用户
+                    </button>
+                    <div class="dropdown-menu" aria-labelledby="dropdownMenuButton" style="width:220px">
+                        <div class="mb-2 p-2"><input class="form-control form-control-sm" placeholder="姓名/手机 检索" id="member-search" autocomplete="off"></div>
+                        <dl class="list-unstyled book-list">
+                            <% accountGroup.forEach((group, idx) => { %>
+                            <dt><a href="javascript: void(0);" class="acc-btn" data-groupid="<%- idx %>" data-type="hide"><i class="fa fa-plus-square"></i></a> <%- group.groupName %></dt>
+                            <div class="dd-content" data-toggleid="<%- idx %>">
+                                <% group.groupList.forEach(item => { %>
+                                <dd class="border-bottom p-2 mb-0 " data-id="<%- item.id %>" >
+                                    <p class="mb-0 d-flex"><span class="text-primary"><%- item.name %></span><span
+                                                class="ml-auto"><%- item.mobile %></span></p>
+                                    <span class="text-muted"><%- item.role %></span>
+                                </dd>
+                                <% });%>
+                            </div>
+                            <% }) %>
+                        </dl>
+                    </div>
+                </div>
+                <div class="mt-1">
+                    <table class="table table-bordered">
+                        <thead class="text-center">
+                        <tr>
+                            <th class="align-middle" rowspan="2">成员名称</th>
+                            <th class="align-middle" rowspan="2">角色/职位</th>
+                            <% for (const pb of permissionBlock) { %>
+                            <th colspan="<%- pb.permission.filter(x => { return !x.isDefault; }).length %>"><%- pb.name %></th>
+                            <% } %>
+                            <th class="align-middle" rowspan="2">操作</th>
+                        </tr>
+                        <tr>
+                            <% for (const pb of permissionBlock) { %>
+                                <% for (const p of pb.permission) { %>
+                                    <% if (p.isDefault) continue; %>
+                                    <th><%- p.title %></th>
+                                <% } %>
+                            <% } %>
+                        </tr>
+                        </thead>
+                        <tbody id="member-list">
+                        </tbody>
+                    </table>
+                </div>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-primary" id="member-ok">确认修改</button>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const accountList = JSON.parse('<%- JSON.stringify(accountList) %>');
+    const accountGroup = JSON.parse('<%- JSON.stringify(accountGroup) %>');
+    const permissionConst = JSON.parse('<%- JSON.stringify(permissionConst) %>');
+    const permissionBlock = JSON.parse('<%- JSON.stringify(permissionBlock) %>');
+</script>

+ 2 - 2
app/view/stage/bwtz.ejs

@@ -36,7 +36,7 @@
                     <a id="exportBwtz" class="btn btn-primary btn-sm" href="javascript: void(0)">导出部位台账Excel</a>
                 </div>
                 <div class="d-inline-block ml-2">
-                    <div class="alert alert-warning p-1"><i class="fa Example of exclamation-circle fa-exclamation-circle "></i> 计量台账与部位台账金额,会存在数学误差,属正常现象。软件以计量台账金额为准。</div>
+                    <div class="alert alert-warning p-1"><i class="fa Example of exclamation-circle fa-exclamation-circle "></i> 计量台账与部位台账金额,会存在数学误差,属正常现象。软件以计量台账金额为准。(此页面的金额数据,不适用于“总价合同”的标段)</div>
                 </div>
             </div>
             <div class="ml-auto">
@@ -248,7 +248,7 @@
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_contract_tp', hAlign: 2, width: 60, type: 'Number'},
             {title: '截止本期数量变更|数量', colSpan: '3|1', rowSpan: '1|1', field: 'end_qc_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_qc_tp', hAlign: 2, width: 60, type: 'Number'},
-            {title: '|不计价', colSpan: '|1', rowSpan: '|1', field: 'qc_minus_qty', hAlign: 2, width: 60, type: 'Number', visible: <%- minusNoValue %>},
+            {title: '|不计价', colSpan: '|1', rowSpan: '|1', field: 'end_qc_minus_qty', hAlign: 2, width: 60, type: 'Number', visible: <%- minusNoValue %>},
             {title: '截止本期完成计量|数量', colSpan: '3|1', rowSpan: '1|1', field: 'end_gather_qty', hAlign: 2, width: 60, type: 'Number'},
             {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_gather_tp', hAlign: 2, width: 60, type: 'Number'},
             {title: '|完成率(%)', colSpan: '|1', rowSpan: '|1', field: 'end_final_1_percent', hAlign: 2, width: 80, type: 'Number'},

+ 92 - 0
config/web.js

@@ -2141,6 +2141,98 @@ const JsFiles = {
                 mergeFile: 'spss_compare_stage',
             },
         },
+        quality: {
+            tender: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/PinYinOrder.bundle.js',
+                    '/public/js/shares/tender_list_order.js',
+                    '/public/js/shares/show_level.js',
+                    '/public/js/shares/tender_permission.js',
+                    '/public/js/tender_showhide.js',
+                    '/public/js/tender_list_base.js',
+                    '/public/js/quality_tender.js',
+                ],
+                mergeFile: 'quality_tender',
+            },
+            info: {
+                files: [
+                    '/public/js/axios/axios.min.js', '/public/js/file-saver/FileSaver.js', '/public/js/js-xlsx/jszip.min.js',
+                    '/public/js/shares/aliyun-oss-sdk.min.js',
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/ali_oss.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/quality_info.js',
+                ],
+                mergeFile: 'quality_info',
+            },
+            flaw: {
+                files: [
+                    '/public/js/axios/axios.min.js', '/public/js/file-saver/FileSaver.js', '/public/js/js-xlsx/jszip.min.js',
+                    '/public/js/shares/aliyun-oss-sdk.min.js',
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/component/menu.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/ali_oss.js',
+                    '/public/js/path_tree.js',
+                ],
+                mergeFile: 'quality_flaw',
+            },
+            lab: {
+                files: [
+                    '/public/js/axios/axios.min.js', '/public/js/file-saver/FileSaver.js', '/public/js/js-xlsx/jszip.min.js',
+                    '/public/js/shares/aliyun-oss-sdk.min.js',
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/shares/ali_oss.js',
+                    '/public/js/path_tree.js',
+                ],
+                mergeFile: 'quality_lab',
+            },
+            rule: {
+                files: [
+                    '/public/js/moment/moment.min.js',
+                    '/public/js/component/menu.js',
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/sub_menu.js',
+                    '/public/js/div_resizer.js',
+                    '/public/js/spreadjs_rela/spreadjs_zh.js',
+                    '/public/js/shares/sjs_setting.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/quality_rule.js',
+                ],
+                mergeFile: 'quality_rule',
+            },
+        },
     },
 };