Files
performance-evaluation-system/backend/src/dao/PerformanceDAO.ts
2026-04-11 11:51:54 +08:00

320 lines
9.6 KiB
TypeScript

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<PerfItemRow, 'perf_id' | 'item_id'>[];
attendance?: Omit<AttendanceRow, 'perf_id' | 'attendance_id'>;
}
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<number> {
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<ResultSetHeader>(
`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<any[]>(
'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<PerformanceRow | null> {
const [rows] = await pool.query<any[]>(
'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<PerformanceListResult> {
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<any[]>(
`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<any[]>(
`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<void> {
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<PerformanceListResult> {
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<any[]>(
`SELECT COUNT(*) AS total FROM performance_month pm WHERE ${where}`,
params
);
const total: number = countRows[0].total;
const [rows] = await pool.query<any[]>(
`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<any[]>(
'SELECT * FROM performance_month WHERE perf_id = ? LIMIT 1',
[perfId]
);
if (perfRows.length === 0) return null;
const [itemRows] = await pool.query<any[]>(
'SELECT * FROM perf_item WHERE perf_id = ?',
[perfId]
);
const [attRows] = await pool.query<any[]>(
'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<void> {
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();
}
}