Pārlūkot izejas kodu

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

Tony Kang 1 gadu atpakaļ
vecāks
revīzija
058687121e
60 mainītis faili ar 3242 papildinājumiem un 89 dzēšanām
  1. 2 0
      app/const/page_show.js
  2. 57 0
      app/const/profile.js
  3. 12 0
      app/const/spread.js
  4. 92 0
      app/controller/file_controller.js
  5. 11 0
      app/controller/measure_controller.js
  6. 121 0
      app/controller/profile_controller.js
  7. 13 4
      app/controller/settle_controller.js
  8. 2 1
      app/controller/sub_proj_controller.js
  9. 57 1
      app/controller/tender_controller.js
  10. 5 0
      app/extend/helper.js
  11. 1 0
      app/middleware/tender_check.js
  12. 1 1
      app/public/js/file_detail.js
  13. 3 1
      app/public/js/file_list.js
  14. 265 0
      app/public/js/filing_template.js
  15. 405 0
      app/public/js/profile_cert.js
  16. 9 2
      app/public/js/project_spread.js
  17. 283 0
      app/public/js/settle_gather.js
  18. 1 0
      app/public/js/settle_ledger.js
  19. 5 4
      app/public/js/shares/drag_tree.js
  20. 16 3
      app/public/js/spreadjs_rela/spreadjs_zh.js
  21. 5 5
      app/public/js/stage_pay.js
  22. 9 1
      app/public/js/sub_project.js
  23. 374 0
      app/public/js/tender_cert.js
  24. 17 4
      app/router.js
  25. 153 0
      app/service/account_cert.js
  26. 45 18
      app/service/filing.js
  27. 226 0
      app/service/filing_template.js
  28. 108 0
      app/service/filing_template_list.js
  29. 16 0
      app/service/project_spread.js
  30. 35 23
      app/service/stage.js
  31. 9 3
      app/service/stage_audit.js
  32. 1 0
      app/service/stage_jgcl.js
  33. 1 1
      app/service/stage_other.js
  34. 30 1
      app/service/stage_pay.js
  35. 1 0
      app/service/stage_safe_prod.js
  36. 1 0
      app/service/stage_temp_land.js
  37. 5 2
      app/service/sub_project.js
  38. 112 0
      app/service/tender_cert.js
  39. 1 1
      app/view/file/index.ejs
  40. 71 0
      app/view/file/template.ejs
  41. 4 0
      app/view/measure/stage.ejs
  42. 156 0
      app/view/profile/cert.ejs
  43. 22 0
      app/view/profile/cert_modal.ejs
  44. 15 2
      app/view/profile/info.ejs
  45. 1 1
      app/view/setting/spread.ejs
  46. 190 0
      app/view/settle/gather.ejs
  47. 1 1
      app/view/stage/audit_btn.ejs
  48. 1 1
      app/view/stage_extra/bonus.ejs
  49. 1 1
      app/view/stage_extra/jgcl.ejs
  50. 1 1
      app/view/stage_extra/other.ejs
  51. 1 1
      app/view/stage_extra/safe_prod.ejs
  52. 1 1
      app/view/stage_extra/temp_land.ejs
  53. 2 1
      app/view/sub_proj/index.ejs
  54. 10 0
      app/view/sub_proj/modal.ejs
  55. 37 0
      app/view/tender/cert.ejs
  56. 68 0
      app/view/tender/cert_modal.ejs
  57. 1 0
      app/view/tender/detail.ejs
  58. 57 1
      config/web.js
  59. 15 0
      db_script/project_spread.js
  60. 77 2
      sql/update.sql

+ 2 - 0
app/const/page_show.js

@@ -50,6 +50,8 @@ const defaultSetting = {
     isPreset: 0,
     isOnlyChecked: 1,
     openSettle: 0,
+    openMultiStageCalc: 0,
+    maxMultiStageCount: 5,
     openStageStart: 0,
     openDataCollect: 1,
     openFile: 1,

+ 57 - 0
app/const/profile.js

@@ -0,0 +1,57 @@
+'use strict';
+
+/**
+ * 个人信息通用 相关常量
+ *
+ * @author ELlisran
+ * @date 2024/03/26
+ * @version
+ */
+const cert_type = [
+    { name: '执业注册', value: 1, source: 'cert_reg', sort: 1 },
+    { name: '执业资格', value: 2, source: 'cert_qual', sort: 2 },
+];
+// 执业注册和执业资格的value值需要不同,这样取名称时可以合起来获取
+// 执业注册
+const cert_reg = [
+    { name: '一级注册建筑师', value: 1, sort: 1 },
+    { name: '二级注册建筑师', value: 2, sort: 2 },
+    { name: '一级注册结构工程师', value: 3, sort: 3 },
+    { name: '二级注册结构工程师', value: 4, sort: 4 },
+    { name: '注册监理工程师', value: 5, sort: 5 },
+    { name: '一级注册造价工程师', value: 6, sort: 6 },
+    { name: '二级注册造价工程师', value: 7, sort: 7 },
+    { name: '一级注册建造师', value: 8, sort: 8 },
+    { name: '二级注册建造师', value: 9, sort: 9 },
+    { name: '注册土木工程师', value: 10, sort: 10 },
+    { name: '注册安全工程师', value: 11, sort: 11 },
+    { name: '注册咨询工程师', value: 12, sort: 12 },
+    { name: '注册结构工程师', value: 13, sort: 13 },
+];
+// 执业资格
+const cert_qual = [
+    { name: '监理工程师', value: 14, sort: 1 },
+    { name: '造价工程师', value: 15, sort: 2 },
+    { name: '建造师', value: 16, sort: 3 },
+];
+
+const post_cert_const = ['type', 'name', 'code', 'reg_unit', 'job_title', 'file_name', 'file_path', 'edu_json'];
+const edu_json = {
+    id: null,
+    date: null,
+    unit: null,
+    file_path: null,
+    file_name: null,
+};
+
+const cert = {
+    certType: cert_type,
+    certReg: cert_reg,
+    certQual: cert_qual,
+    postCertConst: post_cert_const,
+    eduJsonConst: edu_json,
+};
+
+module.exports = {
+    cert,
+};

+ 12 - 0
app/const/spread.js

@@ -155,6 +155,7 @@ const BaseSetCol = {
         { key: 'memo', name: '备注', fixed: [], bills: 1, pos: 0 },
         { key: 'ex_memo2', name: '备注2', fixed: [], bills: 1, pos: 1, },
         { key: 'ex_memo3', name: '备注3', fixed: [], bills: 1, pos: 1, },
+        { key: 'add_stage_order', name: '添加期数', fixed: [], bills: 1, pos: 1, },
         { key: 'is_tp', name: '总额计量', fixed: [], bills: 1, pos: 0 },
         { key: 'gxby', name: '工序报验', fixed: ['valid', 'alias'], bills: 1, pos: 1, },
         { key: 'dagl', name: '档案管理', fixed: ['valid', 'alias'], bills: 1, pos: 1, },
@@ -251,6 +252,7 @@ const glSpreadTemplate = {
         { key: 'ex_memo2', valid: 0 },
         { key: 'ex_memo3', valid: 0 },
         { key: 'is_tp', valid: 1},
+        { key: 'add_stage_order', valid: 1},
         { key: 'gxby', valid: 1},
         { key: 'dagl', valid: 1},
     ],
@@ -345,6 +347,7 @@ const szSpreadTemplate = {
         { key: 'ex_memo2', valid: 0 },
         { key: 'ex_memo3', valid: 0 },
         { key: 'is_tp', valid: 1},
+        { key: 'add_stage_order', valid: 1},
         { key: 'gxby', valid: 1},
         { key: 'dagl', valid: 1},
     ],
@@ -439,6 +442,7 @@ const fjSpreadTemplate = {
         { key: 'ex_memo2', valid: 0 },
         { key: 'ex_memo3', valid: 0 },
         { key: 'is_tp', valid: 1},
+        { key: 'add_stage_order', valid: 1},
         { key: 'gxby', valid: 1},
         { key: 'dagl', valid: 1},
     ],
@@ -591,6 +595,13 @@ const withoutClReplace = {
         {title: '设计量', colSpan: '1', rowSpan: '2', field: 'sgfh_qty', hAlign: 2, width: 120, type: 'Number'},
     ],
 };
+const SpreadSpec = {
+    gcl_stage_set: {
+        pos: [
+            { condition: { key: 'field', value: ['name', 'position'] }, update: { readOnly: false } },
+        ]
+    }
+};
 
 const withCl = {
     ledger: {
@@ -1190,6 +1201,7 @@ module.exports = {
     BaseSetCol,
     ProjectSpreadTemplate,
     BaseSpreadColSetting,
+    SpreadSpec,
     withoutClReplace,
     withCl,
     withoutCl,

+ 92 - 0
app/controller/file_controller.js

@@ -31,6 +31,9 @@ module.exports = app => {
                     auditConst,
                 };
                 renderData.projectList = await ctx.service.subProject.getFileProject(ctx.session.sessionProject.id, ctx.session.sessionUser.accountId, ctx.session.sessionUser.is_admin);
+                for (const p of renderData.projectList) {
+                    if (!p.is_folder) p.file_count = await this.service.filing.sumFileCount(p.id);
+                }
                 renderData.tenderList = await ctx.service.tender.getList4Select('stage');
                 renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.session.sessionProject.id);
                 await this.layout('file/index.ejs', renderData, 'file/modal.ejs');
@@ -353,6 +356,95 @@ module.exports = app => {
                 ctx.body = this.ajaxErrorBody(error, '导入附件失败,请重试');
             }
         }
+
+        async template(ctx) {
+            const defaultTemplate = await ctx.service.filingTemplateList.getOriginTemplate();
+            ctx.redirect('/file/template/' + defaultTemplate.id);
+        }
+
+        async templateDetail(ctx) {
+            try {
+                const renderData = {
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.file.template),
+                };
+                renderData.templateList = await ctx.service.filingTemplateList.getAllTemplate(ctx.session.sessionProject.id);
+                renderData.FtType = ctx.service.filingTemplateList.FtType;
+                renderData.template = renderData.templateList.find(x => { return x.id === ctx.params.id });
+                if (!renderData.template) throw '查看的资料模板不存在';
+
+                renderData.templateData = await ctx.service.filingTemplate.getData(renderData.template.id);
+                await this.layout('file/template.ejs', renderData);
+            } catch (err) {
+                ctx.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect(this.menu.menu.dashboard.url);
+            }
+        }
+
+        async saveTemplate(ctx) {
+            try {
+                const id = ctx.query.id;
+                const name = ctx.request.body.name;
+                const [save, templateId] = await ctx.service.filingTemplateList.save(name, id);
+                if (!save) throw '保存数据失败';
+                ctx.redirect('/file/template/' + templateId);
+            } catch(err) {
+                ctx.log(err);
+                ctx.session.postError = err.toString();
+                ctx.redirect('/file/template');
+            }
+        }
+
+        async resetTemplate(ctx) {
+            try {
+                const id = ctx.query.id;
+                await ctx.service.filingTemplateList.reset(id);
+                ctx.redirect('/file/template/' + id);
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '重置模板失败');
+                ctx.redirect('/file/template');
+            }
+        }
+
+        async delTemplate(ctx) {
+            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 {
+                    ctx.redirect(ctx.request.headers.referer);
+                }
+            } catch (err) {
+                ctx.log(err);
+                ctx.postError(err, '删除模板失败');
+                ctx.redirect('/file/template');
+            }
+        }
+
+        async updateTemplate(ctx) {
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data.updateType) throw '数据错误';
+                let result;
+                if (data.updateType === 'add') {
+                    result = await ctx.service.filingTemplate.add(ctx.params.id, data);
+                } else if (data.updateType === 'del') {
+                    result = await ctx.service.filingTemplate.del(ctx.params.id, data);
+                } else if (data.updateType === 'save') {
+                    result = await ctx.service.filingTemplate.save(data);
+                } else if (data.updateType === 'move') {
+                    if (!data.id || !(data.tree_order >= 0)) throw '数据错误';
+                    result = await ctx.service.filingTemplate.move(ctx.params.id, data);
+                }
+                ctx.body = { err: 0, msg: '', data: result };
+            } catch (err) {
+                ctx.log(err);
+                ctx.ajaxErrorBody(err, '修改失败');
+            }
+        }
     }
 
     return BudgetController;

+ 11 - 0
app/controller/measure_controller.js

@@ -56,6 +56,7 @@ module.exports = app => {
                         s.curAuditors2 = await ctx.service.stageAudit.getAuditorsByStatus(s.id, auditConst.status.checking, s.times);
                     }
                 }
+                renderData.unCompleteStageCount = renderData.stages.filter(s => { return s.status !== auditConst.status.checked; }).length;
                 await this.layout('measure/stage.ejs', renderData, 'measure/stage_modal.ejs');
             } catch (err) {
                 this.log(err);
@@ -97,6 +98,16 @@ module.exports = app => {
                 if (!date || !period) {
                     throw '请选择计量年月和开始-截止日期';
                 }
+                const stages = await ctx.service.stage.getAllDataByCondition({ where: { tid: ctx.tender.id }});
+                const unCompleteStageCount = stages.filter(s => { return s.status !== auditConst.status.checked; }).length;
+                if (unCompleteStageCount.length > 0) {
+                    if (ctx.session.sessionProject.page_show.openMultiStageCalc) {
+                        if (unCompleteStageCount >= ctx.session.sessionProject.page_show.maxMultiStageCount)
+                            throw `最多只可同时进行${ctx.session.sessionProject.page_show.maxMultiStageCount}期计量`;
+                    } else {
+                        throw `最新一起未审批通过,请审批通过后再新增计量`
+                    }
+                }
                 const newStage = await ctx.service.stage.addStage(ctx.tender.id, date, period);
                 if (!newStage) {
                     throw '新增计量期失败,请重试';

+ 121 - 0
app/controller/profile_controller.js

@@ -16,6 +16,7 @@ const path = require('path');
 const sendToWormhole = require('stream-wormhole');
 const loginWay = require('../const/setting').loginWay;
 const wxWork = require('../lib/wx_work');
+const profileConst = require('../const/profile');
 
 module.exports = app => {
 
@@ -83,6 +84,126 @@ module.exports = app => {
 
             ctx.redirect(ctx.request.header.referer);
         }
+        /**
+         * 账号资料页面
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        async cert(ctx) {
+            // 获取当前用户数据
+            const sessionUser = ctx.session.sessionUser;
+
+            // 获取账号数据
+            const accountData = await ctx.service.projectAccount.getDataByCondition({ id: sessionUser.accountId });
+            const certList = await ctx.service.accountCert.getAllDataByCondition({ where: { uid: sessionUser.accountId }, orders: [['create_time', 'desc']] });
+            // json转换
+            certList.forEach(item => {
+                item.edu_json = item.edu_json ? JSON.parse(item.edu_json) : [];
+            });
+            const renderData = {
+                accountData,
+                certList,
+                fujianOssPath: ctx.app.config.fujianOssPath,
+                certTypeConst: profileConst.cert.certType,
+                certSourceConst: {
+                    cert_reg: ctx.helper._.orderBy(profileConst.cert.certReg, ['sort'], ['asc']),
+                    cert_qual: ctx.helper._.orderBy(profileConst.cert.certQual, ['sort'], ['asc']),
+                },
+                jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.profile.cert),
+            };
+            await this.layout('profile/cert.ejs', renderData, 'profile/cert_modal.ejs');
+        }
+
+        async certSave(ctx) {
+            const response = {
+                err: 0,
+                msg: '',
+                data: {},
+            };
+            try {
+                const sessionUser = ctx.session.sessionUser;
+                const data = JSON.parse(ctx.request.body.data);
+                switch (data.type) {
+                    case 'add_cert':
+                        response.data = await ctx.service.accountCert.addCert(sessionUser.accountId);
+                        break;
+                    case 'update_cert':
+                        response.data = await ctx.service.accountCert.updateCert(data.update_data);
+                        break;
+                    case 'add_jx':
+                        response.data = await ctx.service.accountCert.addEduJson(data.id);
+                        break;
+                    case 'update_jx':
+                        response.data = await ctx.service.accountCert.updateEduJson(data.update_data);
+                        break;
+                    case 'del_cert': // 包括删除附件
+                        response.data = await ctx.service.accountCert.delCert(data.delete_data);
+                        break;
+                    default:throw '参数有误';
+                }
+            } catch (error) {
+                response.err = 1;
+                response.msg = error.toString();
+            }
+            ctx.body = response;
+        }
+        /**
+         * 上传证书(单选)
+         *
+         * @param {object} ctx - egg全局变量
+         * @return {void}
+         */
+        async certUpload(ctx) {
+            const responseData = {
+                err: 0, msg: '', data: null,
+            };
+            try {
+                const stream = await ctx.getFileStream();
+                const create_time = Date.parse(new Date()) / 1000;
+                const fileInfo = path.parse(stream.filename);
+                const id = stream.fields && stream.fields.id ? stream.fields.id : 0;
+                if (!id) throw '参数有误';
+                let jxid = '';
+                let type = '';
+                if (stream.fields && stream.fields.type === 'upload_jx') {
+                    jxid = stream.fields.jxid ? stream.fields.jxid : '';
+                    if (!jxid) throw '参数有误';
+                    type = 'jx';
+                } else if (stream.fields && stream.fields.type === 'upload_cert') {
+                    type = 'cert';
+                }
+                if (!type) throw '参数有误';
+                // 判断用户是否选择上传文件
+                if (!stream.filename) {
+                    throw '请选择上传的文件!';
+                }
+                const filename = stream.filename;
+                const filepath = `app/public/upload/profile/${ctx.session.sessionUser.accountId}/cert/zhengshu_${create_time + fileInfo.ext}`;
+                // await ctx.helper.saveStreamFile(stream, path.resolve(this.app.baseDir, filepath));
+                await ctx.app.fujianOss.put(ctx.app.config.fujianOssFolder + filepath, stream);
+                await sendToWormhole(stream);
+                if (type === 'jx') {
+                    const info = await ctx.service.accountCert.getDataById(id);
+                    if (!info) throw '数据有误';
+                    const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+                    const jxInfo = edu_json.find(item => item.id === jxid);
+                    if (!jxInfo) throw '数据有误';
+                    jxInfo.file_path = filepath;
+                    jxInfo.file_name = filename;
+                    await ctx.service.accountCert.update({ edu_json: JSON.stringify(edu_json) }, { id });
+                } else {
+                    await ctx.service.accountCert.update({ file_path: filepath, file_name: filename }, { id });
+                }
+                responseData.data = { file_path: filepath, file_name: filename };
+            } catch (err) {
+                console.log(err);
+                responseData.err = 1;
+                responseData.msg = err;
+            }
+            ctx.body = responseData;
+        }
+
 
         /**
          * 修改密码操作

+ 13 - 4
app/controller/settle_controller.js

@@ -629,8 +629,8 @@ module.exports = app => {
         async loadGatherData(ctx) {
             try {
                 const settle = await this.ctx.service.settle.getLatestCompleteSettle(ctx.tender.id);
-                const bills = await this.ctx.service.settleBills.getDataByCondition({ where: { settle_id: settle.id }});
-                const pos = await this.ctx.service.settlePos.getDataByCondition({ where: { settle_id: settle.id }});
+                const bills = await this.ctx.service.settleBills.getAllDataByCondition({ where: { settle_id: settle.id }});
+                const pos = await this.ctx.service.settlePos.getAllDataByCondition({ where: { settle_id: settle.id }});
                 ctx.body = { err: 0, msg: '', data: { bills, pos } };
             } catch(err) {
                 ctx.log(err);
@@ -640,11 +640,20 @@ module.exports = app => {
         async gather(ctx) {
             try {
                 const renderData = {
+                    settleStatusHint: ctx.service.settle.statusArray,
+                    settleStatusColor: ctx.service.settle.statusColor,
+                    settleStatus: ctx.service.settle.settleStatus,
+                    measureType,
+                    settle: ctx.settle,
+                    thirdParty: {
+                        gxby: ctx.session.sessionProject.gxby_status,
+                        dagl: ctx.session.sessionProject.dagl_status,
+                    },
                     tender: ctx.tender.data,
-                    preUrl: `/tender/${ctx.tender.id}/measure/stage`,
+                    preUrl: `/tender/${ctx.tender.id}/settle`,
                 };
                 renderData.jsFiles = this.app.jsFiles.common.concat(this.app.jsFiles.settle.gather);
-                await this.layout('settle/gather.ejs', renderData, 'settle/gather_modal.ejs');
+                await this.layout('settle/gather.ejs', renderData);
             } catch (err) {
                 ctx.log(err);
                 ctx.redirect(this.menu.menu.dashboard.url);

+ 2 - 1
app/controller/sub_proj_controller.js

@@ -43,6 +43,7 @@ module.exports = app => {
                 renderData.permissionConst = ctx.service.subProjPermission.PermissionConst;
                 renderData.categoryData = await this.ctx.service.category.getAllCategory(this.ctx.session.sessionProject.id);
                 renderData.companys = await this.ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                renderData.templates = await this.ctx.service.filingTemplateList.getAllTemplate(ctx.session.sessionProject.id);
                 await this.layout('sub_proj/index.ejs', renderData, 'sub_proj/modal.ejs');
             } catch (err) {
                 ctx.log(err);
@@ -124,7 +125,7 @@ module.exports = app => {
                 } else if (data.std_id !== undefined) {
                     result = await ctx.service.subProject.setBudgetStd({ id: data.id, std_id: data.std_id });
                 } else if (data.management !== undefined) {
-                    result = await ctx.service.subProject.setManagement({ id: data.id, management: data.management });
+                    result = await ctx.service.subProject.setManagement({ id: data.id, management: data.management, filingTemplate: data.filingTemplate });
                 }
                 ctx.body = { err: 0, msg: '', data: { update: [result] } };
             } catch(err) {

+ 57 - 1
app/controller/tender_controller.js

@@ -26,6 +26,7 @@ const tenderInfoModel = require('../lib/tender_info');
 const mapConst = require('../const/map');
 const advanceConst = require('../const/advance');
 const projectSetting = require('../const/project_setting');
+const profileConst = require('../const/profile');
 
 module.exports = app => {
 
@@ -1082,6 +1083,61 @@ module.exports = app => {
             }
         }
 
+        async certSet(ctx) {
+            try {
+                const allCertList = await ctx.service.accountCert.getAllCertByPid(ctx.session.sessionProject.id);
+                const tenderCertList = await ctx.service.tenderCert.getListByTid(ctx.tender.id, allCertList);
+                const renderData = {
+                    allCertList,
+                    tenderCertList,
+                    certTypeConst: profileConst.cert.certType,
+                    certSourceConst: [...profileConst.cert.certReg, ...profileConst.cert.certQual],
+                    fujianOssPath: ctx.app.config.fujianOssPath,
+                    jsFiles: this.app.jsFiles.common.concat(this.app.jsFiles.tender.cert),
+                };
+                const accountList = await ctx.service.projectAccount.getAllDataByCondition({
+                    where: { project_id: ctx.session.sessionProject.id, enable: 1, id: ctx.helper._.uniq(ctx.helper._.map(allCertList, 'uid')) },
+                    columns: ['id', 'name', 'company', 'role', 'enable', 'is_admin', 'account_group', 'mobile'],
+                });
+                renderData.accountList = accountList;
+                const unitList = await ctx.service.constructionUnit.getAllDataByCondition({ where: { pid: ctx.session.sessionProject.id } });
+                renderData.accountGroup = unitList.map(item => {
+                    const groupList = accountList.filter(item1 => item1.company === item.name);
+                    return { groupName: item.name, groupList };
+                });
+                await this.layout('tender/cert.ejs', renderData, 'tender/cert_modal.ejs');
+            } catch (err) {
+                this.log(err);
+                ctx.redirect('/tender/' + ctx.tender.id);
+            }
+        }
+
+        async saveCert(ctx) {
+            const response = { err: 0, msg: '', data: {} };
+            try {
+                const data = JSON.parse(ctx.request.body.data);
+                if (!data) {
+                    throw '提交数据错误';
+                }
+                switch (data.type) {
+                    case 'save_user':
+                        response.data = await ctx.service.tenderCert.saveUserCert(ctx.tender.id, data.list);
+                        break;
+                    case 'update_cert':
+                        response.data = await ctx.service.tenderCert.updateOneCert(data.update_data);
+                        break;
+                    case 'paste_cert':
+                        response.data = await ctx.service.tenderCert.updateMoreCert(ctx.tender.id, data.update_data);
+                        break;
+                    default:break;
+                }
+                ctx.body = response;
+            } catch (err) {
+                this.log(err);
+                ctx.body = { err: 1, msg: err.toString(), data: {} };
+            }
+        }
+
         /**
          * 游客账号设置
          * @param {object} ctx - 上下文
@@ -1190,7 +1246,7 @@ module.exports = app => {
                     responseData.data.tenders = tenderList;
                 } else if (data.type === 'ledger') {
                     responseData.data.tenders = tenderList.filter(x => {
-                        return x.ledger_status === auditConst.ledger.status.checked;
+                        return x.ledger_status === auditConst.ledger.status.checked;    ``
                     });
                     const history = await this.ctx.service.sumLoadHistory.getLedgerHistory(data.tid, data.lid);
                     if (history) responseData.data.history = { tenders: history.tenders, load_time: history.load_time, type: 'ledger' };

+ 5 - 0
app/extend/helper.js

@@ -1729,4 +1729,9 @@ module.exports = {
         //     endDateTime.getMinutes().toString().padStart(2, '0');
         return endDateTime;
     },
+    showCol4ObjArray(objArray, colName, key = 'value', showKey = 'name') {
+        if (!colName) return '';
+        const info = _.find(objArray, { [key]: colName });
+        return info ? info[showKey] : '';
+    },
 };

+ 1 - 0
app/middleware/tender_check.js

@@ -122,6 +122,7 @@ module.exports = options => {
             tender.schedule_permission = schedule_permission;
             yield next;
         } catch (err) {
+            console.log(err);
             // 输出错误到日志
             if (err.stack) {
                 this.logger.error(err);

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

@@ -470,7 +470,7 @@ $(document).ready(function() {
                 node.name = node.name + (node.source_node.total_file_count > 0 ? `(${node.source_node.total_file_count})` : '');
                 filingObj.filingTree.updateNode(node);
             },
-            beforeRemove: function(e, key, node, isCancel) {
+            beforeRemove: function(key, node, isCancel) {
                 $('#del-filing').modal('show');
                 return false;
             },

+ 3 - 1
app/public/js/file_list.js

@@ -28,7 +28,7 @@ $(document).ready(() => {
             getRowTdHtml: function (node, tree) {
                 const html = [];
                 // 名称
-                html.push('<td width="20%" class="in-' + node.tree_level + '">');
+                html.push('<td class="in-' + node.tree_level + '">');
                 if (node.is_folder) {
                     if (node.children.length > 0) {
                         html.push('<span onselectstart="return false" style="{-moz-user-select:none}" class="fold-switch mr-1" title="收起" id="'+ node.id +'"><i class="fa fa-minus-square-o"></i></span> <i class="fa fa-folder-o"></i> ', node.name);
@@ -40,6 +40,8 @@ $(document).ready(() => {
                     html.push(`<a href="/sp/${node.id}/file" name="name" id="${node.id}">`, node.name, '</a>');
                 }
                 html.push('</td>');
+                // 文件个数
+                html.push(`<td class="text-center">${ node.file_count || ''}</td>`);
                 // 管理单位
                 html.push(`<td class="text-center">${node.management}</td>`);
                 // 创建时间

+ 265 - 0
app/public/js/filing_template.js

@@ -0,0 +1,265 @@
+$(document).ready(function() {
+    autoFlashHeight();
+    $('#filing').height($(".sjs-height-0").height() - $('#add-slibing').parent().parent().height() - 10);
+    class FilingObj {
+        constructor(setting) {
+            // 原始数据整理后的树结构,用来整理zTree显示
+            this.dragTree = createDragTree({
+                id: 'id',
+                pid: 'tree_pid',
+                level: 'tree_level',
+                order: 'tree_order',
+                rootId: '-1'
+            });
+            // 界面显示的zTree
+            this.setting = setting;
+            this.filingTree = null;
+            $('#filing').height($(".sjs-height-0").height()-$('.d-flex',".sjs-height-0").height() - 10);
+        }
+        _loadFilingSourceNode() {
+            const self = this;
+            const loadChildren = function(children) {
+                for (const child of children) {
+                    if (child.children && child.children.length > 0) loadChildren(child.children);
+                    child.source_node = self.dragTree.getItems(child.id);
+                }
+            };
+            const nodes = this.filingTree.getNodes();
+            loadChildren(nodes);
+        }
+        loadFiling() {
+            if (this.filingTree) $.fn.zTree.destroy(this.setting.treeId);
+            const sortNodes = this.dragTree.nodes.map(x => {
+                const result = {
+                    id: x.id,
+                    tree_pid: x.tree_pid,
+                    name: x.name + (x.total_file_count > 0 ? `(${x.total_file_count})` : ''),
+                    spid: x.spid,
+                };
+                return result;
+            });
+            this.filingTree = $.fn.zTree.init($('#filing'), this.setting, sortNodes);
+            this._loadFilingSourceNode();
+            const curCache = getLocalCache(this.curFilingKey);
+            const curNode = curCache ? this.filingTree.getNodeByParam('id', curCache) : null;
+            if (curNode){
+                this.filingTree.selectNode(curNode);
+                filingObj.setCurFiling(curNode);
+            }
+        }
+        analysisFiling(data) {
+            this.dragTree.loadDatas(data);
+            this.loadFiling();
+        }
+        addSiblingFiling(node) {
+            const self = this;
+            postData(`${window.location.pathname}/update`, { updateType: 'add', tree_pid: node.tree_pid, tree_pre_id: node.id }, function(result) {
+                const refreshData = self.dragTree.loadPostData(result);
+                const newNode = refreshData.create[0];
+                const nodes = self.filingTree.addNodes(node.getParentNode(), node.getIndex() + 1, [{ id: newNode.id, tree_pid: newNode.tree_pid, name: newNode.name, spid: newNode.spid }]);
+                nodes[0].source_node = newNode;
+            });
+        }
+        addChildFiling(node) {
+            const self = this;
+            postData(`${window.location.pathname}/update`, { updateType: 'add', tree_pid: node.id }, function(result) {
+                const refreshData = self.dragTree.loadPostData(result);
+                const newNode = refreshData.create[0];
+                const nodes = self.filingTree.addNodes(node, -1, [{ id: newNode.id, tree_pid: newNode.tree_pid, name: newNode.name, spid: newNode.spid}]);
+                nodes[0].source_node = newNode;
+            });
+        }
+        delFiling(node, callback) {
+            const parent = node.getParentNode();
+            const self = this;
+            postData(`${window.location.pathname}/update`, { updateType: 'del', id: node.id }, function(result) {
+                self.dragTree.loadPostData(result);
+                self.filingTree.removeNode(node);
+                if (parent) {
+                    const path = parent.getPath();
+                    for (const p of path) {
+                        p.name = p.source_node.name + (p.source_node.total_file_count > 0 ? `(${p.source_node.total_file_count})` : '');
+                        filingObj.filingTree.updateNode(p);
+                    }
+                }
+                if (callback) callback();
+            });
+        }
+        async renameFiling(node, newName) {
+            const result = await postDataAsync(`${window.location.pathname}/update`, { updateType:'save', id: node.id, name: newName });
+            node.source_node.name = newName;
+            node.name = node.source_node.name + (node.source_node.total_file_count > 0 ? `(${node.source_node.total_file_count})` : '');
+            return result;
+        }
+        async setCurFiling(node) {
+            filingObj.curFiling = node;
+        }
+        moveFiling(node, tree_pid, tree_order) {
+            if (tree_pid === node.source_node.tree_pid && tree_order === node.source_node.tree_order) return;
+
+            const self = this;
+            postData(`${window.location.pathname}/update`, { updateType: 'move', id: node.id, tree_pid, tree_order }, function(result) {
+                const refresh = self.dragTree.loadPostData(result);
+                const updated = [];
+                for (const u of refresh.update) {
+                    if (!u) continue;
+                    const node = self.filingTree.getNodeByParam('id', u.id);
+                    if (node) {
+                        const path = node.getPath();
+                        for (const p of path) {
+                            if (updated.indexOf(p.id) >= 0) continue;
+
+                            p.name = p.source_node.name + (p.source_node.total_file_count > 0 ? `(${p.source_node.total_file_count})` : '');
+                            filingObj.filingTree.updateNode(p);
+                            updated.push(p.id);
+                        }
+                    }
+                }
+            });
+        }
+    }
+    const levelTreeSetting = {
+        treeId: 'filing',
+        view: {
+            selectedMulti: false,
+            showIcon: false,
+        },
+        data: {
+            simpleData: {
+                idKey: 'id',
+                pIdKey: 'tree_pid',
+                rootPId: '-1',
+                enable: true,
+            }
+        },
+        edit: {
+            enable: true,
+            showRemoveBtn: !readOnly,
+            showRenameBtn: !readOnly,
+            renameTitle: '编辑',
+            removeTitle: '删除',
+            drag: {
+                isCopy: false,
+                isMove: true,
+                pre: true,
+                next: true,
+                inner: false,
+            },
+            editNameSelectAll: true,
+        },
+        callback: {
+            onClick: async function (e, key, node) {
+                if (filingObj.curFiling && filingObj.curFiling.id === node.id) return;
+
+                filingObj.setCurFiling(node);
+            },
+            beforeRename: async function(key, node, newName, isCancel) {
+                if (!isCancel) await filingObj.renameFiling(node, newName);
+                return true;
+            },
+            beforeRemove: function(key, node, isCancel) {
+                filingObj.delFiling(node, function() {
+                    $('#del-filing').modal('hide');
+                });
+                return false;
+            },
+            beforeDrop: function(key, nodes, target, moveType, isCopy) {
+                if (readOnly) return false;
+                if (!target) return false;
+
+                const order = nodes[0].getIndex() + 1;
+                const targetOrder = target.getIndex() + 1;
+                const targetParent = target.getParentNode();
+                const targetMax = targetParent ? targetParent.children.length : filingObj.dragTree.children.length;
+                if (moveType === 'prev') {
+                    if (target.tree_pid === nodes[0].tree_pid) {
+                        if (targetOrder > order) {
+                            filingObj.moveFiling(nodes[0], target.tree_pid, targetOrder === 1 ? 1 : targetOrder - 1);
+                        } else {
+                            filingObj.moveFiling(nodes[0], target.tree_pid, targetOrder === 1 ? 1 : targetOrder);
+                        }
+                    } else {
+                        filingObj.moveFiling(nodes[0], target.tree_pid, targetOrder === 1 ? 1 : targetOrder);
+                    }
+                } else if (moveType === 'next') {
+                    if (target.tree_pid === nodes[0].tree_pid) {
+                        if (targetOrder < order) {
+                            filingObj.moveFiling(nodes[0], target.tree_pid, targetOrder === targetMax ? targetMax : targetOrder + 1);
+                        } else {
+                            filingObj.moveFiling(nodes[0], target.tree_pid, targetOrder === targetMax ? targetMax : targetOrder);
+                        }
+                    } else {
+                        filingObj.moveFiling(nodes[0], target.tree_pid, targetOrder + 1);
+                    }
+                } else if (moveType === 'inner') {
+                    filingObj.moveFiling(nodes[0], target.tree_id, targetMax + 1);
+                }
+            }
+        }
+    };
+    const filingObj = new FilingObj(levelTreeSetting);
+    filingObj.analysisFiling(templateData);
+    $('#add-slibing').click(() => {
+        if (!filingObj.curFiling) return;
+        filingObj.addSiblingFiling(filingObj.curFiling);
+    });
+    $('#add-child').click(() => {
+        if (!filingObj.curFiling) return;
+        filingObj.addChildFiling(filingObj.curFiling);
+    });
+
+    const hiddenSubmit = function(action, extraName, extraValue) {
+        $('#hiddenForm').attr('action', action);
+        if (extraName) {
+            $('#extra').attr('name', extraName);
+            $('#extra').val(extraValue);
+        };
+        $('#hiddenForm').submit();
+    };
+    $('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=renameTemplate]', function(e){
+        $(this).parents('.table-file').attr('renaming', '1');
+        $(`#${this.getAttribute('aria-describedby')}`).remove();
+        const tempId = $(this).parents('.table-file').attr('tempId');
+        const template = templateList.find(x => { return x.id === tempId; });
+        if (!template) return;
+
+        const html = [];
+        html.push(`<div><input type="text" class="form-control form-control-sm" style="width: 300px" value="${template.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[tempId=${tempId}]`).html(html.join(''));
+        e.stopPropagation();
+    });
+    $('body').on('click', 'a[name=renameOk]', function(){
+        const tempId = $(this).parents('.table-file').attr('tempId');
+        const newName = $(this).parents('.table-file').find('input').val();
+        hiddenSubmit('/file/template/save?id='+tempId, 'name', newName);
+        $(this).parents('.table-file').attr('renaming', '0');
+    });
+    $('body').on('click', 'a[name=delTemplate]', function(e){
+        e.stopPropagation();
+        const tempId = $(this).parents('.table-file').attr('tempId');
+        hiddenSubmit('/file/template/del?id='+tempId);
+    });
+    $('body').on('click', 'a[name=renameCancel]', function() {
+        $(this).parents('.table-file').attr('renaming', '0');
+        const tempId = $(this).parents('.table-file').attr('tempId');
+        const template = templateList.find(x => { return x.id === tempId; });
+        if (!template) return;
+
+        const html = [];
+        html.push(`<div>${template.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="renameTemplate"><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="delTemplate"><i class="fa fa-trash-o fa-fw text-danger"></i></a>',
+            '</div>');
+        $(`.table-file[tempId=${tempId}]`).html(html.join(''));
+    });
+});

+ 405 - 0
app/public/js/profile_cert.js

@@ -0,0 +1,405 @@
+/**
+ * 账号相关js
+ *
+ * @author CaiAoLin
+ * @date 2018/1/26
+ * @version
+ */
+$(document).ready(function() {
+    autoFlashHeight();
+
+    $('#addCert').click(function() {
+        postData('/profile/cert/save', { type: 'add_cert' }, function(result) {
+            console.log(result);
+            const html = insertCertHtml(result.total, result.data);
+            $('#certs').prepend(html);
+        });
+    });
+
+    $('body').on('click', '#certs .add-jx-btn', function() {
+        const _self = $(this);
+        postData('/profile/cert/save', { type: 'add_jx', id: $(this).parents('.one-cert').data('cid') }, function(result) {
+            console.log(result);
+            const html = insertJxHtml(result.data, result.jxData);
+            _self.parents('.pull-right').siblings('.all-jx').prepend(html);
+            console.log(_self.parents('.pull-right').siblings('.all-jx').children('.one-jx').eq(0).find('.jx-date'));
+            _self.parents('.pull-right').siblings('.all-jx').children('.one-jx').eq(0).find('.jx-date').datepicker({
+                onShow: function (res) {
+                    res.$el.attr('readOnly', true);
+                },
+                onHide: function (res, animationCompleted) {
+                    if(animationCompleted) {
+                        res.$el.attr('readOnly', false);
+                        const val = res.$el.val();
+                        const oldVal = res.$el.attr('data-old-date') || '';
+                        // 日期格式判断
+                        if (val !== '' && !(isNaN(val) && !isNaN(Date.parse(val)))) {
+                            toastr.error('日期格式有误!');
+                            res.$el.val(oldVal);
+                            if (oldVal === '') {
+                                res.clear();
+                            } else {
+                                res.selectDate(new Date(oldVal));
+                            }
+                        } else if(val !== oldVal) {
+                            const data = {
+                                id: res.$el.parents('.one-cert').data('cid'),
+                                jxid: res.$el.parents('.one-jx').data('jxid'),
+                                key: 'date',
+                                value: val,
+                            };
+                            updateJxDataFun(data);
+                            res.$el.attr('data-old-date', val);
+                        }
+                    }
+                }
+            });
+        });
+    });
+
+    $('body').on('change', '#certs select', function() {
+        const value = parseInt($(this).val());
+        const key = $(this).data('type');
+        const update_data = {
+            id: $(this).parents('.one-cert').data('cid'),
+            key,
+            value,
+        };
+        if (key === 'type') {
+            let html = '';
+            if (value === 0) {
+                update_data.other = {
+                    key: 'name',
+                    value: null,
+                };
+                html = '<option value="0">请选择</option>';
+            } else {
+                update_data.other = {
+                    key: 'name',
+                    value: certSourceConst[showCol4ObjArray(certTypeConst, value, 'value', 'source')][0].value,
+                };
+                html = changeSourceHtml(certSourceConst[showCol4ObjArray(certTypeConst, value, 'value', 'source')]);
+            }
+            $(this).parents('.one-cert-msg').find('.select-cert-name').html(html);
+        }
+        console.log(update_data);
+        postData('/profile/cert/save', { type: 'update_cert', update_data }, function(result) {
+            console.log(result);
+        });
+    });
+
+    function changeSourceHtml(source = []) {
+        let html = '';
+        for (const s of source) {
+            html += `<option value="${s.value}">${s.name}</option>`;
+        }
+        return html;
+    }
+
+    $('body').on('change', '#certs .one-cert-msg input[type="text"]', function() {
+        const value = $(this).val();
+        const key = $(this).data('type');
+        const update_data = {
+            id: $(this).parents('.one-cert').data('cid'),
+            key,
+            value,
+        };
+        console.log(update_data);
+        postData('/profile/cert/save', { type: 'update_cert', update_data }, function(result) {
+            console.log(result);
+        });
+    });
+
+    $('body').on('change', '#certs .one-jx input[type="text"]', function() {
+        const value = $(this).val();
+        const key = $(this).data('type');
+        const update_data = {
+            id: $(this).parents('.one-cert').data('cid'),
+            jxid: $(this).parents('.one-jx').data('jxid'),
+            key,
+            value,
+        };
+        console.log(update_data);
+        updateJxDataFun(update_data);
+    });
+    // 设置默认值
+    $('.jx-date').each(function() {
+        const defaultValue = $(this).val(); // 获取当前元素的 value 属性作为默认值
+        if (defaultValue) {
+            // 初始化日期选择器,并设置默认值
+            $(this).datepicker().data('datepicker').selectDate(new Date(defaultValue));
+        }
+    });
+    $('body .jx-date').datepicker({
+        onShow: function (res) {
+            res.$el.attr('readOnly', true);
+        },
+        onHide: function (res, animationCompleted) {
+            if(animationCompleted) {
+                res.$el.attr('readOnly', false);
+                const val = res.$el.val();
+                const oldVal = res.$el.attr('data-old-date') || '';
+                // 日期格式判断
+                if (val !== '' && !(isNaN(val) && !isNaN(Date.parse(val)))) {
+                    toastr.error('日期格式有误!');
+                    res.$el.val(oldVal);
+                    if (oldVal === '') {
+                        res.clear();
+                    } else {
+                        res.selectDate(new Date(oldVal));
+                    }
+                } else if(val !== oldVal) {
+                    const data = {
+                        id: res.$el.parents('.one-cert').data('cid'),
+                        jxid: res.$el.parents('.one-jx').data('jxid'),
+                        key: 'date',
+                        value: val,
+                    };
+                    updateJxDataFun(data);
+                    res.$el.attr('data-old-date', val);
+                }
+            }
+        }
+    });
+
+    $('body').on('click', '#certs .del-cert-btn', function() {
+        $('#delete-cert-cid').val($(this).attr('data-cid'));
+        $('#delete-cert-jxid').val($(this).attr('data-jxid'));
+        if ($(this).attr('data-filename')) {
+            console.log($(this).attr('data-filename'), $(this).attr('data-jxid'));
+            const txt = $(this).attr('data-jxid') ? '培训证明:' : '证书附件:';
+            $('#delete-cert-title').text(txt + $(this).attr('data-filename'));
+            $('#delete-cert-type').val('file');
+        } else {
+            $('#delete-cert-title').text($(this).parents('.modal-header').find('b').text());
+            $('#delete-cert-type').val('cert');
+        }
+    });
+
+    $('#delete-cert-btn').click(function() {
+        const data = {
+            id: $('#delete-cert-cid').val(),
+            jxid: $('#delete-cert-jxid').val(),
+            type: $('#delete-cert-type').val(),
+        };
+        console.log(data);
+        const _self = $(this);
+        postData('/profile/cert/save', { type: 'del_cert', delete_data: data }, function(result) {
+            _self.parents('.modal').modal('hide');
+            if (data.type === 'file') {
+                if (data.jxid) {
+                    $(`.one-cert[data-cid="${data.id}"] .one-jx[data-jxid="${data.jxid}"]`).find('.file-show').html('<input type="file" class="jx-file-upload">');
+                } else {
+                    $(`.one-cert[data-cid="${data.id}"]`).find('.one-cert-msg').find('.file-show').html('<input type="file" class="cert-file-upload">');
+                }
+            } else {
+                if (data.jxid) {
+                    $(`.one-cert[data-cid="${data.id}"] .one-jx[data-jxid="${data.jxid}"]`).remove();
+                    // 教育信息重新排序
+                    for (let i = 0; i < $(`.one-cert[data-cid="${data.id}"] .one-jx`).length; i++) {
+                        $(`.one-cert[data-cid="${data.id}"] .one-jx`).eq(i).find('.jx-num').text($(`.one-cert[data-cid="${data.id}"] .one-jx`).length - i);
+                    }
+                } else {
+                    $(`.one-cert[data-cid="${data.id}"]`).remove();
+                    // 证书信息重新排序
+                    for (let i = 0; i < $(`#certs .one-cert`).length; i++) {
+                        $(`#certs .one-cert`).eq(i).find('.cert-num').text($(`#certs .one-cert`).length - i);
+                    }
+                }
+            }
+        });
+    });
+
+    // 上传证书附件
+    $('body').on('change', '#certs .cert-file-upload', function () {
+        const file = this.files[0];
+        const formData = new FormData();
+        if (file === undefined) {
+            toastr.error('未选择上传文件!');
+            $(this).val('');
+            return false;
+        }
+        const ext = file.name.toLowerCase().split('.').splice(-1)[0];
+        const imgStr = /(jpg|jpeg|png|bmp|BMP|JPG|PNG|JPEG|pdf|PDF)$/;
+        if (!imgStr.test(ext)) {
+            toastr.error('请上传正确的图片或pdf格式文件');
+            $(this).val('');
+            return
+        }
+        const filesize = file.size;
+        if (filesize > 30 * 1024 * 1024) {
+            toastr.error('上传的文件大小不能超过30MB!');
+            $(this).val('');
+            return false;
+        }
+        const id = $(this).parents('.one-cert').data('cid');
+        formData.append('type', 'upload_cert');
+        formData.append('id', id);
+        formData.append('file', file);
+        console.log(formData);
+        $(this).val('');
+        const _self = $(this).parents('.file-show');
+        postDataWithFile('/profile/cert/upload', formData, function (result) {
+            _self.html(`<div class="col-form-label">
+                                                                <a href="${fujianOssPath + result.file_path}" target="_blank">${result.file_name}</a> &nbsp;<a href="#del-cert" data-cid="${id}" data-jxid="" data-filename="${result.file_name}" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                            </div>`);
+        });
+    });
+
+    // 上传证书附件
+    $('body').on('change', '#certs .jx-file-upload', function () {
+        const file = this.files[0];
+        const formData = new FormData();
+        if (file === undefined) {
+            toastr.error('未选择上传文件!');
+            $(this).val('');
+            return false;
+        }
+        const ext = file.name.toLowerCase().split('.').splice(-1)[0];
+        const imgStr = /(jpg|jpeg|png|bmp|BMP|JPG|PNG|JPEG|pdf|PDF)$/;
+        if (!imgStr.test(ext)) {
+            toastr.error('请上传正确的图片或pdf格式文件');
+            $(this).val('');
+            return
+        }
+        const filesize = file.size;
+        if (filesize > 30 * 1024 * 1024) {
+            toastr.error('上传的文件大小不能超过30MB!');
+            $(this).val('');
+            return false;
+        }
+        const id = $(this).parents('.one-cert').data('cid');
+        const jxid = $(this).parents('.one-jx').data('jxid');
+        formData.append('type', 'upload_jx');
+        formData.append('id', id);
+        formData.append('jxid', jxid);
+        formData.append('file', file);
+        console.log(formData);
+        $(this).val('');
+        const _self = $(this).parents('.file-show');
+        postDataWithFile('/profile/cert/upload', formData, function (result) {
+            _self.html(`<div class="col-form-label">
+                                                                <a href="${fujianOssPath + result.file_path}" target="_blank">${result.file_name}</a> &nbsp;<a href="#del-cert" data-cid="${id}" data-jxid="${jxid}" data-filename="${result.file_name}" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                            </div>`);
+        });
+    })
+
+    function updateJxDataFun(data) {
+        console.log(data);
+        postData('/profile/cert/save', { type: 'update_jx', update_data: data }, function(result) {
+            // console.log(result);
+        });
+    }
+
+    function insertJxHtml(data, jxdata) {
+        const html = `
+            <div class="one-jx" data-jxid="${jxdata.id}">
+                                                        <div class="card mt-3">
+                                                            <div class="modal-header">
+                                                                <b>继续教育<span class="jx-num">${data.edu_json.length}</span></b>
+                                                                <div class="pull-right">
+                                                                    <a href="#del-cert" data-cid="${data.id}" data-jxid="${jxdata.id}" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                                </div>
+                                                            </div>
+                                                            <div class="card-body">
+                                                                <div class="form-group row">
+                                                                    <label for="uname" class="ml-3 col-form-label">培训时间:</label>
+                                                                    <div class="col-sm-10">
+                                                                        <input data-language="zh" data-old-date="${jxdata.date}" data-type="date" placeholder="请选择时间" type="text" data-date-format="yyyy-MM-dd" class="jx-date datepicker-here form-control form-control-sm" value="${jxdata.date ? jxdata.date : ''}">
+                                                                    </div>
+                                                                </div>
+                                                                <div class="form-group row">
+                                                                    <label for="uname" class="ml-3 col-form-label">培训单位:</label>
+                                                                    <div class="col-sm-10">
+                                                                        <input type="text" class="form-control form-control-sm" data-type="unit" value="${jxdata.unit ? jxdata.unit : ''}">
+                                                                    </div>
+                                                                </div>
+                                                                <div class="form-group row">
+                                                                    <label for="uname" class="ml-3 col-form-label">培训证明:</label>
+                                                                    <div class="col-sm-10 file-show">
+                                                                        <input type="file" class="jx-file-upload">
+                                                                    </div>
+                                                                </div>
+                                                            </div>
+                                                        </div>
+                                                    </div>`;
+        return html;
+    }
+
+    function insertCertHtml(i, data) {
+        let typeHtml = ``;
+        for (const r of certTypeConst) {
+            typeHtml += `<option value="${r.value}">${r.name}</option>`;
+        }
+        const html = `<div class="col-6 mt-3 one-cert" data-cid="${data.id}">
+                                        <div class="card">
+                                            <div class="modal-header">
+                                                <b>证书信息<span class="cert-num">${i}</span></b>
+                                                <div class="pull-right">
+                                                    <a href="#del-cert" data-cid="${data.id}" data-jxid="" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                </div>
+                                            </div>
+                                            <div class="card-body">
+                                                <b>持证情况:</b>
+                                                <div class="m-3 one-cert-msg">
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件名称:</label>
+                                                        <div class="row col-sm-10 pr-0">
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select—cert-type" data-type="type">
+                                                                    <option value="0">请选择</option>
+                                                                    ${typeHtml}
+                                                                </select>
+                                                            </div>
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select—cert-name" data-type="name">
+                                                                    <option value="0">请选择</option>
+                                                                </select>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件编号:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="code" value="${data.code ? data.code : ''}">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">注册单位:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="reg_unit" value="${data.reg_unit ? data.reg_unit : ''}">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">技术职称:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="job_title" value="${data.job_title ? data.job_title : ''}">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证书附件:</label>
+                                                        <div class="col-sm-10 file-show">
+                                                            <input type="file" class="cert-file-upload">
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <div>
+                                                    <b>继续教育情况:</b>
+                                                    <div class="pull-right">
+                                                        <a href="javascript:void(0);" class="add-jx-btn">+添加</a>
+                                                    </div>
+                                                    <div class="all-jx">
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                </div>`;
+        return html;
+    }
+
+    function showCol4ObjArray(arr, col, key, showKey) {
+        if (!col) return '';
+        const obj = _.find(arr, { [key]: col });
+        return obj ? obj[showKey] : '';
+    }
+});

+ 9 - 2
app/public/js/project_spread.js

@@ -2,12 +2,15 @@ $(document).ready(() => {
     autoFlashHeight();
     const colSpread = SpreadJsObj.createNewSpread($('#spread-col')[0]);
     const colSheet = colSpread.getActiveSheet();
+    const validData = function(data, col) {
+        return data && data[col.field] ? '√' : '';
+    };
     const colSpreadSetting = {
         cols: [
             { title: 'key', colSpan: '1', rowSpan: '2', field: 'key', hAlign: 0, width: 0, visible: false, formatter: '@', readOnly: true, },
             { title: '可显示列', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 200, formatter: '@', readOnly: true, },
-            { title: '项目节\n清单', colSpan: '1', rowSpan: '2', field: 'bills', hAlign: 1, width: 80, readOnly: true, },
-            { title: '计量单元', colSpan: '1', rowSpan: '2', field: 'pos', hAlign: 1, width: 80, readOnly: true, },
+            { title: '项目节\n清单', colSpan: '1', rowSpan: '2', field: 'bills', hAlign: 1, width: 80, readOnly: true, getValue: validData },
+            { title: '计量单元', colSpan: '1', rowSpan: '2', field: 'pos', hAlign: 1, width: 80, readOnly: true, getValue: validData },
             { title: '配置项|是否显示', colSpan: '2|1', rowSpan: '1|1', field: 'valid', hAlign: 1, width: 60, cellType: 'checkbox', readOnly: true },
             { title: '|别名', colSpan: '|1', rowSpan: '1|1', field: 'alias', hAlign: 0, width: 120, formatter: '@',  },
         ],
@@ -36,6 +39,10 @@ $(document).ready(() => {
             const baseCol = BaseSetCol.find(b => { return x.key === b.key });
             data.push({ ...x, ...baseCol });
         });
+        BaseSetCol.forEach(x => {
+            const col = colSet.find(c => { return c.key === x.key });
+            if (!col) data.push({ ...x, valid: 0});
+        });
         SpreadJsObj.loadSheetData(colSheet, SpreadJsObj.DataType.Data, data);
     };
 

+ 283 - 0
app/public/js/settle_gather.js

@@ -0,0 +1,283 @@
+function getGxbyText(data) {
+    const def = thirdParty.gxby.find(function (x) {
+        return x.value === data.gxby_status;
+    });
+    return def ? def.name : '';
+}
+function getDaglText(data) {
+    const def = thirdParty.dagl.find(function (x) {
+        return x.value === data.dagl_status;
+    });
+    return def ? def.name : '';
+}
+
+const ckBillsSpread = window.location.pathname + '-billsSelect';
+
+$(document).ready(() => {
+    autoFlashHeight();
+
+    let searchLedger, settleAtt;
+    const settleTreeSetting = {
+        id: 'tree_id',
+        pid: 'tree_pid',
+        order: 'tree_order',
+        level: 'tree_level',
+        fullPath: 'tree_full_path',
+        isLeaf: 'tree_is_leaf',
+        rootId: -1,
+        keys: ['id', 'tid', 'tree_id'],
+        stageId: 'id',
+        autoExpand: 3,
+        markExpandKey: 'settle-select-expand',
+        markExpandSubKey: window.location.pathname.split('/')[2],
+        calcFields: ['total_price', 'cur_contract_tp', 'cur_qc_tp', 'cur_gather_tp', 'cur_correct_tp'],
+        calcFun: function(node) {
+            if (!node.children || node.children.length === 0) {
+                node.cur_gather_qty = ZhCalc.add(node.cur_contract_qty, node.cur_qc_qty);
+                if (node.cur_contract_qty) {
+                    node.cur_correct_tp = ZhCalc.add(node.cur_qc_tp, ZhCalc.mul(node.cur_contract_qty, node.unit_price, tenderInfo.decimal.tp));
+                } else {
+                    node.cur_correct_tp = node.cur_gather_tp;
+                }
+                node.pre_gather_qty = ZhCalc.add(node.pre_contract_qty, node.pre_qc_qty);
+                node.end_gather_qty = ZhCalc.add(node.end_contract_qty, node.end_qc_qty);
+                if (node.end_contract_qty) {
+                    node.end_correct_tp = ZhCalc.add(node.end_qc_tp, ZhCalc.mul(node.end_contract_qty, node.unit_price, tenderInfo.decimal.tp));
+                } else {
+                    node.end_correct_tp = node.end_gather_tp;
+                }
+            }
+            node.cur_gather_tp = ZhCalc.add(node.cur_contract_tp, node.cur_qc_tp);
+            node.cur_gather_percent = ZhCalc.mul(ZhCalc.div(node.cur_gather_tp, node.cur_final_tp), 100, 2);
+            node.cur_correct_percent = ZhCalc.mul(ZhCalc.div(node.cur_correct_tp, node.cur_final_tp), 100, 2);
+            node.pre_gather_tp = ZhCalc.add(node.pre_contract_tp, node.pre_qc_tp);
+            node.pre_gather_percent = ZhCalc.mul(ZhCalc.div(node.pre_gather_tp, node.pre_final_tp), 100, 2);
+            node.pre_correct_percent = ZhCalc.mul(ZhCalc.div(node.pre_correct_tp, node.pre_final_tp), 100, 2);
+            node.end_gather_tp = ZhCalc.add(node.end_contract_tp, node.end_qc_tp);
+            node.end_gather_percent = ZhCalc.mul(ZhCalc.div(node.end_gather_tp, node.end_final_tp), 100, 2);
+            node.end_correct_percent = ZhCalc.mul(ZhCalc.div(node.end_correct_tp, node.end_final_tp), 100, 2);
+        }
+    };
+    const settleTree = createNewPathTree('stage', settleTreeSetting);
+    const settlePosSetting = {
+        id: 'id', ledgerId: 'lid',
+        calcFun: function(pos) {
+            pos.cur_gather_qty = ZhCalc.add(pos.cur_contract_qty, pos.cur_qc_qty);
+            pos.pre_gather_qty = ZhCalc.add(pos.pre_contract_qty, pos.pre_qc_qty);
+            pos.end_gather_qty = ZhCalc.add(pos.end_contract_qty, pos.end_qc_qty);
+        }
+    };
+    const settlePos = new StagePosData(settlePosSetting);
+
+    const slSpread = SpreadJsObj.createNewSpread($('#settle-bills')[0]);
+    const slSheet = slSpread.getActiveSheet();
+    slSheet.frozenColumnCount(billsSpreadSetting.cols.findIndex(x => { return x.field === 'total_price'; }) + 1);
+    slSheet.options.frozenlineColor = '#93b5e4';
+    const ratioCol = billsSpreadSetting.cols.find(x => {return x.field === 'cur_final_1_percent' || x.field === 'cur_correct_1_percent'});
+    if (ratioCol) ratioCol.field = tenderInfo.display.stage.correct ? 'cur_correct_1_percent' : 'cur_final_1_percent';
+    billsSpreadSetting.getColor = function (sheet, data, row, col, defaultColor) {
+        if (!data) return defaultColor;
+        if (data.children && data.children.length > 0) return defaultColor;
+
+        if (col.field === 'gxby') {
+            const def = thirdParty.gxby.find(function (x) {
+                return x.value === data.gxby_status;
+            });
+            if (def && def.color) return def.color;
+        } else if (col.field === 'dagl') {
+            const def = thirdParty.dagl.find(function (x) {
+                return x.value === data.dagl_status;
+            });
+            if (def && def.color) return def.color;
+        }
+    };
+    sjsSettingObj.setFxTreeStyle(billsSpreadSetting, sjsSettingObj.FxTreeStyle.jz);
+    sjsSettingObj.set3FCols(billsSpreadSetting.cols, [
+        {field: 'gxby', getValue: getGxbyText, url_field: 'gxby_url'},
+        {field: 'dagl', getValue: getDaglText, url_field: 'dagl_url'},
+    ]);
+    billsSpreadSetting.afterLocate = function (node) {
+        settleAtt.getCurAttHtml(node);
+    };
+    SpreadJsObj.initSheet(slSheet, billsSpreadSetting);
+
+    const spSpread = SpreadJsObj.createNewSpread($('#settle-pos')[0]);
+    const spSheet = spSpread.getActiveSheet();
+    spSheet.frozenColumnCount(posSpreadSetting.cols.findIndex(x => { return x.field === 'total_price'; }) + 1);
+    spSheet.options.frozenlineColor = '#93b5e4';
+    posSpreadSetting.getColor = function (sheet, data, row, col, defaultColor) {
+        if (!data) return defaultColor;
+
+        if (col.field === 'gxby') {
+            const def = thirdParty.gxby.find(function (x) {
+                return x.value === data.gxby_status;
+            });
+            if (def && def.color) return def.color;
+        } else if (col.field === 'dagl') {
+            const def = thirdParty.dagl.find(function (x) {
+                return x.value === data.dagl_status;
+            });
+            if (def && def.color) return def.color;
+        }
+    };
+    sjsSettingObj.set3FCols(posSpreadSetting.cols, [
+        {field: 'gxby', getValue: getGxbyText, url_field: 'gxby_url'},
+        {field: 'dagl', getValue: getDaglText, url_field: 'dagl_url'},
+    ]);
+    SpreadJsObj.initSheet(spSheet, posSpreadSetting);
+
+    const settleBillsObj = {
+        loadRelaData: function() {
+            SpreadJsObj.saveTopAndSelect(slSheet, ckBillsSpread);
+            SpreadJsObj.resetTopAndSelect(spSheet);
+            settlePosObj.loadCurPosData();
+        },
+        selectionChanged: function(e, info) {
+            if (!info.oldSelections || !info.oldSelections[0] || info.newSelections[0].row !== info.oldSelections[0].row) {
+                settleBillsObj.loadRelaData();
+            }
+        },
+        topRowChanged(e, info) {
+            SpreadJsObj.saveTopAndSelect(info.sheet, ckBillsSpread);
+        }
+    };
+    slSpread.bind(spreadNS.Events.SelectionChanged, settleBillsObj.selectionChanged);
+    slSpread.bind(spreadNS.Events.TopRowChanged, settleBillsObj.topRowChanged);
+
+    const settlePosObj = {
+        loadCurPosData: function() {
+            const billsNode = SpreadJsObj.getSelectObject(slSheet);
+            if (billsNode) {
+                const posRange = settlePos.getLedgerPos(billsNode.lid) || [];
+                SpreadJsObj.loadSheetData(spSheet, SpreadJsObj.DataType.Data, posRange, readOnly);
+            } else {
+                SpreadJsObj.loadSheetData(spSheet, SpreadJsObj.DataType.Data, [], true);
+            }
+        }
+    };
+
+    postData('gather/load', null, function(result) {
+        settleTree.loadDatas(result.bills);
+        treeCalc.calculateAll(settleTree);
+        settlePos.loadDatas(result.pos);
+        settlePos.calculateAll();
+        SpreadJsObj.loadSheetData(slSheet, SpreadJsObj.DataType.Tree, settleTree);
+        SpreadJsObj.loadTopAndSelect(slSpread.getActiveSheet(), ckBillsSpread);
+        settlePosObj.loadCurPosData();
+    });
+
+    // 展开收起工具栏
+    $('a', '.right-nav').bind('click', function () {
+        const tab = $(this), tabPanel = $(tab.attr('content'));
+        if (!tab.hasClass('active')) {
+            $('a', '.side-menu').removeClass('active');
+            $('.tab-content .tab-select-show').removeClass('active');
+            tab.addClass('active');
+            tabPanel.addClass('active');
+            showSideTools(tab.hasClass('active'));
+            if (tab.attr('content') === '#search' && !searchLedger) {
+                searchLedger = $.billsSearch({
+                    selector: '#search',
+                    searchSpread: slSpread,
+                    searchOver: true,
+                    searchEmpty: true,
+                    keyId: 'tree_id',
+                    resultSpreadSetting: {
+                        cols: [
+                            {title: '项目节编号', field: 'code', hAlign: 0, width: 90, formatter: '@'},
+                            {title: '清单编号', field: 'b_code', hAlign: 0, width: 80, formatter: '@'},
+                            {title: '名称', field: 'name', width: 150, hAlign: 0, formatter: '@'},
+                            {title: '单位', field: 'unit', width: 50, hAlign: 1, formatter: '@'},
+                            {title: '单价', field: 'unit_price', hAlign: 2, width: 50},
+                            {title: '数量', field: 'quantity', hAlign: 2, width: 50},
+                        ],
+                        emptyRows: 0,
+                        headRows: 1,
+                        headRowHeight: [32],
+                        headColWidth: [30],
+                        defaultRowHeight: 21,
+                        headerFont: '12px 微软雅黑',
+                        font: '12px 微软雅黑',
+                        selectedBackColor: '#fffacd',
+                        readOnly: true,
+                    },
+                    afterLocated: function () {
+                        settlePosObj.loadCurPosData();
+                    },
+                });
+                searchLedger.spread.refresh();
+            }
+        } else {
+            tab.removeClass('active');
+            tabPanel.removeClass('active');
+            showSideTools(tab.hasClass('active'));
+        }
+        slSpread.refresh();
+        spSpread.refresh();
+    });
+
+    $.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();
+        }
+    });
+    // 加载上下窗口resizer
+    $.divResizer({
+        select: '#main-resize',
+        callback: function () {
+            slSpread.refresh();
+            let bcontent = $(".bcontent-wrap") ? $(".bcontent-wrap").height() : 0;
+            $(".sp-wrap").height(bcontent-30);
+            spSpread.refresh();
+            window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
+        }
+    });
+    // 工具栏resizer
+    $.divResizer({
+        select: '#right-spr',
+        callback: function () {
+            slSpread.refresh();
+            spSpread.refresh();
+            window.getSelection ? window.getSelection().removeAllRanges() : document.selection.empty();
+            if (searchLedger) searchLedger.spread.refresh();
+        }
+    });
+
+    // 显示层次
+    (function (select, sheet) {
+        $(select).click(function () {
+            const tag = $(this).attr('tag');
+            const tree = sheet.zh_tree;
+            if (!tree) return;
+            setTimeout(() => {
+                showWaitingView();
+                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]', slSheet);
+});

+ 1 - 0
app/public/js/settle_ledger.js

@@ -285,6 +285,7 @@ $(document).ready(() => {
                     searchSpread: slSpread,
                     searchOver: true,
                     searchEmpty: true,
+                    keyId: 'tree_id',
                     resultSpreadSetting: {
                         cols: [
                             {title: '项目节编号', field: 'code', hAlign: 0, width: 90, formatter: '@'},

+ 5 - 4
app/public/js/shares/drag_tree.js

@@ -14,6 +14,7 @@ const createDragTree = function (setting) {
             this.children = [];
             // 树设置
             this.setting = setting;
+            if (!this.setting.itemsPre) this.setting.itemsPre = 'id_';
         }
         /**
          * 树结构根据显示排序
@@ -57,7 +58,7 @@ const createDragTree = function (setting) {
                 return a[self.setting.level] - b[self.setting.level];
             });
             for (const data of datas) {
-                const keyName = itemsPre + data[this.setting.id];
+                const keyName = this.setting.itemsPre + data[this.setting.id];
                 if (this.items[keyName]) continue;
 
                 const item = JSON.parse(JSON.stringify(data));
@@ -87,7 +88,7 @@ const createDragTree = function (setting) {
          * @returns {Object}
          */
         getItems(id) {
-            return this.items[itemsPre + id];
+            return this.items[this.setting.itemsPre + id];
         };
         getNodeIndex(node) {
             return this.nodes.indexOf(node);
@@ -353,7 +354,7 @@ const createDragTree = function (setting) {
                     }
                     loadedData.push(node);
                 } else {
-                    const keyName = itemsPre + data[this.setting.id];
+                    const keyName = this.setting.itemsPre + data[this.setting.id];
                     const node = JSON.parse(JSON.stringify(data));
                     this.items[keyName] = node;
                     this.datas.push(node);
@@ -403,7 +404,7 @@ const createDragTree = function (setting) {
                 if (node) {
                     freeDatas.push(node);
                     node.deleteIndex = this.nodes.indexOf(node);
-                    delete this.items[itemsPre + node[this.setting.id]];
+                    delete this.items[this.setting.itemsPre + node[this.setting.id]];
                     if (node[this.setting.pid] !== this.setting.rootId) {
                         const parent = this.getItems(node[this.setting.pid]);
                         if (parent) {

+ 16 - 3
app/public/js/spreadjs_rela/spreadjs_zh.js

@@ -426,7 +426,7 @@ const SpreadJsObj = {
         const cell = sheet.getCell(iRow, iCol);
 
         if (col.getValue && Object.prototype.toString.apply(col.getValue) === "[object Function]") {
-            cell.value(col.getValue(data));
+            cell.value(col.getValue(data, col));
         } else if (col.field !== '' && data[col.field]) {
             cell.value(data[col.field]);
         }
@@ -523,7 +523,7 @@ const SpreadJsObj = {
         sheet.zh_setting.cols.forEach(function (col, j) {
             const cell = sheet.getCell(row, j);
             if (col.getValue && Object.prototype.toString.apply(col.getValue) === "[object Function]") {
-                cell.value(col.getValue(data));
+                cell.value(col.getValue(data, col));
             } else if (col.field !== '' && data[col.field]) {
                 cell.value(data[col.field]);
             }
@@ -1916,7 +1916,20 @@ const SpreadJsObj = {
                     }
                     // Drawing Text
                     spreadNS.CellTypes.Text.prototype.paint.apply(this, [canvas, value, x, y, w, h, style, options]);
-                } else {
+                } else if (style.hAlign === spreadNS.HorizontalAlign.center) {
+                    if (img) {
+                        if (style.backColor) {
+                            canvas.save();
+                            canvas.fillStyle = style.backColor;
+                            canvas.fillRect((x + x + w - indent - img.width) / 2, y, (indent + img.width + img.width) / 2, h);
+                            canvas.restore();
+                        }
+                        canvas.drawImage(img, (x + 10 + x + w - indent - img.width) / 2, y + (h - img.height) / 2);
+                        w = w - indent - img.width;
+                    }
+                    // Drawing Text
+                    spreadNS.CellTypes.Text.prototype.paint.apply(this, [canvas, value, x, y, w, h, style, options]);
+                } else  {
                     if (img) {
                         if (style.backColor) {
                             canvas.save();

+ 5 - 5
app/public/js/stage_pay.js

@@ -556,14 +556,14 @@ $(document).ready(() => {
             }
             return [true, ''];
         },
-        _checkSfExpr: function (text, data) {
+        _checkSfExpr: function (text, data, order) {
             if (text) {
                 const num = _.toNumber(text);
                 if (num) {
                     data.expr = num;
                 } else {
                     const expr = $.trim(text).replace('\t', '').replace('=', '').toLowerCase();
-                    const [valid, msg] = this._checkExprValid(expr);
+                    const [valid, msg] = this._checkExprValid(expr, [], order);
                     if (!valid) return [valid, msg];
                     data.expr = expr;
                 }
@@ -740,7 +740,7 @@ $(document).ready(() => {
                 switch(col.field) {
                     case 'tp':
                         const [tpValid, tpMsg] = payBase.isSF(select)
-                            ? paySpreadObj._checkSfExpr(validText, data.updateData)
+                            ? paySpreadObj._checkSfExpr(validText, data.updateData, select.order)
                             : paySpreadObj._checkExpr(validText, data.updateData, select.order);
                         if (!tpValid) {
                             toastr.warning(tpMsg);
@@ -913,7 +913,7 @@ $(document).ready(() => {
                         switch (col.field) {
                             case 'tp':
                                 const [tpValid, tpMsg] = payBase.isSF(node)
-                                    ? paySpreadObj._checkSfExpr(validText, updateData)
+                                    ? paySpreadObj._checkSfExpr(validText, updateData, select.order)
                                     : paySpreadObj._checkExpr(validText, updateData, select.order);
                                 if (!tpValid) {
                                     toastr.warning(tpMsg);
@@ -988,7 +988,7 @@ $(document).ready(() => {
                 data.type = 'stage';
                 data.updateData = { pid: select.pid, tp: null, expr: newValue };
                 const [valid, msg] = payBase.isSF(select)
-                    ? paySpreadObj._checkSfExpr(newValue, data.updateData)
+                    ? paySpreadObj._checkSfExpr(newValue, data.updateData, select.order)
                     : paySpreadObj._checkExpr(newValue, data.updateData, select.order);
                 if (!valid) {
                     toastr.warning(msg);

+ 9 - 1
app/public/js/sub_project.js

@@ -233,6 +233,13 @@ $(document).ready(function() {
             const node = ProjectTree.getItems(treeId);
             if (node.is_folder) return;
             $('#sm-management').attr('tree_id', treeId);
+            $('#sm-management').val(node.management);
+            if (node.filing_template_id) {
+                const ft = $(`[value=${node.filing_template_id}]`)[0];
+                if (ft) $('#sm-ft')[0].selectedIndex = Array.prototype.indexOf.call(ft.parentNode.children, ft);
+            }
+            //$('#sm-ft').val(node.filing_template_name);
+            $('#sm-ft').attr('disabled', !!node.management);
             $('#set-management').modal('show');
         });
         $('body').on('click', 'a[name=refresh-management]', function(e) {
@@ -382,7 +389,8 @@ $(document).ready(function() {
         const select = $('#sm-management');
         const id = select.attr('tree_id');
         const management = select.val();
-        postData('/subproj/save', { id, management }, function(result) {
+        const filingTemplate =$('#sm-ft').val();
+        postData('/subproj/save', { id, management, filingTemplate }, function(result) {
             projectTreeObj.refreshRow(result);
             $('#set-management').modal('hide');
             $('#sm-management').attr('tree_id', '');

+ 374 - 0
app/public/js/tender_cert.js

@@ -0,0 +1,374 @@
+$(function () {
+    autoFlashHeight();
+    $.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();
+        }
+    });
+
+    // 打开添加用户加载数据
+    $('#import').on('show.bs.modal', function (e) {
+        let html = '';
+        for (const tc of tenderCertList) {
+            let certHtml = '';
+            for (const c of tc.account_info.certs) {
+                certHtml += `<option value="${c.id}" ${c.id === tc.cert_id ? 'selected': ''}>${showCol4ObjArray(certSourceConst, c.name, 'value', 'name')}</option>`;
+            }
+            html += `<tr class="text-center" data-insert="0" data-id="${tc.id}" data-certid="${tc.cert_id}" data-remove="0">
+                        <td>${tc.account_info.name}</td>
+                        <td>${tc.account_info.role}</td>
+                        <td>
+                            <select class="form-control form-control-sm">
+                                ${certHtml}
+                            </select>
+                        </td>
+                        <td class="text-danger">移除</td>
+                    </tr>`;
+        }
+        $('#select-certs-table').html(html);
+    });
+
+    let timer = null
+    let oldSearchVal = null
+    $('#gr-search').bind('input propertychange', function (e) {
+        oldSearchVal = e.target.value
+        timer && clearTimeout(timer)
+        timer = setTimeout(() => {
+            const newVal = $('#gr-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'))
+        if (auditorId) {
+            const userInfo = _.find(accountList, { id: auditorId });
+            const certList = _.filter(allCertList, { uid: parseInt(auditorId) });
+            let certHtml = '';
+            for (const c of certList) {
+                certHtml += `<option value="${c.id}">${showCol4ObjArray(certSourceConst, c.name, 'value', 'name')}</option>`;
+            }
+            const html = `<tr class="text-center" data-insert="1" data-remove="0" data-uid="${userInfo.id}" data-certid="${certList.length > 0 ? certList[0].id : 0}">
+                        <td>${userInfo.name}</td>
+                        <td>${userInfo.role}</td>
+                        <td>
+                            <select class="form-control form-control-sm">
+                                ${certHtml}
+                            </select>
+                        </td>
+                        <td class="text-danger">移除</td>
+                    </tr>`;
+            $('#select-certs-table').append(html);
+        }
+    });
+
+    $('body').on('click', '#select-certs-table .text-danger', function () {
+        $(this).parent().addClass('bg-gray').attr('data-remove', 1);
+        $(this).siblings('td').find('select').prop('disabled', true);
+        $(this).removeClass('text-danger').text('已移除');
+    });
+
+    $('body').on('change', '#select-certs-table select', function () {
+        $(this).parents('tr').attr('data-certid', $(this).val());
+    });
+
+    $('#add_cert_btn').click(function () {
+        // 判断增删改
+        const insertList = [];
+        if ($('#select-certs-table tr[data-insert="1"][data-remove="0"]').length > 0) {
+            $('#select-certs-table tr[data-insert="1"][data-remove="0"]').each(function () {
+                insertList.push({
+                    uid: parseInt($(this).attr('data-uid')),
+                    cert_id: parseInt($(this).attr('data-certid'))
+                });
+            });
+        }
+        const removeList = [];
+        if ($('#select-certs-table tr[data-insert="0"][data-remove="1"]').length > 0) {
+            $('#select-certs-table tr[data-insert="0"][data-remove="1"]').each(function () {
+                removeList.push(parseInt($(this).attr('data-id')));
+            });
+        }
+        const updateList = [];
+        if ($('#select-certs-table tr[data-insert="0"][data-remove="0"]').length > 0) {
+            $('#select-certs-table tr[data-insert="0"][data-remove="0"]').each(function () {
+                const cert_id = parseInt($(this).attr('data-certid'))
+                const id = parseInt($(this).attr('data-id'));
+                const tcInfo = _.find(tenderCertList, { id });
+                if (tcInfo.cert_id !== cert_id) {
+                    updateList.push({
+                        id,
+                        cert_id
+                    });
+                }
+            });
+        }
+        console.log(insertList, removeList, updateList);
+        postData('/tender/' + tid + '/cert/save', { type: 'save_user', list: { insertList, removeList, updateList} }, function (result) {
+            tenderCertList = result;
+            SpreadJsObj.loadSheetData(certSpread.getActiveSheet(), SpreadJsObj.DataType.Data, tenderCertList);
+            $('#import').modal('hide');
+        });
+    });
+
+    // sjs展示
+    const certSpread = SpreadJsObj.createNewSpread($('#cert-spread')[0]);
+    const certSpreadSetting = {
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 32],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+    };
+    const certSpreadSettingCols = [
+        {title: '姓名', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 80, formatter: '@', readOnly: true, getValue: 'getValue.name'},
+        {title: '技术职称', colSpan: '1', rowSpan: '2', field: 'job_title', hAlign: 0, width: 100, formatter: '@', readOnly: true, getValue: 'getValue.job_title'},
+        {title: '所在部门', colSpan: '1', rowSpan: '2', field: 'department', hAlign: 0, width: 100, formatter: '@'},
+        {title: '职务', colSpan: '1', rowSpan: '2', field: 'role', hAlign: 0, width: 80, formatter: '@', readOnly: true, getValue: 'getValue.role'},
+        {title: '在岗时间', colSpan: '1', rowSpan: '2', field: 'job_time', hAlign: 0, width: 150, formatter: '@'},
+        {title: '持证情况|证件名称', colSpan: '4|1', rowSpan: '1|1', field: 'cert_name', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.cert_name'},
+        {title: '|证书编号', colSpan: '|1', rowSpan: '|1', field: 'cert_code', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.cert_code'},
+        {title: '|注册单位', colSpan: '|1', rowSpan: '|1', field: 'reg_unit', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.reg_unit'},
+        {title: '|证书附件', colSpan: '|1', rowSpan: '|1', field: 'file_path', hAlign: 1, width: 55, readOnly: true, cellType: 'imageBtn',
+            normalImg: '#file_clip', hoverImg: '#file_clip_hover' , showImage: function (data) { return data && data.cert_info && data.cert_info.file_path; }},
+        {title: '继续教育情况|培训时间', colSpan: '3|1', rowSpan: '1|1', field: 'jx_date', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.jx_date'},
+        {title: '|培训单位', colSpan: '|1', rowSpan: '|1', field: 'jx_unit', hAlign: 0, width: 150, readOnly: true, getValue: 'getValue.jx_unit'},
+        {title: '|培训证明', colSpan: '|1', rowSpan: '|1', field: 'jx_path', hAlign: 1, width: 55, readOnly: true, cellType: 'imageBtn',
+            normalImg: '#file_clip', hoverImg: '#file_clip_hover', showImage: function (data) { return data && data.cert_info && data.cert_info.eduInfo && data.cert_info.eduInfo.file_path; }},
+        {title: '备注', colSpan: '1', rowSpan: '2', field: 'remark', hAlign: 0, width: 100},
+    ];
+    certSpreadSetting.cols = certSpreadSettingCols;
+
+    certSpreadSetting.imageClick = function (data, info) {
+        const col = info.sheet.zh_setting.cols[info.col];
+        if (col.field === 'file_path' && data && data.cert_info && data.cert_info.file_path) {
+            window.open(fujianOssPath + data.cert_info.file_path);
+        } else if (col.field === 'jx_path' && data && data.cert_info && data.cert_info.eduInfo && data.cert_info.eduInfo.file_path) {
+            window.open(fujianOssPath + data.cert_info.eduInfo.file_path);
+        }
+    };
+
+    const certCol = {
+        getValue: {
+            name: function (data) {
+                return data.account_info ? data.account_info.name : '';
+            },
+            job_title: function (data) {
+                return data.cert_info ? data.cert_info.job_title : '';
+            },
+            role: function (data) {
+                return data.account_info ? data.account_info.role : '';
+            },
+            cert_name: function (data) {
+                return data.cert_info ? showCol4ObjArray(certSourceConst, data.cert_info.name, 'value', 'name') : '';
+            },
+            cert_code: function (data) {
+                return data.cert_info ? data.cert_info.code : '';
+            },
+            reg_unit: function (data) {
+                return data.cert_info ? data.cert_info.reg_unit : '';
+            },
+            file_path: function (data) {
+                // return data.cert_info ? fujianOssPath + data.cert_info.file_path : '';
+            },
+            jx_date: function (data) {
+                return data.cert_info && data.cert_info.eduInfo ? data.cert_info.eduInfo.date : '';
+            },
+            jx_unit: function (data) {
+                return data.cert_info && data.cert_info.eduInfo ? data.cert_info.eduInfo.unit : '';
+            },
+            jx_path: function (data) {
+                // return data.cert_info && data.cert_info.eduInfo ? fujianOssPath + data.cert_info.eduInfo.file_path : '';
+            }
+        },
+        readOnly: {
+        },
+    };
+
+    SpreadJsObj.initSpreadSettingEvents(certSpreadSetting, certCol);
+    SpreadJsObj.initSheet(certSpread.getActiveSheet(), certSpreadSetting);
+    SpreadJsObj.loadSheetData(certSpread.getActiveSheet(), SpreadJsObj.DataType.Data, tenderCertList);
+
+    const certSpreadObj = {
+        certSheetReset: function (redo = false) {
+
+            const newCertData = _.cloneDeep(tenderCertList);
+            if (redo) {
+                certSpread.getActiveSheet().reset();
+                SpreadJsObj.initSpreadSettingEvents(certSpreadSetting, certCol);
+                SpreadJsObj.initSheet(certSpread.getActiveSheet(), certSpreadSetting);
+            }
+            SpreadJsObj.loadSheetData(certSpread.getActiveSheet(), SpreadJsObj.DataType.Data, newCertData);
+        },
+        editEnded: function (e, info) {
+            if (info.sheet.zh_setting) {
+                const select = SpreadJsObj.getSelectObject(info.sheet);
+                const col = info.sheet.zh_setting.cols[info.col];
+                // 未改变值则不提交
+                let validText = col.type === 'Number' && is_numeric(info.editingText) ? parseFloat(info.editingText) : (info.editingText ? trimInvalidChar(info.editingText) : null);
+                const orgValue = select[col.field];
+                if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                    return;
+                }
+                const update_data = {
+                    id: select.id,
+                }
+                update_data[col.field] = validText;
+                select[col.field] = validText;
+                // delete select.waitingLoading;
+
+                console.log(select);
+
+                // 更新至服务器
+                postData('/tender/' + tid + '/cert/save', { type: 'update_cert', update_data }, function (result) {
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                }, function () {
+                    select[col.field] = orgValue;
+                    SpreadJsObj.reLoadRowData(info.sheet, info.row);
+                });
+            }
+        },
+        deletePress: function (sheet) {
+            return;
+        },
+        clipboardPasted(e, info) {
+            const hint = {
+                cellError: {type: 'error', msg: '粘贴内容超出了表格范围'},
+            };
+            const range = info.cellRange;
+            const sortData = info.sheet.zh_data || [];
+            if (info.cellRange.row + info.cellRange.rowCount > sortData.length) {
+                toastMessageUniq(hint.cellError);
+                // SpreadJsObj.loadSheetData(materialSpread.getActiveSheet(), SpreadJsObj.DataType.Data, materialBillsData);
+                SpreadJsObj.reLoadSheetHeader(certSpread.getActiveSheet());
+                SpreadJsObj.reLoadSheetData(certSpread.getActiveSheet());
+                return;
+            }
+            if (sortData.length > 0 && range.col + range.colCount > 13) {
+                toastMessageUniq(hint.cellError);
+                SpreadJsObj.reLoadSheetHeader(certSpread.getActiveSheet());
+                SpreadJsObj.reLoadSheetData(certSpread.getActiveSheet());
+                return;
+            }
+            const data = [];
+            // const rowData = [];
+            for (let iRow = 0; iRow < range.rowCount; iRow++) {
+                let bPaste = true;
+                const curRow = range.row + iRow;
+                // const materialData = JSON.parse(JSON.stringify(sortData[curRow]));
+                const certData = { id: sortData[curRow].id };
+                const hintRow = range.rowCount > 1 ? curRow : '';
+                let sameCol = 0;
+                for (let iCol = 0; iCol < range.colCount; iCol++) {
+                    const curCol = range.col + iCol;
+                    const colSetting = info.sheet.zh_setting.cols[curCol];
+                    if (!colSetting) continue;
+
+                    let validText = info.sheet.getText(curRow, curCol);
+                    validText = colSetting.type === 'Number' && is_numeric(validText) ? parseFloat(validText) : (validText ? trimInvalidChar(validText) : null);
+                    const orgValue = sortData[curRow][colSetting.field];
+                    if (orgValue == validText || ((!orgValue || orgValue === '') && (validText === ''))) {
+                        sameCol++;
+                        if (range.colCount === sameCol)  {
+                            bPaste = false;
+                        }
+                        continue;
+                    }
+                    certData[colSetting.field] = validText;
+                    sortData[curRow][colSetting.field] = validText;
+                }
+                if (bPaste) {
+                    data.push(certData);
+                } else {
+                    SpreadJsObj.reLoadRowData(info.sheet, curRow);
+                }
+            }
+            if (data.length === 0) {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                return;
+            }
+            console.log(data);
+            // 更新至服务器
+            postData('/tender/' + tid + '/cert/save', { type: 'paste_cert', update_data: data }, function (result) {
+                tenderCertList = result;
+                certSpreadObj.certSheetReset();
+            }, function () {
+                SpreadJsObj.reLoadRowData(info.sheet, info.cellRange.row, info.cellRange.rowCount);
+                return;
+            });
+        },
+    }
+
+    certSpread.bind(spreadNS.Events.ClipboardPasted, certSpreadObj.clipboardPasted);
+    SpreadJsObj.addDeleteBind(certSpread, certSpreadObj.deletePress);
+    certSpread.bind(spreadNS.Events.EditEnded, certSpreadObj.editEnded);
+
+    function showCol4ObjArray(arr, col, key, showKey) {
+        if (!col) return '';
+        const obj = _.find(arr, { [key]: col });
+        return obj ? obj[showKey] : '';
+    }
+});

+ 17 - 4
app/router.js

@@ -192,6 +192,8 @@ module.exports = app => {
     app.post('/tender/:id/map/upload', sessionAuth, tenderCheck, uncheckTenderCheck, 'tenderController.uploadMap');
     app.post('/tender/:id/load', sessionAuth, tenderCheck, 'tenderController.loadData');
     app.post('/tender/:id/saveRela', sessionAuth, tenderCheck, 'tenderController.saveRelaData');
+    app.get('/tender/:id/cert', sessionAuth, tenderCheck, 'tenderController.certSet');
+    app.post('/tender/:id/cert/save', sessionAuth, tenderCheck, 'tenderController.saveCert');
 
     app.get('/tender/:id/ctrl-price', sessionAuth, tenderCheck, 'ctrlPriceController.index');
     app.post('/tender/:id/ctrl-price/load', sessionAuth, tenderCheck, 'ctrlPriceController.load');
@@ -434,6 +436,9 @@ module.exports = app => {
     app.post('/tender/:id/settle/add', sessionAuth, tenderCheck, uncheckTenderCheck, tenderBuildCheck, 'settleController.add');
     app.post('/tender/:id/settle/save', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.save');
     app.post('/tender/:id/settle/delete', sessionAuth, tenderCheck, uncheckTenderCheck, tenderBuildCheck, 'settleController.delete');
+    // 结算汇总
+    app.get('/tender/:id/settle/gather', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.gather');
+    app.post('/tender/:id/settle/gather/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.loadGatherData');
     // 结算期
     app.get('/tender/:id/settle/:sorder', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.index');
     app.get('/tender/:id/settle/:sorder/select', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.select');
@@ -450,9 +455,6 @@ module.exports = app => {
     app.post('/tender/:id/settle/:sorder/audit/check', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.auditCheck');
     app.post('/tender/:id/settle/:sorder/audit/checkAgain', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.auditCheckAgain');
     app.post('/tender/:id/settle/:sorder/audit/checkCancel', sessionAuth, tenderCheck, uncheckTenderCheck, settleCheck, 'settleController.auditCheckCancel');
-    // 结算汇总
-    app.get('/tender/:id/settle/gather', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.gather');
-    app.get('/tender/:id/settle/gather/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'settleController.loadGatherData');
 
     // 报表
     app.get('/tender/:id/report', sessionAuth, tenderCheck, uncheckTenderCheck, 'reportController.index');
@@ -654,6 +656,9 @@ module.exports = app => {
     app.post('/tender/:id/measure/material/gcl/load', sessionAuth, tenderCheck, uncheckTenderCheck, 'materialController.loadGclData');
     // 个人账号相关
     app.get('/profile/info', sessionAuth, 'profileController.info');
+    app.get('/profile/cert', sessionAuth, 'profileController.cert');
+    app.post('/profile/cert/save', sessionAuth, 'profileController.certSave');
+    app.post('/profile/cert/upload', sessionAuth, 'profileController.certUpload');
     app.get('/profile/sms', sessionAuth, 'profileController.sms');
     app.post('/profile/sms/type', sessionAuth, 'profileController.smsType');
     app.get('/profile/sign', sessionAuth, 'profileController.sign');
@@ -800,8 +805,16 @@ module.exports = app => {
     app.post('/budget/:id/:btype/update', sessionAuth, budgetCheck, 'budgetController.detailUpdate');
     app.post('/budget/:id/:btype/upload-excel/:ueType', sessionAuth, budgetCheck, 'budgetController.detailUploadExcel');
     app.post('/budget/:id/decimal', sessionAuth, budgetCheck, 'budgetController.decimal');
-    // 电子档案
+    // 资料归集-列表
     app.get('/file', sessionAuth, 'fileController.index');
+    // 资料归集-模板
+    app.get('/file/template', sessionAuth, projectManagerCheck, 'fileController.template');
+    app.post('/file/template/save', sessionAuth, projectManagerCheck, 'fileController.saveTemplate');
+    app.post('/file/template/reset', sessionAuth, projectManagerCheck, 'fileController.resetTemplate');
+    app.post('/file/template/del', sessionAuth, projectManagerCheck, 'fileController.delTemplate');
+    app.get('/file/template/:id', sessionAuth, projectManagerCheck, 'fileController.templateDetail');
+    app.post('/file/template/:id/update', sessionAuth, projectManagerCheck, 'fileController.updateTemplate');
+    // 资料归集-文件
     app.get('/sp/:id/file', sessionAuth, subProjectCheck, 'fileController.file');
     app.post('/sp/:id/permission', sessionAuth, projectManagerCheck, subProjectCheck, 'fileController.getFilingTypePermission');
     app.post('/sp/:id/permission/save', sessionAuth, projectManagerCheck, subProjectCheck, 'fileController.saveFilingTypePermission');

+ 153 - 0
app/service/account_cert.js

@@ -0,0 +1,153 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const profileConst = require('../const/profile');
+module.exports = app => {
+
+    class AccountCert extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'account_cert';
+        }
+
+        async getAllCertByPid(pid, WithoutNullName = 0) {
+            const list = await this.getAllDataByCondition({ where: { pid }, orders: [['create_time', 'desc']] });
+            if (WithoutNullName) {
+                return this._.filter(list, l => l.name !== null && l.name !== '');
+            }
+            return list;
+        }
+
+        async addCert(uid) {
+            const result = await this.db.insert(this.tableName, {
+                pid: this.ctx.session.sessionProject.id,
+                uid,
+                create_time: new Date(),
+            });
+            return { total: await this.count({ uid }), data: await this.getDataById(result.insertId) };
+        }
+
+        async updateCert(data) {
+            const updateData = {
+                id: data.id,
+            };
+            // 判断key是否在常量里,并转换为对应的值
+            if (!this._.includes(profileConst.cert.postCertConst, data.key)) throw '参数有误';
+            updateData[data.key] = data.value ? data.value : null;
+            if (data.other && this._.includes(profileConst.cert.postCertConst, data.other.key)) {
+                updateData[data.other.key] = data.other.value ? data.other.value : null;
+            }
+            return await this.db.update(this.tableName, updateData);
+        }
+
+        async addEduJson(id) {
+            const info = await this.getDataById(id);
+            if (!info) throw '数据有误';
+            const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+            const newEdu = this._.cloneDeep(profileConst.cert.eduJsonConst);
+            newEdu.id = this.ctx.app.uuid.v4();
+            edu_json.push(newEdu);
+            info.edu_json = edu_json;
+            const result = await this.db.update(this.tableName, {
+                id: info.id,
+                edu_json: JSON.stringify(edu_json),
+            });
+            return { data: info, jxData: newEdu };
+        }
+
+        async updateEduJson(data) {
+            const info = await this.getDataById(data.id);
+            if (!info) throw '数据有误';
+            const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+            if (edu_json.length === 0) throw '数据有误';
+            const updateEdu = this._.find(edu_json, { id: data.jxid });
+            if (!updateEdu) throw '数据有误';
+            updateEdu[data.key] = data.value ? data.value : null;
+            return await this.db.update(this.tableName, {
+                id: info.id,
+                edu_json: JSON.stringify(edu_json),
+            });
+        }
+
+        async delCert(data) {
+            if (!data.id) throw '参数有误';
+            const info = await this.getDataById(data.id);
+            if (!info) throw '数据有误1';
+            if (!data.type) throw '参数有误';
+            if (data.type === 'cert') {
+                const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+                if (data.jxid) {
+                    // 删除继续教育
+                    if (edu_json.length === 0) throw '数据有误';
+                    const delEdu = this._.findIndex(edu_json, { id: data.jxid });
+                    if (delEdu === -1) throw '数据有误2';
+                    if (edu_json[delEdu].file_path) {
+                        await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + edu_json[delEdu].file_path);
+                    }
+                    edu_json.splice(delEdu, 1);
+                    // 如果存在文件,需同步移除
+                    return await this.db.update(this.tableName, {
+                        id: info.id,
+                        edu_json: JSON.stringify(edu_json),
+                    });
+                }
+                // 删除整个证书
+                // 如果存在文件,需同步移除
+                // 判断是否已调用,已调用需要先删除再删除这里
+                const isUsed = await this.ctx.service.tenderCert.count({ cert_id: info.id });
+                if (isUsed > 0) throw '该证书已被添加到标段从业人员,无法删除';
+                if (info.file_path) {
+                    await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + info.file_path);
+                    if (edu_json.length > 0) {
+                        for (const item of edu_json) {
+                            if (item.file_path) {
+                                await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + item.file_path);
+                            }
+                        }
+                    }
+                }
+                return await this.db.delete(this.tableName, { id: data.id });
+            } else if (data.type === 'file') {
+                if (data.jxid) {
+                    const edu_json = info.edu_json ? JSON.parse(info.edu_json) : [];
+                    if (edu_json.length === 0) throw '数据有误';
+                    const eduInfo = this._.find(edu_json, {id: data.jxid});
+                    if (!eduInfo) throw '数据有误2';
+                    // 如果存在文件,需同步移除
+                    if (!eduInfo.file_path) throw '不存在培训证明';
+                    await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + eduInfo.file_path);
+                    eduInfo.file_path = null;
+                    eduInfo.file_name = null;
+                    return await this.db.update(this.tableName, {
+                        id: info.id,
+                        edu_json: JSON.stringify(edu_json),
+                    });
+                }
+                // 删除证书附件
+                if (!info.file_path) throw '不存在证书附件';
+                await this.ctx.app.fujianOss.delete(this.ctx.app.config.fujianOssFolder + info.file_path);
+                return await this.db.update(this.tableName, {
+                    id: info.id,
+                    file_path: null,
+                    file_name: null,
+                });
+            }
+            throw '参数有误';
+        }
+    }
+
+    return AccountCert;
+};

+ 45 - 18
app/service/filing.js

@@ -43,17 +43,24 @@ module.exports = app => {
             return filingType.map(x => { return x.value });
         }
 
-        async initFiling(spid, transaction) {
+        async initFiling(spid, templateId, transaction) {
             const count = await this.count({ spid });
             if (count > 0) return;
 
+            const templateFiling = await this.ctx.service.filingTemplate.getAllDataByCondition({
+                where: { temp_id: templateId },
+                orders: [['tree_level', 'asc']],
+            });
             const insertData = [];
-            for (const [i, f] of filingType.entries()) {
-                insertData.push({
-                    id: this.uuid.v4(), tree_pid: -1, tree_level: 1, tree_order: i + 1,
-                    spid, add_user_id: this.ctx.session.sessionUser.accountId, is_fixed: 1,
-                    filing_type: f.value, name: f.name,
-                });
+            for (const f of templateFiling) {
+                f.newId = this.uuid.v4();
+                const parent = f.tree_pid !== rootId ? templateFiling.find(x => { return x.id === f.tree_pid; }) : null;
+                const newData = {
+                    id: f.newId, tree_pid : parent ? parent.newId : rootId, tree_level: f.tree_level, tree_order: f.tree_order,
+                    spid, add_user_id: this.ctx.session.sessionUser.accountId, is_fixed: f.tree_level === 1,
+                    filing_type: f.filing_type, name: f.name,
+                };
+                insertData.push(newData);
             }
             if (transaction) {
                 await transaction.insert(this.tableName, insertData);
@@ -103,7 +110,7 @@ module.exports = app => {
             if (parent.file_count > 0) throw `分类【${parent.name}】下存在文件,不可添加子分类`;
 
             const sibling = await this.getAllDataByCondition({ where: { tree_pid: parent.id }, orders: [['tree_order', 'asc']]});
-            const preChild = data.tree_pre_id ? sibling.find(x => { x.id === data.tree_pre_id; }) : null;
+            const preChild = data.tree_pre_id ? sibling.find(x => { return x.id === data.tree_pre_id; }) : null;
 
             const conn = await this.db.beginTransaction();
             try {
@@ -148,7 +155,6 @@ module.exports = app => {
                 throw '更新数据失败';
             }
         }
-
         async del(data) {
             const filing = await this.getDataById(data.id);
             this._checkFixed(filing);
@@ -171,46 +177,67 @@ module.exports = app => {
                 return { delete: delData.map(x => { return x.id }), update: updateData };
             } catch(err) {
                 await conn.rollback();
-                throw error;
+                throw err;
             }
         }
-
         async move(data) {
             const filing = await this.getDataById(data.id);
             if (!filing) throw '移动的分类不存在,请刷新页面后重试';
             const parent = await this.getDataById(data.tree_pid);
             if (!parent) throw '移动后的分类不存在,请刷新页面后重试';
             const sibling = await this.getAllDataByCondition({ where: { tree_pid: data.tree_pid, is_deleted: 0 } });
-            const updateData = [{ id: filing.id, tree_order: data.tree_order, tree_pid: data.tree_pid, tree_level: parent.tree_level + 1 }];
+            const posterity = await this.getPosterityData(filing.id);
+            const updateData = { id: filing.id, tree_order: data.tree_order, tree_pid: data.tree_pid, tree_level: parent.tree_level + 1 };
+            const posterityUpdateData = posterity.map(x => {
+               updateData.push({ id: x.id,  tree_level: parent.tree_level + 1 - filing.tree_level + x.tree_level });
+            });
+            const siblingUpdateData = [];
             if (data.tree_pid === filing.tree_pid) {
                 if (data.tree_order < filing.tree_order) {
                     sibling.forEach(x => {
                         if (x.id === filing.id) return;
                         if (x.tree_order < data.tree_order) return;
                         if (x.tree_order > filing.tree_order) return;
-                        updateData.push({id: x.id, tree_order: x.tree_order + 1});
+                        siblingUpdateData.push({id: x.id, tree_order: x.tree_order + 1});
                     });
                 } else {
                     sibling.forEach(x => {
                         if (x.id === filing.id) return;
                         if (x.tree_order < filing.tree_order) return;
                         if (x.tree_order > data.tree_order) return;
-                        updateData.push({id: x.id, tree_order: x.tree_order - 1});
+                        siblingUpdateData.push({id: x.id, tree_order: x.tree_order - 1});
                     });
                 }
             } else {
                 const orgSibling = await this.getAllDataByCondition({ where: { tree_pid: filing.tree_pid, is_deleted: 0 } });
                 orgSibling.forEach(x => {
+                    if (x.id === filing.id) return;
                     if (x.tree_order < filing.tree_order) return;
-                    updateData.push({id: x.id, tree_order: x.tree_order - 1});
+                    siblingUpdateData.push({id: x.id, tree_order: x.tree_order - 1});
                 });
                 sibling.forEach(x => {
+                    if (x.id === filing.id) return;
                     if (x.tree_order < data.tree_order) return;
-                    updateData.push({id: x.id, tree_order: x.tree_order + 1});
+                    siblingUpdateData.push({id: x.id, tree_order: x.tree_order + 1});
                 })
             }
-            await this.db.updateRows(this.tableName, updateData);
-            return { update: updateData };
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, updateData);
+                if (posterityUpdateData.length > 0) await conn.updateRows(this.tableName, posterityUpdateData);
+                if (siblingUpdateData.length > 0) await conn.updateRows(this.tableName, siblingUpdateData);
+                await conn.commit();
+            } catch (err) {
+                await conn.rollback();
+                throw err;
+            }
+            return { update: [updateData, ...posterityUpdateData, ...siblingUpdateData] };
+        }
+
+        async sumFileCount(spid) {
+            const result = await this.db.queryOne(`SELECT SUM(file_count) AS file_count FROM ${this.tableName} WHERE spid = '${spid}' and is_deleted = 0`);
+            return result.file_count;
         }
     }
 

+ 226 - 0
app/service/filing_template.js

@@ -0,0 +1,226 @@
+'use strict';
+
+/**
+ *
+ * 2024/3/21
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const rootId = '-1';
+const filingType = [
+    { value: 1, name: '立项文件' },
+    { value: 2, name: '招标投标、合同协议文件' },
+    { value: 3, name: '勘察、设计文件' },
+    { value: 4, name: '征地、拆迁、移民文件' },
+    { value: 5, name: '项目管理文件' },
+    { value: 6, name: '施工文件' },
+    { value: 7, name: '信息系统开发文件' },
+    { value: 8, name: '设备文件' },
+    { value: 9, name: '监理文件' },
+    { value: 10, name: '科研项目文件' },
+    { value: 11, name: '生产技术准备、试运行文件' },
+    { value: 12, name: '竣工验收文件' },
+];
+const maxFilingType = 12;
+
+module.exports = app => {
+
+    class FilingTemplate extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'filing_template';
+        }
+
+        async initTemplate(transaction, template) {
+            const insertData = [];
+            for (const [i, f] of filingType.entries()) {
+                insertData.push({
+                    id: this.uuid.v4(), temp_id: template.id, tree_pid: -1, tree_level: 1, tree_order: i + 1,
+                    add_user_id: this.ctx.session.sessionUser.accountId, is_fixed: true,
+                    name: f.name, filing_type: f.value,
+                });
+            }
+            if (transaction) {
+                await transaction.insert(this.tableName, insertData);
+            } else {
+                await this.db.insert(this.tableName, insertData);
+            }
+        }
+
+        async getData(templateId) {
+            return await this.getAllDataByCondition({
+                where: { temp_id: templateId }
+            })
+        }
+
+        async getPosterityData(templateId, id){
+            const result = [];
+            let cur = await this.getAllDataByCondition({ where: { temp_id: templateId, tree_pid: id } });
+            let iLevel = 1;
+            while (cur.length > 0 && iLevel < 6) {
+                result.push(...cur);
+                cur = await this.getAllDataByCondition({ where: { temp_id: templateId, tree_pid: cur.map(x => { return x.id })} });
+                iLevel += 1;
+            }
+            return result;
+        }
+        async getNewName(templateId, name = '新增文件类别') {
+            const data = await this.db.query(`SELECT * FROM ${this.tableName} WHERE temp_id = '${templateId}' AND name LIKE '${name}%'`);
+            if (data.length === 0) return name;
+
+            const _ = this._;
+            const names = data.map(x => { return _.toInteger(x.name.replace(name, '')) });
+            const filterNames = names.filter(x => { return x > 0 });
+            const max = filterNames.reduce((pre, cur) => { return Math.max(pre, cur); }, 0);
+            return max >= 0 ? name + (max + 1) : name;
+        }
+        async getNewFilingType(templateId) {
+            const max = await this.db.queryOne(`SELECT filing_type FROM ${this.tableName} WHERE temp_id = '${templateId}' ORDER BY filing_type DESC`);
+            return max && max.filing_type ? max.filing_type + 1 : maxFilingType + 1;
+        }
+        async add(templateId, data) {
+            const parent = await this.getDataById(data.tree_pid);
+
+            const sibling = await this.getAllDataByCondition({ where: { temp_id: templateId, tree_pid: parent ? parent.id : rootId }, orders: [['tree_order', 'asc']]});
+            const preChild = data.tree_pre_id ? sibling.find(x => { return x.id === data.tree_pre_id; }) : null;
+            const filing_type = parent ? parent.filing_type : await this.getNewFilingType(templateId);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                // 获取当前用户信息
+                const sessionUser = this.ctx.session.sessionUser;
+                // 获取当前项目信息
+                const sessionProject = this.ctx.session.sessionProject;
+
+                const tree_order = preChild ? preChild.tree_order + 1 : (sibling.length > 0 ? sibling[sibling.length - 1].tree_order + 1 : 1);
+                const name = await this.getNewName(templateId);
+                const insertData = {
+                    id: this.uuid.v4(), temp_id: templateId, add_user_id: sessionUser.accountId,
+                    tree_pid: parent ? parent.id : rootId, tree_level: parent ? parent.tree_level + 1 : 1, tree_order,
+                    name, filing_type,
+                };
+                const operate = await conn.insert(this.tableName, insertData);
+                if (operate.affectedRows === 0) throw '新增文件夹失败';
+
+                const updateData = [];
+                if (preChild) {
+                    sibling.forEach(x => {
+                        if (x.tree_order >= tree_order) updateData.push({ id: x.id, tree_order: x.tree_order + 1 });
+                    });
+                }
+                if (updateData.length > 0) await conn.updateRows(this.tableName, updateData);
+
+                await conn.commit();
+                return { create: [insertData], update: updateData };
+            } catch (error) {
+                await conn.rollback();
+                throw error;
+            }
+        }
+        async save(data) {
+            const filing = await this.getDataById(data.id);
+            if (!filing) throw '分类不存在,请刷新页面后重试';
+
+            const result = await this.db.update(this.tableName, { id: data.id, name: data.name });
+            if (result.affectedRows > 0) {
+                return data;
+            } else {
+                throw '更新数据失败';
+            }
+        }
+        async del(templateId, data) {
+            const filing = await this.getDataById(data.id);
+            if (!filing) throw '分类不存在,请刷新页面后重试';
+
+            const posterity = await this.getPosterityData(templateId, data.id);
+            const delData = posterity.map(x => {return x.id; });
+            delData.push(data.id);
+
+            const sibling = await this.getAllDataByCondition({ where: { temp_id: templateId, tree_pid: filing.tree_pid } });
+            const updateData = [];
+            sibling.forEach(x => {
+                if (x.tree_order > filing.tree_order) updateData.push({ id: x.id, tree_order: x.tree_order - 1});
+            });
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id: delData });
+                if (updateData.length > 0) conn.updateRows(this.tableName, updateData);
+
+                await conn.commit();
+                return { delete: delData, update: updateData };
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+        async move(templateId, data) {
+            console.log(data);
+            const filing = await this.getDataById(data.id);
+            if (!filing) throw '移动的分类不存在,请刷新页面后重试';
+            const parent = await this.getDataById(data.tree_pid);
+            const sibling = await this.getAllDataByCondition({ where: { temp_id: templateId, tree_pid: data.tree_pid } });
+            const filing_type = parent ? parent.filing_type : await this.getNewFilingType(templateId);
+            const posterity = await this.getPosterityData(templateId, filing.id);
+
+            const result = [];
+            const updateData = { id: filing.id, tree_order: data.tree_order, tree_pid: data.tree_pid, tree_level: parent ? parent.tree_level + 1 : 1, filing_type };
+            result.push(updateData);
+            const posterityUpdateData = posterity.map(x => { return { id: x.id, filing_type, tree_level: parent.tree_level + 1 - filing.tree_level + x.tree_level }});
+            result.push(posterityUpdateData);
+            const siblingUpdateData = [];
+            if (data.tree_pid === filing.tree_pid) {
+                if (data.tree_order < filing.tree_order) {
+                    sibling.forEach(x => {
+                        if (x.id === filing.id) return;
+                        if (x.tree_order < data.tree_order) return;
+                        if (x.tree_order > filing.tree_order) return;
+                        siblingUpdateData.push({id: x.id, tree_order: x.tree_order + 1});
+                    });
+                } else {
+                    sibling.forEach(x => {
+                        if (x.id === filing.id) return;
+                        if (x.tree_order < filing.tree_order) return;
+                        if (x.tree_order > data.tree_order) return;
+                        siblingUpdateData.push({id: x.id, tree_order: x.tree_order - 1});
+                    });
+                }
+            } else {
+                const orgSibling = await this.getAllDataByCondition({ where: { temp_id: templateId, tree_pid: filing.tree_pid } });
+                orgSibling.forEach(x => {
+                    if (x.id === filing.id) return;
+                    if (x.tree_order < filing.tree_order) return;
+                    siblingUpdateData.push({id: x.id, tree_order: x.tree_order - 1});
+                });
+                sibling.forEach(x => {
+                    if (x.id === filing.id) return;
+                    if (x.tree_order < data.tree_order) return;
+                    siblingUpdateData.push({id: x.id, tree_order: x.tree_order + 1});
+                })
+            }
+            result.push(...siblingUpdateData);
+
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.update(this.tableName, updateData);
+                if (posterityUpdateData.length > 0) await conn.updateRows(this.tableName, posterityUpdateData);
+                if (siblingUpdateData.length > 0) await conn.updateRows(this.tableName, siblingUpdateData);
+                await conn.commit();
+                return { update: result };
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+    }
+
+    return FilingTemplate;
+};

+ 108 - 0
app/service/filing_template_list.js

@@ -0,0 +1,108 @@
+'use strict';
+
+/**
+ *
+ * 2024/3/21
+ * @author Mai
+ * @date
+ * @version
+ */
+
+const FtType = {
+    org: 0,
+    add: 1,
+};
+
+module.exports = app => {
+
+    class FilingTemplateList extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'filing_template_list';
+            this.FtType = FtType;
+        }
+
+        async getOriginTemplate() {
+            return await this.getDataByCondition({ ft_type: FtType.org });
+        }
+
+        async getAllTemplate(pid) {
+            const sql = `SELECT * FROM ${this.tableName} WHERE ft_type = ${FtType.org} OR project_id = ? ORDER BY create_time asc`;
+            return await this.db.query(sql, [pid]);
+        }
+
+        /**
+         * 保存/新增数据
+         *
+         * @param {Object} name - 模板名称
+         * @param {String} id - 存在则为保存,反之新增
+         * @return {boolean} - 操作结果
+         */
+        async save(name, id = '') {
+            if (id) {
+                const result = await this.defaultUpdate({ id, name });
+                return [result.affectedRows > 0, id];
+            } else {
+                const conn = await this.db.beginTransaction();
+                try {
+                    const newTemplate = {
+                        id: this.uuid.v4(), project_id: this.ctx.session.sessionProject.id,
+                        user_id: this.ctx.session.sessionUser.accountId,
+                        name: name || '新增XXX模板库', ft_type: FtType.add,
+                    };
+                    await conn.insert(this.tableName, newTemplate);
+                    await this.ctx.service.filingTemplate.initTemplate(conn, newTemplate);
+                    await conn.commit();
+                    return [true, newTemplate.id];
+                } catch (err) {
+                    this.ctx.log(err);
+                    await conn.rollback();
+                    return [false, ''];
+                }
+            }
+        }
+
+        /**
+         * 删除模板
+         *
+         * @param {string} id - 删除的id
+         * @return {Boolean} - 返回删除的结果
+         */
+        async delete(id) {
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.tableName, { id });
+                await conn.delete(this.ctx.service.filingTemplate.tableName, { temp_id: id });
+                await conn.commit();
+                return true;
+            } catch (error) {
+                this.ctx.log(error);
+                await conn.rollback();
+                return false;
+            }
+        }
+
+        async reset(id) {
+            const template = await this.getDataById(id);
+            const conn = await this.db.beginTransaction();
+            try {
+                await conn.delete(this.ctx.service.filingTemplate.tableName, { temp_id: id });
+                await this.ctx.service.filingTemplate.initTemplate(conn, template);
+                await conn.commit();
+            } catch(err) {
+                await conn.rollback();
+                throw err;
+            }
+        }
+
+    }
+
+    return FilingTemplateList;
+};

+ 16 - 0
app/service/project_spread.js

@@ -74,11 +74,27 @@ module.exports = app => {
             return spreadSetting;
         }
 
+        updateSpreadWithSpec(spreadSetting, specSetting) {
+            if (!specSetting) return;
+            for (const s of specSetting) {
+                for (const c of spreadSetting.cols) {
+                    if (s.condition.value.indexOf(c[s.condition.key]) >= 0) {
+                        this._.assignIn(c, s.update);
+                    }
+                }
+            }
+        }
+
         generateRelaSpread(colSetType, colSet) {
             const baseSetCol = colSetType.indexOf('stage') > 0 ? SpreadConst.BaseSetCol.Stage : SpreadConst.BaseSetCol.Ledger;
             const baseSpreadColSetting = colSetType.indexOf('stage') > 0 ? SpreadConst.BaseSpreadColSetting.Stage : SpreadConst.BaseSpreadColSetting.Ledger;
             const billsSpread = this.generateSpreadSetting(colSet, SpreadConst.EmptySpreadSetting[colSetType].bills, baseSetCol, baseSpreadColSetting.bills);
             const posSpread = this.generateSpreadSetting(colSet, SpreadConst.EmptySpreadSetting[colSetType].pos, baseSetCol, baseSpreadColSetting.pos, 'pos');
+            const spreadSpec = SpreadConst.SpreadSpec[colSetType];
+            if (spreadSpec) {
+                this.updateSpreadWithSpec(billsSpread, spreadSpec.bills);
+                this.updateSpreadWithSpec(posSpread, spreadSpec.pos);
+            }
             return [billsSpread, posSpread];
         }
 

+ 35 - 23
app/service/stage.js

@@ -128,15 +128,20 @@ module.exports = app => {
 
         async loadPreCheckedStage(stage) {
             if (stage.order > 1) {
-                const preCheckedStages = await this.ctx.service.stage.getAllDataByCondition({
-                    where: { tid: stage.tid, status: auditConst.stage.status.checked },
-                    orders: [['order', 'desc']],
-                });
-                stage.preCheckedStage = preCheckedStages[0];
+                if (stage.status === auditConst.stage.status.checked) {
+                    stage.preCheckedStage = await this.getDataByCondition({ tid: stage.tid, order: stage.order - 1 });
+                } else {
+                    const preCheckedStages = await this.getAllDataByCondition({
+                        where: { tid: stage.tid, status: auditConst.stage.status.checked },
+                        orders: [['order', 'desc']],
+                    });
+                    stage.preCheckedStage = preCheckedStages[0];
+                }
+                stage.isCheckFirst = stage.order > 1 ? (stage.preCheckedStage ? stage.preCheckedStage.order === stage.order - 1 : false) : true;
             } else {
                 stage.preCheckedStage = undefined;
+                stage.isCheckFirst = true;
             }
-            stage.isCheckFirst = stage.order > 1 ? (stage.preCheckedStage ? stage.preCheckedStage.order === stage.order - 1 : false) : true;
         }
 
         async doCheckStage(stage, force = false) {
@@ -388,34 +393,37 @@ module.exports = app => {
             }
         }
 
+        async _checkStageValid(stage) {
+            if (stage.status === auditConst.stage.status.uncheck && !this.ctx.tender.isTourist) {
+                const assist = await this.ctx.service.auditAss.getAllDataByCondition({ where: { tid: stage.tid, user_id: stage.user_id } });
+                const assistId = assist.map(x => { return x.ass_user_id });
+                return stage.user_id === this.ctx.session.sessionUser.accountId || assistId.indexOf(this.ctx.session.sessionUser.accountId) >= 0;
+            } else {
+                return true;
+            }
+        }
         /**
          * 获取标段下的全部计量期,按倒序
          * @param tenderId
          * @return {Promise<void>}
          */
         async getValidStages(tenderId) {
-            const stages = await this.db.select(this.tableName, {
+            let stages = await this.db.select(this.tableName, {
                 where: { tid: tenderId },
                 orders: [['order', 'desc']],
             });
             for (const s of stages) {
                 s.tp_history = s.tp_history ? JSON.parse(s.tp_history) : [];
+                s.valid = await this._checkStageValid(s);
             }
             if (stages.length !== 0 && !this.ctx.session.sessionUser.is_admin) {
-                const lastStage = stages[0];
-                if (lastStage.status === auditConst.stage.status.uncheck && !this.ctx.tender.isTourist) {
-                    const assist = await this.ctx.service.auditAss.getAllDataByCondition({ where: { tid: tenderId, user_id: lastStage.user_id } });
-                    const assistId = assist.map(x => { return x.ass_user_id });
-                    if (lastStage.user_id !== this.ctx.session.sessionUser.accountId && assistId.indexOf(this.ctx.session.sessionUser.accountId) < 0) {
-                        stages.splice(0, 1);
-                    }
-                }
+                stages = stages.filter(x => { return x.valid; });
             }
             // 最新一期计量(未审批完成),当前操作人的期详细数据,应实时计算
             if (stages.length === 0) return stages;
 
-            await this.checkStageGatherData(stages[0], true);
             for (const s of stages) {
+                if (s.status !== auditConst.stage.status.checked) await this.checkStageGatherData(s, true);
                 s.tp = this.ctx.helper.sum([s.contract_tp, s.qc_tp, s.pc_tp]);
                 s.pre_tp = this.ctx.helper.add(s.pre_contract_tp, s.pre_qc_tp);
                 s.end_tp = this.ctx.helper.add(s.pre_tp, s.tp);
@@ -455,9 +463,9 @@ module.exports = app => {
         async addStage(tenderId, date, period) {
             const stages = await this.getAllDataByCondition({
                 where: { tid: tenderId },
-                order: [['order', 'asc']],
+                orders: [['order', 'DESC']],
             });
-            const preStage = stages[stages.length - 1];
+            const preStage = stages[0];
             const preCheckedStage = stages.find(x => { return x.status === auditConst.stage.status.checked; });
             const order = stages.length + 1;
             const newStage = {
@@ -481,10 +489,10 @@ module.exports = app => {
                 newStage.im_gather_node = preStage.im_gather_node;
                 if (preCheckedStage) {
                     newStage.pre_contract_tp = this.ctx.helper.sum([preCheckedStage.pre_contract_tp, preCheckedStage.contract_tp, preCheckedStage.contract_pc_tp]);
-                    newStage.pre_qc_tp = this.ctx.helper.sum([preCheckedStage.pre_qc_tp, preStage.qc_tp, preCheckedStage.qc_pc_tp]);
+                    newStage.pre_qc_tp = this.ctx.helper.sum([preCheckedStage.pre_qc_tp, preCheckedStage.qc_tp, preCheckedStage.qc_pc_tp]);
                     newStage.pre_positive_qc_tp = this.ctx.helper.sum([preCheckedStage.pre_positive_qc_tp, preCheckedStage.positive_qc_tp, preCheckedStage.positive_qc_pc_tp]);
                     newStage.pre_negative_qc_tp = this.ctx.helper.sum([preCheckedStage.pre_negative_qc_tp, preCheckedStage.negative_qc_tp, preCheckedStage.negative_qc_pc_tp]);
-                    newStage.pre_yf_tp = this.ctx.helper.add(preCheckedStage.pre_yf_tp, preStage.yf_tp);
+                    newStage.pre_yf_tp = this.ctx.helper.add(preCheckedStage.pre_yf_tp, preCheckedStage.yf_tp);
                     if (preCheckedStage.order === 1 || preCheckedStage.pre_sf_tp) {
                         newStage.pre_sf_tp = this.ctx.helper.add(preCheckedStage.pre_sf_tp, preCheckedStage.sf_tp);
                     } else {
@@ -502,6 +510,7 @@ module.exports = app => {
                 const result = await transaction.insert(this.tableName, newStage);
                 if (result.affectedRows === 1) {
                     newStage.id = result.insertId;
+                    newStage.preCheckedStage = preCheckedStage;
                 } else {
                     throw '新增期数据失败';
                 }
@@ -534,7 +543,9 @@ module.exports = app => {
                     const priceCalc = new RevisePrice(this.ctx);
                     pcTp = await priceCalc.newStagePriceChange(newStage, preStage, transaction);
                 }
-                await this.ctx.service.tenderCache.updateStageCache4Add(transaction, newStage, pcTp);
+                if (order === 0 || (preStage && preCheckedStage && preStage.order === preCheckedStage.order)) {
+                    await this.ctx.service.tenderCache.updateStageCache4Add(transaction, newStage, pcTp);
+                }
                 // 新增期拷贝报表相关配置/签名角色 等
                 if (preStage) {
                     const rptResult = await this.ctx.service.rptCustomDefine.addInitialStageData(newStage, preStage, transaction);
@@ -681,9 +692,10 @@ module.exports = app => {
          * @return {Promise<void>}
          */
         async deleteStage(id) {
+            const stageInfo = await this.getDataById(id);
+            await this.loadPreCheckedStage(stageInfo);
             const transaction = await this.db.beginTransaction();
             try {
-                const stageInfo = await this.getDataById(id);
                 // 通知发送 - 第三方更新
                 // if (this.ctx.session.sessionProject.custom && syncApiConst.notice_type.indexOf(this.ctx.session.sessionProject.customType) !== -1) {
                 //     const base_data = {
@@ -698,7 +710,7 @@ module.exports = app => {
                 //     await this.ctx.helper.syncNoticeSend(this.ctx.session.sessionProject.customType, JSON.stringify(base_data));
                 // }
                 await transaction.delete(this.tableName, { id });
-                await this.ctx.service.tenderCache.updateStageCache4Del(transaction, stageInfo);
+                if (stageInfo.isCheckFirst) await this.ctx.service.tenderCache.updateStageCache4Del(transaction, stageInfo);
                 await transaction.delete(this.ctx.service.pos.tableName, { add_stage: id });
                 await transaction.delete(this.ctx.service.stageAudit.tableName, { sid: id });
                 await transaction.delete(this.ctx.service.stageBills.tableName, { sid: id });

+ 9 - 3
app/service/stage_audit.js

@@ -566,7 +566,13 @@ module.exports = app => {
                         // 当前期不是最新一起时
                         if (this.ctx.stage.highOrder !== this.ctx.stage.order) {
                             const nextStages = await this.ctx.service.stage.getNextStages(this.ctx.stage.tid, this.ctx.stage.order);
-                            await this.ctx.service.stagePay.refreshNextStagePreTp(transaction, nextStages, stagePays);
+                            for (const ns of nextStages) {
+                                await this.ctx.service.stagePay.reInitialStageData(this.ctx.stage, ns, transaction);
+                                await this.ctx.service.stageOther.addInitialStageData(ns, this.ctx.stage, transaction);
+                                await this.ctx.service.stageSafeProd.addInitialStageData(ns, this.ctx.stage, transaction);
+                                await this.ctx.service.stageJgcl.addInitialStageData(ns, this.ctx.stage, transaction);
+                                await this.ctx.service.stageTempLand.addInitialStageData(ns, this.ctx.stage, transaction);
+                            }
                             const preStageTp = {
                                 pre_contract_tp: this.ctx.helper.sum([this.ctx.stage.pre_contract_tp, stageTp.contract_tp, stageTp.contract_pc_tp]),
                                 pre_qc_tp: this.ctx.helper.sum([this.ctx.stage.pre_qc_tp, stageTp.qc_tp, stageTp.qc_pc_tp]),
@@ -575,7 +581,7 @@ module.exports = app => {
                                 pre_yf_tp: this.ctx.helper.add(this.ctx.stage.pre_yf_tp, stageTp.yf_tp),
                                 pre_sf_tp: this.ctx.helper.add(this.ctx.stage.pre_sf_tp, stageTp.sf_tp),
                             };
-                            await transaction.update(this.ctx.service.stage.tableName, nextStages.map(x => { return { id: x.id, ...preStageTp }; }));
+                            await transaction.updateRows(this.ctx.service.stage.tableName, nextStages.map(x => { return { id: x.id, ...preStageTp }; }));
                         }
 
                         // 添加短信通知-审批通过提醒功能
@@ -1895,7 +1901,7 @@ module.exports = app => {
                 });
                 // 同步 期信息
                 const time = new Date();
-                await this.ctx.service.tenderCache.updateStageCache4DelTimes(transaction, this.ctx.stage, nowTimes);
+                if (this.ctx.stage.isCheckFirst) await this.ctx.service.tenderCache.updateStageCache4DelTimes(transaction, this.ctx.stage, nowTimes);
                 await transaction.update(this.ctx.service.stage.tableName, {
                     id: this.ctx.stage.id,
                     status: auditConst.status.checkNo,

+ 1 - 0
app/service/stage_jgcl.js

@@ -247,6 +247,7 @@ module.exports = app => {
                     delete pd.deduct_qty;
                     pd.sid = stage.id;
                 }
+                await transaction.delete(this.tableName, { sid: stage.id });
                 const result = await transaction.insert(this.tableName, preDatas);
                 return result.affectedRows === preDatas.length;
             } else {

+ 1 - 1
app/service/stage_other.js

@@ -213,12 +213,12 @@ module.exports = app => {
                     pd.sid = stage.id;
                     pd.sorder = stage.order;
                 }
+                await transaction.delete(this.tableName, { sid: stage.id });
                 const result = await transaction.insert(this.tableName, preDatas);
                 return result.affectedRows === preDatas.length;
             } else {
                 return true;
             }
-
         }
 
         async deleteStageTimesData(sid, times, transaction) {

+ 30 - 1
app/service/stage_pay.js

@@ -134,6 +134,35 @@ module.exports = app => {
             }
             return result.affectedRows === stagePays.length;
         }
+        async reInitialStageData(preStage, stage, transaction) {
+            if (!stage || !preStage || !transaction) throw '检查合同支付数据失败';
+
+            const basesReg = new RegExp(payConst.calcBase.map(x => {return '(' + x.code + ')'}).join('|'));
+            const orderReg = /f\d+/im;
+
+            let pays = await this.ctx.service.pay.getAllDataByCondition({where: { tid: this.ctx.tender.id } });
+            const stagePays = [];
+            const prePays = await this.getStageLastestPays(preStage);
+            for (const pp of prePays) {
+                const p = this._.find(pays, {id: pp.pid});
+                if (!p.valid) continue;
+                stagePays.push({
+                    tid: p.tid, sid: stage.id, pid: p.id,
+                    stimes: stage.times, sorder: 0,
+                    name: pp.name,
+                    expr: (p.ptype === payConst.payType.normal || p.ptype === payConst.payType.sf)
+                        ? (basesReg.test(pp.expr) || orderReg.test(pp.expr) ? pp.expr : null)
+                        : (p.type === payConst.wc ? pp.expr : null),
+                    pause: pp.pause,
+                    pre_tp: pp.end_tp,
+                    pre_used: pp.pre_used || !this.ctx.helper.checkZero(pp.tp),
+                    pre_finish: pp.pre_finish || (pp.rprice ? pp.end_tp === pp.rprice : false),
+                    start_stage_order: pp.start_stage_order,
+                });
+            }
+            await transaction.delete(this.tableName, { sid: stage.id });
+            await transaction.insert(this.tableName, stagePays);
+        }
 
         async getStagePay(stage, pid) {
             return await this.getAuditorStagePay(pid, stage.id, stage.curTimes, stage.curOrder);
@@ -522,7 +551,7 @@ module.exports = app => {
                     }
                 }
             }
-            if (updateData.length > 0) await transaction.update(this.tableName, updateData);
+            if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
         }
     }
 

+ 1 - 0
app/service/stage_safe_prod.js

@@ -246,6 +246,7 @@ module.exports = app => {
                     pd.sid = stage.id;
                     pd.sorder = stage.order;
                 }
+                await transaction.delete(this.tableName, { sid: stage.id });
                 const result = await transaction.insert(this.tableName, preDatas);
                 return result.affectedRows === preDatas.length;
             } else {

+ 1 - 0
app/service/stage_temp_land.js

@@ -215,6 +215,7 @@ module.exports = app => {
                     pd.sid = stage.id;
                     pd.sorder = stage.order;
                 }
+                await transaction.delete(this.tableName, { sid: stage.id });
                 const result = await transaction.insert(this.tableName, preDatas);
                 return result.affectedRows === preDatas.length;
             } else {

+ 5 - 2
app/service/sub_project.js

@@ -378,10 +378,13 @@ module.exports = app => {
                     im.push({ id: this.uuid.v4(), spid: subProject.id, pid: subProject.project_id, uid: u.id, file_permission, filing_type });
                 }
             }
+            const template = await this.ctx.service.filingTemplateList.getDataById(data.filingTemplate);
+            if (!template) throw '选择的文件类别不存在';
+
             const conn = await this.db.beginTransaction();
             try {
-                await conn.update(this.tableName, { id: subProject.id, management: data.management });
-                await this.ctx.service.filing.initFiling(subProject.id, conn);
+                await conn.update(this.tableName, { id: subProject.id, management: data.management, filing_template_id: template.id, filing_template_name: template.name });
+                await this.ctx.service.filing.initFiling(subProject.id, data.filingTemplate, conn);
                 if (dm.length > 0) await conn.delete(this.ctx.service.subProjPermission.tableName, { id: dm });
                 if (um.length > 0) await conn.updateRows(this.ctx.service.subProjPermission.tableName, um);
                 if (im.length > 0) await conn.insert(this.ctx.service.subProjPermission.tableName, im);

+ 112 - 0
app/service/tender_cert.js

@@ -0,0 +1,112 @@
+'use strict';
+
+/**
+ *
+ *
+ * @author Mai
+ * @date
+ * @version
+ */
+const profileConst = require('../const/profile');
+module.exports = app => {
+
+    class TenderCert extends app.BaseService {
+
+        /**
+         * 构造函数
+         *
+         * @param {Object} ctx - egg全局变量
+         * @return {void}
+         */
+        constructor(ctx) {
+            super(ctx);
+            this.tableName = 'tender_cert';
+        }
+
+        async getListByTid(tid, allCertList) {
+            const list = await this.getAllDataByCondition({ where: { tid } });
+            if (list.length > 0) {
+                const accountList = await this.ctx.service.projectAccount.getAllDataByCondition({ columns: ['id', 'account', 'name', 'role'], where: { id: this._.uniq(this._.map(list, 'uid')) } });
+                for (const l of list) {
+                    const acInfo = this._.find(allCertList, { id: l.cert_id });
+                    if (acInfo) {
+                        acInfo.edu_json = acInfo.edu_json ? JSON.parse(acInfo.edu_json) : null;
+                        acInfo.eduInfo = acInfo.edu_json && acInfo.edu_json.length > 0 ? acInfo.edu_json[acInfo.edu_json.length - 1] : null;
+                        l.cert_info = acInfo;
+                    }
+                    const accountInfo = this._.find(accountList, { id: l.uid });
+                    if (accountInfo) {
+                        const certs = this._.filter(allCertList, { uid: l.uid });
+                        l.account_info = accountInfo;
+                        l.account_info.certs = certs;
+                    }
+                }
+            }
+            return list;
+        }
+
+        async saveUserCert(tid, list) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                if (list.insertList.length > 0) {
+                    const insertList = [];
+                    for (const i of list.insertList) {
+                        insertList.push({
+                            uid: i.uid,
+                            cert_id: i.cert_id,
+                            tid,
+                            create_time: new Date(),
+                        });
+                    }
+                    await transaction.insert(this.tableName, insertList);
+                }
+                if (list.removeList.length > 0) {
+                    await transaction.delete(this.tableName, { id: list.removeList });
+                }
+                if (list.updateList.length > 0) {
+                    await transaction.updateRows(this.tableName, list.updateList);
+                }
+                await transaction.commit();
+                const allCertList = await this.ctx.service.accountCert.getAllCertByPid(this.ctx.session.sessionProject.id);
+                return await this.getListByTid(tid, allCertList);
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async updateOneCert(data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const info = await transaction.get(this.tableName, { id: data.id });
+                if (!info) throw '数据已不存在';
+                await transaction.update(this.tableName, data);
+                await transaction.commit();
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+
+        async updateMoreCert(tid, data) {
+            const transaction = await this.db.beginTransaction();
+            try {
+                const updateData = [];
+                for (const d of data) {
+                    const info = await transaction.get(this.tableName, { id: d.id });
+                    if (!info) throw '数据已不存在';
+                    updateData.push(d);
+                }
+                if (updateData.length > 0) await transaction.updateRows(this.tableName, updateData);
+                await transaction.commit();
+                const allCertList = await this.ctx.service.accountCert.getAllCertByPid(this.ctx.session.sessionProject.id);
+                return await this.getListByTid(tid, allCertList);
+            } catch (err) {
+                await transaction.rollback();
+                throw err;
+            }
+        }
+    }
+
+    return TenderCert;
+};

+ 1 - 1
app/view/file/index.ejs

@@ -15,7 +15,7 @@
                 </div>
                 <% } else { %>
                 <table class="table table-hover table-bordered">
-                    <tr class="text-center"><th style="min-width: 200px">项目名称</th><th>管理单位</th><th>创建时间</th><th>操作</th></tr>
+                    <tr class="text-center"><th style="min-width: 200px; width: 45%">项目名称</th><th>文件个数</th><th>管理单位</th><th>创建时间</th><th>操作</th></tr>
                     <tbody id="projectList">
                     </tbody>
                 </table>

+ 71 - 0
app/view/file/template.ejs

@@ -0,0 +1,71 @@
+<div class="panel-content">
+    <div class="panel-title fluid">
+        <div class="title-main  d-flex justify-content-between">
+            <div>资料归集模板库</div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0 row">
+                <div class="col-3">
+                    <div class="d-flex flex-row">
+                        <form class="ml-2 p-2" method="POST" action="/file/template/save">
+                            <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                            <button class="btn btn-sm btn-light text-primary"><i class="fa fa-plus" aria-hidden="true" type="submit"></i> 新增模板库</button>
+                        </form>
+                    </div>
+                    <div>
+                        <dl class="list-group">
+                            <% for (const temp of templateList) { %>
+                            <dd class="list-group-item <%- (temp.id === template.id ? 'bg-warning' : '')%>">
+                                <div class="d-flex justify-content-between align-items-center table-file" tempId="<%- temp.id %>">
+                                    <div><%if (temp.ft_type === FtType.org) { %><span class="text-success mr-1" style="border: 1px solid">默认</span><% } %><%- temp.name %>%></div>
+                                    <% if (temp.ft_type) { %>
+                                    <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="renameTemplate"><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="delTemplate"><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">
+                        <% if (template.ft_type !== FtType.org) { %>
+                        <div class="p-2">
+                            <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="add-slibing"><i class="fa fa-plus" aria-hidden="true"></i> 同层</a>
+                            <a href="javascript: void(0);" class="btn btn-sm btn-light text-primary" id="add-child"><i class="fa fa-plus" aria-hidden="true"></i> 子项</a>
+                        </div>
+                        <% } %>
+                        <form class="ml-2 p-2" method="POST" action="/file/template/reset?id=<%- template.id %>">
+                            <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+                            <button class="btn btn-sm btn-primary"><i class="fa fa-refresh" aria-hidden="true"></i> 初始化模板</button>
+                        </form>
+                    </div>
+                    <div>
+                        <ul id="filing" class="ztree" style="overflow: auto"></ul>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div stype="display: none">
+        <form id="hiddenForm" action="" method="POST">
+            <input type="hidden" name="_csrf_j" value="<%= ctx.csrf %>" />
+            <input type="hidden" id="extra" name="" value="">
+        </form>
+    </div>
+</div>
+<script>
+    const templateList = JSON.parse('<%- JSON.stringify(templateList) %>');
+    const templateData = JSON.parse('<%- JSON.stringify(templateData) %>');
+    $('.table-file').click(function(e) {
+        if (this.getAttribute('renaming') === '1') return;
+        if (e.target.tagName === 'A' || e.target.tagName === 'I' || e.target.tagName === 'INPUT') return;
+        window.location.href = '/file/template/' +  this.getAttribute('tempId');
+    });
+    const readOnly = <%- template.ft_type === FtType.org %>;
+</script>

+ 4 - 0
app/view/measure/stage.ejs

@@ -14,7 +14,11 @@
                     <% if (!ctx.session.sessionProject.page_show.close1stStageCheckDealParam && ctx.helper.checkZero(ctx.tender.info.deal_param.contractPrice) && stages.length === 0) { %>
                         <a href="#add-qi" data-toggle="modal" data-target="#tips" class="btn btn-primary btn-sm">开始新一期</a>
                     <% } else { %>
+                        <% if (ctx.session.sessionProject.page_show.openMultiStageCalc && unCompleteStageCount < ctx.session.sessionProject.page_show.maxMultiStageCount) { %>
                         <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">开始新一期</a>
+                        <% } else if (unCompleteStageCount === 0) {%>
+                        <a href="#add-qi" data-toggle="modal" data-target="#add-qi" class="btn btn-primary btn-sm">开始新一期</a>
+                        <% } %>
                     <% } %>
                 <% } %>
             </div>

+ 156 - 0
app/view/profile/cert.ejs

@@ -0,0 +1,156 @@
+<% include ./sub_menu.ejs %>
+<div class="panel-content" id="app">
+    <div class="panel-title">
+        <div class="title-main d-flex justify-content-between">
+            <div class="d-inline-block">
+                <div class="btn-group group-tab">
+                    <a class="btn btn-sm btn-light" href="/profile/info">
+                        账号资料
+                    </a>
+                    <a class="btn btn-sm btn-light active" href="javascript:void(0);">
+                        证书信息
+                    </a>
+                </div>
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body">
+            <div class="sjs-height-0">
+                <div class="row m-0 mt-3">
+                    <div class="col-12">
+                        <a href="javascript:void(0);" id="addCert">+添加证书</a>
+                        <div class="row mb-3" id="certs">
+                            <% if (certList.length > 0) { %>
+                                <% for (const [i,c] of certList.entries()) { %>
+                                    <div class="col-6 mt-3 one-cert" data-cid="<%- c.id %>">
+                                        <div class="card">
+                                            <div class="modal-header">
+                                                <b>证书信息<span class="cert-num"><%- certList.length-i %></span></b>
+                                                <div class="pull-right">
+                                                    <a href="#del-cert" data-cid="<%- c.id %>" data-jxid="" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                </div>
+                                            </div>
+                                            <div class="card-body">
+                                                <b>持证情况:</b>
+                                                <div class="m-3 one-cert-msg">
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件名称:</label>
+                                                        <div class="row col-sm-10 pr-0">
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select-cert-type" data-type="type">
+                                                                    <option value="0">请选择</option>
+                                                                    <% for (const t of certTypeConst) { %>
+                                                                    <option value="<%- t.value %>" <% if (c.type === t.value) { %>selected<% } %>><%- t.name %></option>
+                                                                    <% } %>
+                                                                </select>
+                                                            </div>
+                                                            <div class="col-6 pr-0">
+                                                                <select class="form-control form-control-sm select-cert-name" data-type="name">
+                                                                    <% if (c.type) { %>
+                                                                        <% for (const q of certSourceConst[ctx.helper.showCol4ObjArray(certTypeConst, c.type, 'value', 'source')]) { %>
+                                                                            <option value="<%- q.value %>" <% if (c.name === q.value) { %>selected<% } %>><%- q.name %></option>
+                                                                        <% } %>
+                                                                    <% } else { %>
+                                                                        <option value="0">请选择</option>
+                                                                    <% } %>
+                                                                </select>
+                                                            </div>
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证件编号:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="code" value="<%- c.code %>">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">注册单位:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="reg_unit" value="<%- c.reg_unit %>">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">技术职称:</label>
+                                                        <div class="col-sm-10">
+                                                            <input type="text" class="form-control form-control-sm" data-type="job_title" value="<%- c.job_title %>">
+                                                        </div>
+                                                    </div>
+                                                    <div class="form-group row">
+                                                        <label for="uname" class="ml-3 col-form-label">证书附件:</label>
+                                                        <div class="col-sm-10 file-show">
+                                                            <% if (c.file_path) { %>
+                                                            <div class="col-form-label">
+                                                                <a href="<%- fujianOssPath + c.file_path %>" target="_blank"><%- c.file_name %></a> &nbsp;<a href="#del-cert" data-cid="<%- c.id %>" data-jxid="" data-filename="<%- c.file_name %>" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                            </div>
+                                                            <% } else { %>
+                                                                <input type="file" class="cert-file-upload">
+                                                            <% } %>
+                                                        </div>
+                                                    </div>
+                                                </div>
+                                                <div>
+                                                    <b>继续教育情况:</b>
+                                                    <div class="pull-right">
+                                                        <a href="javascript:void(0);" class="add-jx-btn">+添加</a>
+                                                    </div>
+                                                    <div class="all-jx">
+                                                        <% if (c.edu_json.length > 0) { %>
+                                                            <% for (const [j, e] of c.edu_json.reverse().entries()) { %>
+                                                                <div class="one-jx" data-jxid="<%- e.id %>">
+                                                                    <div class="card mt-3">
+                                                                        <div class="modal-header">
+                                                                            <b>继续教育<span class="jx-num"><%- c.edu_json.length-j %></span></b>
+                                                                            <div class="pull-right">
+                                                                                <a href="#del-cert" data-cid="<%- c.id %>" data-jxid="<%- e.id %>" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                                            </div>
+                                                                        </div>
+                                                                        <div class="card-body">
+                                                                            <div class="form-group row">
+                                                                                <label for="uname" class="ml-3 col-form-label">培训时间:</label>
+                                                                                <div class="col-sm-10">
+                                                                                    <input data-language="zh" data-old-date="<%- e.date %>" data-type="date" placeholder="请选择时间" type="text" data-date-format="yyyy-mm-dd" class="jx-date datepicker-here form-control form-control-sm" value="<%- e.date %>">
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="form-group row">
+                                                                                <label for="uname" class="ml-3 col-form-label">培训单位:</label>
+                                                                                <div class="col-sm-10">
+                                                                                    <input type="text" class="form-control form-control-sm" data-type="unit" value="<%- e.unit %>">
+                                                                                </div>
+                                                                            </div>
+                                                                            <div class="form-group row">
+                                                                                <label for="uname" class="ml-3 col-form-label">培训证明:</label>
+                                                                                <div class="col-sm-10 file-show">
+                                                                                    <% if (e.file_path) { %>
+                                                                                        <div class="col-form-label">
+                                                                                            <a href="<%- fujianOssPath + e.file_path %>" target="_blank"><%- e.file_name %></a> &nbsp;<a href="#del-cert" data-cid="<%- c.id %>" data-jxid="<%- e.id %>" data-filename="<%- e.file_name %>" data-toggle="modal" data-target="#del-cert" class="del-cert-btn text-danger">删除</a>
+                                                                                        </div>
+                                                                                    <% } else { %>
+                                                                                        <input type="file" class="jx-file-upload">
+                                                                                    <% } %>
+                                                                                </div>
+                                                                            </div>
+                                                                        </div>
+                                                                    </div>
+                                                                </div>
+                                                            <% } %>
+                                                        <% } %>
+                                                    </div>
+                                                </div>
+                                            </div>
+                                        </div>
+                                    </div>
+                                <% } %>
+                            <% } %>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    const certTypeConst = JSON.parse(unescape('<%- escape(JSON.stringify(certTypeConst)) %>'));
+    const certSourceConst = JSON.parse(unescape('<%- escape(JSON.stringify(certSourceConst)) %>'));
+    const fujianOssPath = JSON.parse(unescape('<%- escape(JSON.stringify(fujianOssPath)) %>'));
+</script>

+ 22 - 0
app/view/profile/cert_modal.ejs

@@ -0,0 +1,22 @@
+<div class="modal fade" id="del-cert" >
+    <div class="modal-dialog" role="document">
+        <form class="modal-content">
+            <div class="modal-header">
+                <h5 class="modal-title">删除</h5>
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">×</span>
+                </button>
+            </div>
+            <div class="modal-body">
+                <h6>确认删除<span id="delete-cert-title"></span>?</h6>
+            </div>
+            <div class="modal-footer">
+                <input type="hidden" id="delete-cert-cid">
+                <input type="hidden" id="delete-cert-jxid">
+                <input type="hidden" id="delete-cert-type">
+                <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                <button type="button" class="btn btn-sm btn-danger" data-dismiss="modal" id="delete-cert-btn">确定删除</button>
+            </div>
+        </form>
+    </div>
+</div>

+ 15 - 2
app/view/profile/info.ejs

@@ -1,13 +1,26 @@
 <% include ./sub_menu.ejs %>
 <div class="panel-content" id="app">
     <div class="panel-title">
-        <div class="title-main">
-            <h2>账号资料</h2>
+        <div class="title-main d-flex justify-content-between">
+            <div class="d-inline-block">
+                <div class="btn-group group-tab">
+                    <a class="btn btn-sm btn-light active" href="javascript:void(0);">
+                        账号资料
+                    </a>
+                    <a class="btn btn-sm btn-light" href="/profile/cert">
+                        证书信息
+                    </a>
+                </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" href="javascript:void(0);">账号资料</a>-->
+<!--                    <a class="nav-item nav-link" href="/profile/cert">证书信息</a>-->
+<!--                </nav>-->
                 <div class="row m-0">
                     <div class="col-5 my-3">
                         <!--账号资料-->

+ 1 - 1
app/view/setting/spread.ejs

@@ -3,7 +3,7 @@
     <div class="panel-title">
         <div class="title-main d-flex">
             <div>
-                <div class="d-inline-block">显示设置 - 列设置 -【<%= (sType.indexOf('gcl') === 0 ? '工程量清单模式' : '0号台账模式') %>】 <%= (sType.indexOf('stage') === 0 ? '计量台账' : '0号台账') %></div>
+                <div class="d-inline-block">显示设置 - 列设置 -【<%= (sType.indexOf('gcl') === 0 ? '工程量清单模式' : '0号台账模式') %>】 <%= (sType.indexOf('stage') > 0 ? '计量台账' : '0号台账') %></div>
                 <div class="d-inline-block">
                     <a href="javascript:void(0)" name="base-opr" type="up-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="上移"><i class="fa fa-arrow-up" aria-hidden="true"></i></a>
                     <a href="javascript:void(0)" name="base-opr" type="down-move" class="btn btn-sm btn-light text-primary" data-toggle="tooltip" data-placement="bottom" title="" data-original-title="下移"><i class="fa fa-arrow-down" aria-hidden="true"></i></a>

+ 190 - 0
app/view/settle/gather.ejs

@@ -0,0 +1,190 @@
+<% include ./list_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <div class="title-main d-flex">
+            <% include ./list_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">
+                            <i class="fa fa-list-ol"></i> 显示层级
+                        </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="5" href="javascript: void(0);">第五层</a>
+                            <a class="dropdown-item" name="showLevel" tag="last" href="javascript: void(0);">最底层</a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap pr-46">
+        <div class="c-header p-0">
+        </div>
+        <div class="row w-100 sub-content">
+            <div id="left-view" class="c-body" style="width: 100%">
+                <!--上部分-->
+                <div class="sjs-height-1" id="settle-bills">
+                </div>
+                <!--下部分-->
+                <div class="bcontent-wrap" id="main-bottom">
+                    <div id="main-resize" class="resize-y" r-Type="height" div1="#settle-bills" div2="#main-bottom" store-id="settle-main" store-version="1.0.0" min="100"></div>
+                    <div class="bc-bar mb-1">
+                        <ul class="nav nav-tabs">
+                            <li class="nav-item">
+                                <a class="nav-link active" href="#">计量单元</a>
+                            </li>
+                        </ul>
+                    </div>
+                    <div class="sp-wrap" id="settle-pos">
+                    </div>
+                </div>
+            </div>
+            <div id="right-view" class="c-body" style="display: none; width: 33%;">
+                <div class="resize-x" id="right-spr" r-Type="width" div1="#left-view" div2="#right-view" title="调整大小" a-type="percent"><!--调整左右高度条--></div>
+                <div class="tab-content">
+                    <!--查找定位-->
+                    <div id="search" class="tab-pane tab-select-show">
+                        <div class="sjs-bar-1">
+                            <div class="input-group input-group-sm pb-1">
+                                <div class="input-group-prepend">
+                                    <div class="input-group-text">
+                                        <input type="radio" name="searchType" id="over"> 超计
+                                    </div>
+                                    <div class="input-group-text">
+                                        <input type="radio" name="searchType" id="empty"> 漏计
+                                    </div>
+                                </div>
+                                <input type="text" class="form-control form-control-sm" placeholder="可查找 项目节编号 / 清单编号 /名称" id="keyword">
+                                <div class="input-group-append">
+                                    <button class="btn btn-outline-secondary btn-sm" type="button" id="searchLedger">搜索</button>
+                                </div>
+                            </div>
+                        </div>
+                        <div id="search-result" class="sjs-sh-1">
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="side-menu">
+            <!--右侧菜单-->
+            <ul class="nav flex-column right-nav">
+                <li class="nav-item">
+                    <a class="nav-link" content="#search" href="javascript: void(0);">查找定位</a>
+                </li>
+            </ul>
+        </div>
+    </div>
+</div>
+<div style="display: none">
+    <img src="/public/images/ellipsis_horizontal.png" id="ellipsis-icon" />
+    <img src="/public/images/icon-ok.png" id="icon-ok" />
+    <img src="/public/images/file_clip.png" id="rela-file-icon">
+    <img src="/public/images/file_clip_hover.png" id="rela-file-hover">
+</div>
+<script>
+    const readOnly = true;
+    const tenderName = '<%- ctx.tender.name %>';
+    const tenderInfo = JSON.parse(unescape('<%- escape(JSON.stringify(ctx.tender.info)) %>'));
+    const thousandth = <%- ctx.tender.info.display.thousandth %>;
+    const thirdParty = JSON.parse('<%- JSON.stringify(thirdParty) %>');
+    const settleStatusHint = JSON.parse('<%- JSON.stringify(settleStatusHint )%>');
+    const settleStatus = JSON.parse('<%- JSON.stringify(settleStatus )%>');
+    const settleStatusColor = JSON.parse('<%- JSON.stringify(settleStatusColor )%>');
+    const billsSpreadSetting = {
+        cols: [
+            {title: '结算状态', colSpan: '1', rowSpan: '2', field: 'settle_status', hAlign: 1, width: 60, formatter: '@', readOnly: true, getValue: function(data) {
+                    // return data.b_code ? '' : settleStatusHint[data.settle_status] || ''
+                    return settleStatusHint[data.settle_status] || '';
+                }},
+            {title: '项目节编号', colSpan: '1', rowSpan: '2', field: 'code', hAlign: 0, width: 145, formatter: '@', readOnly: true, cellType: 'tree'},
+            {title: '清单编号', colSpan: '1', rowSpan: '2', field: 'b_code', hAlign: 0, width: 70, formatter: '@', readOnly: true},
+            {title: '名称', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 185, formatter: '@', readOnly: true},
+            {title: '单位', colSpan: '1', rowSpan: '2', field: 'unit', hAlign: 1, width: 60, formatter: '@', readOnly: true, cellType: 'unit'},
+            {title: '单价', colSpan: '1', rowSpan: '2', field: 'unit_price', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '台账|数量', colSpan: '2|1', rowSpan: '1|1', field: 'quantity', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'total_price', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '本期合同结算|数量', colSpan: '2|1', rowSpan: '1|1', field: 'cur_contract_qty', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'cur_contract_tp', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '本期数量变更|数量', colSpan: '2|1', rowSpan: '1|1', field: 'cur_qc_qty', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'cur_qc_tp', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '本期完成结算|数量', colSpan: '3|1', rowSpan: '1|1', field: 'cur_gather_qty', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'cur_gather_tp', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|完成率(%)', colSpan: '1', rowSpan: '|1', field: 'cur_final_1_percent', hAlign: 2, width: 80, readOnly: true, type: 'Number'},
+            {title: '截止本期合同结算|数量', colSpan: '2|1', rowSpan: '1|1', field: 'end_contract_qty', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_contract_tp', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '截止本期数量变更|数量', colSpan: '2|1', rowSpan: '1|1', field: 'end_qc_qty', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_qc_tp', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '截止本期完成结算|数量', colSpan: '3|1', rowSpan: '1|1', field: 'end_gather_qty', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|金额', colSpan: '|1', rowSpan: '|1', field: 'end_gather_tp', hAlign: 2, width: 60, readOnly: true, type: 'Number'},
+            {title: '|完成率(%)', colSpan: '1', rowSpan: '|1', field: 'end_final_1_percent', hAlign: 2, width: 80, readOnly: true, type: 'Number'},
+            {title: '图(册)号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 80, formatter: '@', readOnly: true},
+            {title: '备注', colSpan: '1', rowSpan: '2', field: 'memo', hAlign: 0, width: 100, formatter: '@', cellType: 'ellipsisAutoTip', readOnly: true},
+            <% if (ctx.session.sessionProject.gxby) { %>
+            {title: '工序报验', colSpan: '1', rowSpan: '2', field: 'gxby', hAlign: 1, width: 80, formatter: '@', readOnly: true},
+            <% } %>
+            <% if (ctx.session.sessionProject.dagl) { %>
+            {title: '档案管理', colSpan: '1', rowSpan: '2', field: 'dagl', hAlign: 1, width: 80, formatter: '@', readOnly: true},
+            <% } %>
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        frozenColCount: 5,
+        frozenLineColor: '#93b5e4',
+        readOnly: true,
+        getForeColor: function(sheet, data, row, col, foreColor) {
+            if (col.field === 'settle_status') {
+                return data ? settleStatusColor[data.settle_status] || foreColor : foreColor;
+            } else {
+                return foreColor;
+            }
+        }
+    };
+    const posSpreadSetting = {
+        cols: [
+            {title: '结算状态', colSpan: '1', rowSpan: '2', field: 'settle_status', hAlign: 1, width: 60, formatter: '@', readOnly: true, getValue: function(data) { return settleStatusHint[data.settle_status] || '' }},
+            {title: '计量单元', colSpan: '1', rowSpan: '2', field: 'name', hAlign: 0, width: 180, formatter: '@', readOnly: true},
+            {title: '位置', colSpan: '1', rowSpan: '2', field: 'position', hAlign: 0, width: 60, formatter: '@', readOnly: true},
+            {title: '台账数量', colSpan: '1', rowSpan: '2', field: 'quantity', hAlign: 2, width: 60, formatter: '@', readOnly: true},
+            {title: '本期结算|合同', colSpan: '3|1', rowSpan: '1|1', field: 'cur_contract_qty', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+            {title: '|数量变更', colSpan: '|1', rowSpan: '|1', field: 'cur_qc_qty', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+            {title: '|完成', colSpan: '|1', rowSpan: '|1', field: 'cur_gather_qty', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+            {title: '截止本期结算|合同', colSpan: '3|1', rowSpan: '1|1', field: 'end_contract_qty', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+            {title: '|数量变更', colSpan: '|1', rowSpan: '|1', field: 'end_qc_qty', hAlign: 2, width: 80, type: 'Number', readOnly: true},
+            {title: '|完成', colSpan: '|1', rowSpan: '|1', field: 'end_gather_qty', hAlign: 2, width: 60, type: 'Number', readOnly: true},
+            {title: '图册号', colSpan: '1', rowSpan: '2', field: 'drawing_code', hAlign: 0, width: 80, formatter: '@'},
+            <% if (ctx.session.sessionProject.gxby) { %>
+            {title: '工序报验', colSpan: '1', rowSpan: '2', field: 'gxby', hAlign: 1, width: 80, formatter: '@', readOnly: true},
+            <% } %>
+            <% if (ctx.session.sessionProject.dagl) { %>
+            {title: '档案管理', colSpan: '1', rowSpan: '2', field: 'dagl', hAlign: 1, width: 80, formatter: '@', readOnly: true},
+            <% } %>
+        ],
+        emptyRows: 0,
+        headRows: 2,
+        headRowHeight: [25, 25],
+        headColWidth: [30],
+        defaultRowHeight: 21,
+        headerFont: '12px 微软雅黑',
+        font: '12px 微软雅黑',
+        readOnly: true,
+        getForeColor: function(sheet, data, row, col, foreColor) {
+            if (col.field === 'settle_status') {
+                return data ? settleStatusColor[data.settle_status] || foreColor : foreColor;
+            } else {
+                return foreColor;
+            }
+        }
+    };
+</script>

+ 1 - 1
app/view/stage/audit_btn.ejs

@@ -2,7 +2,7 @@
     <% if (ctx.stage.status === auditConst.status.uncheck) { %>
     <% if (ctx.session.sessionUser.accountId === ctx.stage.user_id) { %>
 
-    <% if (ctx.stage.order > 1 && ctx.stage.preCheckedStage && ctx.stage.preCheckedStage.order === ctx.stage.order - 1) { %>
+    <% if (ctx.stage.isCheckFirst) { %>
     <% if (!ctx.session.sessionProject.page_show.openStageStart || (ctx.session.sessionProject.page_show.openStageStart && ctx.stage.startPermission)) { %>
     <a id="sub-sp-btn" href="javascript: void(0);" data-toggle="modal" data-target="#sub-sp" class="btn btn-primary btn-sm btn-block">上报审批</a>
     <% } else { %>

+ 1 - 1
app/view/stage_extra/bonus.ejs

@@ -47,7 +47,7 @@
 <script>
     const stageId = <%- ctx.stage.id %>;
     const stageUserId = <%- ctx.stage.user_id %>;
-    const readOnly = <%- ctx.stage.readOnly %>;
+    const readOnly = <%- ctx.stage.readOnly || ctx.stage.revising || (ctx.stage.order > 1 && (!ctx.stage.preCheckedStage || ctx.stage.preCheckedStage.order < ctx.stage.order - 1)) %>;
     const whiteList = JSON.parse('<%- JSON.stringify(ctx.app.config.multipart.whitelist) %>');
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
     const stageChecked = <%- ctx.stage.status === auditConst.status.checked %>;

+ 1 - 1
app/view/stage_extra/jgcl.ejs

@@ -36,6 +36,6 @@
 <script>
     const stageId = <%- ctx.stage.id %>;
     const stageUserId = <%- ctx.stage.user_id %>;
-    const readOnly = <%- ctx.stage.readOnly %>;
+    const readOnly = <%- ctx.stage.readOnly || ctx.stage.revising || (ctx.stage.order > 1 && (!ctx.stage.preCheckedStage || ctx.stage.preCheckedStage.order < ctx.stage.order - 1)) %>;
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
 </script>

+ 1 - 1
app/view/stage_extra/other.ejs

@@ -40,6 +40,6 @@
 <script>
     const stageId = <%- ctx.stage.id %>;
     const stageUserId = <%- ctx.stage.user_id %>;
-    const readOnly = <%- ctx.stage.readOnly %>;
+    const readOnly = <%- ctx.stage.readOnly || ctx.stage.revising || (ctx.stage.order > 1 && (!ctx.stage.preCheckedStage || ctx.stage.preCheckedStage.order < ctx.stage.order - 1)) %>;
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
 </script>

+ 1 - 1
app/view/stage_extra/safe_prod.ejs

@@ -37,6 +37,6 @@
 <script>
     const stageId = <%- ctx.stage.id %>;
     const stageUserId = <%- ctx.stage.user_id %>;
-    const readOnly = <%- ctx.stage.readOnly %>;
+    const readOnly = <%- ctx.stage.readOnly || ctx.stage.revising || (ctx.stage.order > 1 && (!ctx.stage.preCheckedStage || ctx.stage.preCheckedStage.order < ctx.stage.order - 1)) %>;
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
 </script>

+ 1 - 1
app/view/stage_extra/temp_land.ejs

@@ -37,6 +37,6 @@
 <script>
     const stageId = <%- ctx.stage.id %>;
     const stageUserId = <%- ctx.stage.user_id %>;
-    const readOnly = <%- ctx.stage.readOnly %>;
+    const readOnly = <%- ctx.stage.readOnly || ctx.stage.revising || (ctx.stage.order > 1 && (!ctx.stage.preCheckedStage || ctx.stage.preCheckedStage.order < ctx.stage.order - 1)) %>;
     const thousandth = <%- ctx.tender.info.display.thousandth %>;
 </script>

+ 2 - 1
app/view/sub_proj/index.ejs

@@ -6,7 +6,8 @@
             <% if (ctx.session.sessionUser.is_admin) { %>
             <div class="ml-auto">
                 <a href="#add-folder" name="add" data-toggle="modal" data-target="#add-folder" class="btn btn-sm btn-primary">新建文件夹</a>
-                <a href="#add-project" name="add" data-toggle="modal" data-target="#add-project" class="btn btn-sm btn-primary pull-right ml-2">新建项目</a>
+                <a href="#add-project" name="add" data-toggle="modal" data-target="#add-project" class="btn btn-sm btn-primary ml-2">新建项目</a>
+                <a href="/file/template" class="btn btn-sm btn-primary ml-2">资料模板库</a>
             </div>
             <% } %>
         </div>

+ 10 - 0
app/view/sub_proj/modal.ejs

@@ -201,6 +201,16 @@
                             </select>
                         </div>
                     </div>
+                    <div class="form-group row">
+                        <label for="text" class="col-sm-2 col-form-labelcol-form-label-sm">文件类别</label>
+                        <div class="col-sm-10">
+                            <select id="sm-ft" class="form-control form-control-sm">
+                                <% for( const t of templates) { %>
+                                <option value="<%- t.id %>"><%- t.name %></option>
+                                <% } %>
+                            </select>
+                        </div>
+                    </div>
                 </form>
             </div>
             <div class="modal-footer">

+ 37 - 0
app/view/tender/cert.ejs

@@ -0,0 +1,37 @@
+<% include ./tender_sub_menu.ejs %>
+<div class="panel-content">
+    <div class="panel-title">
+        <!--工具栏-->
+        <div class="title-main d-flex justify-content-between">
+            <% include ./tender_sub_mini_menu.ejs %>
+            <!--工具栏-->
+            <div>
+                <div class="d-inline-block">
+                    <a class="btn btn-sm btn-primary" href="#addusers" data-toggle="modal" data-target="#import" >添加用户</a>
+                </div>
+            </div>
+            <div class="ml-auto">
+            </div>
+        </div>
+    </div>
+    <div class="content-wrap">
+        <div class="c-body col">
+            <div class="sjs-height-1" id="cert-spread">
+            </div>
+        </div>
+    </div>
+</div>
+<div style="display: none">
+    <img src="/public/images/file_clip.png" id="file_clip" />
+    <img src="/public/images/file_clip_hover.png" id="file_clip_hover" />
+</div>
+<script>
+    const tid = parseInt('<%- ctx.tender.id %>');
+    const accountGroup = JSON.parse(unescape('<%- escape(JSON.stringify(accountGroup)) %>'));
+    const accountList = JSON.parse(unescape('<%- escape(JSON.stringify(accountList)) %>'));
+    const fujianOssPath = JSON.parse(unescape('<%- escape(JSON.stringify(fujianOssPath)) %>'));
+    let tenderCertList = JSON.parse(unescape('<%- escape(JSON.stringify(tenderCertList)) %>'));
+    const allCertList = JSON.parse(unescape('<%- escape(JSON.stringify(allCertList)) %>'));
+    const certTypeConst = JSON.parse(unescape('<%- escape(JSON.stringify(certTypeConst)) %>'));
+    const certSourceConst = JSON.parse(unescape('<%- escape(JSON.stringify(certSourceConst)) %>'));
+</script>

+ 68 - 0
app/view/tender/cert_modal.ejs

@@ -0,0 +1,68 @@
+<!--导入-->
+<div class="modal fade" id="import" 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 pt-1">
+                <div class="d-flex flex-row bg-graye">
+                    <div class="p-2 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="gr-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>
+                <table class="table table-bordered">
+                    <thead>
+                    <tr>
+                        <th>用户名</th>
+                        <th>角色/职位</th>
+                        <th>选择证书</th>
+                        <th>操作</th>
+                    </tr>
+                    </thead>
+                    <tbody id="select-certs-table">
+                    <% for (const tc of tenderCertList) { %>
+                    <tr class="text-center">
+                        <td><%- tc.account_info.name %></td>
+                        <td><%- tc.account_info.role %></td>
+                        <td>
+                            <select class="form-control form-control-sm">
+                                <% for (const c of tc.account_info.certs) { %>
+                                <option value="<%- c.id %>" <% if (tc.cert_id === c.id) { %>selected<% } %>><%- ctx.helper.showCol4ObjArray(certSourceConst, c.name, 'value', 'name') %></option>
+                                <% } %>
+                            </select>
+                        </td>
+                        <td class="text-danger">移除</td>
+                    </tr>
+                    <% } %>
+                    </tbody>
+                </table>
+            </div>
+            <div class="modal-footer d-flex justify-content-between">
+                <div class="ml-auto">
+                    <button type="button" class="btn btn-sm btn-secondary" data-dismiss="modal">取消</button>
+                    <button type="button" class="btn btn-sm btn-primary" id="add_cert_btn">添加</button>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>

+ 1 - 0
app/view/tender/detail.ejs

@@ -55,6 +55,7 @@
                         <a href="#bd-set-6" data-toggle="modal" data-target="#bd-set-6" class="dropdown-item" >章节设置</a>
                         <a href="#bd-set-7" data-toggle="modal" data-target="#bd-set-7" class="dropdown-item" >付款账号</a>
                         <a class="dropdown-item" href="javascript: void(0);" id="copyBtn">拷贝设置</a>
+                        <a class="dropdown-item" href="/tender/<%- tender.id %>/cert">从业人员</a>
                     </div>
                 </div>
                 <% if (ctx.session.sessionUser.is_admin) { %>

+ 57 - 1
config/web.js

@@ -138,6 +138,21 @@ const JsFiles = {
                 ],
                 mergeFile: 'tender_shenpi',
             },
+            cert: {
+                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/zh_calc.js',
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                    '/public/js/tender_cert.js',
+                ],
+                mergeFile: 'tender_cert',
+            },
             ctrlPrice: {
                 files: [
                     '/public/js/js-xlsx/xlsx.full.min.js',
@@ -1148,6 +1163,14 @@ const JsFiles = {
                 ],
                 mergeFile: 'file_detail',
             },
+            template: {
+                files: ['/public/js/ztree/jquery.ztree.core.js', '/public/js/ztree/jquery.ztree.exedit.js',],
+                mergeFiles: [
+                    '/public/js/shares/drag_tree.js',
+                    '/public/js/filing_template.js',
+                ],
+                mergeFile: 'filing_template',
+            },
         },
         budget: {
             list: {
@@ -1395,6 +1418,18 @@ const JsFiles = {
                 mergeFile: 'setting_user',
             },
         },
+        profile: {
+            cert: {
+                files: [
+                    '/public/js/datepicker/datepicker.min.js',
+                    '/public/js/datepicker/datepicker.zh.js',
+                ],
+                mergeFiles: [
+                    '/public/js/profile_cert.js',
+                ],
+                mergeFile: 'profile_cert',
+            },
+        },
         settle: {
             list: {
                 files: [],
@@ -1448,7 +1483,28 @@ const JsFiles = {
                     '/public/js/settle_ledger.js',
 
                 ],
-                mergeFile: 'settle',
+                mergeFile: 'settle_ledger',
+            },
+            gather: {
+                files: [
+                    '/public/js/spreadjs/sheets/v11/gc.spread.sheets.all.11.2.2.min.js',
+                    '/public/js/spreadjs/sheets/v11/interop/gc.spread.excelio.11.2.2.min.js',
+                    '/public/js/decimal.min.js',
+                ],
+                mergeFiles: [
+                    '/public/js/shares/cs_tools.js',
+                    '/public/js/component/menu.js',
+                    '/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/shares/tools_att.js',
+                    '/public/js/zh_calc.js',
+                    '/public/js/path_tree.js',
+                    '/public/js/settle_gather.js',
+
+                ],
+                mergeFile: 'settle_gather',
             }
         }
     },

+ 15 - 0
db_script/project_spread.js

@@ -43,12 +43,27 @@ const generateSpreadSetting = function(colSet, emptyBase, BaseSetCol, BaseSpread
     }
     return spreadSetting;
 };
+const updateSpreadWithSpec = function(spreadSetting, specSetting) {
+    if (!specSetting) return;
+    for (const s of specSetting) {
+        for (const c of spreadSetting.cols) {
+            if (s.condition.value.indexOf(c[s.condition.key]) >= 0) {
+                this._.assignIn(c, s.update);
+            }
+        }
+    }
+};
 
 const generateRelaSpread = function (colSetType, colSet) {
     const baseSetCol = colSetType.indexOf('stage') > 0 ? SpreadConst.BaseSetCol.Stage : SpreadConst.BaseSetCol.Ledger;
     const baseSpreadColSetting = colSetType.indexOf('stage') > 0 ? SpreadConst.BaseSpreadColSetting.Stage : SpreadConst.BaseSpreadColSetting.Ledger;
     const billsSpread = generateSpreadSetting(colSet, SpreadConst.EmptySpreadSetting[colSetType].bills, baseSetCol, baseSpreadColSetting.bills);
     const posSpread = generateSpreadSetting(colSet, SpreadConst.EmptySpreadSetting[colSetType].pos, baseSetCol, baseSpreadColSetting.pos, 'pos');
+    const spreadSpec = SpreadConst.SpreadSpec[colSetType];
+    if (spreadSpec) {
+        this.updateSpreadWithSpec(billsSpread, spreadSpec.bills);
+        this.updateSpreadWithSpec(posSpread, spreadSpec.pos);
+    }
     return [billsSpread, posSpread];
 };
 

+ 77 - 2
sql/update.sql

@@ -1,2 +1,77 @@
-ALTER TABLE `calculation`.`zh_tender_info`
-ADD COLUMN `s_type` varchar(20) NOT NULL DEFAULT '' COMMENT '标段类型(公路gl,房建fj,市政sz)' AFTER `dagl_info`;
+ALTER TABLE `zh_tender_info`
+ADD COLUMN `s_type` varchar(20) NOT NULL DEFAULT '' COMMENT '标段类型(公路gl,房建fj,市政sz)' AFTER `dagl_info`;
+
+ALTER TABLE `zh_sub_project`
+ADD COLUMN `filing_template_id` varchar(36) NOT NULL DEFAULT '' COMMENT '资料归集模板id' AFTER `std_name`,
+ADD COLUMN `filing_template_name` varchar(255) NOT NULL DEFAULT '' COMMENT '资料归集模板名称' AFTER `filing_template_id`;
+
+CREATE TABLE `zh_filing_template_list`  (
+  `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `project_id` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '项目id',
+  `ft_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '模板类型(0默认,1新增)',
+  `user_id` int(11) UNSIGNED NOT NULL COMMENT '添加用户',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL,
+  `memo` varchar(1000) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '备注',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
+
+CREATE TABLE `zh_filing_template`  (
+  `id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `temp_id` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'template_id(zh_filing_template_list)',
+  `tree_pid` varchar(36) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL COMMENT 'uuid',
+  `tree_order` int(11) UNSIGNED NOT NULL COMMENT '树结构-同层排序',
+  `tree_level` int(11) UNSIGNED NOT NULL COMMENT '树结构-层级',
+  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NOT NULL DEFAULT '' COMMENT '名称',
+  `add_user_id` int(11) NOT NULL DEFAULT 0 COMMENT '添加人',
+  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最后更新时间',
+  `is_fixed` tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '是否为固定项',
+  `filing_type` int(11) NOT NULL COMMENT '文件类型(跟父项相关,类型为最顶层父项名称)',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
+
+INSERT INTO `zh_filing_template_list` (`id`, `project_id`, `ft_type`, `user_id`, `name`, `memo`, `create_time`, `update_time`) VALUES ('698e87d8-e947-4049-98e4-15aae7c5c7fc', 0, 0, 0, '建设项目档案管理规范DA_T 28-2018', '', '2024-03-22 15:04:31', '2024-03-22 15:06:07');
+
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('0395ade6-5fe6-4b36-9b0a-c3696b044aaf', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 1, 1, '立项文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 1);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('5efafc28-fef5-46b3-be92-581a47cc454e', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 2, 1, '招标投标、合同协议文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 2);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('ef7ce0ba-f503-4364-b73f-ed9a1fe8c98d', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 3, 1, '勘察、设计文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 3);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('e2ac9fb4-08f7-4968-b5d1-c2487c2f3f61', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 4, 1, '征地、拆迁、移民文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 4);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('da3b9fb7-646f-49fa-8522-64d1d5ee53b1', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 5, 1, '项目管理文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 5);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('32be53bb-86db-4e4d-ae2d-03d3586e3965', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 6, 1, '施工文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 6);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('4c759d53-50f4-40fe-8f3b-b5b6ba8d3db3', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 7, 1, '信息系统开发文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 7);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('94505d98-f661-4be8-b381-2735476948b9', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 8, 1, '设备文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 8);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('5aca0493-089e-4064-83fd-3ac061cc1b04', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 9, 1, '监理文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 9);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('52f26ad6-bcd3-4bea-b533-283281fe6124', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 10, 1, '科研项目文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 10);
+INSERT INTO `zh_filing_template` (`id`, `temp_id`, `tree_pid`, `tree_order`, `tree_level`, `name`, `add_user_id`, `create_time`, `update_time`, `is_fixed`, `filing_type`) VALUES ('b95ecebd-fbd4-4110-abf4-7c055255d88f', '698e87d8-e947-4049-98e4-15aae7c5c7fc', '-1', 11, 1, '生产技术准备、试运行文件', 13, '2024-03-25 17:16:46', '2024-03-25 17:16:46', 1, 11);
+
+UPDATE zh_sub_project SET filing_template_id = "698e87d8-e947-4049-98e4-15aae7c5c7fc", filing_template_name = '建设项目档案管理规范DA_T 28-2018' WHERE management <> "";
+
+CREATE TABLE `zh_account_cert`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `pid` int NULL COMMENT '项目id',
+  `uid` int NULL COMMENT '用户id',
+  `type` tinyint(5) NULL DEFAULT NULL COMMENT '证书名称分类',
+  `name` tinyint(5) NULL DEFAULT NULL COMMENT '证书名称',
+  `code` varchar(255) NULL DEFAULT NULL COMMENT '证件编号',
+  `reg_unit` varchar(255) NULL DEFAULT NULL COMMENT '注册单位',
+  `job_title` varchar(255) NULL DEFAULT NULL COMMENT '技术职称',
+  `file_name` varchar(255) NULL COMMENT '文件名称',
+  `file_path` varchar(255) NULL COMMENT '文件下载地址',
+  `edu_json` json NULL COMMENT '继续教育json',
+  `create_time` datetime NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT = '用户证书表';
+
+CREATE TABLE `zh_tender_cert`  (
+  `id` int NOT NULL AUTO_INCREMENT,
+  `tid` int NULL COMMENT '标段id',
+  `uid` int NULL COMMENT '用户id',
+  `cert_id` int NULL COMMENT '个人证书id',
+  `department` varchar(255) NULL DEFAULT NULL COMMENT '所在部门',
+  `job_time` varchar(255) NULL DEFAULT NULL COMMENT '在岗时间',
+  `remark` varchar(1000) NULL DEFAULT NULL COMMENT '备注',
+  `create_time` datetime NULL COMMENT '入库时间',
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci COMMENT = '标段从业人员表';