buildNotifications.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. import {
  2. GROUP_ORDER,
  3. aggregateReportRecords,
  4. extractFillMonth,
  5. extractPeriod,
  6. formatDateTime,
  7. getAuditTime,
  8. getFillTime,
  9. getRejectReason,
  10. normalizeGroupName,
  11. } from '@/utils/aggregateReportRecords';
  12. import { NOTIFY_TZLX } from '@/utils/notificationFields';
  13. import { buildManageRoute } from '@/utils/manageRoutes';
  14. const pad2 = (n) => String(n).padStart(2, '0');
  15. const getCurrentPeriod = () => {
  16. const now = new Date();
  17. return `${now.getFullYear()}-${pad2(now.getMonth() + 1)}`;
  18. };
  19. const getPreviousPeriod = (period) => {
  20. const [year, month] = period.split('-').map(Number);
  21. const prevMonth = month === 1 ? 12 : month - 1;
  22. const prevYear = month === 1 ? year - 1 : year;
  23. return `${prevYear}-${pad2(prevMonth)}`;
  24. };
  25. /** 账期截止:次月 1 日 23:59:59 */
  26. const getPeriodDeadlineDate = (period) => {
  27. const [year, month] = period.split('-').map(Number);
  28. const nextMonth = month === 12 ? 1 : month + 1;
  29. const nextYear = month === 12 ? year + 1 : year;
  30. return new Date(nextYear, nextMonth - 1, 1, 23, 59, 59);
  31. };
  32. const toTimestamp = (val) => {
  33. if (val == null || val === '') return 0;
  34. const n = Number(val);
  35. if (Number.isFinite(n) && n > 1e11) return n;
  36. const d = new Date(val);
  37. return Number.isNaN(d.getTime()) ? 0 : d.getTime();
  38. };
  39. const matchesGroupPeriod = (item, group, period) => {
  40. const g = normalizeGroupName(item.c_ssqpjt);
  41. if (g !== group) return false;
  42. const fillMonth = extractFillMonth(item);
  43. if (fillMonth) return fillMonth === period;
  44. return extractPeriod(item, period) === period;
  45. };
  46. const groupHasAnySubmission = (lists, group, period) =>
  47. lists.some((list) => list.some((item) => matchesGroupPeriod(item, group, period)));
  48. const groupHasApproved = (approvedList, group, period) =>
  49. approvedList.some((item) => matchesGroupPeriod(item, group, period));
  50. const groupHasRejected = (rejectedList, group, period) =>
  51. rejectedList.some((item) => matchesGroupPeriod(item, group, period));
  52. const buildRoute = (tab, query = {}) => buildManageRoute({ tab, ...query });
  53. const notifyYwid = (prefix, period, group, suffix = '') =>
  54. suffix ? `${prefix}:${period}:${group}:${suffix}` : `${prefix}:${period}:${group}`;
  55. /** 月报聚合记录唯一 ywid(账期 + 青浦集团 + 所属集团,避免同集团多条记录互相覆盖) */
  56. const notifyAggregateYwid = (prefix, record) => {
  57. const qingpuGroup = (record.qingpuGroup ?? '').trim() || 'unknown';
  58. const groupName = (record.group ?? '').trim() || qingpuGroup;
  59. return `${prefix}:${record.period}:${qingpuGroup}:${groupName}`;
  60. };
  61. /** 通知列表过滤、展示用青浦集团 */
  62. const notifyDisplayGroup = (record) => record.qingpuGroup || record.group;
  63. /** 截止前剩余天数 → c_tbtx(1=7天,2=3天,3=1天) */
  64. const getReminderTbtx = (daysLeft) => {
  65. if (daysLeft <= 0) return 0;
  66. if (daysLeft <= 1) return 3;
  67. if (daysLeft <= 3) return 2;
  68. if (daysLeft <= 7) return 1;
  69. return 0;
  70. };
  71. const reminderTitle = (period, group, daysLeft) => {
  72. const label = daysLeft <= 1 ? '1 天' : `${daysLeft} 天`;
  73. return `${period} 月报截止倒计时 ${label}(${group})`;
  74. };
  75. /**
  76. * 生成当前时刻所有适用的通知(幂等,同 c_ywid 不会重复入库)
  77. */
  78. export const buildNotifications = ({
  79. pendingList = [],
  80. approvedList = [],
  81. rejectedList = [],
  82. } = {}, {
  83. currentGroup = '',
  84. } = {}) => {
  85. const notifications = [];
  86. const now = Date.now();
  87. const currentPeriod = getCurrentPeriod();
  88. const deadline = getPeriodDeadlineDate(currentPeriod);
  89. const daysLeft = Math.ceil((deadline.getTime() - now) / (86400000));
  90. const reminderTbtx = getReminderTbtx(daysLeft);
  91. // ── 待审核 ────────────────────────────────────────────────
  92. aggregateReportRecords(pendingList, { fallbackPeriod: currentPeriod }).forEach((record) => {
  93. const group = notifyDisplayGroup(record);
  94. if (currentGroup && group !== currentGroup) return;
  95. const latest = record.items?.reduce((best, item) => {
  96. const t = toTimestamp(getFillTime(item));
  97. return t >= toTimestamp(getFillTime(best)) ? item : best;
  98. }, record.items?.[0]);
  99. const title = `${group} ${record.period} 资产月报(${record.count} 条)待审核`;
  100. notifications.push({
  101. ywid: notifyAggregateYwid('pending', record),
  102. time: record.submittedAt,
  103. type: '待审核',
  104. title,
  105. content: title,
  106. timestamp: toTimestamp(getFillTime(latest)),
  107. group,
  108. tzlx: NOTIFY_TZLX.PENDING,
  109. tbtx: 0,
  110. shjg: 0,
  111. route: buildRoute('audit', {
  112. sub: 'pending',
  113. group: record.qingpuGroup || group,
  114. period: record.period,
  115. }),
  116. });
  117. });
  118. // ── 已通过 ────────────────────────────────────────────────
  119. aggregateReportRecords(approvedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
  120. const group = notifyDisplayGroup(record);
  121. if (currentGroup && group !== currentGroup) return;
  122. const latest = record.items?.reduce((best, item) => {
  123. const t = toTimestamp(getAuditTime(item));
  124. return t >= toTimestamp(getAuditTime(best)) ? item : best;
  125. }, record.items?.[0]);
  126. const title = `${record.period} 月报已通过(${record.group},${record.count} 条)`;
  127. notifications.push({
  128. ywid: notifyAggregateYwid('approved', record),
  129. time: formatDateTime(getAuditTime(latest)),
  130. type: '审核结果',
  131. title,
  132. content: title,
  133. timestamp: toTimestamp(getAuditTime(latest)),
  134. group,
  135. tzlx: NOTIFY_TZLX.AUDIT_RESULT,
  136. tbtx: 0,
  137. shjg: 1,
  138. route: buildRoute('archive', {
  139. sub: 'history',
  140. group: record.qingpuGroup || group,
  141. period: record.period,
  142. status: '已通过',
  143. }),
  144. });
  145. });
  146. // ── 已驳回 ────────────────────────────────────────────────
  147. aggregateReportRecords(rejectedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
  148. const group = notifyDisplayGroup(record);
  149. if (currentGroup && group !== currentGroup) return;
  150. const latest = record.items?.reduce((best, item) => {
  151. const t = toTimestamp(getAuditTime(item));
  152. return t >= toTimestamp(getAuditTime(best)) ? item : best;
  153. }, record.items?.[0]);
  154. const reason = getRejectReason(latest);
  155. const title = `${record.period} 月报已被驳回(${record.group})${reason ? `:${reason}` : ''}`;
  156. notifications.push({
  157. ywid: notifyAggregateYwid('rejected', record),
  158. time: formatDateTime(getAuditTime(latest)),
  159. type: '审核结果',
  160. title,
  161. content: title,
  162. timestamp: toTimestamp(getAuditTime(latest)),
  163. group,
  164. tzlx: NOTIFY_TZLX.AUDIT_RESULT,
  165. tbtx: 0,
  166. shjg: 0,
  167. route: buildRoute('archive', {
  168. sub: 'history',
  169. group: record.qingpuGroup || group,
  170. period: record.period,
  171. status: '已驳回',
  172. }),
  173. });
  174. });
  175. // ── 填报提醒(截止前 7/3/1 天,尚未提交的集团)────────────
  176. if (reminderTbtx > 0) {
  177. const allLists = [pendingList, approvedList, rejectedList];
  178. GROUP_ORDER.forEach((group) => {
  179. if (currentGroup && group !== currentGroup) return;
  180. if (groupHasAnySubmission(allLists, group, currentPeriod)) return;
  181. const title = reminderTitle(currentPeriod, group, daysLeft);
  182. notifications.push({
  183. ywid: notifyYwid('reminder', currentPeriod, group, String(reminderTbtx)),
  184. time: formatDateTime(now),
  185. type: '填报提醒',
  186. title,
  187. content: title,
  188. timestamp: now,
  189. group,
  190. tzlx: NOTIFY_TZLX.REMINDER,
  191. tbtx: reminderTbtx,
  192. shjg: 0,
  193. route: buildRoute('archive'),
  194. });
  195. });
  196. }
  197. // ── 逾期催办 ──────────────────────────────────────────────
  198. const prevPeriod = getPreviousPeriod(currentPeriod);
  199. const prevDeadline = getPeriodDeadlineDate(prevPeriod);
  200. if (now > prevDeadline.getTime()) {
  201. GROUP_ORDER.forEach((group) => {
  202. if (currentGroup && group !== currentGroup) return;
  203. if (groupHasApproved(approvedList, group, prevPeriod)) return;
  204. const rejected = groupHasRejected(rejectedList, group, prevPeriod);
  205. const hasAny = groupHasAnySubmission(
  206. [pendingList, approvedList, rejectedList],
  207. group,
  208. prevPeriod
  209. );
  210. if (!hasAny) {
  211. const title = `${prevPeriod} 月报已逾期,请补报(${group})`;
  212. notifications.push({
  213. ywid: notifyYwid('overdue', prevPeriod, group),
  214. time: formatDateTime(prevDeadline),
  215. type: '逾期催办',
  216. title,
  217. content: title,
  218. timestamp: prevDeadline.getTime(),
  219. group,
  220. tzlx: NOTIFY_TZLX.OVERDUE,
  221. tbtx: 0,
  222. shjg: 0,
  223. route: buildRoute('archive'),
  224. });
  225. return;
  226. }
  227. if (rejected) {
  228. const title = `${prevPeriod} 月报被驳回且未重报,请尽快处理(${group})`;
  229. notifications.push({
  230. ywid: notifyYwid('overdue-reject', prevPeriod, group),
  231. time: formatDateTime(now),
  232. type: '逾期催办',
  233. title,
  234. content: title,
  235. timestamp: now,
  236. group,
  237. tzlx: NOTIFY_TZLX.OVERDUE,
  238. tbtx: 0,
  239. shjg: 0,
  240. route: buildRoute('archive', { sub: 'history', group, period: prevPeriod, status: '已驳回' }),
  241. });
  242. }
  243. });
  244. }
  245. // ── 租约到期/预警 ─────────────────────────────────────────
  246. const expiryItems = approvedList
  247. .filter((item) => item.status === 'red' || item.status === 'orange')
  248. .slice(0, 8);
  249. expiryItems.forEach((item) => {
  250. const label = item.c_bh || item.c_qymc || '资产';
  251. const withinOneMonth = item.status === 'red';
  252. const group = normalizeGroupName(item.c_ssqpjt) || '未知';
  253. if (currentGroup && group !== currentGroup) return;
  254. const title = `${label} 租约${withinOneMonth ? '将于一个月内到期' : '将于三个月内到期'},请关注`;
  255. notifications.push({
  256. ywid: `expiry:${item.id}`,
  257. time: formatDateTime(item.update_time || now),
  258. type: withinOneMonth ? '租约到期' : '租约预警',
  259. title,
  260. content: title,
  261. timestamp: toTimestamp(item.update_time || now),
  262. group,
  263. tzlx: withinOneMonth ? NOTIFY_TZLX.LEASE_EXPIRY : NOTIFY_TZLX.LEASE_WARNING,
  264. tbtx: 0,
  265. shjg: 0,
  266. route: buildRoute('assets'),
  267. });
  268. });
  269. return notifications.sort((a, b) => b.timestamp - a.timestamp);
  270. };