import pool from '../config/database'; import { ResultSetHeader } from 'mysql2'; export type PerformanceStatus = 'draft' | 'submitted' | 'under_review' | 'completed' | 'rejected'; export type PerformanceLevel = 'excellent' | 'qualified' | 'need_motivation' | 'unqualified'; export interface PerformanceRow { perf_id: number; user_id: number; month: string; status: PerformanceStatus; self_score: number | null; ai_score: number | null; manager_score: number | null; total_score: number | null; level: PerformanceLevel | null; reward_punish: string | null; work_summary: string | null; submit_time: Date | null; review_time: Date | null; review_opinion: string | null; created_at: Date; updated_at: Date; } export interface PerfItemRow { item_id?: number; perf_id: number; item_name: string; item_category: 'business' | 'comprehensive'; weight: number; user_content: string | null; self_score: number | null; ai_score: number | null; ai_explanation: string | null; manager_score: number | null; manager_explanation: string | null; evidence_url: string | null; } export interface AttendanceRow { attendance_id?: number; perf_id: number; leave_days: number; late_times: number; absent_days: number; lack_card_times: number; attendance_score: number | null; remark: string | null; } export interface UpsertPerformanceData { userId: number; month: string; status: PerformanceStatus; selfScore?: number; workSummary?: string; items?: Omit[]; attendance?: Omit; } export interface PerformanceFilter { month?: string; department?: string; employeeName?: string; status?: PerformanceStatus; } export interface PageInfo { page: number; pageSize: number; } export interface ReviewData { managerScore: number; reviewOpinion: string; totalScore: number; level: PerformanceLevel; rewardPunish: string; itemScores: { itemName: string; managerScore: number; managerExplanation: string }[]; } export interface PerformanceListResult { total: number; records: PerformanceRow[]; } /** * Create or update a performance record along with its items and attendance. * Uses INSERT ... ON DUPLICATE KEY UPDATE to handle the unique constraint on (user_id, month). */ export async function upsert(data: UpsertPerformanceData): Promise { const conn = await pool.getConnection(); try { await conn.beginTransaction(); // Upsert performance_month const submitTime = data.status === 'submitted' ? new Date() : null; const [result] = await conn.query( `INSERT INTO performance_month (user_id, month, status, self_score, work_summary, submit_time) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), self_score = VALUES(self_score), work_summary = VALUES(work_summary), submit_time = COALESCE(VALUES(submit_time), submit_time)`, [data.userId, data.month, data.status, data.selfScore ?? null, data.workSummary ?? null, submitTime] ); // Resolve perf_id (insertId is 0 on UPDATE, so fetch it) let perfId: number = result.insertId; if (perfId === 0) { const [rows] = await conn.query( 'SELECT perf_id FROM performance_month WHERE user_id = ? AND month = ?', [data.userId, data.month] ); perfId = rows[0].perf_id; } // Upsert perf_items if (data.items && data.items.length > 0) { // Delete existing items and re-insert for simplicity await conn.query('DELETE FROM perf_item WHERE perf_id = ?', [perfId]); for (const item of data.items) { await conn.query( `INSERT INTO perf_item (perf_id, item_name, item_category, weight, user_content, self_score, evidence_url) VALUES (?, ?, ?, ?, ?, ?, ?)`, [perfId, item.item_name, item.item_category, item.weight, item.user_content ?? null, item.self_score ?? null, item.evidence_url ?? null] ); } } // Upsert attendance if (data.attendance) { const att = data.attendance; await conn.query( `INSERT INTO attendance (perf_id, leave_days, late_times, absent_days, lack_card_times, attendance_score, remark) VALUES (?, ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE leave_days = VALUES(leave_days), late_times = VALUES(late_times), absent_days = VALUES(absent_days), lack_card_times = VALUES(lack_card_times), attendance_score = VALUES(attendance_score), remark = VALUES(remark)`, [perfId, att.leave_days, att.late_times, att.absent_days, att.lack_card_times, att.attendance_score ?? null, att.remark ?? null] ); } await conn.commit(); return perfId; } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } } /** Query a performance record for a specific user and month, including items and attendance. */ export async function findByUserAndMonth(userId: number, month: string): Promise { const [rows] = await pool.query( 'SELECT * FROM performance_month WHERE user_id = ? AND month = ? LIMIT 1', [userId, month] ); return rows.length > 0 ? (rows[0] as PerformanceRow) : null; } /** Query subordinates' performance records with optional filters and pagination. */ export async function findByManagerId( managerId: number, filters: PerformanceFilter, page: PageInfo ): Promise { const conditions: string[] = ['u.manager_id = ?']; const params: any[] = [managerId]; if (filters.month) { conditions.push('pm.month = ?'); params.push(filters.month); } if (filters.department) { conditions.push('u.department = ?'); params.push(filters.department); } if (filters.employeeName) { conditions.push('u.name LIKE ?'); params.push(`%${filters.employeeName}%`); } if (filters.status) { conditions.push('pm.status = ?'); params.push(filters.status); } const where = conditions.join(' AND '); const offset = (page.page - 1) * page.pageSize; const [countRows] = await pool.query( `SELECT COUNT(*) AS total FROM performance_month pm JOIN user u ON pm.user_id = u.user_id WHERE ${where}`, params ); const total: number = countRows[0].total; const [rows] = await pool.query( `SELECT pm.* FROM performance_month pm JOIN user u ON pm.user_id = u.user_id WHERE ${where} ORDER BY pm.month DESC, pm.created_at DESC LIMIT ? OFFSET ?`, [...params, page.pageSize, offset] ); return { total, records: rows as PerformanceRow[] }; } /** Update only the status of a performance record. */ export async function updateStatus(perfId: number, status: PerformanceStatus): Promise { await pool.query( 'UPDATE performance_month SET status = ? WHERE perf_id = ?', [status, perfId] ); } /** Query performance records for a specific employee with optional month filter and pagination. */ export async function findByUserId( userId: number, month: string | undefined, page: PageInfo ): Promise { const conditions: string[] = ['pm.user_id = ?']; const params: any[] = [userId]; if (month) { conditions.push('pm.month = ?'); params.push(month); } const where = conditions.join(' AND '); const offset = (page.page - 1) * page.pageSize; const [countRows] = await pool.query( `SELECT COUNT(*) AS total FROM performance_month pm WHERE ${where}`, params ); const total: number = countRows[0].total; const [rows] = await pool.query( `SELECT pm.* FROM performance_month pm WHERE ${where} ORDER BY pm.month DESC, pm.created_at DESC LIMIT ? OFFSET ?`, [...params, page.pageSize, offset] ); return { total, records: rows as PerformanceRow[] }; } /** Query full detail of a performance record including items and attendance. */ export async function findDetailByPerfId(perfId: number): Promise<{ performance: PerformanceRow; items: PerfItemRow[]; attendance: AttendanceRow | null; } | null> { const [perfRows] = await pool.query( 'SELECT * FROM performance_month WHERE perf_id = ? LIMIT 1', [perfId] ); if (perfRows.length === 0) return null; const [itemRows] = await pool.query( 'SELECT * FROM perf_item WHERE perf_id = ?', [perfId] ); const [attRows] = await pool.query( 'SELECT * FROM attendance WHERE perf_id = ? LIMIT 1', [perfId] ); return { performance: perfRows[0] as PerformanceRow, items: itemRows as PerfItemRow[], attendance: attRows.length > 0 ? (attRows[0] as AttendanceRow) : null, }; } /** Update manager review results and archive the performance record. */ export async function updateReview(perfId: number, reviewData: ReviewData): Promise { const conn = await pool.getConnection(); try { await conn.beginTransaction(); await conn.query( `UPDATE performance_month SET manager_score = ?, review_opinion = ?, total_score = ?, level = ?, reward_punish = ?, status = 'completed', review_time = NOW() WHERE perf_id = ?`, [reviewData.managerScore, reviewData.reviewOpinion, reviewData.totalScore, reviewData.level, reviewData.rewardPunish, perfId] ); for (const item of reviewData.itemScores) { await conn.query( `UPDATE perf_item SET manager_score = ?, manager_explanation = ? WHERE perf_id = ? AND item_name = ?`, [item.managerScore, item.managerExplanation, perfId, item.itemName] ); } await conn.commit(); } catch (err) { await conn.rollback(); throw err; } finally { conn.release(); } }