Files
daywork/intern-log/src/domain/report.ts
T

186 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getWorkRule, listAttendanceBetween, listLogItemsBetween, listProjects, listWeeklyNotesBetween } from '../db/repo'
import { type LogItem } from '../db/schema'
import { getLogCategories } from '../db/categories'
import { todayStr } from '../lib/date'
import { computeDay } from './attendance'
export interface ReportRange {
start: string
end: string
label: string
}
export interface CategoryStat {
key: string
label: string
count: number
percent: number
}
export interface ProjectStat {
name: string
count: number
}
export interface ReportData {
range: ReportRange
attendDays: number
totalHours: string
makeupCount: number
leaveCount: number
itemCount: number
categoryStats: CategoryStat[]
projectStats: ProjectStat[]
locationStats: { name: string; count: number }[]
milestones: { date: string; content: string }[]
followups: { date: string; content: string; remark: string }[]
takeaways: { date: string; text: string }[]
weekNotes: { weekStart: string; highlights: string; challenges: string; plan_next: string }[]
}
export async function buildReport(start: string, end: string, label: string): Promise<ReportData> {
const t = todayStr()
const results = await Promise.all([
getWorkRule(),
listAttendanceBetween(start, end),
listLogItemsBetween(start, end),
listProjects(true),
listWeeklyNotesBetween(start, end),
getLogCategories()
])
const rule = results[0]
const atts = results[1]
const items = results[2]
const projects = results[3]
const notes = results[4]
const logCats: string[] = results[5] as string[]
let attendDays = 0
let workedMin = 0
let makeupCount = 0
for (const a of atts) {
if (a.clock_in_at || a.kind === 'makeup') attendDays++
if (a.kind === 'makeup') makeupCount++
const info = computeDay(a, a.date, rule, a.date === t)
workedMin += info.workedMinutes ?? 0
}
const lcount = new Map<string, number>()
for (const a of atts) {
if (!(a.clock_in_at || a.kind === 'makeup')) continue
const name = a.location || '未记录'
lcount.set(name, (lcount.get(name) || 0) + 1)
}
const locationStats = Array.from(lcount.entries())
.map(pair => ({ name: pair[0], count: pair[1] }))
.sort((a, b) => b.count - a.count)
const total = items.length || 1
const catCount2 = new Map<string, number>()
for (const it of items) catCount2.set(it.category, (catCount2.get(it.category) || 0) + 1)
const catSet2 = new Set(logCats)
for (const k of catCount2.keys()) if (!catSet2.has(k)) logCats.push(k)
const categoryStats: CategoryStat[] = logCats
.map(label => {
const count = catCount2.get(label) || 0
return { key: label, label, count, percent: Math.round((count / total) * 100) }
})
.filter(c => c.count > 0)
.sort((a, b) => b.count - a.count)
const pname = new Map<number, string>(projects.map(p => [p.id as number, p.name]))
const pcount = new Map<string, number>()
for (const it of items) {
const name = it.project_id ? (pname.get(it.project_id) || '已删除项目') : '无项目'
pcount.set(name, (pcount.get(name) || 0) + 1)
}
const projectStats: ProjectStat[] = Array.from(pcount.entries())
.map(pair => ({ name: pair[0], count: pair[1] }))
.sort((a, b) => b.count - a.count)
const milestones = items.filter(i => i.is_milestone === 1).map(i => ({ date: i.date, content: i.content }))
const followups = items.filter(i => i.needs_followup === 1).map(i => ({ date: i.date, content: i.content, remark: i.remark || '' }))
const takeaways = items.filter(i => !!i.takeaway).map(i => ({ date: i.date, text: i.takeaway as string }))
const weekNotes = notes.map(n => ({
weekStart: n.week_start,
highlights: n.highlights || '',
challenges: n.challenges || '',
plan_next: n.plan_next || ''
}))
return {
range: { start, end, label },
attendDays,
totalHours: (workedMin / 60).toFixed(1),
makeupCount,
leaveCount: atts.filter(a => a.kind === 'leave').length,
itemCount: items.length,
categoryStats,
projectStats,
locationStats,
milestones,
followups,
takeaways,
weekNotes
}
}
export function reportToMarkdown(d: ReportData): string {
const out: string[] = []
out.push('# 实习工作总结(' + d.range.start + ' ~ ' + d.range.end + ')')
out.push('')
out.push('## 一、考勤概况')
out.push('')
out.push('出勤 ' + d.attendDays + ' 天,累计工时 ' + d.totalHours + ' 小时,补卡 ' + d.makeupCount + ' 次' + (d.leaveCount ? ',请假 ' + d.leaveCount + ' 天' : ''))
const locParts = d.locationStats.filter(l => l.name !== '未记录')
if (locParts.length) {
out.push('其中:' + locParts.map(l => l.name + ' ' + l.count + ' 天').join(','))
}
out.push('')
out.push('## 二、工作内容分布')
out.push('')
out.push(d.categoryStats.length
? d.categoryStats.map(c => c.label + ' ' + c.percent + '%(' + c.count + ' 条)').join(',')
: '(无记录)')
out.push('')
out.push('## 三、项目投入')
out.push('')
out.push(d.projectStats.length
? d.projectStats.map(p => p.name + ' ' + p.count + ' 条').join(',')
: '(无记录)')
out.push('')
out.push('## 四、主要成果')
out.push('')
if (d.milestones.length) {
d.milestones.forEach((m, i) => out.push((i + 1) + '. 【成果】' + m.content + '(' + m.date + ')'))
} else {
out.push('(无)')
}
out.push('')
out.push('## 五、待跟进事项')
out.push('')
if (d.followups.length) {
d.followups.forEach(f => out.push('- 【待跟进】' + f.content + (f.remark ? '(' + f.date + ' 备注:' + f.remark + ')' : '(' + f.date + ')')))
} else {
out.push('(无)')
}
out.push('')
out.push('## 六、学习与成长')
out.push('')
if (d.takeaways.length) {
d.takeaways.forEach(tk => out.push('- ' + tk.text + '(' + tk.date + ')'))
} else {
out.push('(无)')
}
for (const w of d.weekNotes) {
out.push('')
out.push('### 周记(' + w.weekStart + ' 起)')
out.push('')
out.push('- 本周亮点:' + (w.highlights || '(无)'))
out.push('- 踩的坑:' + (w.challenges || '(无)'))
out.push('- 下周计划:' + (w.plan_next || '(无)'))
}
out.push('')
return out.join('\n')
}