|
|
|
|
@@ -25,6 +25,8 @@ interface TaskDefinition {
|
|
|
|
|
sql: (scope: StudentAccessScope) => { sql: string; params: unknown[] };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SqlBuilder = (scope: StudentAccessScope) => { sql: string; params: unknown[] };
|
|
|
|
|
|
|
|
|
|
function can(context: AgentToolContext, permission: string): boolean {
|
|
|
|
|
return context.isSuperAdmin || context.permissions.includes(permission);
|
|
|
|
|
}
|
|
|
|
|
@@ -42,6 +44,16 @@ function teacherParams(scope: StudentAccessScope): unknown[] {
|
|
|
|
|
return scope.type === 'teacher' ? [scope.userId] : [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 无作用域参数的计数查询:统一包装,避免各条目重复写 () => ({ sql, params }) 结构 */
|
|
|
|
|
function countSql(sql: string, params: unknown[] | (() => unknown[]) = []): SqlBuilder {
|
|
|
|
|
return () => ({ sql, params: typeof params === 'function' ? params() : params });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 教师作用域计数查询:按作用域注入班级过滤片段与 userId 参数 */
|
|
|
|
|
function teacherScopedCountSql(sql: (scope: StudentAccessScope) => string): SqlBuilder {
|
|
|
|
|
return (scope) => ({ sql: sql(scope), params: teacherParams(scope) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function today(): string {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const year = now.getFullYear();
|
|
|
|
|
@@ -67,17 +79,14 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
permission: 'student:view',
|
|
|
|
|
workflowKeys: ['student_teaching'],
|
|
|
|
|
teacherRestricted: true,
|
|
|
|
|
sql: () => ({
|
|
|
|
|
sql: `
|
|
|
|
|
SELECT COUNT(*) AS cnt FROM students s
|
|
|
|
|
WHERE s.status = 'active'
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM class_student cs
|
|
|
|
|
WHERE cs.student_id = s.id AND cs.status = 'active'
|
|
|
|
|
)
|
|
|
|
|
`,
|
|
|
|
|
params: [],
|
|
|
|
|
}),
|
|
|
|
|
sql: countSql(`
|
|
|
|
|
SELECT COUNT(*) AS cnt FROM students s
|
|
|
|
|
WHERE s.status = 'active'
|
|
|
|
|
AND NOT EXISTS (
|
|
|
|
|
SELECT 1 FROM class_student cs
|
|
|
|
|
WHERE cs.student_id = s.id AND cs.status = 'active'
|
|
|
|
|
)
|
|
|
|
|
`),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'students_without_checkin',
|
|
|
|
|
@@ -85,8 +94,8 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
entity: 'occupancy',
|
|
|
|
|
permission: 'occupancy:view',
|
|
|
|
|
workflowKeys: ['dormitory_billing'],
|
|
|
|
|
sql: (scope) => ({
|
|
|
|
|
sql: `
|
|
|
|
|
sql: teacherScopedCountSql(
|
|
|
|
|
(scope) => `
|
|
|
|
|
SELECT COUNT(DISTINCT s.id) AS cnt FROM students s
|
|
|
|
|
INNER JOIN class_student cs ON cs.student_id = s.id AND cs.status = 'active'
|
|
|
|
|
LEFT JOIN occupancies o ON o.student_id = s.id AND o.status = 'active'
|
|
|
|
|
@@ -94,8 +103,7 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
${teacherScoped(scope) ? TEACHER_CLASS_FILTER_SQL : ''}
|
|
|
|
|
AND o.id IS NULL
|
|
|
|
|
`,
|
|
|
|
|
params: teacherParams(scope),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'occupancies_without_bill',
|
|
|
|
|
@@ -103,8 +111,8 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
entity: 'bill',
|
|
|
|
|
permission: 'bill:view',
|
|
|
|
|
workflowKeys: ['dormitory_billing'],
|
|
|
|
|
sql: (scope) => ({
|
|
|
|
|
sql: `
|
|
|
|
|
sql: teacherScopedCountSql(
|
|
|
|
|
(scope) => `
|
|
|
|
|
SELECT COUNT(DISTINCT o.id) AS cnt FROM occupancies o
|
|
|
|
|
${teacherScoped(scope) ? "INNER JOIN class_student cs ON cs.student_id = o.student_id AND cs.status = 'active'" : ''}
|
|
|
|
|
LEFT JOIN bills b ON b.student_id = o.student_id
|
|
|
|
|
@@ -112,8 +120,7 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
${teacherScoped(scope) ? TEACHER_CLASS_FILTER_SQL : ''}
|
|
|
|
|
AND b.id IS NULL
|
|
|
|
|
`,
|
|
|
|
|
params: teacherParams(scope),
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'rentals_without_contract',
|
|
|
|
|
@@ -121,13 +128,10 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
entity: 'rental',
|
|
|
|
|
permission: 'rental:view',
|
|
|
|
|
workflowKeys: ['classroom_rental'],
|
|
|
|
|
sql: () => ({
|
|
|
|
|
sql: `
|
|
|
|
|
SELECT COUNT(*) AS cnt FROM classroom_rentals r
|
|
|
|
|
WHERE r.status = 'active' AND r.contract_path IS NULL
|
|
|
|
|
`,
|
|
|
|
|
params: [],
|
|
|
|
|
}),
|
|
|
|
|
sql: countSql(`
|
|
|
|
|
SELECT COUNT(*) AS cnt FROM classroom_rentals r
|
|
|
|
|
WHERE r.status = 'active' AND r.contract_path IS NULL
|
|
|
|
|
`),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'rentals_ending_soon',
|
|
|
|
|
@@ -135,13 +139,13 @@ const TASKS: readonly TaskDefinition[] = [
|
|
|
|
|
entity: 'rental',
|
|
|
|
|
permission: 'rental:view',
|
|
|
|
|
workflowKeys: ['classroom_rental'],
|
|
|
|
|
sql: () => ({
|
|
|
|
|
sql: `
|
|
|
|
|
sql: countSql(
|
|
|
|
|
`
|
|
|
|
|
SELECT COUNT(*) AS cnt FROM classroom_rentals r
|
|
|
|
|
WHERE r.status = 'active' AND r.end_date BETWEEN ? AND ?
|
|
|
|
|
`,
|
|
|
|
|
params: [today(), inDays(7)],
|
|
|
|
|
}),
|
|
|
|
|
() => [today(), inDays(7)],
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|