瀏覽代碼

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

TonyKang 4 年之前
父節點
當前提交
a52a5a6f10

+ 10 - 0
app/const/schedule.js

@@ -0,0 +1,10 @@
+'use strict';
+
+const plan_mode = {
+    tp: 1,
+    gcl: 2,
+};
+
+module.exports = {
+    plan_mode,
+};

+ 55 - 4
app/controller/schedule_controller.js

@@ -10,19 +10,27 @@
 
 const moment = require('moment');
 const measureType = require('../const/tender').measureType;
+const scheduleConst = require('../const/schedule');
 const billsPosConvert = require('../lib/bills_pos_convert');
 const _ = require('lodash');
 
 module.exports = app => {
     class ScheduleController extends app.BaseController {
+
+        async _getSelectedLedgerList(ctx) {
+            const scheduleLedgerList = await ctx.service.scheduleLedger.getAllDataByCondition({ where: { tid: ctx.tender.id } });
+            return _.map(scheduleLedgerList, 'ledger_id');
+        }
+
         async index(ctx) {
             try {
                 const renderData = {
                     tender: ctx.tender.data,
                     tenderMenu: this.menu.tenderMenu,
+                    scheduleLedgerList: await this._getSelectedLedgerList(ctx),
                     preUrl: '/tender/' + ctx.tender.id,
                 };
-                await this.layout('schedule/index.ejs', renderData);
+                await this.layout('schedule/index.ejs', renderData, 'schedule/modal.ejs');
             } catch (err) {
                 this.log(err);
                 ctx.redirect(this.menu.menu.dashboard.url);
@@ -31,15 +39,31 @@ module.exports = app => {
 
         async ledger(ctx) {
             const tender = ctx.tender;
-            const scheduleLedgerList = await ctx.service.scheduleLedger.getAllDataByCondition({ where: { tid: tender.id } });
             const renderData = {
                 tender: tender.data,
                 tenderInfo: tender.info,
                 measureType,
-                scheduleLedgerList: _.map(scheduleLedgerList, 'ledger_id'),
+                scheduleLedgerList: await this._getSelectedLedgerList(ctx),
                 jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.schedule.ledger),
             };
-            await this.layout('schedule/ledger.ejs', renderData, 'schedule/ledger_modal.ejs');
+            await this.layout('schedule/ledger.ejs', renderData);
+        }
+
+        async plan(ctx) {
+            const tender = ctx.tender;
+            const schedule = await ctx.service.schedule.getDataByCondition({ tid: tender.id });
+            const scheduleMonth = await ctx.service.scheduleMonth.getAllDataByCondition({ tid: tender.id });
+            const renderData = {
+                tender: tender.data,
+                tenderInfo: tender.info,
+                schedule,
+                scheduleMonth,
+                measureType,
+                mode: scheduleConst.plan_mode,
+                scheduleLedgerList: await this._getSelectedLedgerList(ctx),
+                jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.schedule.plan),
+            };
+            await this.layout('schedule/plan.ejs', renderData, 'schedule/plan_modal.ejs');
         }
 
         /**
@@ -79,6 +103,33 @@ module.exports = app => {
                 ctx.body = { err: 1, msg: err.toString(), data: [] };
             }
         }
+
+        /**
+         * 计划进度计算方式提交(Ajax)
+         *
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async savePlan(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                const responseData = {
+                    err: 0,
+                    msg: '',
+                    data: {},
+                };
+                switch (data.type) {
+                    case 'mode':
+                        responseData.data = await ctx.service.schedule.saveMode(data.postData);
+                        break;
+                    default: throw '参数有误';
+                }
+                ctx.body = responseData;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
     }
 
     return ScheduleController;

+ 3 - 0
app/controller/stage_controller.js

@@ -326,6 +326,9 @@ module.exports = app => {
                             });
                         case 'tag':
                             responseData.data.tags = await ctx.service.ledgerTag.getDatas(ctx.tender.id, ctx.stage.id);
+                        case 'cooperation':
+                            responseData.data.cooperation = await this.ctx.service.ledgerCooperation.getValidData(
+                                ctx.tender.id, ctx.session.sessionUser.accountId);
                             break;
                     }
                 }

+ 73 - 3
app/controller/tender_controller.js

@@ -16,6 +16,10 @@ const auditConst = require('../const/audit');
 const shenpiConst = require('../const/shenpi');
 const accountGroup = require('../const/account_group').group;
 const accountPermission = require('../const/account_permission');
+const measureType = require('../const/tender').measureType;
+const billsPosConvert = require('../lib/bills_pos_convert');
+const path = require('path');
+const sendToWormhole = require('stream-wormhole');
 
 module.exports = app => {
 
@@ -796,14 +800,25 @@ module.exports = app => {
                 });
             }
             const categoryData = await ctx.service.category.getAllCategory(ctx.session.sessionProject.id);
+            // 是否修订中
+            const lastRevise = await ctx.service.ledgerRevise.getLastestRevise(ctx.tender.id);
+            const revising = (lastRevise && lastRevise.status !== auditConst.revise.status.checked) || false;
+
+            const cooperationNum = await ctx.service.ledgerCooperation.count({ tid: ctx.tender.id, status: 1 });
+
             const renderData = {
                 shenpi: shenpiConst,
                 accountList,
                 accountGroup: accountGroupList,
                 tenders,
                 categoryData,
+                auditConst,
+                revising,
+                measureType,
+                cooperationNum,
+                jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.tender.shenpi),
             };
-            await this._list('tender/shenpi.ejs', renderData, 'tender/shenpi_modal.ejs');
+            await this.layout('tender/shenpi.ejs', renderData, 'tender/shenpi_modal.ejs');
         }
 
         async saveTenderInfoShenpi(ctx) {
@@ -828,6 +843,10 @@ module.exports = app => {
                 // } else {
                 const postData = ctx.tender.info.shenpi;
                 postData[data.code] = data.status;
+                if (data.code === shenpiConst.sp_lc[shenpiConst.sp_type.stage - 1].code) {
+                    const status = parseInt(data.status) === shenpiConst.sp_status.gdspl ? 1 : 0;
+                    await ctx.service.ledgerCooperation.changeAllStatus(status);
+                }
                 // }
                 // console.log(postData);
                 await ctx.service.tenderInfo.saveTenderInfo(ctx.tender.id, { shenpi: postData });
@@ -854,6 +873,7 @@ module.exports = app => {
                 if (ctx.session.sessionUser.is_admin === 0) {
                     throw '你没有权限修改审批流程';
                 }
+                let info = '';
                 switch (data.type) {
                     case 'add':
                         const result = await ctx.service.shenpiAudit.addAudit(data);
@@ -870,14 +890,42 @@ module.exports = app => {
                     case 'copy2os':
                         await ctx.service.shenpiAudit.copyAudit2otherShenpi(data);
                         break;
+                    case 'pwd':
+                        info = await ctx.service.ledgerCooperation.save(data);
+                        break;
                     default:break;
                 }
-                ctx.body = { err: 0, msg: '' };
+                ctx.body = { err: 0, msg: '', data: info };
             } catch (err) {
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '保存审批流程设置失败');
             }
         }
+
+        /**
+         * 签名,上传图片 (Ajax)
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async saveCooperateSign(ctx) {
+            try {
+                const stream = await ctx.getFileStream();
+                const create_time = Date.parse(new Date()) / 1000;
+                const id = stream.fields.id;
+                const fileInfo = path.parse(stream.filename);
+                const fileName = path.join('public/upload', ctx.tender.id.toString(), 'sign', 'signImg_' + create_time + fileInfo.ext);
+                await ctx.helper.saveStreamFile(stream, path.join(this.app.baseDir, 'app', fileName));
+                if (stream) {
+                    await sendToWormhole(stream);
+                }
+                await ctx.service.ledgerCooperation.saveSign(id, fileName);
+                ctx.body = { err: 0, msg: '', data: fileName };
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: null };
+            }
+        }
+
         /**
          * 拷贝设置
          * @param {object} ctx - 上下文
@@ -894,11 +942,33 @@ module.exports = app => {
             }
         }
 
+        /**
+         * 获取部位明细数据(Ajax)
+         *
+         * @param ctx
+         * @return {Promise<void>}
+         */
+        async loadLedgerData(ctx) {
+            try {
+                const ledgerData = await ctx.service.ledger.getData(ctx.tender.id);
+                const posData = ctx.tender.data.measure_type === measureType.tz.value
+                    ? await ctx.service.pos.getPosData({ tid: ctx.tender.id }) : [];
+                const convert = new billsPosConvert(ctx);
+                convert.loadData(ledgerData, posData, []);
+                const result = await convert.convert();
+                const ledgerCooperationList = await ctx.service.ledgerCooperation.getAllDataByCondition({ where: { tid: ctx.tender.id, status: 1 } });
+                ctx.body = { err: 0, msg: '', data: { ledgerList: result, ledgerCooperationList } };
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: [] };
+            }
+        }
+
         async billsTag(ctx) {
             try {
                 const data = JSON.parse(ctx.request.body.data);
                 const result = await ctx.service.ledgerTag.update(data);
-                ctx.body = {err: 0, msg: '', data: result};
+                ctx.body = { err: 0, msg: '', data: result };
             } catch (err) {
                 this.log(err);
                 ctx.body = this.ajaxErrorBody(err, '书签数据错误');

+ 1 - 1
app/middleware/url_parse.js

@@ -22,7 +22,7 @@ module.exports = options => {
         this.urlInfo = urlInfo;
         // 防止为空
         this.request.headers.referer = this.request.headers.referer === undefined ? '/dashboard' : this.request.headers.referer;
-        // 清空原Message
+        // 清空原Message---有bug,不应该在有message未展示前被清除
         if (this.session) {
             this.session.message = null;
         }

+ 51 - 0
app/public/js/path_tree.js

@@ -1118,6 +1118,36 @@ const createNewPathTree = function (type, setting) {
                 this.stageItems[keyName] = data;
             }
         }
+        // 解锁相关
+        _loadPwdCache() {
+            const cache = getLocalCache(this.pwdCacheKey);
+            if (!cache) return;
+
+            const cacheArr = cache.split('|');
+            for (const ca of cacheArr) {
+                if (!ca) continue;
+
+                const [ledger_id, pwd] = ca.split(':');
+                if (!ledger_id || !pwd) continue;
+
+                const p = this.pwd.find(x => {return x.ledger_id == ledger_id});
+                p.check = p.pwd === pwd;
+            }
+        }
+        loadPwd(data, cacheKey) {
+            this.loadingPwd = true;
+            try {
+                this.pwdCacheKey = cacheKey;
+                this.pwd = data;
+                this._loadPwdCache();
+                for (const p of this.pwd) {
+                    p.node = this.getItems(p.ledger_id);
+                    this.lockNode(p, !p.check);
+                }
+            } catch(err) {}
+            this.loadingPwd = false;
+        }
+
         getStageItems(id) {
             return this.stageItems[itemsPre + id];
         }
@@ -1274,6 +1304,27 @@ const createNewPathTree = function (type, setting) {
             }
             return result;
         }
+
+        _savePwdCache() {
+            const cache = [];
+            for (const p of this.pwd) {
+                if (p.check && p.pwd) cache.push(p.ledger_id + ':' + p.pwd);
+            }
+            setLocalCache(this.pwdCacheKey, cache.join('|'));
+        }
+        lockNode(p, isLock) {
+            const refresh = [];
+            p.check = !isLock;
+            p.node.lock = isLock;
+            refresh.push(this.getNodeIndex(p.node));
+            const posterity = this.getPosterity(p.node);
+            for (const pn of posterity) {
+                pn.lock = isLock;
+                refresh.push(this.getNodeIndex(pn));
+            }
+            if (!this.loadingPwd) this._savePwdCache();
+            return refresh;
+        }
     }
 
     class MasterTree extends FxTree {

+ 19 - 1
app/public/js/schedule_ledger.js

@@ -176,7 +176,25 @@ $(function () {
                 window.location.reload();
             },1500);
         });
-    })
+    });
+
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            ledgerSpread.refresh();
+            autoFlashHeight();
+        }
+    });
 
 
     // // 显示层次

+ 155 - 0
app/public/js/schedule_plan.js

@@ -0,0 +1,155 @@
+/**
+ * 进度台账相关js
+ *
+ * @author Ellisran
+ * @date 2020/11/6
+ * @version
+ */
+function getTenderId() {
+    return window.location.pathname.split('/')[2];
+}
+$(function () {
+    autoFlashHeight();
+    if(schedule && !schedule.mode) {
+        $('#mode').modal('show');
+    }
+    // 初始化台账
+    const ledgerSpread = SpreadJsObj.createNewSpread($('#ledger-spread')[0]);
+    const treeSetting = {
+        id: 'ledger_id',
+        pid: 'ledger_pid',
+        order: 'order',
+        level: 'level',
+        rootId: -1,
+        fullPath: 'full_path',
+        calcFields: ['total_price']
+        //treeCacheKey: 'ledger_bills_fold' + '_' + getTenderId(),
+        // markFoldKey: 'bills-fold',
+        // markFoldSubKey: window.location.pathname.split('/')[2],
+    };
+    treeSetting.calcFun = function (node) {
+        node.dgn_price = ZhCalc.round(ZhCalc.div(node.total_price, node.dgn_qty1), 2);
+    };
+    const ledgerTree = createNewPathTree('base', treeSetting);
+
+    const ledgerSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 145, formatter: '@', readOnly: true, cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', readOnly: true},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 50, formatter: '@', readOnly: true},
+            {title: '经济指标', colSpan: '1', rowSpan: '2', field: 'dgn_price', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+            {title: '总设计|工程量', colSpan: '2|1', rowSpan: '1|1', field: 'dgn_qty1', hAlign: 2, width: 70, type: 'Number', readOnly: true},
+            {title: '|金额(万元)', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 70, type: 'Number', readOnly: true},
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        // readOnly: true,
+        localCache: {
+            key: 'ledger-bills',
+            colWidth: true,
+        }
+    };
+
+    sjsSettingObj.setFxTreeStyle(ledgerSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    if (thousandth) sjsSettingObj.setTpThousandthFormat(ledgerSpreadSetting);
+    SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), ledgerSpreadSetting);
+    SpreadJsObj.selChangedRefreshBackColor(ledgerSpread.getActiveSheet());
+
+    postData('/tender/' + getTenderId() + '/schedule/ledger/load', {}, function (data) {
+        console.log(data, selectedLedgerList);
+        const treeData = [];
+        for(const sl of selectedLedgerList) {
+            const one = _.find(data, { 'ledger_id' : sl });
+            treeData.push(one);
+        }
+        ledgerTree.loadDatas(treeData);
+        treeCalc.calculateAll(ledgerTree);
+        // console.log(ledgerTree);
+        SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), SpreadJsObj.DataType.Tree, ledgerTree);
+    }, null, true);
+
+    const ledgerSpreadObj = {
+        refreshTree: function (sheet, data) {
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    data.delete.sort(function (x, y) {
+                        return y.deleteIndex - x.deleteIndex;
+                    });
+                    for (const d of data.delete) {
+                        sheet.deleteRows(d.deleteIndex, 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            return a.index - b.index;
+                        });
+
+                        for (const node of newNodes) {
+                            sheet.addRows(node.index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+        },
+    };
+    // 进度计算方式选择
+    $('.mode-select').on('click', function () {
+        const _self = $(this);
+        postData(window.location.pathname + '/save', {type: 'mode', postData: $(this).data('mode')}, function (result) {
+            _self.addClass('disabled').attr('disabled', true);
+            _self.parents('.col-6').siblings('.col-6').find('button').removeClass('disabled').removeAttr('disabled');
+            $('#mode-tips').show();
+            $('#mode-cancel').show();
+            $('#mode').modal('hide');
+        })
+    });
+
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            ledgerSpread.refresh();
+            autoFlashHeight();
+        }
+    });
+});

+ 458 - 0
app/public/js/shenpi.js

@@ -305,6 +305,7 @@ $(document).ready(function () {
                 }
                 addhtml += '</ul>\n';
                 _self.parents('.form-group').siblings('.lc-show').html(addhtml);
+
             } else if (this_status === sp_status.gdzs) {
                 let addhtml = '<ul class="list-unstyled">\n' +
                     '                                        <li class="d-flex justify-content-start mb-3">\n' +
@@ -317,6 +318,14 @@ $(document).ready(function () {
                 addhtml += '</ul>\n';
                 _self.parents('.form-group').siblings('.lc-show').html(addhtml);
             }
+
+            if(this_code === 'stage') {
+                if(this_status === sp_status.gdspl) {
+                    $('#stage_cooperation').show();
+                } else {
+                    $('#stage_cooperation').hide();
+                }
+            }
         });
     });
 
@@ -559,4 +568,453 @@ $(document).ready(function () {
             }, 1000)
         })
     });
+
+    const ledgerSpread = SpreadJsObj.createNewSpread($('#ledger-spread')[0]);
+    const treeSetting = {
+        id: 'ledger_id',
+        pid: 'ledger_pid',
+        order: 'order',
+        level: 'level',
+        rootId: -1,
+        fullPath: 'full_path',
+    };
+    const ledgerTree = createNewPathTree('base', treeSetting);
+
+    const ledgerSpreadSetting = {
+        cols: [
+            {title: '编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 165, formatter: '@', readOnly: true, cellType: 'tree'},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', readOnly: true},
+            {title: '密码', colSpan: '1', rowSpan: '2', field: 'pwd', hAlign: 0, width: 100, formatter: '@', getValue:'getValue.pwd', readOnly: 'readOnly.pwd'},
+        ],
+        emptyRows: 0,
+        headRows: 1,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        // readOnly: true,
+        localCache: {
+            key: 'ledger-cooperation',
+            colWidth: true,
+        }
+    };
+
+    const ledgerCol = {
+        getValue: {
+            pwd: function (data) {
+                let txt = '';
+                if (data.is_leaf) {
+                    if (data.pwd && data.pwd !== '' && data.pwd !== null) {
+                        txt = data.pwd;
+                    } else {
+                        txt = '设置密码';
+                    }
+                }
+                return txt;
+            }
+        },
+        readOnly: {
+            pwd: function (data) {
+                return !data.is_leaf;
+            },
+        },
+    };
+
+    const ledgerSpreadObj = {
+        setFontColor: function(row = null) {
+            if(row) {
+                const value = ledgerSpread.getActiveSheet().getValue(row, 2);
+                if (value === '设置密码') {
+                    ledgerSpread.getActiveSheet().getCell(row, 2).foreColor('#007bff');
+                } else {
+                    ledgerSpread.getActiveSheet().getCell(row, 2).foreColor('#000');
+                }
+            } else {
+                const rowCount = ledgerSpread.getActiveSheet().getRowCount();
+                for(let i = 0; i < rowCount; i++){
+                    const value = ledgerSpread.getActiveSheet().getValue(i, 2);
+                    if (value === '设置密码') {
+                        ledgerSpread.getActiveSheet().getCell(i, 2).foreColor('#007bff');
+                    } else {
+                        ledgerSpread.getActiveSheet().getCell(i, 2).foreColor('#000');
+                    }
+                }
+            }
+        },
+        setAllRightPwd: function(uid) {
+            const selects = [];
+            for (const l of ledgerTree.datas) {
+                const coo = _.find(ledger_cooperation_list, { 'ledger_id': l.ledger_id, 'user_id': parseInt(uid) });
+                if (l.pwd && !coo) {
+                    delete l.pwd;
+                    selects.push(l);
+                } else if(coo) {
+                    l.pwd = coo.pwd;
+                    selects.push(l);
+                }
+            }
+            if(selects.length > 0) {
+                const refreshNode = ledgerTree.loadPostData({update: selects});
+                ledgerSpreadObj.refreshTree(ledgerSpread.getActiveSheet(), refreshNode);
+                ledgerSpreadObj.setFontColor();
+            }
+        },
+        refreshTree: function (sheet, data) {
+            SpreadJsObj.massOperationSheet(sheet, function () {
+                const tree = sheet.zh_tree;
+                // 处理删除
+                if (data.delete) {
+                    data.delete.sort(function (x, y) {
+                        return y.deleteIndex - x.deleteIndex;
+                    });
+                    for (const d of data.delete) {
+                        sheet.deleteRows(d.deleteIndex, 1);
+                    }
+                }
+                // 处理新增
+                if (data.create) {
+                    const newNodes = data.create;
+                    if (newNodes) {
+                        newNodes.sort(function (a, b) {
+                            return a.index - b.index;
+                        });
+
+                        for (const node of newNodes) {
+                            sheet.addRows(node.index, 1);
+                            SpreadJsObj.reLoadRowData(sheet, tree.nodes.indexOf(node), 1);
+                        }
+                    }
+                }
+                // 处理更新
+                if (data.update) {
+                    const rows = [];
+                    for (const u of data.update) {
+                        rows.push(tree.nodes.indexOf(u));
+                    }
+                    SpreadJsObj.reLoadRowsData(sheet, rows);
+                }
+                // 处理展开
+                if (data.expand) {
+                    const expanded = [];
+                    for (const e of data.expand) {
+                        if (expanded.indexOf(e) === -1) {
+                            const posterity = tree.getPosterity(e);
+                            for (const p of posterity) {
+                                sheet.setRowVisible(tree.nodes.indexOf(p), p.visible);
+                                expanded.push(p);
+                            }
+                        }
+                    }
+                }
+            });
+        },
+        editStarting: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                ledgerSpread.getActiveSheet().getCell(info.row, 2).foreColor('#000');
+                if(col.getValue(select) === '设置密码') {
+                    ledgerSpread.getActiveSheet().setValue(info.row, 2, '');
+                }
+            }
+        },
+        editEnded: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                const validText = trimInvalidChar(info.editingText);
+                const user_id = parseInt($('#stage_audits').val());
+                const orgValue = select[col.field];
+                const reg = /^[0-9a-zA-Z]+$/;
+                if(validText !== '' && !reg.test(validText)) {
+                    toastr.error('不能输入非数字和字母的字符');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    ledgerSpreadObj.setFontColor(info.row);
+                    return;
+                }
+                if((validText === '' && orgValue === undefined) || validText == orgValue) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    ledgerSpreadObj.setFontColor(info.row);
+                    return;
+                }
+                const num = _.filter(ledger_cooperation_list, { pwd: validText, status: 1, user_id });
+                if(num.length > 0) {
+                    toastr.error('同一个审批人密码不能相同');
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    ledgerSpreadObj.setFontColor(info.row);
+                    return;
+                }
+                select.pwd = validText;
+                const data = {
+                    type: 'pwd',
+                    ledger_id: select.ledger_id,
+                    user_id,
+                    pwd: validText,
+                };
+                postData('/tender/' + cur_tenderid + '/shenpi/audit/save', data, function (result) {
+                    const lcindex = _.findIndex(ledger_cooperation_list, { ledger_id: select.ledger_id, user_id });
+                    if(lcindex !== -1) {
+                        validText === '' ? ledger_cooperation_list.splice(lcindex, 1) : ledger_cooperation_list.splice(lcindex, 1, result);
+                    } else {
+                        ledger_cooperation_list.push(result);
+                    }
+                    $('#cooperation-num').text(ledger_cooperation_list.length);
+                    setLeftTable(ledgerTree.datas, ledger_cooperation_list, user_id, $('#stage_audits option:selected').text());
+                    const refreshNode = ledgerTree.loadPostData({update: select});
+                    ledgerSpreadObj.refreshTree(info.sheet, refreshNode);
+                    ledgerSpreadObj.setFontColor();
+                });
+            }
+        },
+        deletePress: function (sheet) {
+            return;
+        },
+        clipboardPasted(e, info) {
+            // 禁止复制粘贴
+            SpreadJsObj.reLoadSheetHeader(ledgerSpread.getActiveSheet());
+            SpreadJsObj.reLoadSheetData(ledgerSpread.getActiveSheet());
+            return;
+        }
+    };
+
+    sjsSettingObj.setFxTreeStyle(ledgerSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    if (thousandth) sjsSettingObj.setTpThousandthFormat(ledgerSpreadSetting);
+    SpreadJsObj.initSpreadSettingEvents(ledgerSpreadSetting, ledgerCol);
+    SpreadJsObj.initSheet(ledgerSpread.getActiveSheet(), ledgerSpreadSetting);
+    SpreadJsObj.selChangedRefreshBackColor(ledgerSpread.getActiveSheet());
+    ledgerSpread.bind(spreadNS.Events.EditStarting, ledgerSpreadObj.editStarting);
+    ledgerSpread.bind(spreadNS.Events.EditEnded, ledgerSpreadObj.editEnded);
+    SpreadJsObj.addDeleteBind(ledgerSpread, ledgerSpreadObj.deletePress);
+    ledgerSpread.bind(spreadNS.Events.ClipboardPasted, ledgerSpreadObj.clipboardPasted);
+    let ledger_data, ledger_cooperation_list = [];
+
+    // 多人协同
+    $('#cooperation').on('shown.bs.modal', function () {
+        // 执行一些动作...
+        // 更新新的多人协同表格信息
+        const newUidList = [];
+        $('.stage_div ul li').each(function (k, v) {
+            const uid = $(v).find('a').data('id');
+            if(uid) newUidList.push(uid);
+        });
+        const oldUidList = [];
+        $('#stage_audits option').each(function (k, v) {
+            const uid = parseInt($(v).val());
+            if(k !== 0) oldUidList.push(uid);
+        });
+        if (!_.isEqual(oldUidList, newUidList)) {
+            const yb = _.find(accountList, { 'id': cur_uid });
+            let newhtml = '<option value="' + yb.id + '">' + yb.name + '(原报)</option>';
+            if(newUidList.length > 0) {
+                for (const [i,id] of newUidList.entries()) {
+                    const audit = _.find(accountList, { 'id': id });
+                    newhtml += '<option value="' + audit.id + '">' + audit.name + '(' + transFormToChinese(i+1) + '审)</option>';
+                }
+            }
+            $('#stage_audits').html(newhtml);
+            if(ledger_data) {
+                setLeftTable(ledgerTree.datas, ledger_cooperation_list, cur_uid, yb.name + '(原报)');
+                ledgerSpreadObj.setAllRightPwd(cur_uid);
+            }
+        }
+        if(!ledger_data) {
+            postData('/tender/' + cur_tenderid + '/shenpi/ledger/load', {}, function (data) {
+                ledger_data = true;
+                const ledgerList = setRightData(data.ledgerList, data.ledgerCooperationList);
+                ledgerTree.loadDatas(ledgerList);
+                ledger_cooperation_list = data.ledgerCooperationList;
+                const yb = _.find(accountList, { 'id': cur_uid });
+                setLeftTable(ledgerList, ledger_cooperation_list, cur_uid, yb.name + '(原报)');
+                // treeCalc.calculateAll(ledgerTree);
+                console.log(ledgerTree);
+                SpreadJsObj.loadSheetData(ledgerSpread.getActiveSheet(), SpreadJsObj.DataType.Tree, ledgerTree);
+                ledgerSpreadObj.setFontColor();
+            }, null, true);
+        }
+        ledgerSpread.refresh();
+    });
+
+    $('#stage_audits').change(function () {
+        const uid = $(this).val();
+        const title = $("#stage_audits option:selected").text();
+        setLeftTable(ledgerTree.datas, ledger_cooperation_list, uid, title);
+        ledgerSpreadObj.setAllRightPwd(uid);
+    });
+
+    $('body').on('click', '.del-pwd', function () {
+        const ledger_id = parseInt($(this).data('lid'));
+        const user_id = parseInt($(this).data('uid'));
+        const data = {
+            type: 'pwd',
+            user_id,
+            ledger_id,
+            pwd: '',
+        };
+        postData('/tender/' + cur_tenderid + '/shenpi/audit/save', data, function (result) {
+            const lcindex = _.findIndex(ledger_cooperation_list, { ledger_id, user_id });
+            ledger_cooperation_list.splice(lcindex, 1);
+            setLeftTable(ledgerTree.datas, ledger_cooperation_list, user_id, $('#stage_audits option:selected').text());
+            const select = _.find(ledgerTree.datas, { ledger_id });
+            delete select.pwd;
+            const refreshNode = ledgerTree.loadPostData({update: select});
+            ledgerSpreadObj.refreshTree(ledgerSpread.getActiveSheet(), refreshNode);
+            ledgerSpreadObj.setFontColor();
+            $('#cooperation-num').text(ledger_cooperation_list.length);
+        });
+    });
+
+    $('body').on('click', '.edit-pwd', function () {
+        const pwd = $(this).data('pwd');
+        const html = `<div class="input-group input-group-sm">
+            <input type="text" class="form-control" value="${pwd}" placeholder="输入新密码" aria-describedby="button-${$(this).data('lid')}" style="width:50px">
+            <div class="input-group-append" id="button-${$(this).data('lid')}">
+                <button class="btn btn-outline-primary confirm-btn" data-pwd="${pwd}" data-lid="${$(this).data('lid')}" data-uid="${$(this).data('uid')}" type="button">确认</button>
+                <button class="btn btn-outline-secondary cancel-btn" data-pwd="${pwd}" data-lid="${$(this).data('lid')}" data-uid="${$(this).data('uid')}" type="button">取消</button>
+            </div>
+            </div>`;
+        $(this).parents('td').html(html);
+    });
+
+    $('body').on('click', '.cancel-btn', function () {
+        const html = `<p class="mb-0">${$(this).data('pwd')}</p><a href="javascript:void(0);" data-lid="${$(this).data('lid')}" data-uid="${$(this).data('uid')}" data-pwd="${$(this).data('pwd')}" class="edit-pwd">修改</a> <a href="javascript:void(0)" data-lid="${$(this).data('lid')}" data-uid="${$(this).data('uid')}" class="del-pwd text-danger">移除</a>`;
+        $(this).parents('td').html(html);
+    });
+
+    $('body').on('click', '.confirm-btn', function () {
+        const validText = $(this).parents('td').find('input').val();
+        const orgValue = $(this).data('pwd');
+        const ledger_id = parseInt($(this).data('lid'));
+        const user_id = parseInt($(this).data('uid'));
+        const reg = /^[0-9a-zA-Z]+$/;
+        if(!reg.test(validText)) {
+            toastr.error('不能输入非数字和字母的字符');
+            return;
+        }
+        if(validText == orgValue) {
+            const html = `<p class="mb-0">${$(this).data('pwd')}</p><a href="javascript:void(0);" data-lid="${$(this).data('lid')}" data-uid="${$(this).data('uid')}" data-pwd="${$(this).data('pwd')}" class="edit-pwd">修改</a> <a href="javascript:void(0)" data-lid="${$(this).data('lid')}" data-uid="${$(this).data('uid')}" class="del-pwd text-danger">移除</a>`;
+            $(this).parents('td').html(html);
+            return;
+        }
+        const num = _.filter(ledger_cooperation_list, { pwd: validText, status: 1, user_id });
+        if(num.length > 0) {
+            toastr.error('同一个审批人密码不能相同');
+            return;
+        }
+        const data = {
+            type: 'pwd',
+            ledger_id,
+            user_id,
+            pwd: validText,
+        };
+        postData('/tender/' + cur_tenderid + '/shenpi/audit/save', data, function (result) {
+            const lcindex = _.findIndex(ledger_cooperation_list, { ledger_id, user_id });
+            ledger_cooperation_list.splice(lcindex, 1, result);
+            setLeftTable(ledgerTree.datas, ledger_cooperation_list, user_id, $('#stage_audits option:selected').text());
+            const select = _.find(ledgerTree.datas, { ledger_id });
+            select.pwd = validText;
+            const refreshNode = ledgerTree.loadPostData({update: select});
+            ledgerSpreadObj.refreshTree(ledgerSpread.getActiveSheet(), refreshNode);
+            ledgerSpreadObj.setFontColor();
+        });
+    });
+
+    // 上传图片
+    $('body').on('click', '.upload-img', function () {
+        $(this).siblings('input').trigger('click');
+        // $('.upload-img-file').trigger('click');
+    });
+    $('body').on('change', '.upload-img-file', function () {
+        const file = this.files[0];
+        const ext = file.name.toLowerCase().split('.').splice(-1)[0];
+        const imgStr = /(png|PNG)$/;
+        if (!imgStr.test(ext)) {
+            toastr.error('请上传签名大小为600x300,格式PNG透明背景。');
+            return
+        }
+        if ($(this).val()) {
+            var _self = $(this);
+            const formData = new FormData();
+            formData.append('id', $(this).data('id'));
+            formData.append('file', this.files[0]);
+            postDataWithFile(window.location.pathname + '/save-sign', formData, function (result) {
+                _self.siblings('img').attr('src', '/' + result);
+                _self.siblings('img').show();
+                _self.siblings('a').removeClass('btn btn-outline-primary btn-sm').addClass('d-inline-flex').text('更改');
+                _self.val('');
+                const lcindex = _.findIndex(ledger_cooperation_list, { id: _self.data('id') });
+                ledger_cooperation_list[lcindex].sign_path = result;
+                ledger_cooperation_list.splice(lcindex, 1, ledger_cooperation_list[lcindex]);
+            });
+        }
+    });
+
+
+    $.subMenu({
+        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
+        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
+        key: 'menu.1.0.0',
+        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
+        callback: function (info) {
+            if (info.mini) {
+                $('.panel-title').addClass('fluid');
+                $('#sub-menu').removeClass('panel-sidebar');
+            } else {
+                $('.panel-title').removeClass('fluid');
+                $('#sub-menu').addClass('panel-sidebar');
+            }
+            autoFlashHeight();
+        }
+    });
 });
+
+function setRightData(datas, coolist) {
+    const newdatas = [];
+    const reg = /(^GD([a-zA-Z0-9\-]+))|(^([0-9\-]+)$)/;
+    for (const l of datas) {
+        if (reg.test(l.code) && l.level <= 4) {
+            if(l.level === 4 && l.is_leaf === false) {
+                l.is_leaf = true;
+            }
+            if(l.is_leaf) {
+                const coo = _.find(coolist, { 'ledger_id': l.ledger_id, 'user_id': cur_uid });
+                if(coo) {
+                    l.pwd = coo.pwd;
+                }
+            }
+            newdatas.push(l);
+        }
+    }
+    return newdatas;
+}
+
+function setLeftTable(ledgerList, coolist, uid, title) {
+    $('#stage_audit').text(title);
+    const showCooList = _.filter(coolist, { 'user_id': parseInt(uid) });
+    const removeList = [];
+    for (const sc of showCooList) {
+        const info = _.find(ledgerList, { 'ledger_id': sc.ledger_id });
+        if (info) {
+            sc.code = info.code;
+            sc.name = info.name;
+        } else {
+            removeList.push(sc);
+        }
+    }
+    if (removeList.length > 0) {
+        // 移除
+        _.pull(showCooList, ...removeList);
+        console.log(removeList);
+    }
+
+    let html = '';
+    for (const sc of showCooList) {
+        const pichtml = sc.sign_path ? `<img src="/${sc.sign_path}" width="60"><input type="file" data-id="${sc.id}" class="upload-img-file" style="display: none;"><a href="javascript: void(0);" class="d-inline-flex upload-img">更改</a>`
+            : `<img src="" style="display: none" width="60"><input type="file" data-id="${sc.id}" class="upload-img-file" style="display: none;"><a href="javascript: void(0);" class="btn btn-outline-primary btn-sm upload-img">上传签名</a>`;
+        html += `<tr>` +
+            `<td>${sc.code} ${sc.name}</td>` +
+            `<td><p class="mb-0">${sc.pwd}</p><a href="javascript:void(0);" data-lid="${sc.ledger_id}" data-uid="${sc.user_id}" data-pwd="${sc.pwd}" class="edit-pwd">修改</a> <a href="javascript:void(0)" data-lid="${sc.ledger_id}" data-uid="${sc.user_id}" class="del-pwd text-danger">移除</a></td>` +
+            `<td>${pichtml}</td>` +
+            `</tr>`;
+    }
+    $('#coo_table').html(html);
+}

+ 27 - 13
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -472,7 +472,7 @@ const SpreadJsObj = {
             return backColor;
         }
     },
-    _loadRowStyle: function (sheet, row) {
+    _loadRowStyle: function (sheet, data, row) {
         sheet.zh_setting.cols.forEach(function (col, j) {
             const cell = sheet.getCell(row, j);
 
@@ -484,10 +484,11 @@ const SpreadJsObj = {
                 cell.foreColor(col.foreColor);
             }
 
-            if (col.readOnly && Object.prototype.toString.apply(col.readOnly) !== "[object Function]") {
-                cell.locked(col.readOnly || sheet.zh_setting.readOnly || false);
-            }
-            cell.vAlign(1).hAlign(col.hAlign);
+            const readOnly1 = (sheet.zh_setting.readOnly && Object.prototype.toString.apply(sheet.zh_setting.readOnly) === "[object Function]")
+                ? sheet.zh_setting.readOnly(data) : (sheet.zh_setting.readOnly || false);
+            const readOnly2 = (col.readOnly && Object.prototype.toString.apply(col.readOnly) === "[object Function]")
+                ? col.readOnly(data) : col.readOnly || false;
+            cell.locked(readOnly1 || readOnly2).vAlign(1).hAlign(col.hAlign);
 
             if(col.type === 'Number') {
                 if (col.formatter) {
@@ -530,11 +531,11 @@ const SpreadJsObj = {
                 }
             }
 
-            if (col.readOnly && Object.prototype.toString.apply(col.readOnly) === "[object Function]") {
-                cell.locked(col.readOnly(data) || sheet.zh_setting.readOnly || false).vAlign(1).hAlign(col.hAlign);
-            } else {
-                cell.locked(col.readOnly || sheet.zh_setting.readOnly || false).vAlign(1).hAlign(col.hAlign);
-            }
+            const readOnly1 = (sheet.zh_setting.readOnly && Object.prototype.toString.apply(sheet.zh_setting.readOnly) === "[object Function]")
+                ? sheet.zh_setting.readOnly(data) : (sheet.zh_setting.readOnly || false);
+            const readOnly2 = (col.readOnly && Object.prototype.toString.apply(col.readOnly) === "[object Function]")
+                ? col.readOnly(data) : col.readOnly || false;
+            cell.locked(readOnly1 || readOnly2).vAlign(1).hAlign(col.hAlign);
 
             if(col.type === 'Number') {
                 if (col.formatter) {
@@ -719,9 +720,9 @@ const SpreadJsObj = {
                     self._loadRowData(sheet, data, i);
                     sheet.setRowVisible(i, data.visible);
                 });
-                for (let iRow = sortData.length - 1; iRow < totalRow; iRow++) {
-                    self._loadRowStyle(sheet, iRow);
-                }
+                // for (let iRow = sortData.length - 1; iRow < totalRow; iRow++) {
+                //     self._loadRowStyle(sheet, iRow);
+                // }
             }
             // 设置列单元格格式
             sheet.zh_setting.cols.forEach(function (col, j) {
@@ -804,6 +805,19 @@ const SpreadJsObj = {
             this.endMassOperation(sheet);
         }
     },
+    reloadRowsReadonly: function (sheet, rows) {
+        const sortData = sheet.zh_dataType === 'tree' ? sheet.zh_tree.nodes : sheet.zh_data;
+
+        this.beginMassOperation(sheet);
+        try {
+            for (const row of rows) {
+                if (row < 0) { continue; }
+                this._loadRowStyle(sheet, sortData[row], row);
+            };
+        } catch (err) {
+        }
+        this.endMassOperation(sheet);
+    },
     reloadColData: function (sheet, col, count = 1) {
         const cols = [];
         for (let i = 0; i < count; i++) {

+ 69 - 4
app/public/js/stage.js

@@ -254,6 +254,16 @@ $(document).ready(() => {
     };
     const stagePos = new StagePosData(stagePosSetting);
 
+    const reloadCooperationHtml = function () {
+        const html = [];
+        for (const p of stageTree.pwd) {
+            html.push('<tr>', `<td>${p.node.code}</td>`, `<td>${p.node.name}</td>`);
+            html.push('<td>', p.check ? `已解锁:${p.pwd}` : `<a name="ledger-unlock" lid="${p.ledger_id}" href="javascript: void(0);">解锁</a>`, '</td>');
+            html.push('</tr>');
+        }
+        $('#cooperationList').html(html.join(''));
+    };
+
     class Changes {
         constructor(obj) {
             const self = this;
@@ -546,7 +556,7 @@ $(document).ready(() => {
     const ratioCol = ledgerSpreadSetting.cols.find(x => {return x.field === 'end_gather_percent' || x.field === 'end_correct_percent'});
     ratioCol.field = tenderInfo.display.stage.correct ? 'end_correct_percent' : 'end_gather_percent';
     ledgerSpreadSetting.imageClick = function (data) {
-        if (data.children && data.children.length > 0) return;
+        if (data.children && data.children.length > 0 || data.lock) return;
 
         const nodePos = stagePos.getLedgerPos(data.id);
         if (nodePos && nodePos.length > 0) return;
@@ -621,6 +631,10 @@ $(document).ready(() => {
             },
         },
     ];
+    ledgerSpreadSetting.readOnly = function (data) {
+        if (!data) return false;
+        return data.lock || false;
+    };
     SpreadJsObj.initSheet(slSpread.getActiveSheet(), ledgerSpreadSetting);
     slSpread.getActiveSheet().frozenColumnCount(5);
     slSpread.getActiveSheet().options.frozenlineColor = '#93b5e4';
@@ -1411,7 +1425,9 @@ $(document).ready(() => {
          * 加载计量单元 根据当前台账选择节点
          */
         loadCurPosData: function () {
-            const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+            const sheet = slSpread.getActiveSheet();
+            const node = SpreadJsObj.getSelectObject(sheet);
+            if (node.lock) spSpread.getActiveSheet().zh_setting.readOnly = node.lock;
             if (node) {
                 const posData = stagePos.ledgerPos[itemsPre + node.id] || [];
                 SpreadJsObj.loadSheetData(spSpread.getActiveSheet(), 'data', posData);
@@ -1683,6 +1699,8 @@ $(document).ready(() => {
             }
         },
         deletePress: function (sheet) {
+            if (sheet.zh_setting.readOnly) return;
+
             if (sheet.zh_setting && sheet.zh_data) {
                 const sortData = sheet.zh_data;
                 if (!sortData || sortData.length === 0) { return; }
@@ -1797,12 +1815,18 @@ $(document).ready(() => {
     });
 
     // 加载计量单元数据 - 暂时统一加载,如有需要,切换成动态加载并缓存
-    postData(window.location.pathname + '/load', { filter: 'ledger;pos;detail;change;tag' }, function (result) {
+    postData(window.location.pathname + '/load', { filter: 'ledger;pos;detail;change;tag;cooperation' }, function (result) {
         // 加载树结构
         stageTree.loadDatas(result.ledgerData);
         // stageTree.loadCurStageData(curStageData);
         // stageTree.loadPreStageData(preStageData);
         treeCalc.calculateAll(stageTree);
+        // 加载解锁相关
+        if (result.cooperation) {
+            stageTree.loadPwd(result.cooperation, 'bills-p-' + userID + '-' + window.location.pathname.split('/')[2]);
+            $('#cooperationCount').html(stageTree.pwd.length || '');
+            reloadCooperationHtml();
+        }
         // 加载部位明细
         stagePos.loadDatas(result.posData);
         stagePos.calculateAll();
@@ -1889,6 +1913,9 @@ $(document).ready(() => {
                     return !checkTzMeasureType();
                 },
                 disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+                    if (!node || node.lock) return true;
+
                     const sheet = spSpread.getActiveSheet();
                     if (sheet.zh_data && !readOnly) {
                         const selection = sheet.getSelections();
@@ -1925,7 +1952,7 @@ $(document).ready(() => {
                 },
                 disabled: function (key, opt) {
                     const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
-                    return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '';
+                    return _.isNil(node) || _.isNil(node.b_code) || node.b_code === '' || node.lock;
                 },
                 callback: function (key, opt) {
                     mergePeg.show();
@@ -1937,6 +1964,10 @@ $(document).ready(() => {
                     const data = spSpread.getActiveSheet().zh_data;
                     return data && data.length > 0;
                 },
+                disabled: function (key, opt) {
+                    const node = SpreadJsObj.getSelectObject(slSpread.getActiveSheet());
+                    return node.lock;
+                },
                 callback: function (key, opt) {
                     $('#cbr-ratio').val('');
                     $('#apply2sibling')[0].checked = false;
@@ -3910,4 +3941,38 @@ $(document).ready(() => {
         }
         stageTreeSpreadObj.measureByBatch(posName, ratio, apply2sibling);
     });
+
+    $('body').on('click', '[name=ledger-unlock]', function() {
+        $('#cooperation').modal('hide');
+        const lid = this.getAttribute('lid');
+        if (!lid) return;
+
+        const p = stageTree.pwd.find(x => {return x.ledger_id == lid});
+        if (!p) return;
+
+        $('#unlock-info').html(`${p.node.code} ${p.node.name} 解锁密码<b class="text-danger">*</b>`);
+        $('#unlock-pwd').val('').removeClass('is-invalid');
+        $('.invalid-feedback', '#unlock').hide();
+        $('.alert-warning', '#unlock').hide();
+        $('#unlock-ok').attr('lid', lid);
+        $('#unlock').modal('show');
+    });
+    $('#unlock-ok').click(function () {
+        const lid = this.getAttribute('lid');
+        if (!lid) return;
+
+        const p = stageTree.pwd.find(x => {return x.ledger_id == lid});
+        if (!p) return;
+
+        if (p.pwd === $('#unlock-pwd').val()) {
+            const refresh = stageTree.lockNode(p, false);
+            SpreadJsObj.reloadRowsReadonly(slSpread.getActiveSheet(), refresh);
+            $('#unlock').modal('hide');
+            reloadCooperationHtml();
+        } else {
+            $('#unlock-pwd').addClass('is-invalid');
+            $('.invalid-feedback', '#unlock').show();
+            $('.alert-warning', '#unlock').show();
+        }
+    })
 });

+ 7 - 3
app/router.js

@@ -112,9 +112,11 @@ module.exports = app => {
     app.post('/tender/:id/save', sessionAuth, tenderCheck, 'tenderController.saveTenderInfo');
     app.post('/tender/rule', sessionAuth, 'tenderController.rule');
     app.post('/tender/:id/rule/first', sessionAuth, tenderCheck, 'tenderController.ruleFirst');
-    app.get('/tender/:id/shenpi', sessionAuth, tenderCheck, 'tenderController.shenpiSet');
-    app.post('/tender/:id/shenpi/save', sessionAuth, tenderCheck, 'tenderController.saveTenderInfoShenpi');
-    app.post('/tender/:id/shenpi/audit/save', sessionAuth, tenderCheck, 'tenderController.saveShenpiAudit');
+    app.get('/tender/:id/shenpi', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.shenpiSet');
+    app.post('/tender/:id/shenpi/save', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.saveTenderInfoShenpi');
+    app.post('/tender/:id/shenpi/audit/save', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.saveShenpiAudit');
+    app.post('/tender/:id/shenpi/ledger/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.loadLedgerData');
+    app.post('/tender/:id/shenpi/save-sign', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.saveCooperateSign');
     app.post('/tender/:id/copy-setting', sessionAuth, tenderCheck, 'tenderController.copyTender');
 
     // 预付款
@@ -435,6 +437,8 @@ module.exports = app => {
     app.get('/tender/:id/schedule/ledger', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.ledger');
     app.post('/tender/:id/schedule/ledger/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.loadLedgerData');
     app.post('/tender/:id/schedule/ledger/save', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.saveLedger');
+    app.get('/tender/:id/schedule/plan', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.plan');
+    app.post('/tender/:id/schedule/plan/save', sessionAuth, tenderCheck, uncheckTenderCheck, 'scheduleController.savePlan');
 
     // 书签
     app.post('/tender/:id/ledger/tag', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.billsTag');

+ 71 - 0
app/service/ledger_cooperation.js

@@ -0,0 +1,71 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date 2018/6/1
+ * @version
+ */
+
+module.exports = app => {
+    class LedgerCooperation extends app.BaseService {
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'ledger_cooperation';
+        }
+
+        async save(data) {
+            delete data.type;
+            data.tid = this.ctx.tender.id;
+            const info = await this.getDataByCondition({ tid: this.ctx.tender.id, ledger_id: data.ledger_id, user_id: data.user_id });
+            if (data.pwd === '' && info) {
+                await this.deleteById(info.id);
+            } else if (data.pwd !== '' && !info) {
+                const result = await this.db.insert(this.tableName, data);
+                data.id = result.insertId;
+            } else if (data.pwd !== '' && info) {
+                data.id = info.id;
+                await this.db.update(this.tableName, data);
+            }
+            return data;
+        }
+
+        async changeAllStatus(status) {
+            const options = {
+                where: {
+                    tid: this.ctx.tender.id,
+                },
+            };
+            const updateData = {
+                status,
+            };
+            return await this.db.update(this.ctx.service.ledgerCooperation.tableName, updateData, options);
+        }
+
+        async saveSign(id, path) {
+            const updateData = {
+                id,
+                sign_path: path,
+            };
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async getValidData(tid, uid) {
+            const condition = { where: { tid, status: 1 } };
+            if (uid) {
+                condition.where.user_id = uid;
+                condition.colums = ['ledger_id', 'pwd'];
+            }
+            return await this.getAllDataByCondition(condition);
+        }
+    }
+
+    return LedgerCooperation;
+};

+ 4 - 0
app/service/report.js

@@ -162,6 +162,10 @@ module.exports = app => {
                                 customDefine.stage_select, customSelect ? customSelect.stage_select : null));
                             runnableKey.push(filter);
                             break;
+                        case 'ledger_cooperation':
+                            runnableRst.push(service.ledgerCooperation.getValidData(params.tender_id));
+                            runnableKey.push(filter);
+                            break;
                         default:
                             break;
                     }

+ 31 - 0
app/service/schedule.js

@@ -0,0 +1,31 @@
+'use strict';
+
+module.exports = app => {
+    class Schedule extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'schedule';
+        }
+
+        async saveMode(data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const options = {
+                    where: {
+                        tid: this.ctx.tender.id,
+                    },
+                };
+                const updateData = {
+                    mode: data,
+                };
+                await transaction.update(this.tableName, updateData, options);
+                await transaction.commit();
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+    return Schedule;
+};

+ 8 - 0
app/service/schedule_ledger.js

@@ -32,6 +32,14 @@ module.exports = app => {
                     }
                 }
                 if (insertDatas.length > 0) await transaction.insert(this.tableName, insertDatas);
+                // 判断是否已创建了形象进度表
+                const scheduleInfo = await this.ctx.service.schedule.getDataByCondition({ tid: this.ctx.tender.id });
+                if (!scheduleInfo) {
+                    const newSchedule = {
+                        tid: this.ctx.tender.id,
+                    };
+                    await transaction.insert(this.ctx.service.schedule.tableName, newSchedule);
+                }
                 await transaction.commit();
                 return true;
             } catch (err) {

+ 11 - 0
app/service/schedule_month.js

@@ -0,0 +1,11 @@
+'use strict';
+
+module.exports = app => {
+    class ScheduleMonth extends app.BaseService {
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'schedule_month';
+        }
+    }
+    return ScheduleMonth;
+};

+ 54 - 15
app/service/shenpi_audit.js

@@ -39,24 +39,63 @@ module.exports = app => {
         }
 
         async addAudit(data) {
-            const insertData = {
-                tid: this.ctx.tender.id,
-                sp_type: data.code,
-                sp_status: data.status,
-                audit_id: data.audit_id,
-            };
-            const result = await this.db.insert(this.tableName, insertData);
-            return result.effectRows === 1;
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (parseInt(data.code) === shenpiConst.sp_type.stage && parseInt(data.status) === shenpiConst.sp_status.gdspl) {
+                    const options = {
+                        where: {
+                            tid: this.ctx.tender.id,
+                            user_id: data.audit_id,
+                        },
+                    };
+                    const updateData = {
+                        status: 1,
+                    };
+                    await transaction.update(this.ctx.service.ledgerCooperation.tableName, updateData, options);
+                }
+                const insertData = {
+                    tid: this.ctx.tender.id,
+                    sp_type: data.code,
+                    sp_status: data.status,
+                    audit_id: data.audit_id,
+                };
+                const result = await transaction.insert(this.tableName, insertData);
+                await transaction.commit();
+                return result.effectRows === 1;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
         }
 
         async removeAudit(data) {
-            const delData = {
-                tid: this.ctx.tender.id,
-                sp_type: data.code,
-                sp_status: data.status,
-                audit_id: data.audit_id,
-            };
-            return await this.db.delete(this.tableName, delData);
+            const transaction = await this.db.beginTransaction();
+            try {
+                const delData = {
+                    tid: this.ctx.tender.id,
+                    sp_type: data.code,
+                    sp_status: data.status,
+                    audit_id: data.audit_id,
+                };
+                await transaction.delete(this.tableName, delData);
+                if (parseInt(data.code) === shenpiConst.sp_type.stage && parseInt(data.status) === shenpiConst.sp_status.gdspl) {
+                    const options = {
+                        where: {
+                            tid: this.ctx.tender.id,
+                            user_id: data.audit_id,
+                        },
+                    };
+                    const updateData = {
+                        status: 0,
+                    };
+                    await transaction.update(this.ctx.service.ledgerCooperation.tableName, updateData, options);
+                }
+                await transaction.commit();
+                return true;
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
         }
 
         async copyAudit2otherTender(data) {

+ 5 - 5
app/view/schedule/index.ejs

@@ -295,11 +295,11 @@
                 精度:3,
             }
         ];
-        const spread = new GC.Spread.Sheets.Workbook($('#option-spread1')[0], {
-            sheetCount: 1
-        });
-        spread.getActiveSheet().setDataSource(data);
-        spread.options.tabStripVisible = false;
+        // const spread = new GC.Spread.Sheets.Workbook($('#option-spread1')[0], {
+        //     sheetCount: 1
+        // });
+        // spread.getActiveSheet().setDataSource(data);
+        // spread.options.tabStripVisible = false;
     })
 </script>
 <script src="/public/js/sub_menu.js"></script>

+ 2 - 19
app/view/schedule/ledger.ejs

@@ -29,9 +29,11 @@
                     </a>
                 </div>
             </div>
+            <% if (tender.user_id === ctx.session.sessionUser.accountId) { %>
             <div class="ml-auto">
                 <button type="button" id="ledger_submit" class="btn btn-primary btn-sm pull-right">确认提交</button>
             </div>
+            <% } %>
         </div>
     </div>
     <div class="content-wrap">
@@ -44,7 +46,6 @@
         </div>
     </div>
 </div>
-<script src="/public/js/sub_menu.js"></script>
 <script>
     const tender = JSON.parse('<%- JSON.stringify(tender) %>');
     const tenderInfo = JSON.parse(unescape('<%- escape(JSON.stringify(tenderInfo)) %>'));
@@ -52,21 +53,3 @@
     const measureType = JSON.parse('<%- JSON.stringify(measureType) %>');
     const selectedLedgerList = JSON.parse('<%- JSON.stringify(scheduleLedgerList) %>');
 </script>
-<script>
-    $.subMenu({
-        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
-        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
-        key: 'menu.1.0.0',
-        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
-        callback: function (info) {
-            if (info.mini) {
-                $('.panel-title').addClass('fluid');
-                $('#sub-menu').removeClass('panel-sidebar');
-            } else {
-                $('.panel-title').removeClass('fluid');
-                $('#sub-menu').addClass('panel-sidebar');
-            }
-            autoFlashHeight();
-        }
-    });
-</script>

+ 0 - 21
app/view/schedule/ledger_modal.ejs

@@ -1,21 +0,0 @@
-<!--首次使用提示-->
-<div class="modal fade" id="first" 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">
-                <h5>首次使用形象进度需要进行进度台帐初始化设置</h5>
-            </div>
-            <div class="modal-footer">
-                <button type="button" class="btn btn-sm btn-primary" data-dismiss="modal">开始设置</button>
-            </div>
-        </div>
-    </div>
-</div>
-<script type="text/javascript">
-    $(function () {
-        // $('#first').modal('show');
-    })
-</script>

+ 35 - 0
app/view/schedule/modal.ejs

@@ -0,0 +1,35 @@
+<!--首次使用提示-->
+<div class="modal fade" id="first" data-backdrop="static">
+    <div class="modal-dialog" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">进度台帐</h5>
+            </div>
+            <% if (tender.user_id !== ctx.session.sessionUser.accountId) { %>
+            <div class="modal-body">
+                <h5>未设置进度台账无法查看</h5>
+            </div>
+            <div class="modal-footer">
+                <a href="/tender/<%- ctx.tender.id %>" class="btn btn-sm btn-primary">回到标段概况</a>
+            </div>
+            <% } else { %>
+            <div class="modal-body">
+                <h5>首次使用形象进度需要进行进度台帐初始化设置</h5>
+            </div>
+            <div class="modal-footer">
+                <a href="/tender/<%- ctx.tender.id %>/schedule/ledger" class="btn btn-sm btn-primary">开始设置</a>
+            </div>
+            <% } %>
+        </div>
+    </div>
+</div>
+<script>
+    const selectedLedgerList = JSON.parse('<%- JSON.stringify(scheduleLedgerList) %>');
+</script>
+<script type="text/javascript">
+    $(function () {
+        if (selectedLedgerList.length === 0) {
+            $('#first').modal('show');
+        }
+    })
+</script>

+ 31 - 0
app/view/schedule/plan.ejs

@@ -0,0 +1,31 @@
+<% include ../tender/tender_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ../tender/tender_sub_mini_menu.ejs %>
+            <h2>
+                计划至至 2020年12月
+            </h2>
+            <div class="ml-auto">
+                <a href="#mode" data-toggle="modal" data-target="#mode" class="btn btn-sm btn-outline-primary">计算方式</a>
+                <a href="#edit-plan" data-toggle="modal" data-target="#edit-plan" class="btn btn-sm btn-outline-primary">管理计划</a>
+                <a href="#add-plan" data-toggle="modal" data-target="#add-plan" class="btn btn-sm btn-primary">新增计划</a>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0" style="overflow: auto;" id="ledger-spread">
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const tender = JSON.parse('<%- JSON.stringify(tender) %>');
+    const tenderInfo = JSON.parse(unescape('<%- escape(JSON.stringify(tenderInfo)) %>'));
+    const thousandth = <%- ctx.tender.info.display.thousandth %>;
+    const measureType = JSON.parse('<%- JSON.stringify(measureType) %>');
+    const schedule = JSON.parse('<%- JSON.stringify(schedule) %>');
+    const scheduleMonth = JSON.parse('<%- JSON.stringify(scheduleMonth) %>');
+    const mode = JSON.parse('<%- JSON.stringify(mode) %>');
+</script>

+ 89 - 0
app/view/schedule/plan_modal.ejs

@@ -0,0 +1,89 @@
+<!--新增计划-->
+<div class="modal fade" id="add-plan" 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">
+                    <label>计划周期</label>
+                    <input class="datepicker-here form-control" placeholder="点击选择时间" data-view="months" data-min-view="months" data-date-format="yyyy-MM" data-range="true" data-multiple-dates-separator=" ~ " data-language="zh" type="text">
+                </div>
+                <div class="alert alert-danger">2020-05 已创建</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" >确认新增</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--管理计划-->
+<div class="modal fade" id="edit-plan" 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">
+                <table class="table table-bordered table-hover">
+                    <tr><th>计划月</th><th width="100">删除</th></tr>
+                    <tr><td>2020-1</td><td></td></tr>
+                    <tr><td>2020-2</td><td><label><input type="checkbox"></label></td></tr>
+                    <tr><td>2020-3</td><td><label><input type="checkbox"></label></td></tr>
+                </table>
+                <div class="alert alert-danger">确认删除「2020-2」「2020-3」计划进度?删除后,数据无法恢复,请谨慎操作。</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-danger" >确认删除</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--选择计算方式-->
+<div class="modal fade" id="mode" 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="row mb-2">
+                    <div class="col-6">
+                        <div class="card">
+                            <div class="card-body">
+                                <h5 class="card-title"><i class="fa fa-bookmark"></i> 金额</h5>
+                                <p class="card-text">填写计划金额,通过经济指标反算工程量</p>
+                                <% if (schedule && schedule.mode === mode.tp) { %>
+                                <button class="btn btn-primary btn-sm disabled mode-select" disabled data-mode="<%- mode.tp %>">当前</button>
+                                <% } else { %>
+                                <button class="btn btn-sm btn-primary mode-select" data-mode="<%- mode.tp %>">选择</button>
+                                <% } %>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="col-6">
+                        <div class="card">
+                            <div class="card-body">
+                                <h5 class="card-title"><i class="fa fa-bookmark"></i> 工程量</h5>
+                                <p class="card-text">填写工程量,金额=工程量×经济指标</p>
+                                <% if (schedule && schedule.mode === mode.gcl) { %>
+                                <button class="btn btn-primary btn-sm disabled mode-select" disabled data-mode="<%- mode.gcl %>">当前</button>
+                                <% } else { %>
+                                <button class="btn btn-sm btn-primary mode-select" data-mode="<%- mode.gcl %>">选择</button>
+                                <% } %>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="alert alert-danger" id="mode-tips" style=" <% if (schedule && !schedule.mode) { %>display: none<% } %>">更改计算方式,未使用的计划月数据都将按新计算方式重新计算,请谨慎选择。</div>
+            </div>
+            <div class="modal-footer" id="mode-cancel" style=" <% if (schedule && !schedule.mode) { %>display: none <% } %>">
+                <button type="button" class="btn btn-sm btn-outline-secondary" data-dismiss="modal">关闭</button>
+            </div>
+        </div>
+    </div>
+</div>
+<% include ./modal.ejs %>

+ 3 - 0
app/view/stage/index.ejs

@@ -36,6 +36,9 @@
                     <a id="exportExcel" class="btn btn-primary btn-sm" href="javascript: void(0)">导出计量台账Excel</a>
                     <a class="btn btn-sm btn-primary" href="javascript: void(0);" id="ledger-check2">数据检查</a>
                 </div>
+                <div class="d-inline-block">
+                    <a class="btn btn-sm btn-danger" href="#cooperation" data-toggle="modal" data-target="#cooperation">多人协同 <i class="fa fa-lock"></i> <span id="cooperationCount"></span></a>
+                </div>
             </div>
             <div class="ml-auto">
             </div>

+ 50 - 0
app/view/stage/modal.ejs

@@ -450,6 +450,56 @@
         </div>
     </div>
 </div>
+<!--多人协同-->
+<div class="modal fade" id="cooperation" 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="alert alert-warning">以下项目节及其子项被锁定,请输入密码解锁。</div>
+                <div class="modal-height-300">
+                    <table class="table table-hover table-bordered">
+                        <thead>
+                        <tr><th>项目节编号</th><th>项目节名称</th><th>解锁  </th></tr>
+                        </thead>
+                        <tbody id="cooperationList">
+                        </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">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
+<!--解锁-->
+<div class="modal fade" id="unlock" 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">
+                    <label id="unlock-info">1-2-2  挖方 解锁密码<b class="text-danger">*</b></label>
+                    <input class="form-control form-control-sm is-invalid" type="password" id="unlock-pwd">
+                    <div class="invalid-feedback">
+                        密码错误
+                    </div>
+                </div>
+                <div class="alert alert-warning">忘记密码请联系管理员。</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="unlock-ok">确认</button>
+            </div>
+        </div>
+    </div>
+</div>
 <% include ./audit_modal.ejs %>
 <% include ../shares/merge_peg_modal.ejs %>
 <% include ../shares/check_modal2.ejs %>

+ 7 - 25
app/view/tender/shenpi.ejs

@@ -16,6 +16,9 @@
                                 <div class="card-body <%- sp.code %>_div">
                                     <a class="pull-right set-otherTender" data-name="<%- sp.name %>" data-code="<%- sp.code %>" href="#batch" data-toggle="modal" data-target="#batch">设置其他标段</a>
                                     <a class="pull-right set-otherShenpi mr-3" data-name="<%- sp.name %>" data-code="<%- sp.code %>" href="#batch2" data-toggle="modal" data-target="#batch2">设置其他流程</a>
+                                    <% if (sp.code === 'stage' && !revising && ctx.tender.data.ledger_status === auditConst.ledger.status.checked) { %>
+                                    <a class="pull-right mr-3" id="stage_cooperation" <% if (sp.status !== shenpi.sp_status.gdspl) { %>style="display: none"<% } %> data-name="<%- sp.name %>" data-code="<%- sp.code %>" href="#cooperation" data-toggle="modal" data-target="#cooperation">多人协同 <i class="fa fa-lock"></i> <span id="cooperation-num"><%- cooperationNum %></span></a>
+                                    <% } %>
                                     <h5 class="card-title"><%- sp.name %></h5>
                                     <div class="form-group">
                                         <div class="form-group form-check">
@@ -51,9 +54,9 @@
                                                     <button class="btn btn-outline-primary btn-sm dropdown-toggle" type="button" id="<%- sp.code %>_dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                                                         选择审批人
                                                     </button>
-                                                    <div class="dropdown-menu dropdown-menu-right" aria-labelledby="<%- sp.code %>_dropdownMenuButton" style="width:220px">
+                                                    <div class="dropdown-menu dropdown-menu-right" id="<%- sp.code %>_dropdownMenu" aria-labelledby="<%- sp.code %>_dropdownMenuButton" style="width:220px">
                                                         <div class="mb-2 p-2"><input class="form-control form-control-sm gr-search"
-                                                                                     placeholder="姓名/手机 检索" autocomplete="off"></div>
+                                                                                     placeholder="姓名/手机 检索" autocomplete="off" data-code="<%- sp.code %>"></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>
@@ -139,7 +142,6 @@
         </div>
     </div>
 </div>
-<script src="/public/js/sub_menu.js"></script>
 <script>
     const sp_lc = JSON.parse('<%- JSON.stringify(shenpi.sp_lc) %>');
     const sp_type = JSON.parse('<%- JSON.stringify(shenpi.sp_type) %>');
@@ -151,26 +153,6 @@
     const cur_tenderid = parseInt('<%- ctx.tender.id %>');
     const tenders = JSON.parse(unescape('<%- escape(JSON.stringify(tenders)) %>'));
     const category = JSON.parse(unescape('<%- escape(JSON.stringify(categoryData)) %>'));
-</script>
-<script src="/public/js/decimal.min.js"></script>
-<script src="/public/js/zh_calc.js"></script>
-<script src="/public/js/shenpi.js"></script>
-<script src="/public/js/tender_showhide.js"></script>
-<script>
-    $.subMenu({
-        menu: '#sub-menu', miniMenu: '#sub-mini-menu', miniMenuList: '#mini-menu-list',
-        toMenu: '#to-menu', toMiniMenu: '#to-mini-menu',
-        key: 'menu.1.0.0',
-        miniHint: '#sub-mini-hint', hintKey: 'menu.hint.1.0.1',
-        callback: function (info) {
-            if (info.mini) {
-                $('.panel-title').addClass('fluid');
-                $('#sub-menu').removeClass('panel-sidebar');
-            } else {
-                $('.panel-title').removeClass('fluid');
-                $('#sub-menu').addClass('panel-sidebar');
-            }
-            autoFlashHeight();
-        }
-    });
+    const thousandth = <%- ctx.tender.info.display.thousandth %>;
+    const measureType = JSON.parse('<%- JSON.stringify(measureType) %>');
 </script>

+ 45 - 0
app/view/tender/shenpi_modal.ejs

@@ -79,3 +79,48 @@
         </div>
     </div>
 </div>
+<!--多人协同-->
+<div class="modal fade" id="cooperation" data-backdrop="static">
+    <div class="modal-dialog modal-xl" role="document">
+        <div class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">多人协同</h5>
+            </div>
+            <div class="modal-body">
+                <div class="row">
+                    <div class="col-6">
+                        <% const yb = ctx.helper._.find(accountList, { id: ctx.tender.data.user_id }) %>
+                        <div class="modal-height-300">
+                            <table class="table table-hover table-bordered">
+                                <thead>
+                                <tr><th colspan="3" class="text-center" id="stage_audit"><%- yb.name %>(原报)</th></tr>
+                                <tr><th>项目节编号/名称</th><th>密码</th><th>签名</th></tr>
+                                </thead>
+                                <tbody id="coo_table">
+                                </tbody>
+                            </table>
+                        </div>
+                    </div>
+                    <div class="col-6">
+                        <div class="mb-2">
+                            <select class="form-control form-control-sm" id="stage_audits">
+                                <option value="<%- ctx.tender.data.user_id %>"><%- yb.name %>(原报)</option>
+                                <% if (shenpi.sp_lc[shenpi.sp_type.stage-1].auditList) { %>
+                                <% for (const [i, audit] of shenpi.sp_lc[shenpi.sp_type.stage-1].auditList.entries()) { %>
+                                <option value="<%- audit.audit_id %>"><%- audit.name %>(<%- ctx.helper.transFormToChinese(i+1) %>审)</option>
+                                <% } %>
+                                <% } %>
+                            </select>
+                        </div>
+                        <div class="modal-height-300" style="overflow: auto;" id="ledger-spread">
+                        </div>
+                    </div>
+                </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">确认</button>-->
+            </div>
+        </div>
+    </div>
+</div>

+ 8 - 0
app/view/tender/tender_sub_menu.ejs

@@ -44,6 +44,14 @@
             </ul>
         </div>
         <div class="nav-box">
+            <h3><i class="fa fa-bar-chart "></i> 形象进度</h3>
+            <ul class="nav-list list-unstyled sub-list">
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/schedule' || ctx.url === '/tender/' + ctx.tender.id + '/schedule/ledger') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/schedule"><span>进度概况</span></a></li>
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/schedule/plan') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/schedule/plan"><span>计划进度</span></a></li>
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/schedule/stage') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/schedule/stage"><span>计量进度</span></a></li>
+            </ul>
+        </div>
+        <div class="nav-box">
             <ul class="nav-list list-unstyled">
                 <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/report') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/report"><i class="fa fa-file-text-o"></i> <span>报表</span></a></li>
             </ul>

+ 8 - 0
app/view/tender/tender_sub_mini_menu.ejs

@@ -42,6 +42,14 @@
             </ul>
         </div>
         <div class="nav-box">
+            <h3><i class="fa fa-bar-chart "></i> 形象进度</h3>
+            <ul class="nav-list list-unstyled sub-list">
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/schedule' || ctx.url === '/tender/' + ctx.tender.id + '/schedule/ledger') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/schedule"><span>进度概况</span></a></li>
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/schedule/plan') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/schedule/plan"><span>计划进度</span></a></li>
+                <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/schedule/stage') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/schedule/stage"><span>计量进度</span></a></li>
+            </ul>
+        </div>
+        <div class="nav-box">
             <ul class="nav-list list-unstyled">
                 <li <% if (ctx.url === '/tender/' + ctx.tender.id + '/report') { %>class="active"<% } %>><a href="/tender/<%- ctx.tender.id %>/report"><i class="fa fa-file-text-o"></i> <span>报表</span></a></li>
             </ul>

+ 17 - 0
builder_report_index_define.js

@@ -46,6 +46,21 @@ const advance_pay = {
         { name: '结束时间', field: 'end_time', type: dataType.str },
     ],
 };
+const ledger_cooperation = {
+    name: '台账-协作(ledger_cooperation)',
+    remark: '',
+    id: 45,
+    key: 'ledger_cooperation',
+    prefix: '台账-协作',
+    cols: [
+        { name: 'id', field: 'id', type: dataType.int },
+        { name: '标段id', field: 'tid', type: dataType.int },
+        { name: '审批人id', field: 'user_id', type: dataType.int },
+        { name: '台账id', field: 'ledger_id', type: dataType.int },
+        { name: '密码', field: 'pwd', type: dataType.str },
+        { name: '电子签名地址', field: 'sign_path', type: dataType.str },
+    ]
+}
 // 其他台账
 const stage_jgcl = {
     name: '期-甲供材料(mem_stage_jgcl)',
@@ -1142,6 +1157,7 @@ const stage_sum_pay = {
     ],
 };
 
+
 const recursiveMkdirSync = async function(pathName) {
     if (!fs.existsSync(pathName)) {
         const upperPath = path.dirname(pathName);
@@ -1233,6 +1249,7 @@ const exportTableDefine = async function(define) {
 };
 
 const defines = [
+    ledger_cooperation,
     advance_pay,
     union_data,
     month_progress,

+ 36 - 0
config/web.js

@@ -108,6 +108,23 @@ const JsFiles = {
                 ],
                 mergeFile: 'tender',
             },
+            shenpi: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.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/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/shenpi.js',
+                    '/public/js/tender_showhide.js',
+                ],
+                mergeFile: 'tender_shenpi',
+            },
         },
         ledger: {
             explode: {
@@ -276,6 +293,7 @@ const JsFiles = {
                     '/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/zh_calc.js',
                     '/public/js/path_tree.js',
                     '/public/js/shares/gcl_gather_compare.js',
@@ -678,6 +696,24 @@ const JsFiles = {
                 ],
                 mergeFile: 'ledger',
             },
+            plan: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/decimal.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/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/schedule_plan.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFile: 'ledger_plan',
+            },
         },
         change: {
             information: {