feat(M08-B): 补保洁统计明细
This commit is contained in:
@@ -111,6 +111,32 @@ interface SettlementItemRow extends RowDataPacket {
|
||||
completedAt: Date | null;
|
||||
createdAt: Date;
|
||||
}
|
||||
interface ManagerStatusStatsRow extends RowDataPacket {
|
||||
status: CleaningTaskStatus;
|
||||
total: number;
|
||||
rewardCents: number | null;
|
||||
}
|
||||
interface ManagerStoreStatsRow extends RowDataPacket {
|
||||
storeId: string;
|
||||
storeName: string;
|
||||
taskTotal: number;
|
||||
pendingReview: number;
|
||||
completed: number;
|
||||
rejected: number;
|
||||
rewardCents: number | null;
|
||||
}
|
||||
interface ManagerSettlementStatsRow extends RowDataPacket {
|
||||
status: CleaningSettlementStatus;
|
||||
total: number;
|
||||
rewardCents: number | null;
|
||||
}
|
||||
interface ManagerMemberStatsRow extends RowDataPacket {
|
||||
cleanerUserId: string;
|
||||
cleanerName: string;
|
||||
taskCount: number;
|
||||
pendingSettlementCents: number | null;
|
||||
settledRewardCents: number | null;
|
||||
}
|
||||
|
||||
export class CleaningTaskRepository {
|
||||
constructor(private readonly pool: MySqlPool) {}
|
||||
@@ -714,6 +740,99 @@ export class CleaningTaskRepository {
|
||||
};
|
||||
}
|
||||
|
||||
async managerStatistics(input: CleaningActor & { from?: string; to?: string; storeId?: string }) {
|
||||
this.assertCleaner(input.access, 'read');
|
||||
this.assertSettlement(input.access, 'read');
|
||||
const taskWhere = [
|
||||
't.tenant_id = ?',
|
||||
't.deleted_at IS NULL',
|
||||
storeScopeSql(input.access, 't.store_id')
|
||||
];
|
||||
const taskParams: Array<string | number> = [input.tenantId];
|
||||
if (input.storeId) {
|
||||
taskWhere.push('t.store_id = ?');
|
||||
taskParams.push(input.storeId);
|
||||
}
|
||||
if (input.from) {
|
||||
taskWhere.push('t.created_at >= ?');
|
||||
taskParams.push(input.from);
|
||||
}
|
||||
if (input.to) {
|
||||
taskWhere.push('t.created_at < ?');
|
||||
taskParams.push(input.to);
|
||||
}
|
||||
const taskWhereSql = taskWhere.join(' AND ');
|
||||
const [statusRows] = await this.pool.execute<ManagerStatusStatsRow[]>(
|
||||
`SELECT t.status, COUNT(*) AS total, COALESCE(SUM(t.reward_cents), 0) AS rewardCents
|
||||
FROM qipai_cleaning_tasks t
|
||||
WHERE ${taskWhereSql}
|
||||
GROUP BY t.status
|
||||
ORDER BY FIELD(t.status, 'WAITING', 'CLAIMED', 'STARTED', 'SUBMITTED', 'REJECTED',
|
||||
'COMPLETED', 'SETTLED', 'EXEMPT', 'CANCELLED')`,
|
||||
taskParams
|
||||
);
|
||||
const [storeRows] = await this.pool.execute<ManagerStoreStatsRow[]>(
|
||||
`SELECT t.store_id AS storeId, s.name AS storeName,
|
||||
COUNT(*) AS taskTotal,
|
||||
SUM(CASE WHEN t.status = 'SUBMITTED' THEN 1 ELSE 0 END) AS pendingReview,
|
||||
SUM(CASE WHEN t.status IN ('COMPLETED', 'SETTLED') THEN 1 ELSE 0 END) AS completed,
|
||||
SUM(CASE WHEN t.status = 'REJECTED' THEN 1 ELSE 0 END) AS rejected,
|
||||
COALESCE(SUM(t.reward_cents), 0) AS rewardCents
|
||||
FROM qipai_cleaning_tasks t
|
||||
INNER JOIN qipai_stores s ON s.tenant_id = t.tenant_id AND s.id = t.store_id
|
||||
WHERE ${taskWhereSql}
|
||||
GROUP BY t.store_id, s.name
|
||||
ORDER BY pendingReview DESC, taskTotal DESC, t.store_id ASC
|
||||
LIMIT 20`,
|
||||
taskParams
|
||||
);
|
||||
const settlementWhere = [
|
||||
's.tenant_id = ?',
|
||||
's.deleted_at IS NULL',
|
||||
storeScopeSql(input.access, 'COALESCE(s.store_id, 0)')
|
||||
];
|
||||
const settlementParams: Array<string | number> = [input.tenantId];
|
||||
if (input.storeId) {
|
||||
settlementWhere.push('s.store_id = ?');
|
||||
settlementParams.push(input.storeId);
|
||||
}
|
||||
if (input.from) {
|
||||
settlementWhere.push('s.created_at >= ?');
|
||||
settlementParams.push(input.from);
|
||||
}
|
||||
if (input.to) {
|
||||
settlementWhere.push('s.created_at < ?');
|
||||
settlementParams.push(input.to);
|
||||
}
|
||||
const settlementWhereSql = settlementWhere.join(' AND ');
|
||||
const [settlementRows] = await this.pool.execute<ManagerSettlementStatsRow[]>(
|
||||
`SELECT s.status, COUNT(*) AS total, COALESCE(SUM(s.total_reward_cents), 0) AS rewardCents
|
||||
FROM qipai_cleaning_settlements s
|
||||
WHERE ${settlementWhereSql}
|
||||
GROUP BY s.status
|
||||
ORDER BY FIELD(s.status, 'DRAFT', 'CONFIRMED', 'PAID', 'CANCELLED')`,
|
||||
settlementParams
|
||||
);
|
||||
const [memberRows] = await this.pool.execute<ManagerMemberStatsRow[]>(
|
||||
`SELECT m.user_id AS cleanerUserId, u.nickname AS cleanerName,
|
||||
COUNT(DISTINCT m.task_id) AS taskCount,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN m.removed_at IS NULL AND m.settled_at IS NULL AND t.status IN ('SUBMITTED', 'COMPLETED')
|
||||
THEN m.reward_cents ELSE 0 END), 0) AS pendingSettlementCents,
|
||||
COALESCE(SUM(CASE
|
||||
WHEN m.settled_at IS NOT NULL THEN m.reward_cents ELSE 0 END), 0) AS settledRewardCents
|
||||
FROM qipai_cleaning_task_members m
|
||||
INNER JOIN qipai_cleaning_tasks t ON t.tenant_id = m.tenant_id AND t.id = m.task_id
|
||||
INNER JOIN qipai_users u ON u.tenant_id = m.tenant_id AND u.id = m.user_id
|
||||
WHERE ${taskWhereSql}
|
||||
GROUP BY m.user_id, u.nickname
|
||||
ORDER BY pendingSettlementCents DESC, settledRewardCents DESC, taskCount DESC
|
||||
LIMIT 20`,
|
||||
taskParams
|
||||
);
|
||||
return publicManagerStatistics(statusRows, storeRows, settlementRows, memberRows);
|
||||
}
|
||||
|
||||
private async listByWhere(
|
||||
where: string[],
|
||||
params: Array<string | number>,
|
||||
@@ -1136,6 +1255,66 @@ function publicSettlementItem(row: SettlementItemRow) {
|
||||
};
|
||||
}
|
||||
|
||||
function publicManagerStatistics(
|
||||
statusRows: ManagerStatusStatsRow[],
|
||||
storeRows: ManagerStoreStatsRow[],
|
||||
settlementRows: ManagerSettlementStatsRow[],
|
||||
memberRows: ManagerMemberStatsRow[]
|
||||
) {
|
||||
const byStatus = statusRows.map((row) => ({
|
||||
status: row.status,
|
||||
total: Number(row.total),
|
||||
rewardCents: Number(row.rewardCents ?? 0)
|
||||
}));
|
||||
const settlementByStatus = settlementRows.map((row) => ({
|
||||
status: row.status,
|
||||
total: Number(row.total),
|
||||
rewardCents: Number(row.rewardCents ?? 0)
|
||||
}));
|
||||
return {
|
||||
summary: {
|
||||
taskTotal: byStatus.reduce((sum, row) => sum + row.total, 0),
|
||||
pendingReview: countStatus(byStatus, 'SUBMITTED'),
|
||||
active: countStatus(byStatus, 'WAITING') + countStatus(byStatus, 'CLAIMED') + countStatus(byStatus, 'STARTED'),
|
||||
rejected: countStatus(byStatus, 'REJECTED'),
|
||||
completed: countStatus(byStatus, 'COMPLETED') + countStatus(byStatus, 'SETTLED'),
|
||||
rewardCents: byStatus.reduce((sum, row) => sum + row.rewardCents, 0),
|
||||
pendingSettlementCents: memberRows.reduce((sum, row) => sum + Number(row.pendingSettlementCents ?? 0), 0),
|
||||
confirmedSettlementCents: settlementByStatus
|
||||
.filter((row) => row.status === 'CONFIRMED')
|
||||
.reduce((sum, row) => sum + row.rewardCents, 0),
|
||||
paidSettlementCents: settlementByStatus
|
||||
.filter((row) => row.status === 'PAID')
|
||||
.reduce((sum, row) => sum + row.rewardCents, 0)
|
||||
},
|
||||
byStatus,
|
||||
byStore: storeRows.map((row) => ({
|
||||
storeId: String(row.storeId),
|
||||
storeName: row.storeName,
|
||||
taskTotal: Number(row.taskTotal),
|
||||
pendingReview: Number(row.pendingReview),
|
||||
completed: Number(row.completed),
|
||||
rejected: Number(row.rejected),
|
||||
rewardCents: Number(row.rewardCents ?? 0)
|
||||
})),
|
||||
settlements: settlementByStatus,
|
||||
members: memberRows.map((row) => ({
|
||||
cleanerUserId: String(row.cleanerUserId),
|
||||
cleanerName: row.cleanerName,
|
||||
taskCount: Number(row.taskCount),
|
||||
pendingSettlementCents: Number(row.pendingSettlementCents ?? 0),
|
||||
settledRewardCents: Number(row.settledRewardCents ?? 0)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
function countStatus(
|
||||
rows: Array<{ status: CleaningTaskStatus; total: number }>,
|
||||
status: CleaningTaskStatus
|
||||
) {
|
||||
return rows.find((row) => row.status === status)?.total ?? 0;
|
||||
}
|
||||
|
||||
function minDate(values: Array<Date | null>) {
|
||||
const dates = values.filter((value): value is Date => value instanceof Date);
|
||||
if (dates.length === 0) return null;
|
||||
|
||||
Reference in New Issue
Block a user