|
|
@@ -128,6 +128,189 @@ module.exports = app => {
|
|
|
throw error;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ async getFinishedProjects() {
|
|
|
+ return this.getStatusProjects('已建');
|
|
|
+ }
|
|
|
+
|
|
|
+ async getStatusProjects(status, options = {}) {
|
|
|
+ const ctx = this.ctx;
|
|
|
+ const user = ctx.session.sessionUser;
|
|
|
+ const pid = ctx.session.sessionProject.id;
|
|
|
+ const nodes = await ctx.service.subProject.getSubProject(pid, user.accountId, user.is_admin);
|
|
|
+ const infos = await ctx.service.subProjInfo.getAllDataByCondition({ where: { project_id: pid, proj_status: status || ['拟建', '在建', '已建'] } });
|
|
|
+ const account = await ctx.service.projectAccount.getDataById(user.accountId);
|
|
|
+ const permission = account ? ctx.service.projectAccount.parseProjectScreenPermission(account.permission) : null;
|
|
|
+ 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 = [];
|
|
|
+ const selectedNodes = nodes.filter(n => !n.is_folder && (!options.id || n.id === options.id) && infos.some(info => info.id === n.id)).sort((a, b) => a.tree_order - b.tree_order);
|
|
|
+ const allTenders = [];
|
|
|
+ if (selectedNodes.length && (user.is_admin === 1 || (permission && permission.tender && permission.tender.indexOf('2') !== -1))) {
|
|
|
+ allTenders.push(...await ctx.service.tender.getAllDataByCondition({ where: { spid: selectedNodes.map(n => n.id) } }));
|
|
|
+ allTenders.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN'));
|
|
|
+ } else {
|
|
|
+ // Batch projects with identical permission-related page settings.
|
|
|
+ const permissionGroups = new Map();
|
|
|
+ for (const node of selectedNodes) {
|
|
|
+ const sub = Object.assign({}, node, { page_show: ctx.service.subProject.getPageShow(node.page_show) });
|
|
|
+ const key = JSON.stringify(['openChangeProject', 'openChangeApply', 'openChangePlan'].map(flag => !!sub.page_show[flag]));
|
|
|
+ if (!permissionGroups.has(key)) permissionGroups.set(key, { id: [], page_show: sub.page_show });
|
|
|
+ permissionGroups.get(key).id.push(node.id);
|
|
|
+ }
|
|
|
+ for (const sub of permissionGroups.values()) {
|
|
|
+ allTenders.push(...await ctx.service.tender.getList('', permission, user.is_admin, '', sub));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await ctx.service.tenderCache.loadTenderCaches(allTenders, user.accountId);
|
|
|
+ const allChanges = !options.summary && status === '在建' && allTenders.length ? await ctx.service.change.getAllCheckedChanges(allTenders.map(t => t.id)) : [];
|
|
|
+ const allProgress = !options.summary && selectedNodes.length && status !== '已建' ? 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 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 = progressRows.filter(row => row.tree_level > 1 && row.tree_is_leaf === 1).map(row => ({
|
|
|
+ id: row.id, name: row.name,
|
|
|
+ path: String(row.tree_full_path).split('-').map(id => progressRows.find(p => String(p.tree_id) === id)).filter(Boolean).map(p => p.name).join(' / ') || row.name,
|
|
|
+ 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 : '未分组',
|
|
|
+ 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}。` : '暂无阶段动态',
|
|
|
+ amounts: ['lx', 'cb', 'sgt', 'pf'].map(prefix => toWan(info[prefix + '_tp'], info[prefix + '_tp_unit']))
|
|
|
+ .concat([tenders.length ? toWan(contract, '元') : null, hasMeasured ? toWan(measured, '元') : null, finalAmount]) });
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ async getOverview(planned, building, finished) {
|
|
|
+ const ctx = this.ctx;
|
|
|
+ const calc = ctx.helper;
|
|
|
+ const lists = [planned, building, finished];
|
|
|
+ const projects = [].concat(...lists);
|
|
|
+ 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 => 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 = String(row.tree_full_path).split('-').map(id => tree.find(n => String(n.tree_id) === id)).filter(Boolean);
|
|
|
+ if (!path.length || path[path.length - 1].id !== row.id) path.push(row);
|
|
|
+ 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}」` };
|
|
|
+ })
|
|
|
+ };
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
return ProjectScreen;
|