import Base from './base' import * as moment from 'moment' import * as _ from 'lodash' import i18n from '../../../service/i18n' import { App, Context } from '../../../types' import * as config from 'config' import { IUProjectModel } from '../../../model/project' import { IUCustomfieldModel } from '../../../model/customfield' import { IUTasklistModel } from '../../../model/tasklist' import { IUTaskModel } from '../../../model/task' import { IUWorktimeModel } from '../../../model/worktime' import * as Excel from 'exceljs' import { ilog } from 'ilog' import { MinuteToHour } from '../../../service/utils' import { ForeignAid } from './foregin' import FileBase from './filebase' import { IUUserModel } from '../../../model/user' import { IUScenariofieldconfigModel } from '../../../model/scenariofieldconfig' declare namespace FunctionOpts { interface Query { tasklist?: IUTasklistModel customfieldsMap: Map hasUniqueId: boolean worksheet?: Excel.Worksheet scenariofieldsTypeArr: ScenariofieldsTypeArr[] scenariofieldconfigMap: Map defaulSfc: IUScenariofieldconfigModel foreignAid: ForeignAid project: IUProjectModel } interface GroupItem { task: IUTaskModel tasklistName: string uniqueIdPrefix: string customfieldsMap: Map hasUniqueId: boolean lang: string workTimeLogs: IUWorktimeModel[] taskType: string objectlinkMap: Map scenariofieldsTypeArr: ScenariofieldsTypeArr[] } interface ScenariofieldsTypeArr { fieldType: string _customfieldId: string } } type JudgeDelay = { isDelay: boolean delayDays: number } export default class TaskExportFactory extends Base { private project: IUProjectModel private _tasklistId: string; _projectId: string; _lang: string private static FILE_TYPE = { CSV: 'csv', XLSX: 'xlsx' } set tasklistId (tasklistId: string) { this._tasklistId = tasklistId } get tasklistId () { return this._tasklistId } set projectId (projectId: string) { this._projectId = projectId } get projectId () { return this._projectId } set userId (userId: string) { this._userId = userId } get userId () { return this._userId } set lang (lang: string) { this._lang = lang } get lang () { return this._lang } get projectModel () { return this.project } async build () { let taskCount: number switch (this._fileType) { case TaskExportFactory.FILE_TYPE.XLSX: if (!this.projectId) this.ctx.throw(400, '_projectId required') const conds = { _projectId: this.projectId, isDeleted: false, isArchived: null } taskCount = await this.app.db.task.count(conds) const xlsx = new XLSX(this.app, this.ctx) xlsx.taskCount = taskCount await xlsx.setContent(this.projectId, this.userId, this.lang) .execute() // this.project = xlsx.projectModel // return workbook case TaskExportFactory.FILE_TYPE.CSV: if (!this.tasklistId) this.ctx.throw(400, '_tasklistId required') const tasklist: IUTasklistModel = await this.app.db.tasklist.findOne({ _id: this.tasklistId, isDeleted: false }).lean() taskCount = await this.app.db.task.count({ _projectId: tasklist._projectId, _tasklistId: tasklist._id, isDeleted: false, isArchived: null }) if (taskCount > config.EXPORT_LIMIT.TASK_WITH_TASKLIST_METHOD) { this.ctx.throw(400, `ExceedLimit: ${config.EXPORT_LIMIT.TASK_WITH_TASKLIST_METHOD}`) } const project = await this.app.db.project.findById(tasklist._projectId).lean() const csv = new CSV(this.app, this.ctx) csv.tasklist = tasklist csv.project = project break default: break } } } // 导出为 xlsx class XLSX { protected app: App protected ctx: Context private _taskCount: number private projectId: string private project: IUProjectModel private userId: string; lang: string constructor (app: App, ctx: Context) { this.app = app this.ctx = ctx } set taskCount (taskCount: number) { this._taskCount = taskCount } get taskCount () { return this._taskCount } get projectModel () { return this.project } setContent (_projectId: string, userId: string, lang: string) { this.projectId = _projectId this.userId = userId this.lang = lang return this } async execute () { if (this.taskCount > config.EXPORT_LIMIT.TASK_WITH_PROJECT_METHOD) { this.ctx.throw(`ExceedLimit: ${config.EXPORT_LIMIT.TASK_WITH_PROJECT_METHOD}`) } const workbook: Excel.Workbook = new Excel.Workbook() const project = await this.app.db.project.findById(this.projectId).lean() this.project = project const foreignAid = new ForeignAid(this.app, this.ctx) const [customfieldsMap, scenariofieldsTypeArr, applications, scenariofieldconfigMap, defaulSfc] = await Promise.all([ foreignAid.getCustomfieldMap(project), foreignAid.getScenariofieldsArray(project._id), foreignAid.getenableApplications(project), foreignAid.getScenariofieldconfigMap(project._id), foreignAid.getDefaultConfigByProjectIdObjectType( project._id, 'task' ) ]) const hasUniqueId = foreignAid.hasUniqueId(applications) const conds = { isDeleted: false, _projectId: this.projectId, isArchived: null } const tasklists = await this.app.db.tasklist.find(conds).select('title').exec() const excelheaders = this.buildHeaders( hasUniqueId, customfieldsMap, scenariofieldsTypeArr ) let _conds, worksheet: Excel.Worksheet const options:FunctionOpts.Query = { customfieldsMap, hasUniqueId, scenariofieldsTypeArr, foreignAid, project, scenariofieldconfigMap, defaulSfc } for (const tasklist of tasklists) { worksheet = workbook.addWorksheet(tasklist.title) worksheet.columns = excelheaders _conds = { _tasklistId: tasklist._id, isArchived: null, isDeleted: false, 'ancestorIds.0': { $exists: false }, $or: [ { visible: 'involves', involveMembers: this.userId }, { visible: 'members' } ] } options.tasklist = tasklist options.worksheet = worksheet await this.query( _conds, options ) } workbook.xlsx.createInputStream() const fileName: string = `${this.projectModel ? this.projectModel.name : null}.xlsx` // this.ctx.response.set( // 'Content-Type', // 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' // ) this.ctx.response.set('Set-Cookie', 'fileDownload=true; path=/') this.ctx.status = 200 this.ctx.response.attachment(fileName) await workbook.xlsx.write(this.ctx.res) this.ctx.res.end() } private async query(cond: any, opts: FunctionOpts.Query) { const { tasklist, hasUniqueId, worksheet, foreignAid, project } = opts const { scenariofieldconfigMap, defaulSfc, scenariofieldsTypeArr, customfieldsMap } = opts let taskType: string let groupItemParams: FunctionOpts.GroupItem let objectLinks const __ = i18n.__ const populateOpts = '_creatorId _executorId _stageId tagIds ancestorIds' const tasks = await this.app.db.task .find(cond) .sort('created') .populate(populateOpts) .lean() .exec() const _taskIds = tasks.map((task) => { return task._id }) if (!defaulSfc) { ilog.info({ class: 'export-excel-defaulSfc-error', _projectId: project._id }) this.ctx.throw(404, 'NotFound default scenariofieldconfig') } const worklogMap = await foreignAid.getWorkLogMap(_taskIds) for (const task of tasks) { const workTimeLogs = [] const _conds = _.clone(cond) const worklog = worklogMap.get(`${task._id}`) if (worklog) { const month = new Date(worklog.recordDate).getMonth() + 1 const day = new Date(worklog.recordDate).getDate() const value = MinuteToHour(worklog.value) const workLog = __('csv.workLog', { month: String(month), day: String(day), exector: worklog._executorId.name, value: String(value) }) workTimeLogs.push(workLog) } // 需要优化 objectLinks = await foreignAid.getLinkCountByParentId(task._id) const objectlinkMap = new Map() objectLinks.forEach((objectLink) => { objectlinkMap.set(`${objectLink._id}`, objectLink.count) }) taskType = scenariofieldconfigMap.get(`${task._scenariofieldconfigId}`) || defaulSfc.name _conds['ancestorIds.0'] = task._id groupItemParams = { task, tasklistName: tasklist.title, uniqueIdPrefix: project.uniqueIdPrefix, customfieldsMap, hasUniqueId, lang: this.lang, workTimeLogs, taskType, objectlinkMap, scenariofieldsTypeArr } const row = this.groupItem(groupItemParams) worksheet.addRow(row) await this.query( _conds, { tasklist, customfieldsMap, hasUniqueId, worksheet, scenariofieldsTypeArr, scenariofieldconfigMap, defaulSfc, project, foreignAid } ) } } private groupItem(params: FunctionOpts.GroupItem) { const { task, tasklistName, uniqueIdPrefix, customfieldsMap, hasUniqueId, workTimeLogs, taskType, objectlinkMap, scenariofieldsTypeArr } = params const __ = i18n.__ const tags = task.tagIds.map((tag: { _id: string name: string }) => tag.name).join(',') task.note = task.note ? task.note.replace(/[\n]/g, '\r\n') : null const { delayDays, isDelay } = this.judgeDelay(task.dueDate, task.isDone) const itemHeader = [ task.content, task.ancestorIds[0] ? `${task.ancestorIds[0].content}` : '', taskType, task._executorId ? `${task._executorId.name}(${task._executorId.email})` : '', task.startDate ? moment(task.startDate).format('YYYY-MM-DD HH:mm:ss') : '', task.dueDate ? moment(task.dueDate).format('YYYY-MM-DD HH:mm:ss') : '' ] const itemBody = [] scenariofieldsTypeArr.forEach((scenariofieldsType) => { switch (scenariofieldsType.fieldType) { case 'note': itemBody.push(task.note) break case 'priority': itemBody.push( `${__(`csv.priority.${task.priority}`)}(P${2 - task.priority})` ) break case 'worktimes': if(!task.workTime) { itemBody.push('') break } itemBody.push(MinuteToHour(task.workTime.totalTime)) itemBody.push( workTimeLogs ? MinuteToHour(task.workTime.usedTime) + `${workTimeLogs}` : MinuteToHour(task.workTime.usedTime) ) break case 'tag': itemBody.push(tags) break case 'taskProgress': itemBody.push(task.progress) break case 'storyPoint': itemBody.push(task.storyPoint) break case 'rating': itemBody.push(task.rating) break default: if (_.includes(scenariofieldsType.fieldType, 'customfield')) { const customfield = customfieldsMap.get( `${scenariofieldsType._customfieldId}` ) if (!customfield) break const taskCustomfield = task.customfields.find( (taskCustomfield) => `${taskCustomfield._customfieldId}` === `${customfield._id}` ) if (!taskCustomfield) { return itemBody.push('') } if (taskCustomfield.type === 'date') { return itemBody.push( moment(taskCustomfield.values[0]).format('YYYY-MM-DD HH:mm:ss') ) } if (taskCustomfield.type === 'lookup') { const vals = [] taskCustomfield.value.forEach((val) => { vals.push(val.title) }) return itemBody.push(vals.join(',')) } if ( !['dropDown', 'multipleChoice'].includes(taskCustomfield.type) ) { return itemBody.push(taskCustomfield.values[0]) } const choiceIds = customfield.choices.map(({ _id }) => `${_id}`) const taskChoices = [] for (const choiceId of taskCustomfield.values) { if (!choiceIds.includes(`${choiceId}`)) { continue } taskChoices.push( customfield.choices.find( ({ _id }) => `${_id}` === `${choiceId}` ).value ) } return itemBody.push(taskChoices.join(',')) } break } }) const itemFoot = [ __('csv.objectlinkLog', { task: String(objectlinkMap.get('task') || 0), work: String(objectlinkMap.get('work') || 0), event: String(objectlinkMap.get('event') || 0), post: String(objectlinkMap.get('post') || 0) }), task._creatorId ? `${task._creatorId.name}(${task._creatorId.email})` : '', task.created ? moment(task.created).format('YYYY-MM-DD HH:mm:ss') : '', task.isDone ? 'Y' : 'N', task.accomplished ? moment(task.accomplished).format('YYYY-MM-DD HH:mm:ss') : '', tasklistName, task._stageId ? task._stageId.name : '', delayDays, isDelay ? 'Y' : 'N' ] const item = _.concat(itemHeader, itemBody, itemFoot) if (hasUniqueId) { item.unshift(`${uniqueIdPrefix}-${task.uniqueId}`) } return item } private buildHeaders(hasUniqueId: boolean, customfieldsMap: any, scenariofieldsTypeArr: any) { const __ = i18n.__ const _fieldsHeader = [ this._formatCell(__('csv.content'), 15, 'left'), this._formatCell(__('csv.ancestor'), 20, 'left'), this._formatCell(__('csv.taskType'), 15), this._formatCell(__('csv.executor'), 20, 'left'), this._formatCell(__('csv.startDate'), 20), this._formatCell(__('csv.dueDate'), 20) ] if (hasUniqueId) { _fieldsHeader.unshift(this._formatCell(__('csv.uniqueId'), 10)) } const _fieldsBody = [] scenariofieldsTypeArr.forEach((scenariofield) => { switch (scenariofield.fieldType) { case 'note': _fieldsBody.push(this._formatCell(__('csv.note'), 20, 'left')) break case 'priority': _fieldsBody.push(this._formatCell(__('csv.priority'), 10)) break case 'worktimes': _fieldsBody.push(this._formatCell(__('csv.totaltime'), 15)) _fieldsBody.push(this._formatCell(__('csv.usedtime'), 20, 'left')) break case 'tag': _fieldsBody.push(this._formatCell(__('csv.tag'), 10)) break case 'taskProgress': _fieldsBody.push(this._formatCell(__('csv.taskProgress'), 10)) break case 'storyPoint': _fieldsBody.push(this._formatCell(__('csv.storyPoint'), 15)) break case 'rating': _fieldsBody.push(this._formatCell(__('csv.rating'), 10)) break default: if (_.includes(scenariofield.fieldType, 'customfield')) { const customfield = customfieldsMap.get( `${scenariofield._customfieldId}` ) if (!customfield) break _fieldsBody.push(this._formatCell(customfield.name, 20, 'left')) } break } }) const _fieldsFoot = [ this._formatCell(__('csv.objectlink'), 25), this._formatCell(__('csv.creator'), 20, 'left'), this._formatCell(__('csv.created'), 20), this._formatCell(__('csv.isDone'), 10), this._formatCell(__('csv.accomplished'), 20), this._formatCell(__('csv.tasklist'), 10), this._formatCell(__('csv.stage'), 10), this._formatCell(__('csv.delayDays'), 10), this._formatCell(__('csv.delayed'), 10) ] const header = _.concat(_fieldsHeader, _fieldsBody, _fieldsFoot) return header } /** * * @private * @param {string} dueDate * @param {string} accomplished * @returns {judgeDelay} * @memberof XLSX */ private judgeDelay (dueDate: string, isDone: boolean): JudgeDelay { const judgeDelay: JudgeDelay = { isDelay: false, delayDays: 0 } if (isDone || !dueDate) { return judgeDelay } const now: moment.Moment = moment() if (now.isAfter(dueDate)) { judgeDelay.isDelay = true judgeDelay.delayDays = now.diff(dueDate, 'd') } return judgeDelay } /** * @description 格式化列的内容及属性 */ private _formatCell(content: string, width: number, horizontal = 'center') { return { header: content, width, style: { alignment: { horizontal } } } } } class CSV extends FileBase { set tasklist (_tasklist: IUTasklistModel) { this.tasklist = _tasklist } set project (_projectId: IUProjectModel) { this.project = _projectId } set user (_user: IUUserModel) { this.user = _user } set lang (_lang: string) { this.lang = _lang } async execute () { // const tasklistName = this.tasklist.title // const filename = require('content-disposition')( // decodeURIComponent(`${projectRaw.name}-${tasklistName}` + '.csv') // ) // res.set('content-type', 'text/csv;charset=utf-8') // res.setHeader('Content-Disposition', filename) // res.set('Set-Cookie', 'fileDownload=true; path=/') // const foreignAid = new ForeignAid(this.app, this.ctx) // const [ // applications, // customfieldsMap, // scenariofieldconfigMap, // defaultSfc, // scenariofieldsTypeArr // ] = await Promise.all([ // foreignAid.getenableApplications(this.project), // foreignAid.getCustomfieldMap(this.project), // foreignAid.getScenariofieldconfigMap(this.project._id), // foreignAid.getDefaultConfigByProjectIdObjectType( // this.project._id, // 'task' // ), // foreignAid.getScenariofieldsArray(this.project._id) // ]) const taskflowstatusMap = new Map() if (this.project.normalType === 'taskflow') { const taskflowIds = await this.app.db.taskflow.distinct('_id', { _boundToObjectId: this.project._id, isDeleted: false }) const taskflowstatuses = await this.app.db.taskflowstatus .find({ _taskflowId: { $in: taskflowIds }, isDeleted: false }) .select('name') .lean() taskflowstatuses.map((taskflowstatus) => { taskflowstatusMap.set(String(taskflowstatus._id), taskflowstatus) }) } // const hasUniqueId = foreignAid.hasUniqueId(applications) // const __ = i18n.__ // const fields = exportcsvBll.buildHeaders( // hasUniqueId, // customfieldsMap, // __, // scenariofieldsTypeArr, // this.project // ) // // res.write(Buffer.from('\xEF\xBB\xBF', 'binary')) // // res.write(fields.join(',') + '\n') // const csv = require('csv') // const transform = csv.transform(function (task, cb) { // return thunk(function * () { // const result = yield exportcsvBll.tranformTasks(task, { // tasklistName, // uniqueIdPrefix: projectRaw.uniqueIdPrefix, // customfieldsMap, // hasUniqueId, // lang, // scenariofieldconfigMap, // scenariofieldsTypeArr, // __, // defaultSfc, // taskflowstatusMap, // project: projectRaw // }) // return result // })(cb) // }) // const emitter = exportcsvBll.getTasks(user._id, _tasklistId) // emitter.on('data', (data) => { // if (!transform.writable) { // return // } // transform.write(data) // }) // emitter.on('end', () => transform.end()) // transform.pipe(csv.stringify()).pipe(res) // return taskBll.emit('inhouse.task.export', req._all) } }