support-requests.js 4.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
  1. // Both mobile roles submit category 22 to Guestbook. Keep legacy, untyped requests visible.
  2. async function supportRequestsPage(context, input) {
  3. requireRole(context, ['ops-manager', 'ops-staff', 'ops-auditor']);
  4. const company = companyIdOf(context, input);
  5. if (!company) fail(400, '请先选择帐套');
  6. const kind = text(input.kind), role = text(input.role), search = text(input.search), status = text(input.status);
  7. if (status && !['pending','processed','approved','rejected'].includes(status)) fail(400, '处理状态无效');
  8. if (kind && !['feedback', 'leave', 'legacy'].includes(kind)) fail(400, '申请类型无效');
  9. if (role && !['student', 'teacher', 'other'].includes(role)) fail(400, '提交身份无效');
  10. const from = text(input.dateFrom), to = text(input.dateTo);
  11. for (const day of [from, to]) {
  12. if (day && (!/^\d{4}-\d{2}-\d{2}$/.test(day) || !Number.isFinite(Date.parse(day)) || new Date(day).toISOString().slice(0,10) !== day)) fail(400, '日期无效');
  13. }
  14. if (from && to && from > to) fail(400, '开始日期不能晚于结束日期');
  15. const page = Math.max(1, Math.floor(number(input.page, 1)));
  16. const pageSize = Math.min(100, Math.max(1, Math.floor(number(input.pageSize, 20))));
  17. const result = await Psql.one(`WITH requests AS (
  18. SELECT g."objectId" AS id, g.userid::text AS "userId", COALESCE(g.title,'') AS title,
  19. COALESCE(r.status,'pending') AS status, COALESCE(r.comment,'') AS response, r."handledAt", r."actorName", r."readAt",
  20. md5(COALESCE(g.title,'')||chr(10)||COALESCE(g.tcontent,'')||chr(10)||COALESCE(g.userid::text,'')) AS version,
  21. COALESCE(g.tcontent,'') AS content, COALESCE(g.gdate,g."createdAt") AS "submittedAt",
  22. COALESCE(NULLIF(u."realName",''),NULLIF(u.nickname,''),NULLIF(u.username,''),'账号未匹配') AS "accountName",
  23. CASE WHEN g.title LIKE '【休假申请】%' THEN 'leave'
  24. WHEN g.title LIKE '【意见反馈】%' THEN 'feedback' ELSE 'legacy' END AS kind,
  25. CASE WHEN COALESCE(to_jsonb(u)->>'identityType','')='coach' OR COALESCE(u."legacyGroupId"::text,u."legacyUserData"->>'GroupID')='3' THEN 'teacher'
  26. WHEN COALESCE(to_jsonb(u)->>'identityType','')='member' OR COALESCE(u."legacyGroupId"::text,u."legacyUserData"->>'GroupID')='1' THEN 'student'
  27. ELSE 'other' END AS role
  28. FROM "Guestbook" g
  29. LEFT JOIN "SupportRequestResolution" r ON r.company=g.company AND r."requestId"=g."objectId"
  30. LEFT JOIN LATERAL (SELECT u.* FROM "_User" u WHERE u.company=g.company
  31. AND COALESCE(u."legacyUserId"::text,u."legacyUserData"->>'UserID')=g.userid::text
  32. ORDER BY u."createdAt" DESC,u."objectId" LIMIT 1) u ON TRUE
  33. WHERE g.company=$1 AND COALESCE(g.parentid,0)=0 AND COALESCE(g.status,0)<>-2
  34. AND (g.cateid=22 OR g."sourceKey" LIKE 'cloud:guestbook_add:%')
  35. ), filtered AS (
  36. SELECT * FROM requests WHERE ($9='' OR status=$9) AND ($2='' OR kind=$2) AND ($3='' OR role=$3)
  37. AND ($4='' OR strpos(lower(title||' '||content||' '||"accountName"||' '||COALESCE("userId",'')),lower($4))>0)
  38. AND ($5='' OR "submittedAt">=NULLIF($5,'')::date::timestamp AT TIME ZONE 'Asia/Shanghai')
  39. AND ($6='' OR "submittedAt"<(NULLIF($6,'')::date+1)::timestamp AT TIME ZONE 'Asia/Shanghai')
  40. ), paged AS (SELECT * FROM filtered ORDER BY "submittedAt" DESC NULLS LAST,id DESC LIMIT $7 OFFSET $8)
  41. SELECT COUNT(*)::int AS total,
  42. COUNT(*) FILTER(WHERE kind='feedback')::int AS feedback,
  43. COUNT(*) FILTER(WHERE kind='leave')::int AS leave,
  44. COUNT(*) FILTER(WHERE kind='legacy')::int AS legacy,
  45. COALESCE((SELECT jsonb_agg(p ORDER BY p."submittedAt" DESC NULLS LAST,p.id DESC) FROM paged p),'[]'::jsonb) AS items
  46. FROM filtered`, [company,kind,role,search.slice(0,200),from,to,pageSize,(page-1)*pageSize,status]);
  47. return {canProcess:['ops-manager','ops-staff'].includes(roleOf(context)),items:result.items, total:result.total, page, pageSize,
  48. summary:{feedback:result.feedback,leave:result.leave,legacy:result.legacy},refreshedAt:new Date().toISOString()};
  49. }
  50. async function supportRequestProcess(context,input) {
  51. requireRole(context,['ops-manager','ops-staff']);
  52. const company=companyIdOf(context,input), id=text(input.targetId), status=text(input.status), note=text(input.comment);
  53. if(!company || !id) fail(400,'缺少帐套或申请编号');
  54. if(!['processed','approved','rejected'].includes(status)||!note||note.length>1000)fail(400,'请填写处理结果和 1 至 1000 字的处理意见');
  55. const actorName=text(context.current.get('realName')||context.current.get('nickname')||context.current.get('username')||'管理员');
  56. try {return (await Psql.one('SELECT xs_support_request_process($1,$2,$3,$4,$5,$6,$7) AS result',[company,id,context.current.id,actorName,status,note,text(input.version)])).result;}
  57. catch(error){if(error.code==='P0001')fail(409,error.message);throw error;}
  58. }