320 lines
9.6 KiB
TypeScript
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();
|
|
}
|
|
}
|