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 { 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() 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() 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(projects.map(p => [p.id as number, p.name])) const pcount = new Map() 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') }