feat: 增加员工端工作台后端能力
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
import type { ResultSetHeader, RowDataPacket } from "mysql2/promise";
|
||||
import { pool } from "../../db/pool";
|
||||
import type { AuthUser } from "../auth/auth.types";
|
||||
import type { CreateShiftInput, ListShiftsQuery, Shift, UpdateShiftInput } from "./shift.types";
|
||||
|
||||
type SqlParam = string | number | Date | null;
|
||||
|
||||
interface ShiftRow extends RowDataPacket {
|
||||
id: number;
|
||||
store_id: number;
|
||||
store_name: string;
|
||||
employee_id: number;
|
||||
employee_name: string;
|
||||
role_name: string | null;
|
||||
start_at: Date;
|
||||
end_at: Date;
|
||||
status: Shift["status"];
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
}
|
||||
|
||||
interface CountRow extends RowDataPacket {
|
||||
total: number;
|
||||
}
|
||||
|
||||
function toIso(value: Date): string {
|
||||
return value.toISOString();
|
||||
}
|
||||
|
||||
function toShift(row: ShiftRow): Shift {
|
||||
return {
|
||||
id: row.id,
|
||||
storeId: row.store_id,
|
||||
storeName: row.store_name,
|
||||
employeeId: row.employee_id,
|
||||
employeeName: row.employee_name,
|
||||
roleName: row.role_name,
|
||||
startAt: toIso(row.start_at),
|
||||
endAt: toIso(row.end_at),
|
||||
status: row.status,
|
||||
createdAt: toIso(row.created_at),
|
||||
updatedAt: toIso(row.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
function actorValues(user: AuthUser): [number | null, number | null] {
|
||||
return user.accountType === "SUPER_ADMIN" ? [user.id, null] : [null, user.id];
|
||||
}
|
||||
|
||||
function buildWhere(query: ListShiftsQuery): { whereSql: string; params: SqlParam[] } {
|
||||
const where = ["sh.deleted_at IS NULL"];
|
||||
const params: SqlParam[] = [];
|
||||
|
||||
if (query.storeId !== undefined) {
|
||||
where.push("sh.store_id = ?");
|
||||
params.push(query.storeId);
|
||||
}
|
||||
|
||||
if (query.employeeId !== undefined) {
|
||||
where.push("sh.employee_id = ?");
|
||||
params.push(query.employeeId);
|
||||
}
|
||||
|
||||
if (query.status) {
|
||||
where.push("sh.status = ?");
|
||||
params.push(query.status);
|
||||
}
|
||||
|
||||
if (query.startDate) {
|
||||
where.push("sh.start_at >= ?");
|
||||
params.push(`${query.startDate} 00:00:00.000`);
|
||||
}
|
||||
|
||||
if (query.endDate) {
|
||||
where.push("sh.start_at < DATE_ADD(?, INTERVAL 1 DAY)");
|
||||
params.push(`${query.endDate} 00:00:00.000`);
|
||||
}
|
||||
|
||||
return { whereSql: where.join(" AND "), params };
|
||||
}
|
||||
|
||||
export const shiftRepository = {
|
||||
async employeeBelongsToStore(employeeId: number, storeId: number): Promise<boolean> {
|
||||
const [rows] = await pool.execute<CountRow[]>(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM employees
|
||||
WHERE id = ?
|
||||
AND store_id = ?
|
||||
AND status = 'ACTIVE'
|
||||
AND deleted_at IS NULL
|
||||
`,
|
||||
[employeeId, storeId],
|
||||
);
|
||||
return (rows[0]?.total ?? 0) > 0;
|
||||
},
|
||||
|
||||
async hasOverlappingShift(
|
||||
employeeId: number,
|
||||
startAt: string,
|
||||
endAt: string,
|
||||
excludeShiftId?: number,
|
||||
): Promise<boolean> {
|
||||
const params: SqlParam[] = [employeeId, new Date(endAt), new Date(startAt)];
|
||||
const excludeSql = excludeShiftId === undefined ? "" : "AND id <> ?";
|
||||
|
||||
if (excludeShiftId !== undefined) {
|
||||
params.push(excludeShiftId);
|
||||
}
|
||||
|
||||
const [rows] = await pool.execute<CountRow[]>(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM shifts
|
||||
WHERE employee_id = ?
|
||||
AND deleted_at IS NULL
|
||||
AND status <> 'CANCELLED'
|
||||
AND start_at < ?
|
||||
AND end_at > ?
|
||||
${excludeSql}
|
||||
`,
|
||||
params,
|
||||
);
|
||||
return (rows[0]?.total ?? 0) > 0;
|
||||
},
|
||||
|
||||
async list(query: ListShiftsQuery): Promise<{ items: Shift[]; total: number }> {
|
||||
const { whereSql, params } = buildWhere(query);
|
||||
const offset = (query.page - 1) * query.pageSize;
|
||||
const [countRows] = await pool.execute<CountRow[]>(
|
||||
`
|
||||
SELECT COUNT(*) AS total
|
||||
FROM shifts sh
|
||||
INNER JOIN stores s ON s.id = sh.store_id
|
||||
INNER JOIN employees e ON e.id = sh.employee_id
|
||||
WHERE ${whereSql}
|
||||
`,
|
||||
params,
|
||||
);
|
||||
const [rows] = await pool.execute<ShiftRow[]>(
|
||||
`
|
||||
SELECT sh.*, s.name AS store_name, e.name AS employee_name
|
||||
FROM shifts sh
|
||||
INNER JOIN stores s ON s.id = sh.store_id
|
||||
INNER JOIN employees e ON e.id = sh.employee_id
|
||||
WHERE ${whereSql}
|
||||
ORDER BY sh.start_at ASC, sh.id ASC
|
||||
LIMIT ${query.pageSize} OFFSET ${offset}
|
||||
`,
|
||||
params,
|
||||
);
|
||||
|
||||
return {
|
||||
items: rows.map(toShift),
|
||||
total: countRows[0]?.total ?? 0,
|
||||
};
|
||||
},
|
||||
|
||||
async findById(id: number): Promise<Shift | null> {
|
||||
const [rows] = await pool.execute<ShiftRow[]>(
|
||||
`
|
||||
SELECT sh.*, s.name AS store_name, e.name AS employee_name
|
||||
FROM shifts sh
|
||||
INNER JOIN stores s ON s.id = sh.store_id
|
||||
INNER JOIN employees e ON e.id = sh.employee_id
|
||||
WHERE sh.id = ? AND sh.deleted_at IS NULL
|
||||
LIMIT 1
|
||||
`,
|
||||
[id],
|
||||
);
|
||||
return rows[0] ? toShift(rows[0]) : null;
|
||||
},
|
||||
|
||||
async create(input: CreateShiftInput, user: AuthUser): Promise<number> {
|
||||
const [creatorAdminId, creatorEmployeeId] = actorValues(user);
|
||||
const [result] = await pool.execute<ResultSetHeader>(
|
||||
`
|
||||
INSERT INTO shifts
|
||||
(store_id, employee_id, role_name, start_at, end_at, status, creator_admin_id, creator_employee_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`,
|
||||
[
|
||||
input.storeId,
|
||||
input.employeeId,
|
||||
input.roleName ?? null,
|
||||
new Date(input.startAt),
|
||||
new Date(input.endAt),
|
||||
input.status,
|
||||
creatorAdminId,
|
||||
creatorEmployeeId,
|
||||
],
|
||||
);
|
||||
return result.insertId;
|
||||
},
|
||||
|
||||
async update(id: number, input: UpdateShiftInput): Promise<void> {
|
||||
const fieldMap: Array<[keyof UpdateShiftInput, string]> = [
|
||||
["storeId", "store_id"],
|
||||
["employeeId", "employee_id"],
|
||||
["roleName", "role_name"],
|
||||
["startAt", "start_at"],
|
||||
["endAt", "end_at"],
|
||||
["status", "status"],
|
||||
];
|
||||
const sets: string[] = [];
|
||||
const params: SqlParam[] = [];
|
||||
|
||||
for (const [key, column] of fieldMap) {
|
||||
if (Object.prototype.hasOwnProperty.call(input, key)) {
|
||||
sets.push(`${column} = ?`);
|
||||
params.push((key === "startAt" || key === "endAt") && input[key] ? new Date(input[key]) : (input[key] as SqlParam));
|
||||
}
|
||||
}
|
||||
|
||||
if (sets.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
params.push(id);
|
||||
await pool.execute(
|
||||
`UPDATE shifts SET ${sets.join(", ")} WHERE id = ? AND deleted_at IS NULL`,
|
||||
params,
|
||||
);
|
||||
},
|
||||
|
||||
async cancel(id: number): Promise<void> {
|
||||
await pool.execute(
|
||||
`
|
||||
UPDATE shifts
|
||||
SET status = 'CANCELLED', deleted_at = CURRENT_TIMESTAMP(3)
|
||||
WHERE id = ? AND deleted_at IS NULL
|
||||
`,
|
||||
[id],
|
||||
);
|
||||
},
|
||||
|
||||
async listTodayForEmployee(employeeId: number): Promise<Shift[]> {
|
||||
const [rows] = await pool.execute<ShiftRow[]>(
|
||||
`
|
||||
SELECT sh.*, s.name AS store_name, e.name AS employee_name
|
||||
FROM shifts sh
|
||||
INNER JOIN stores s ON s.id = sh.store_id
|
||||
INNER JOIN employees e ON e.id = sh.employee_id
|
||||
WHERE sh.employee_id = ?
|
||||
AND sh.deleted_at IS NULL
|
||||
AND sh.status = 'SCHEDULED'
|
||||
AND DATE(sh.start_at) = CURRENT_DATE()
|
||||
ORDER BY sh.start_at ASC
|
||||
`,
|
||||
[employeeId],
|
||||
);
|
||||
return rows.map(toShift);
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user