255 lines
6.9 KiB
TypeScript
255 lines
6.9 KiB
TypeScript
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);
|
|
},
|
|
};
|