first commit
This commit is contained in:
93
backend/src/dao/AIResultDAO.ts
Normal file
93
backend/src/dao/AIResultDAO.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import pool from '../config/database';
|
||||
import { ResultSetHeader } from 'mysql2';
|
||||
|
||||
export interface AIScoreItem {
|
||||
itemName: string;
|
||||
weight: number;
|
||||
aiScore: number;
|
||||
scoreExplanation: string;
|
||||
}
|
||||
|
||||
export interface AIResultRow {
|
||||
ai_id: number;
|
||||
perf_id: number;
|
||||
ai_score_json: string;
|
||||
ai_total_score: number;
|
||||
problems: string | null;
|
||||
suggestions: string | null;
|
||||
api_response: string | null;
|
||||
create_time: Date;
|
||||
}
|
||||
|
||||
export interface SaveAIResultData {
|
||||
perfId: number;
|
||||
aiScoreDetail: AIScoreItem[];
|
||||
aiTotalScore: number;
|
||||
aiProblems: string[];
|
||||
aiSuggestions: string[];
|
||||
apiResponse?: string;
|
||||
}
|
||||
|
||||
export interface AIResult {
|
||||
aiId: number;
|
||||
perfId: number;
|
||||
aiScoreDetail: AIScoreItem[];
|
||||
aiTotalScore: number;
|
||||
aiProblems: string[];
|
||||
aiSuggestions: string[];
|
||||
createTime: Date;
|
||||
}
|
||||
|
||||
/** Save AI evaluation result. Replaces any existing result for the same perf_id. */
|
||||
export async function save(data: SaveAIResultData): Promise<number> {
|
||||
const [result] = await pool.query<ResultSetHeader>(
|
||||
`INSERT INTO ai_result (perf_id, ai_score_json, ai_total_score, problems, suggestions, api_response)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
ai_score_json = VALUES(ai_score_json),
|
||||
ai_total_score = VALUES(ai_total_score),
|
||||
problems = VALUES(problems),
|
||||
suggestions = VALUES(suggestions),
|
||||
api_response = VALUES(api_response)`,
|
||||
[
|
||||
data.perfId,
|
||||
JSON.stringify(data.aiScoreDetail),
|
||||
data.aiTotalScore,
|
||||
JSON.stringify(data.aiProblems),
|
||||
JSON.stringify(data.aiSuggestions),
|
||||
data.apiResponse ?? null,
|
||||
]
|
||||
);
|
||||
|
||||
if (result.insertId !== 0) {
|
||||
return result.insertId;
|
||||
}
|
||||
|
||||
// On UPDATE, fetch the existing ai_id
|
||||
const [rows] = await pool.query<any[]>(
|
||||
'SELECT ai_id FROM ai_result WHERE perf_id = ? LIMIT 1',
|
||||
[data.perfId]
|
||||
);
|
||||
return rows[0].ai_id;
|
||||
}
|
||||
|
||||
/** Retrieve the AI result for a given performance record. */
|
||||
export async function findByPerfId(perfId: number): Promise<AIResult | null> {
|
||||
const [rows] = await pool.query<any[]>(
|
||||
'SELECT * FROM ai_result WHERE perf_id = ? LIMIT 1',
|
||||
[perfId]
|
||||
);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const row: AIResultRow = rows[0];
|
||||
return {
|
||||
aiId: row.ai_id,
|
||||
perfId: row.perf_id,
|
||||
aiScoreDetail: JSON.parse(row.ai_score_json) as AIScoreItem[],
|
||||
aiTotalScore: Number(row.ai_total_score),
|
||||
aiProblems: row.problems ? (JSON.parse(row.problems) as string[]) : [],
|
||||
aiSuggestions: row.suggestions ? (JSON.parse(row.suggestions) as string[]) : [],
|
||||
createTime: row.create_time,
|
||||
};
|
||||
}
|
||||
53
backend/src/dao/ConfigDAO.ts
Normal file
53
backend/src/dao/ConfigDAO.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import pool from '../config/database';
|
||||
|
||||
export interface RuleRow {
|
||||
rule_id: number;
|
||||
rule_key: string;
|
||||
rule_value: string; // JSON string
|
||||
description: string | null;
|
||||
effective_cycle: string | null;
|
||||
updated_by: number | null;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
/** Fetch all rules */
|
||||
export async function findAllRules(): Promise<RuleRow[]> {
|
||||
const [rows] = await pool.query<any[]>(
|
||||
'SELECT rule_id, rule_key, rule_value, description, effective_cycle, updated_by, created_at, updated_at FROM performance_rules ORDER BY rule_key'
|
||||
);
|
||||
return rows as RuleRow[];
|
||||
}
|
||||
|
||||
/** Fetch a single rule by key */
|
||||
export async function findRuleByKey(ruleKey: string): Promise<RuleRow | null> {
|
||||
const [rows] = await pool.query<any[]>(
|
||||
'SELECT rule_id, rule_key, rule_value, description, effective_cycle, updated_by, created_at, updated_at FROM performance_rules WHERE rule_key = ? LIMIT 1',
|
||||
[ruleKey]
|
||||
);
|
||||
return rows.length > 0 ? (rows[0] as RuleRow) : null;
|
||||
}
|
||||
|
||||
export interface UpsertRuleData {
|
||||
ruleKey: string;
|
||||
ruleValue: unknown; // will be JSON-serialised
|
||||
description?: string;
|
||||
effectiveCycle?: string;
|
||||
updatedBy: number;
|
||||
}
|
||||
|
||||
/** Insert or update a rule (upsert by rule_key) */
|
||||
export async function upsertRule(data: UpsertRuleData): Promise<void> {
|
||||
const valueJson = JSON.stringify(data.ruleValue);
|
||||
await pool.query(
|
||||
`INSERT INTO performance_rules (rule_key, rule_value, description, effective_cycle, updated_by)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
rule_value = VALUES(rule_value),
|
||||
description = VALUES(description),
|
||||
effective_cycle = VALUES(effective_cycle),
|
||||
updated_by = VALUES(updated_by),
|
||||
updated_at = CURRENT_TIMESTAMP`,
|
||||
[data.ruleKey, valueJson, data.description ?? null, data.effectiveCycle ?? null, data.updatedBy]
|
||||
);
|
||||
}
|
||||
319
backend/src/dao/PerformanceDAO.ts
Normal file
319
backend/src/dao/PerformanceDAO.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
30
backend/src/dao/UserDAO.ts
Normal file
30
backend/src/dao/UserDAO.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import pool from '../config/database';
|
||||
import { UserRole } from '../types';
|
||||
|
||||
export interface UserRow {
|
||||
user_id: number;
|
||||
username: string;
|
||||
password: string;
|
||||
name: string;
|
||||
role: UserRole;
|
||||
department: string;
|
||||
position: string;
|
||||
manager_id: number | null;
|
||||
status: 'active' | 'inactive';
|
||||
}
|
||||
|
||||
export async function findByUsername(username: string): Promise<UserRow | null> {
|
||||
const [rows] = await pool.query<any[]>(
|
||||
'SELECT user_id, username, password, name, role, department, position, manager_id, status FROM user WHERE username = ? LIMIT 1',
|
||||
[username]
|
||||
);
|
||||
return rows.length > 0 ? (rows[0] as UserRow) : null;
|
||||
}
|
||||
|
||||
export async function findSubordinates(managerId: number): Promise<UserRow[]> {
|
||||
const [rows] = await pool.query<any[]>(
|
||||
'SELECT user_id, username, name, role, department, position, manager_id, status FROM user WHERE manager_id = ? AND status = ?',
|
||||
[managerId, 'active']
|
||||
);
|
||||
return rows as UserRow[];
|
||||
}
|
||||
Reference in New Issue
Block a user