186 lines
6.2 KiB
TypeScript
186 lines
6.2 KiB
TypeScript
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')
|
||
}
|