@dssp/project 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/client/pages/lib/select2-component.ts +21 -16
  2. package/client/pages/project/component/project-update-header.ts +16 -13
  3. package/client/pages/project/project-detail.ts +74 -60
  4. package/client/pages/project/project-plan-management.ts +39 -29
  5. package/client/pages/project/project-schedule.ts +4 -3
  6. package/client/pages/project/project-setting-list.ts +9 -9
  7. package/client/pages/project/project-update.ts +53 -29
  8. package/dist-client/pages/lib/select2-component.js +21 -16
  9. package/dist-client/pages/lib/select2-component.js.map +1 -1
  10. package/dist-client/pages/project/component/project-update-header.js +16 -13
  11. package/dist-client/pages/project/component/project-update-header.js.map +1 -1
  12. package/dist-client/pages/project/project-detail.js +74 -60
  13. package/dist-client/pages/project/project-detail.js.map +1 -1
  14. package/dist-client/pages/project/project-plan-management.js +39 -29
  15. package/dist-client/pages/project/project-plan-management.js.map +1 -1
  16. package/dist-client/pages/project/project-schedule.js +4 -3
  17. package/dist-client/pages/project/project-schedule.js.map +1 -1
  18. package/dist-client/pages/project/project-setting-list.js +9 -9
  19. package/dist-client/pages/project/project-setting-list.js.map +1 -1
  20. package/dist-client/pages/project/project-update.js +53 -29
  21. package/dist-client/pages/project/project-update.js.map +1 -1
  22. package/dist-client/tsconfig.tsbuildinfo +1 -1
  23. package/dist-server/controllers/{project-to-excel.js → export-tasks.js} +1 -1
  24. package/dist-server/controllers/export-tasks.js.map +1 -0
  25. package/dist-server/controllers/import-task.d.ts +1 -17
  26. package/dist-server/controllers/import-task.js +24 -14
  27. package/dist-server/controllers/import-task.js.map +1 -1
  28. package/dist-server/controllers/parse-excel.d.ts +4 -0
  29. package/dist-server/controllers/parse-excel.js +75 -0
  30. package/dist-server/controllers/parse-excel.js.map +1 -0
  31. package/dist-server/controllers/types.d.ts +18 -0
  32. package/dist-server/controllers/types.js +3 -0
  33. package/dist-server/controllers/types.js.map +1 -0
  34. package/dist-server/routes.js +2 -2
  35. package/dist-server/routes.js.map +1 -1
  36. package/dist-server/service/project/project-mutation.d.ts +2 -1
  37. package/dist-server/service/project/project-mutation.js +52 -24
  38. package/dist-server/service/project/project-mutation.js.map +1 -1
  39. package/dist-server/service/task/task.d.ts +1 -0
  40. package/dist-server/service/task/task.js +5 -0
  41. package/dist-server/service/task/task.js.map +1 -1
  42. package/dist-server/tsconfig.tsbuildinfo +1 -1
  43. package/package.json +3 -3
  44. package/server/controllers/import-task.ts +25 -31
  45. package/server/controllers/parse-excel.ts +86 -0
  46. package/server/controllers/types.ts +20 -0
  47. package/server/routes.ts +1 -1
  48. package/server/service/project/project-mutation.ts +40 -24
  49. package/server/service/task/task.ts +4 -0
  50. package/dist-server/controllers/project-to-excel.js.map +0 -1
  51. /package/dist-server/controllers/{project-to-excel.d.ts → export-tasks.d.ts} +0 -0
  52. /package/server/controllers/{project-to-excel.ts → export-tasks.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dssp/project",
3
- "version": "0.0.30",
3
+ "version": "0.0.32",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -27,7 +27,7 @@
27
27
  "migration:create": "node ../../node_modules/typeorm/cli.js migration:create -d ./server/migrations"
28
28
  },
29
29
  "dependencies": {
30
- "@dssp/building-complex": "^0.0.30",
30
+ "@dssp/building-complex": "^0.0.32",
31
31
  "@operato/graphql": "^8.0.0-alpha",
32
32
  "@operato/shell": "^8.0.0-alpha",
33
33
  "@things-factory/auth-base": "^8.0.0-alpha",
@@ -38,5 +38,5 @@
38
38
  "@things-factory/shell": "^8.0.0-alpha",
39
39
  "exceljs": "^4.4.0"
40
40
  },
41
- "gitHead": "1091ab6303e2995122bfa3acf7a7a0988e8bc5cc"
41
+ "gitHead": "e69c41de325f16b46022d7f41e9ae1a4723af0b0"
42
42
  }
@@ -4,23 +4,15 @@ import { Project } from '../service/project/project'
4
4
  import { Task, TaskType } from '../service/task/task'
5
5
  import { TaskResource } from '../service/task-resource/task-resource'
6
6
  import { Resource } from '../service/resource/resource'
7
+ import { RawTask } from './types'
7
8
 
8
- export interface RawResource {
9
- type: string
10
- allocated: number
11
- }
9
+ function excelSerialToJSDate(serial) {
10
+ const excelEpoch = new Date(1899, 11, 30) // Excel epoch (30th December 1899)
11
+ const days = Math.floor(serial) // Get the number of days
12
+ const milliseconds = (serial - days) * 86400 * 1000 // Convert the fractional day part to milliseconds
12
13
 
13
- export interface RawTask {
14
- code: string
15
- title: string
16
- type?: TaskType
17
- duration?: number
18
- startDate?: string /* YYYY-MM-DD */
19
- dependsOn?: string
20
- progress?: number
21
- tags?: string[]
22
- resources?: RawResource[]
23
- children?: RawTask[]
14
+ const jsDate = new Date(excelEpoch.getTime() + days * 86400 * 1000 + milliseconds)
15
+ return jsDate
24
16
  }
25
17
 
26
18
  export async function importTasks(project: Project, tasks: RawTask[], context: ResolverContext) {
@@ -34,7 +26,6 @@ export async function importTasks(project: Project, tasks: RawTask[], context: R
34
26
  await taskRepository.softDelete({ project: { id: project.id } })
35
27
 
36
28
  // 2. 태스크 임포트
37
-
38
29
  const importTaskData = async (rawTask: RawTask, parent?: Task) => {
39
30
  if (rawTask.children && rawTask.children.length > 0) {
40
31
  rawTask.type = TaskType.GROUP
@@ -49,27 +40,31 @@ export async function importTasks(project: Project, tasks: RawTask[], context: R
49
40
 
50
41
  if (rawTask.type == TaskType.TASK) {
51
42
  // 시작일, 종료일 계산
52
- var startDate: Date | undefined = rawTask.startDate ? new Date(rawTask.startDate) : undefined
43
+ var startDate: Date = new Date(rawTask.startDate)
44
+
45
+ var endDate
53
46
  if (!startDate && rawTask.dependsOn) {
54
47
  const dependsOnTask = await taskRepository.findOne({ where: { code: rawTask.dependsOn, project: { id: project.id } } })
55
- if (!dependsOnTask || !dependsOnTask.endDate) {
56
- throw new Error(`Task '${rawTask.code}' depends on a task '${rawTask.dependsOn}' that doesn't have a valid end date.`)
48
+ if (dependsOnTask && dependsOnTask.endDate) {
49
+ startDate = new Date(dependsOnTask.endDate)
50
+ startDate.setDate(startDate.getDate() + 1)
51
+ } else {
52
+ // TODO handler error
53
+ // throw new Error(`Task '${rawTask.code}' depends on a task '${rawTask.dependsOn}' that doesn't have a valid end date.`)
57
54
  }
58
- startDate = new Date(dependsOnTask.endDate)
59
- startDate.setDate(startDate.getDate() + 1)
60
55
  }
61
56
 
62
57
  if (!startDate) {
63
58
  throw new Error(`Task '${rawTask.code}' must have either a start date or a valid dependency.`)
64
59
  }
65
60
 
66
- var duration = rawTask.duration
67
- var endDate = new Date(startDate)
61
+ const duration = rawTask.duration
62
+ endDate = new Date(startDate)
68
63
  endDate.setDate(startDate.getDate() + duration - 1)
69
64
  }
70
65
 
71
66
  // 태스크 생성 및 저장
72
- var task = await taskRepository.save({
67
+ var task: Task = await taskRepository.save({
73
68
  code: rawTask.code,
74
69
  name: rawTask.title,
75
70
  type: rawTask.type,
@@ -77,10 +72,11 @@ export async function importTasks(project: Project, tasks: RawTask[], context: R
77
72
  endDate,
78
73
  project,
79
74
  parent,
80
- duration,
75
+ duration: rawTask.duration,
81
76
  dependsOn: rawTask.dependsOn,
82
77
  progress: rawTask.progress,
83
78
  tags: rawTask.tags,
79
+ style: rawTask.style,
84
80
  updater: user,
85
81
  creator: user
86
82
  })
@@ -102,9 +98,9 @@ export async function importTasks(project: Project, tasks: RawTask[], context: R
102
98
  }
103
99
 
104
100
  // 자식 태스크 처리
105
- if (rawTask.children) {
106
- var lastEndDate = null
107
- var lastStartDate = null
101
+ if (rawTask.children && rawTask.children.length > 0) {
102
+ let lastEndDate = null
103
+ let lastStartDate = null
108
104
  for (const childTask of rawTask.children) {
109
105
  const subtask = await importTaskData(childTask, task)
110
106
 
@@ -118,9 +114,7 @@ export async function importTasks(project: Project, tasks: RawTask[], context: R
118
114
  ? Math.ceil((lastEndDate.getTime() - lastStartDate.getTime()) / (1000 * 60 * 60 * 24)) + 1
119
115
  : 0
120
116
 
121
- task = (await taskRepository.findOne({
122
- where: { id: task.id }
123
- })) as any
117
+ task = await taskRepository.findOne({ where: { id: task.id } })
124
118
 
125
119
  return await taskRepository.save({
126
120
  ...task,
@@ -0,0 +1,86 @@
1
+ import ExcelJS from 'exceljs'
2
+ import { RawTask } from './types'
3
+ import { Project } from '../service/project/project'
4
+ import { importTasks } from './import-task'
5
+
6
+ export async function parseExcelAndImportTasks(buffer: Buffer, project: Project, context: ResolverContext) {
7
+ // 1. 엑셀 파일을 읽어들입니다.
8
+ const workbook = new ExcelJS.Workbook()
9
+ await workbook.xlsx.load(buffer)
10
+
11
+ // 2. 첫 번째 워크시트를 가져옵니다.
12
+ const worksheet = workbook.getWorksheet(1) // Index or sheet name can be used
13
+
14
+ // 3. 첫 번째 row를 header로 사용합니다.
15
+ const headers: string[] = []
16
+ let taskCodeColumnIndex = -1
17
+
18
+ const headerRow = worksheet.getRow(1)
19
+ headerRow.eachCell((cell, colNumber) => {
20
+ const headerText = cell.text.toString()
21
+ headers[colNumber - 1] = headerText // Store headers in an array
22
+
23
+ if (headerText === '작업코드') {
24
+ taskCodeColumnIndex = colNumber // Store the column index for "작업코드"
25
+ }
26
+ })
27
+
28
+ if (taskCodeColumnIndex === -1) {
29
+ throw new Error('작업코드 column not found')
30
+ }
31
+
32
+ // 4. 엑셀 데이터를 RawTask 형식으로 변환합니다.
33
+ const tasks: RawTask[] = []
34
+
35
+ // Start processing from the second row onward to skip the header
36
+ for (let rowIndex = 2; rowIndex <= worksheet.rowCount; rowIndex++) {
37
+ const row = worksheet.getRow(rowIndex)
38
+ const taskData: any = {}
39
+
40
+ row.eachCell((cell, colNumber) => {
41
+ const header = headers[colNumber - 1]
42
+
43
+ // Check if the cell has a formula(or sharedFormula) and use the formula result
44
+ let cellValue: any = cell.value
45
+ if (cellValue && typeof cellValue === 'object' && ('formula' in cellValue || 'sharedFormula' in cellValue)) {
46
+ // Cell contains a formula, use the calculated result if available
47
+ cellValue = cellValue.result ?? cellValue.value // Use the result, or fallback to value if result is not calculated
48
+ }
49
+
50
+ taskData[header] = cellValue
51
+ })
52
+
53
+ const taskCodeCell = row.getCell(taskCodeColumnIndex)
54
+ let bgColor = '#FFFFFF'
55
+ const fill = taskCodeCell.style.fill
56
+
57
+ if (fill && fill.type === 'pattern' && fill.pattern === 'solid') {
58
+ const fgColor = fill.fgColor
59
+ if (fgColor && fgColor.argb) {
60
+ // ARGB is a color in the format AARRGGBB, remove the alpha channel (first two characters)
61
+ bgColor = `#${fgColor.argb.slice(2)}`
62
+ }
63
+ }
64
+
65
+ const task: RawTask = {
66
+ code: taskData['작업코드'],
67
+ title: taskData['작업명'],
68
+ type: taskData['세부공종'],
69
+ duration: taskData['기간'],
70
+ startDate: taskData['시작일'],
71
+ dependsOn: taskData['선행작업코드'],
72
+ progress: taskData['진척율'],
73
+ tags: taskData['Tags'] ? taskData['Tags'].split(',') : [],
74
+ resources: taskData['Resources'] ? JSON.parse(taskData['Resources']) : [],
75
+ style: bgColor,
76
+ children: []
77
+ }
78
+
79
+ if (task.code && task.type) {
80
+ tasks.push(task)
81
+ }
82
+ }
83
+
84
+ // 5. 변환된 데이터를 importTasks 함수로 전달합니다.
85
+ await importTasks(project, tasks, context)
86
+ }
@@ -0,0 +1,20 @@
1
+ import { Task, TaskType } from '../service/task/task'
2
+
3
+ export interface RawResource {
4
+ type: string
5
+ allocated: number
6
+ }
7
+
8
+ export interface RawTask {
9
+ code: string
10
+ title: string
11
+ type?: TaskType
12
+ duration?: number
13
+ startDate?: Date | string
14
+ dependsOn?: string
15
+ progress?: number
16
+ tags?: string[]
17
+ style?: string
18
+ resources?: RawResource[]
19
+ children?: RawTask[]
20
+ }
package/server/routes.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import contentDisposition from 'content-disposition'
2
2
 
3
- import { Task, generateExcel } from './controllers/project-to-excel'
3
+ import { Task, generateExcel } from './controllers/export-tasks'
4
4
 
5
5
  // const debug = require('debug')('dssp:project:routes')
6
6
 
@@ -1,19 +1,21 @@
1
1
  import { Resolver, Mutation, Arg, Ctx, Directive } from 'type-graphql'
2
2
  import { In } from 'typeorm'
3
- import { createAttachment, deleteAttachmentsByRef, ATTACHMENT_PATH } from '@things-factory/attachment-base'
3
+ import { getRepository } from '@things-factory/shell'
4
+ import { Attachment, createAttachment, deleteAttachmentsByRef, ATTACHMENT_PATH } from '@things-factory/attachment-base'
4
5
  import { Project, ProjectState } from './project'
5
6
  import { NewProject, ProjectPatch, UploadProjectScheduleTable } from './project-type'
6
7
  import { BuildingComplex, Building, BuildingLevel } from '@dssp/building-complex'
7
8
  import { pdfToImage } from '@things-factory/board-service/dist-server/controllers/headless-pdf-to-image'
8
9
 
10
+ import { parseExcelAndImportTasks } from '../../controllers/parse-excel'
9
11
  @Resolver(Project)
10
12
  export class ProjectMutation {
11
13
  @Directive('@transaction')
12
14
  @Mutation(returns => Project, { description: '프로젝트 생성' })
13
15
  async createProject(@Arg('project') project: NewProject, @Ctx() context: ResolverContext): Promise<Project> {
14
16
  const { domain, user, tx } = context.state
15
- const projectRepo = tx.getRepository(Project)
16
- const buildingComplexRepo = tx.getRepository(BuildingComplex)
17
+ const projectRepo = getRepository(Project, tx)
18
+ const buildingComplexRepo = getRepository(BuildingComplex, tx)
17
19
 
18
20
  const newBuildingComplex = await buildingComplexRepo.save({
19
21
  domain,
@@ -36,10 +38,10 @@ export class ProjectMutation {
36
38
  @Mutation(returns => Project, { description: '프로젝트 업데이트' })
37
39
  async updateProject(@Arg('project') project: ProjectPatch, @Ctx() context: ResolverContext): Promise<Project> {
38
40
  const { user, tx } = context.state
39
- const projectRepo = tx.getRepository(Project)
40
- const buildingComplexRepo = tx.getRepository(BuildingComplex)
41
- const buildingRepo = tx.getRepository(Building)
42
- const buildingLevelRepo = tx.getRepository(BuildingLevel)
41
+ const projectRepo = getRepository(Project, tx)
42
+ const buildingComplexRepo = getRepository(BuildingComplex, tx)
43
+ const buildingRepo = getRepository(Building, tx)
44
+ const buildingLevelRepo = getRepository(BuildingLevel, tx)
43
45
 
44
46
  const buildingComplex = project.buildingComplex
45
47
  const buildings = project.buildingComplex?.buildings || []
@@ -97,10 +99,10 @@ export class ProjectMutation {
97
99
  @Mutation(returns => Project, { description: '프로젝트 도면 업데이트' })
98
100
  async updateProjectPlan(@Arg('project') project: ProjectPatch, @Ctx() context: ResolverContext): Promise<Project> {
99
101
  const { user, tx, domain } = context.state
100
- const projectRepo = tx.getRepository(Project)
101
- const buildingComplexRepo = tx.getRepository(BuildingComplex)
102
- const buildingRepo = tx.getRepository(Building)
103
- const buildingLevelRepo = tx.getRepository(BuildingLevel)
102
+ const projectRepo = getRepository(Project, tx)
103
+ const buildingComplexRepo = getRepository(BuildingComplex, tx)
104
+ const buildingRepo = getRepository(Building, tx)
105
+ const buildingLevelRepo = getRepository(BuildingLevel, tx)
104
106
  const buildingComplex = project.buildingComplex
105
107
  const buildings = project.buildingComplex?.buildings || []
106
108
 
@@ -126,7 +128,7 @@ export class ProjectMutation {
126
128
  // 첨부된 PDF가 있으면 PDF 파일대로 썸네일 생성
127
129
  if (mainDrawingAttatchment) {
128
130
  const mainDrawingUpload = await buildingLevel.mainDrawingUpload
129
- const pdfPath = `/${ATTACHMENT_PATH}/${mainDrawingAttatchment.path}`
131
+ const pdfPath = `/${ATTACHMENT_PATH}/${mainDrawingAttatchment.path}` // TODO ATTACHMENT_PATH 제거, mainDrawingAttachment.fullpath 로 해도 될 것 같은데...
130
132
  const fileName = mainDrawingUpload.filename.replace('.pdf', '')
131
133
  const pngFile = await pdfToImage({ pdfPath, fileName })
132
134
  await createAttachmentAfterDelete(context, pngFile, buildingLevel.id, BuildingLevel.name + '_mainDrawing_image')
@@ -200,11 +202,27 @@ export class ProjectMutation {
200
202
  @Arg('param') param: UploadProjectScheduleTable,
201
203
  @Ctx() context: ResolverContext
202
204
  ): Promise<boolean> {
203
- const { user, tx } = context.state
205
+ const { domain, user, tx } = context.state
204
206
  const { projectId, scheduleTable } = param
205
207
 
206
- // 프로젝트 공정표 파일 업로드
207
- await createAttachmentAfterDelete(context, scheduleTable, projectId, Project.name + '_schedule_table')
208
+ const projectRepo = getRepository(Project, tx)
209
+ const project = await projectRepo.findOne({
210
+ where: { domain: { id: domain.id }, id: projectId }
211
+ })
212
+
213
+ const { createReadStream, filename, mimetype } = await scheduleTable
214
+
215
+ const stream = createReadStream()
216
+
217
+ const chunks = []
218
+ for await (const chunk of stream) {
219
+ chunks.push(chunk)
220
+ }
221
+
222
+ const buffer = Buffer.concat(chunks)
223
+
224
+ await parseExcelAndImportTasks(buffer, project, context)
225
+ // await parseExcelAndImportTasks(attachment.fullpath, project, context)
208
226
 
209
227
  return true
210
228
  }
@@ -214,7 +232,7 @@ export class ProjectMutation {
214
232
  async deleteProject(@Arg('id') id: string, @Ctx() context: ResolverContext): Promise<boolean> {
215
233
  const { domain, tx } = context.state
216
234
 
217
- await tx.getRepository(Project).delete({ domain: { id: domain.id }, id })
235
+ await getRepository(Project, tx).delete({ domain: { id: domain.id }, id })
218
236
  await deleteAttachmentsByRef(null, { refBys: [id] }, context)
219
237
 
220
238
  return true
@@ -222,18 +240,16 @@ export class ProjectMutation {
222
240
  }
223
241
 
224
242
  export async function createAttachmentAfterDelete(context: ResolverContext, file: any, refBy: any, refType: any) {
225
- let result = null
243
+ if (file === undefined) {
244
+ return null
245
+ }
226
246
 
227
- // undefined = 기존 파일 그대로
228
- if (file === undefined) return result
247
+ const { tx } = context.state
229
248
 
230
249
  // 기존 첨부 파일이 있으면 삭제
231
250
  await deleteAttachmentsByRef(null, { refBys: [refBy], refType }, context)
232
251
 
233
- // 파일이 있으면 생성 (null 들어올 경우 delete까지만)
234
- if (file) {
235
- result = await createAttachment(null, { attachment: { file, refType, refBy } }, context)
236
- }
252
+ let result = await createAttachment(null, { attachment: { file, refType, refBy } }, context)
237
253
 
238
- return result
254
+ return await getRepository(Attachment, tx).findOne({ where: { id: result.id } })
239
255
  }
@@ -90,6 +90,10 @@ export class Task {
90
90
  @Field({ nullable: true })
91
91
  progress?: number
92
92
 
93
+ @Column({ nullable: true, comment: '스타일' })
94
+ @Field({ nullable: true })
95
+ style?: string
96
+
93
97
  // @OneToMany(type => Checklist, checklist => checklist.task, { nullable: true })
94
98
  // @Field(type => [Checklist], { nullable: true })
95
99
  // checklists?: Checklist[]
@@ -1 +0,0 @@
1
- {"version":3,"file":"project-to-excel.js","sourceRoot":"","sources":["../../server/controllers/project-to-excel.ts"],"names":[],"mappings":";;;AAAA,qCAA6C;AAS7C,SAAS,gBAAgB,CAAC,KAAa,EAAE,SAAoB,EAAE,QAAgB,CAAC;IAC9E,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;QACnB,MAAM,GAAG,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAA;QAEvE,GAAG,CAAC,YAAY,GAAG,KAAK,CAAA;QAExB,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE;YAC7C,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,EAAE,KAAK,GAAG,CAAC,CAAC,CAAA;SACtD;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAEM,KAAK,UAAU,aAAa,CAAC,KAAa;IAC/C,MAAM,QAAQ,GAAG,IAAI,kBAAQ,EAAE,CAAA;IAC/B,MAAM,SAAS,GAAG,QAAQ,CAAC,YAAY,CAAC,aAAa,CAAC,CAAA;IAEtD,SAAS,CAAC,UAAU,CAAC,iBAAiB,GAAG;QACvC,YAAY,EAAE,KAAK;QACnB,YAAY,EAAE,KAAK;KACpB,CAAA;IAED,SAAS,CAAC,OAAO,GAAG;QAClB,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,EAAE;QAC/C,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE;QACrD,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE;KAClD,CAAA;IAED,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAA;IAElC,OAAO,MAAM,QAAQ,CAAC,IAAI,CAAC,WAAW,EAAE,CAAA;AAC1C,CAAC;AAlBD,sCAkBC","sourcesContent":["import { Workbook, Worksheet } from 'exceljs'\n\nexport interface Task {\n name: string\n startDate: Date\n endDate: Date\n subtasks?: Task[]\n}\n\nfunction createGanttChart(tasks: Task[], worksheet: Worksheet, level: number = 0) {\n tasks.forEach(task => {\n const row = worksheet.addRow([task.name, task.startDate, task.endDate])\n\n row.outlineLevel = level\n\n if (task.subtasks && task.subtasks.length > 0) {\n createGanttChart(task.subtasks, worksheet, level + 1)\n }\n })\n}\n\nexport async function generateExcel(tasks: Task[]) {\n const workbook = new Workbook()\n const worksheet = workbook.addWorksheet('Gantt Chart')\n\n worksheet.properties.outlineProperties = {\n summaryBelow: false,\n summaryRight: false\n }\n\n worksheet.columns = [\n { header: 'Task Name', key: 'name', width: 30 },\n { header: 'Start Date', key: 'startDate', width: 20 },\n { header: 'End Date', key: 'endDate', width: 20 }\n ]\n\n createGanttChart(tasks, worksheet)\n\n return await workbook.xlsx.writeBuffer()\n}\n"]}