| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360 |
- 'use strict';
- const projectScreenConst = require('../const/project_screen');
- const Ledger = require('../lib/ledger');
- module.exports = app => {
- class ProjectScreen extends app.BaseService {
- constructor(ctx) {
- super(ctx);
- this.tableName = 'project_screen';
- }
- async getList(pid, transaction = null) {
- const db = transaction || this.db;
- const result = await db.select(this.tableName, {
- where: { pid },
- orders: [['id', 'asc']],
- });
- return result.map(item => {
- const template = projectScreenConst.getTemplate(item.template_code);
- return Object.assign(item, {
- template: template || {
- code: item.template_code,
- name: item.name,
- description: '',
- previewClass: '',
- },
- });
- });
- }
- async hasScreen(pid) {
- return (await this.count({ pid })) > 0;
- }
- async getViewScreen(pid, id = '') {
- const screens = await this.getList(pid);
- if (screens.length === 0) return null;
- if (id) {
- const selected = screens.find(item => item.id === parseInt(id));
- if (selected) return selected;
- }
- return screens.find(item => item.is_default === 1) || screens[0];
- }
- async addScreens(pid, codes) {
- const selectedCodes = [];
- for (const code of codes || []) {
- if (!projectScreenConst.getTemplate(code)) throw '大屏模板不存在';
- if (selectedCodes.indexOf(code) < 0) selectedCodes.push(code);
- }
- if (selectedCodes.length === 0) throw '请至少选择一个大屏';
- const transaction = await this.db.beginTransaction();
- try {
- const activeList = await transaction.select(this.tableName, {
- where: { pid },
- orders: [['id', 'asc']],
- });
- const needDefault = activeList.length === 0;
- const existingCodes = activeList.map(item => item.template_code);
- const insertCodes = selectedCodes.filter(code => existingCodes.indexOf(code) < 0);
- const insertData = insertCodes.map((code, index) => {
- const template = projectScreenConst.getTemplate(code);
- return {
- pid,
- template_code: code,
- name: template.name,
- is_default: needDefault && index === 0 ? 1 : 0,
- };
- });
- if (insertData.length > 0) await transaction.insert(this.tableName, insertData);
- await transaction.commit();
- return await this.getList(pid);
- } catch (error) {
- await transaction.rollback();
- throw error;
- }
- }
- async setDefault(pid, id) {
- const transaction = await this.db.beginTransaction();
- try {
- const target = await transaction.get(this.tableName, {
- id, pid,
- });
- if (!target) throw '大屏不存在';
- await transaction.update(this.tableName, { is_default: 0 }, {
- where: { pid },
- });
- await transaction.update(this.tableName, {
- is_default: 1,
- }, {
- where: { id, pid },
- });
- await transaction.commit();
- return await this.getList(pid);
- } catch (error) {
- await transaction.rollback();
- throw error;
- }
- }
- async deleteScreen(pid, id) {
- const transaction = await this.db.beginTransaction();
- try {
- const target = await transaction.get(this.tableName, { id, pid });
- if (!target) throw '大屏不存在或已删除';
- await transaction.delete(this.tableName, { id, pid });
- if (target.is_default === 1) {
- const remaining = await transaction.select(this.tableName, {
- where: { pid },
- orders: [['id', 'asc']],
- });
- if (remaining.length > 0) {
- await transaction.update(this.tableName, { is_default: 0 }, {
- where: { pid },
- });
- await transaction.update(this.tableName, { is_default: 1 }, {
- where: { id: remaining[0].id, pid },
- });
- }
- }
- await transaction.commit();
- return await this.getList(pid);
- } catch (error) {
- await transaction.rollback();
- throw error;
- }
- }
- async getFinishedProjects() {
- return this.getStatusProjects('已建');
- }
- async getStatusProjects(status, options = {}) {
- const ctx = this.ctx;
- const pid = ctx.session.sessionProject.id;
- const nodes = await ctx.service.projectScreenTender.getDisplayProjects(pid);
- const infos = await ctx.service.subProjInfo.getAllDataByCondition({ where: { project_id: pid, proj_status: status || ['拟建', '在建', '已建'] } });
- const calc = ctx.helper;
- // Preserve decimal precision during aggregation and unit conversion.
- // 元 fields have eight decimal places; conversion to 万元 needs twelve.
- const toWan = (value, unit) => {
- if (value === null || value === undefined || value === '') return null;
- if (unit === '万元') return calc.add(0, value);
- if (unit === '亿元') return calc.mul(value, 10000, 12);
- return calc.div(value, 10000, 12);
- };
- const result = [];
- // Follow the project list's tree order before filtering by status.
- const projectTree = new Ledger.baseTree(ctx, { id: 'id', pid: 'tree_pid', level: 'tree_level', order: 'tree_order', rootId: '-1' });
- projectTree.loadDatas(nodes);
- const selectedNodes = projectTree.nodes.filter(n => !n.is_folder && (!options.id || n.id === options.id) && infos.some(info => info.id === n.id));
- // Match the live budget totals displayed on the project info page.
- const budgetIds = Array.from(new Set(selectedNodes.map(node => node.budget_id).filter(Boolean)));
- const budgetTotals = (await Promise.all([ctx.service.budgetGu, ctx.service.budgetGai, ctx.service.budgetYu]
- .map(service => service.getSumTpByBudgetIds(budgetIds))))
- .map(rows => new Map(rows.map(row => [String(row.bid), row.total_price])));
- const allTenders = await ctx.service.projectScreenTender.getDisplayTenders(pid, selectedNodes.map(node => node.id));
- await ctx.service.tenderCache.loadTenderCaches(allTenders, '');
- const allChanges = !options.summary && status === '在建' && allTenders.length ? await ctx.service.change.getAllCheckedChanges(allTenders.map(t => t.id)) : [];
- const allProgress = !options.summary && selectedNodes.length ? await ctx.service.subProjProgress.getAllDataByCondition({ where: { spid: selectedNodes.map(n => n.id) }, orders: [['update_time', 'desc'], ['id', 'asc']] }) : [];
- for (const node of selectedNodes) {
- const info = infos.find(i => i.id === node.id);
- if (!info) continue;
- const tenders = allTenders.filter(t => t.spid === node.id);
- let contract = 0;
- let measured = 0;
- let hasMeasured = false;
- const tenderInvestments = [];
- const changeRows = [];
- for (const tender of tenders) {
- contract = calc.add(contract, tender.contract_price);
- const stage = tender.stage_tp || {};
- let tenderMeasured = 0;
- let tenderHasMeasured = false;
- for (const key of ['pre_contract_tp', 'contract_tp', 'contract_pc_tp', 'pre_qc_tp', 'qc_tp', 'qc_pc_tp']) {
- if (stage[key] !== null && stage[key] !== undefined && stage[key] !== '') {
- tenderMeasured = calc.add(tenderMeasured, stage[key]);
- tenderHasMeasured = true;
- }
- }
- measured = calc.add(measured, tenderMeasured);
- hasMeasured = hasMeasured || tenderHasMeasured;
- tenderInvestments.push({ id: tender.id, name: tender.name, measured: tenderHasMeasured ? toWan(tenderMeasured, '元') : null });
- if (!options.summary && status === '在建') {
- const changes = allChanges.filter(change => change.tid === tender.id);
- const totals = [1, 2, 3].map(quality => changes.filter(c => Number(c.quality) === quality).reduce((sum, c) => calc.add(sum, c.total_price), 0));
- const total = changes.reduce((sum, c) => calc.add(sum, c.total_price), 0);
- const ledger = tender.ledger_tp && tender.ledger_tp.total_price;
- changeRows.push({ id: tender.id, name: tender.name, contract: toWan(ledger, '元'),
- changes: totals.map(value => toWan(value, '元')), total: toWan(total, '元'),
- ratio: Number(ledger) ? calc.div(calc.mul(total, 100, 12), ledger, 2) : null });
- }
- }
- let finalAmount = null;
- if (!options.summary && status === '已建' && node.budget_id) {
- const budget = await ctx.service.budget.getDataById(node.budget_id);
- if (budget && budget.final_id) {
- const final = await ctx.service.budgetFinal.getAllDataByCondition({ where: { final_id: budget.final_id } });
- const top = final.filter(row => Number(row.tree_pid) === -1);
- if (top.length) finalAmount = toWan(top.reduce((sum, row) => calc.add(sum, row.final_tp), 0), '元');
- }
- }
- const parent = nodes.find(n => n.id === node.tree_pid);
- const progressRows = allProgress.filter(row => row.spid === node.id);
- const progressPaths = this.getProgressPaths(progressRows);
- const latest = progressRows[0];
- const keys = ['edit', 'submit', 'reply'];
- const latestEvent = latest && keys.map((key, index) => ({ date: latest[key + '_date'], department: latest[key + '_department'], step: ['编制', '报审', '批复'][index] }))
- .filter(event => event.date).sort((a, b) => String(b.date).localeCompare(String(a.date)))[0];
- const stages = this.sortProgressRows(progressRows, progressPaths).filter(row => row.tree_level > 1 && row.tree_is_leaf === 1).map(row => ({
- id: row.id, name: row.name, code: row.code || '',
- path: this.formatProgressPath(progressPaths.get(row.id)),
- shortPath: this.formatProgressPath(progressPaths.get(row.id), true),
- states: keys.map(key => row[key + '_progress'] === '已完成' ? 0 : row[key + '_progress'] === '进行中' ? 1 : 2),
- dates: keys.map(key => row[key + '_date'] || ''), departments: keys.map(key => row[key + '_department'] || '')
- }));
- result.push({ id: node.id, name: node.name, status: info.proj_status, group: parent ? parent.name : '',
- folders: projectTree.getAllParents(node).filter(folder => folder.is_folder).map(folder => ({ id: folder.id, name: folder.name })),
- intro: info.proj_intro || '', date: info.jg_date || '', department: info.jg_department || '',
- measured: hasMeasured ? toWan(measured, '元') : null,
- tenders: tenderInvestments,
- changeRows, stages,
- progressText: latestEvent ? `${latestEvent.date},${latestEvent.department || ''}已完成「${latest.name}」${latestEvent.step}。` : '暂无阶段动态',
- overviewAmounts: ['lx', 'cb', 'sgt'].map(key => toWan(info[key + '_tp'], info[key + '_tp_unit'])),
- amounts: budgetTotals.map(totals => toWan(totals.get(String(node.budget_id)), '元'))
- .concat([toWan(info.pf_tp, info.pf_tp_unit), tenders.length ? toWan(contract, '元') : null, hasMeasured ? toWan(measured, '元') : null, finalAmount]) });
- }
- return result;
- }
- async getOverview(projects) {
- const ctx = this.ctx;
- const calc = ctx.helper;
- const [planned, building, finished] = ['拟建', '在建', '已建'].map(status => projects.filter(project => project.status === status));
- const lists = [planned, building, finished];
- const present = value => value !== null && value !== undefined && value !== '';
- const rank = (list, amount) => list.map(p => ({ id: p.id, name: p.name, value: amount(p) }))
- .filter(p => present(p.value)).sort((a, b) => b.value - a.value).slice(0, 10);
- const rows = projects.length ? await ctx.service.subProjProgress.getAllDataByCondition({
- where: { spid: projects.map(p => p.id) }, orders: [['update_time', 'desc'], ['id', 'asc']]
- }) : [];
- const dateText = value => value instanceof Date ? [value.getFullYear(), String(value.getMonth() + 1).padStart(2, '0'), String(value.getDate()).padStart(2, '0')].join('-') : String(value || '').slice(0, 10);
- const trees = new Map(projects.map(p => [p.id, rows.filter(row => row.spid === p.id)]));
- const leafColumns = tree => {
- const paths = this.getProgressPaths(tree);
- return tree.filter(row => Number(row.tree_level) > 1 && Number(row.tree_is_leaf) === 1)
- .sort((a, b) => a.tree_order - b.tree_order).map(row => {
- const path = paths.get(row.id);
- return { key: JSON.stringify([path[0].name, row.name]), root: path[0].name,
- rootOrder: Number(path[0].tree_order) || 0,
- orderPath: path.slice(1).map(n => Number(n.tree_order) || 0),
- name: row.name, date: dateText(row.reply_date) };
- }).sort((a, b) => {
- if (a.root !== b.root) return a.rootOrder - b.rootOrder || a.root.localeCompare(b.root, 'zh-CN');
- for (let i = 0; i < Math.min(a.orderPath.length, b.orderPath.length); i++) {
- if (a.orderPath[i] !== b.orderPath[i]) return a.orderPath[i] - b.orderPath[i];
- }
- return a.orderPath.length - b.orderPath.length;
- });
- };
- const columnsByProject = new Map(projects.map(p => [p.id, leafColumns(trees.get(p.id))]));
- const approvedKeys = new Set([].concat(...Array.from(columnsByProject.values())).filter(column => column.date).map(column => column.key));
- // Merge same leaf names under the same root, regardless of intermediate paths.
- const rootGroups = new Map();
- for (const project of projects) {
- for (const column of columnsByProject.get(project.id)) {
- // Only expose a column when at least one project has an approval date.
- if (!approvedKeys.has(column.key)) continue;
- if (!rootGroups.has(column.root)) rootGroups.set(column.root, new Map());
- const group = rootGroups.get(column.root);
- if (!group.has(column.key)) group.set(column.key, column);
- }
- }
- // Sort roots only; keep leaf insertion order so different projects do not interleave.
- const columns = [].concat(...Array.from(rootGroups.values()).sort((a, b) => {
- const firstA = a.values().next().value;
- const firstB = b.values().next().value;
- return firstA.rootOrder - firstB.rootOrder || firstA.root.localeCompare(firstB.root, 'zh-CN');
- }).map(group => Array.from(group.values())));
- return {
- counts: lists.map(list => list.length),
- investment: lists.map(list => [0, 1, 2, 3, 4].map(index => {
- const values = list.map(p => p.amounts[index]).filter(present);
- return values.length ? values.reduce((sum, value) => calc.add(sum, value), 0) : null;
- })),
- planned: rank(planned, p => p.amounts[1]), finished: rank(finished, p => p.measured),
- building: building.map(p => ({ id: p.id, name: p.name, contract: p.amounts[4], measured: p.measured,
- rate: present(p.measured) && Number(p.amounts[4]) !== 0 && present(p.amounts[4]) ? calc.div(calc.mul(p.measured, 100, 12), p.amounts[4], 2) : null
- })).sort((a, b) => a.rate === null ? (b.rate === null ? 0 : 1) : b.rate === null ? -1 : b.rate - a.rate),
- columns,
- preview: projects.map(p => ({ id: p.id, name: p.name, group: p.group,
- dates: columns.map(column => {
- const dates = columnsByProject.get(p.id).filter(c => c.key === column.key && c.date).map(c => c.date);
- return dates.sort().pop() || '';
- }) })),
- events: rows.slice(0, 20).map(row => {
- const project = projects.find(p => p.id === row.spid);
- const latest = ['edit', 'submit', 'reply'].map((key, index) => ({ date: dateText(row[key + '_date']),
- department: row[key + '_department'] || '', step: ['编制', '报审', '批复'][index] }))
- .filter(event => event.date).sort((a, b) => b.date.localeCompare(a.date))[0];
- return { name: project.name, date: latest ? latest.date : dateText(row.update_time),
- text: latest ? `${latest.department}已完成「${row.name}」${latest.step}` : `更新「${row.name}」` };
- })
- };
- }
- getProgressPaths(rows) {
- const nodes = new Map(rows.map(row => [String(row.tree_id), row]));
- const paths = new Map();
- for (const row of rows) {
- const path = [];
- const visited = new Set();
- let node = row;
- while (node && !visited.has(String(node.tree_id))) {
- path.unshift(node);
- visited.add(String(node.tree_id));
- node = nodes.get(String(node.tree_pid));
- }
- paths.set(row.id, path);
- }
- return paths;
- }
- sortProgressRows(rows, paths) {
- return rows.slice().sort((a, b) => {
- const pathA = paths.get(a.id);
- const pathB = paths.get(b.id);
- for (let i = 0; i < Math.min(pathA.length, pathB.length); i++) {
- if (pathA[i].id === pathB[i].id) continue;
- return pathA[i].tree_order - pathB[i].tree_order || String(pathA[i].id).localeCompare(String(pathB[i].id));
- }
- return pathA.length - pathB.length;
- });
- }
- formatProgressPath(path, abbreviated = false) {
- const names = path.map(node => node.name);
- if (abbreviated && names.length > 2) return [names[0], '...', names[names.length - 1]].join(' / ');
- return names.join(' / ');
- }
- async saveModuleStatus(pid, enabled) {
- const ctx = this.ctx;
- const service = ctx.service.projectScreenTender;
- const pageShow = await service.withTransaction(async transaction => {
- const settings = service.parseSettings(await service.getProject(pid, transaction, true));
- settings.pageShow.openProjectScreen = enabled;
- await service.saveSettings(pid, settings.pageShow, transaction);
- return settings.pageShow;
- });
- ctx.session.sessionProject.page_show = await ctx.service.projectAccount.getPageShow(JSON.stringify(pageShow));
- }
- }
- return ProjectScreen;
- };
|