172 lines
5.7 KiB
TypeScript
172 lines
5.7 KiB
TypeScript
import { defineEventHandler, getQuery, createError } from 'h3'
|
|
import { query } from '../../../../utils/db'
|
|
|
|
/**
|
|
* 주차별 취합 상세 (프로젝트별 실적/계획 테이블용)
|
|
* GET /api/report/summary/week/detail?year=2026&week=1
|
|
*/
|
|
export default defineEventHandler(async (event) => {
|
|
const queryParams = getQuery(event)
|
|
const year = queryParams.year ? parseInt(queryParams.year as string) : null
|
|
const week = queryParams.week ? parseInt(queryParams.week as string) : null
|
|
|
|
if (!year || !week) {
|
|
throw createError({ statusCode: 400, message: '연도와 주차를 지정해주세요.' })
|
|
}
|
|
|
|
// 해당 주차 취합 보고서 목록
|
|
const summaries = await query(`
|
|
SELECT
|
|
s.summary_id,
|
|
s.project_id,
|
|
p.project_name,
|
|
p.project_code,
|
|
s.week_start_date,
|
|
s.week_end_date,
|
|
s.member_count,
|
|
s.total_work_hours,
|
|
s.ai_work_summary,
|
|
s.ai_plan_summary,
|
|
s.ai_summary_at,
|
|
s.summary_status,
|
|
s.aggregated_at
|
|
FROM wr_aggregated_report_summary s
|
|
JOIN wr_project_info p ON s.project_id = p.project_id
|
|
WHERE s.report_year = $1 AND s.report_week = $2
|
|
ORDER BY p.project_name
|
|
`, [year, week])
|
|
|
|
if (summaries.length === 0) {
|
|
throw createError({ statusCode: 404, message: '해당 주차의 취합 보고서가 없습니다.' })
|
|
}
|
|
|
|
// 프로젝트 ID 목록
|
|
const projectIds = summaries.map((s: any) => s.project_id)
|
|
|
|
// 해당 주차/프로젝트의 모든 Task 조회
|
|
const tasks = await query(`
|
|
SELECT
|
|
t.project_id,
|
|
t.task_type,
|
|
t.task_description,
|
|
t.task_hours,
|
|
t.is_completed,
|
|
r.author_id,
|
|
e.employee_name as author_name
|
|
FROM wr_weekly_report r
|
|
JOIN wr_weekly_report_task t ON r.report_id = t.report_id
|
|
JOIN wr_employee_info e ON r.author_id = e.employee_id
|
|
WHERE r.report_year = $1
|
|
AND r.report_week = $2
|
|
AND t.project_id = ANY($3)
|
|
AND r.report_status IN ('SUBMITTED', 'AGGREGATED')
|
|
ORDER BY t.project_id, t.task_type, e.employee_name
|
|
`, [year, week, projectIds])
|
|
|
|
// Task를 프로젝트별로 그룹핑 + 프로젝트별 인원별 시간 집계 (실적/계획 통합)
|
|
const tasksByProject = new Map<number, { work: any[], plan: any[] }>()
|
|
const membersByProject = new Map<number, Map<number, { name: string, workHours: number, planHours: number }>>()
|
|
|
|
for (const task of tasks) {
|
|
// Task 그룹핑
|
|
if (!tasksByProject.has(task.project_id)) {
|
|
tasksByProject.set(task.project_id, { work: [], plan: [] })
|
|
}
|
|
const group = tasksByProject.get(task.project_id)!
|
|
const taskItem = {
|
|
description: task.task_description,
|
|
hours: parseFloat(task.task_hours) || 0,
|
|
isCompleted: task.is_completed,
|
|
authorId: task.author_id,
|
|
authorName: task.author_name
|
|
}
|
|
|
|
// 프로젝트별 인원별 시간 집계
|
|
if (!membersByProject.has(task.project_id)) {
|
|
membersByProject.set(task.project_id, new Map())
|
|
}
|
|
const members = membersByProject.get(task.project_id)!
|
|
if (!members.has(task.author_id)) {
|
|
members.set(task.author_id, { name: task.author_name, workHours: 0, planHours: 0 })
|
|
}
|
|
|
|
const hours = parseFloat(task.task_hours) || 0
|
|
const member = members.get(task.author_id)!
|
|
|
|
if (task.task_type === 'WORK') {
|
|
group.work.push(taskItem)
|
|
member.workHours += hours
|
|
} else {
|
|
group.plan.push(taskItem)
|
|
member.planHours += hours
|
|
}
|
|
}
|
|
|
|
// 전체 인원별 시간 집계
|
|
const memberHours = new Map<number, { name: string, workHours: number, planHours: number }>()
|
|
for (const task of tasks) {
|
|
const authorId = task.author_id
|
|
if (!memberHours.has(authorId)) {
|
|
memberHours.set(authorId, { name: task.author_name, workHours: 0, planHours: 0 })
|
|
}
|
|
const member = memberHours.get(authorId)!
|
|
const hours = parseFloat(task.task_hours) || 0
|
|
if (task.task_type === 'WORK') {
|
|
member.workHours += hours
|
|
} else {
|
|
member.planHours += hours
|
|
}
|
|
}
|
|
|
|
// 첫번째 summary에서 날짜 정보 추출
|
|
const weekInfo = {
|
|
reportYear: year,
|
|
reportWeek: week,
|
|
weekStartDate: summaries[0].week_start_date,
|
|
weekEndDate: summaries[0].week_end_date,
|
|
totalProjects: summaries.length,
|
|
totalWorkHours: summaries.reduce((sum: number, s: any) => sum + (parseFloat(s.total_work_hours) || 0), 0)
|
|
}
|
|
|
|
// 프로젝트별 데이터 구성
|
|
const projects = summaries.map((s: any) => {
|
|
const taskGroup = tasksByProject.get(s.project_id) || { work: [], plan: [] }
|
|
const projectMembers = membersByProject.get(s.project_id)
|
|
|
|
// 프로젝트별 인원 시간 배열 (실적+계획 통합)
|
|
const memberHoursList = projectMembers
|
|
? Array.from(projectMembers.values()).sort((a, b) => (b.workHours + b.planHours) - (a.workHours + a.planHours))
|
|
: []
|
|
|
|
return {
|
|
summaryId: s.summary_id,
|
|
projectId: s.project_id,
|
|
projectName: s.project_name,
|
|
projectCode: s.project_code,
|
|
memberCount: s.member_count,
|
|
totalWorkHours: parseFloat(s.total_work_hours) || 0,
|
|
aiWorkSummary: s.ai_work_summary,
|
|
aiPlanSummary: s.ai_plan_summary,
|
|
aiSummaryAt: s.ai_summary_at,
|
|
workTasks: taskGroup.work,
|
|
planTasks: taskGroup.plan,
|
|
memberHours: memberHoursList // { name, workHours, planHours }
|
|
}
|
|
})
|
|
|
|
// 전체 인원별 시간 배열로 변환
|
|
const members = Array.from(memberHours.entries()).map(([id, m]) => ({
|
|
employeeId: id,
|
|
employeeName: m.name,
|
|
workHours: m.workHours,
|
|
planHours: m.planHours,
|
|
availableHours: Math.max(0, 40 - m.planHours)
|
|
})).sort((a, b) => b.availableHours - a.availableHours)
|
|
|
|
return {
|
|
weekInfo,
|
|
projects,
|
|
members
|
|
}
|
|
})
|