|
|
@@ -1,13 +1,12 @@
|
|
|
import {
|
|
|
GROUP_ORDER,
|
|
|
- aggregateReportRecords,
|
|
|
- extractFillMonth,
|
|
|
extractPeriod,
|
|
|
formatDateTime,
|
|
|
getAuditTime,
|
|
|
getFillTime,
|
|
|
getRejectReason,
|
|
|
normalizeGroupName,
|
|
|
+ matchesReportPeriod,
|
|
|
} from '@/utils/aggregateReportRecords';
|
|
|
import { NOTIFY_TZLX } from '@/utils/notificationFields';
|
|
|
import { buildManageRoute } from '@/utils/manageRoutes';
|
|
|
@@ -42,112 +41,101 @@ const toTimestamp = (val) => {
|
|
|
return Number.isNaN(d.getTime()) ? 0 : d.getTime();
|
|
|
};
|
|
|
|
|
|
-const matchesGroupPeriod = (item, group, period) => {
|
|
|
- const g = normalizeGroupName(item.c_ssqpjt);
|
|
|
- if (g !== group) return false;
|
|
|
- const fillMonth = extractFillMonth(item);
|
|
|
- if (fillMonth) return fillMonth === period;
|
|
|
- return extractPeriod(item, period) === period;
|
|
|
-};
|
|
|
-
|
|
|
-const groupHasAnySubmission = (lists, group, period) =>
|
|
|
- lists.some((list) => list.some((item) => matchesGroupPeriod(item, group, period)));
|
|
|
+const matchesGroupPeriod = (item, group, period) =>
|
|
|
+ normalizeGroupName(item.c_ssqpjt) === group && matchesReportPeriod(item, period);
|
|
|
|
|
|
const groupHasApproved = (approvedList, group, period) =>
|
|
|
approvedList.some((item) => matchesGroupPeriod(item, group, period));
|
|
|
|
|
|
-const groupHasRejected = (rejectedList, group, period) =>
|
|
|
- rejectedList.some((item) => matchesGroupPeriod(item, group, period));
|
|
|
-
|
|
|
const buildRoute = (tab, query = {}) => buildManageRoute({ tab, ...query });
|
|
|
|
|
|
-const notifyYwid = (prefix, period, group, suffix = '') =>
|
|
|
- suffix ? `${prefix}:${period}:${group}:${suffix}` : `${prefix}:${period}:${group}`;
|
|
|
+const notifyYwid = (prefix, period, group) => `${prefix}:${period}:${group}`;
|
|
|
|
|
|
-/** 月报聚合记录唯一 ywid(账期 + 青浦集团 + 所属集团,避免同集团多条记录互相覆盖) */
|
|
|
-const notifyAggregateYwid = (prefix, record) => {
|
|
|
- const qingpuGroup = (record.qingpuGroup ?? '').trim() || 'unknown';
|
|
|
- const groupName = (record.group ?? '').trim() || qingpuGroup;
|
|
|
- return `${prefix}:${record.period}:${qingpuGroup}:${groupName}`;
|
|
|
-};
|
|
|
+/** 按账期 + 青浦集团整合(同一集团一条) */
|
|
|
+const groupItemsByQingpuGroup = (items, { fallbackPeriod = '2026-01' } = {}) => {
|
|
|
+ const map = new Map();
|
|
|
|
|
|
-/** 通知列表过滤、展示用青浦集团 */
|
|
|
-const notifyDisplayGroup = (record) => record.qingpuGroup || record.group;
|
|
|
+ items.forEach((item) => {
|
|
|
+ const period = extractPeriod(item, fallbackPeriod);
|
|
|
+ const qingpuGroup = normalizeGroupName((item.c_ssqpjt ?? '').trim());
|
|
|
+ if (!qingpuGroup) return;
|
|
|
+ const key = `${period}|${qingpuGroup}`;
|
|
|
|
|
|
-/** 截止前剩余天数 → c_tbtx(1=7天,2=3天,3=1天) */
|
|
|
-const getReminderTbtx = (daysLeft) => {
|
|
|
- if (daysLeft <= 0) return 0;
|
|
|
- if (daysLeft <= 1) return 3;
|
|
|
- if (daysLeft <= 3) return 2;
|
|
|
- if (daysLeft <= 7) return 1;
|
|
|
- return 0;
|
|
|
-};
|
|
|
+ if (!map.has(key)) {
|
|
|
+ map.set(key, { period, qingpuGroup, items: [] });
|
|
|
+ }
|
|
|
+ map.get(key).items.push(item);
|
|
|
+ });
|
|
|
|
|
|
-const reminderTitle = (period, group, daysLeft) => {
|
|
|
- const label = daysLeft <= 1 ? '1 天' : `${daysLeft} 天`;
|
|
|
- return `${period} 月报截止倒计时 ${label}(${group})`;
|
|
|
+ return [...map.values()]
|
|
|
+ .map((batch) => {
|
|
|
+ const latestFill = batch.items.reduce((best, item) => {
|
|
|
+ const t = toTimestamp(getFillTime(item));
|
|
|
+ return t >= toTimestamp(getFillTime(best)) ? item : best;
|
|
|
+ }, batch.items[0]);
|
|
|
+
|
|
|
+ const latestAudit = batch.items.reduce((best, item) => {
|
|
|
+ const t = toTimestamp(getAuditTime(item));
|
|
|
+ return t >= toTimestamp(getAuditTime(best)) ? item : best;
|
|
|
+ }, batch.items[0]);
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...batch,
|
|
|
+ group: batch.qingpuGroup,
|
|
|
+ count: batch.items.length,
|
|
|
+ fillTime: getFillTime(latestFill),
|
|
|
+ submittedAt: formatDateTime(getFillTime(latestFill)),
|
|
|
+ latestAuditItem: latestAudit,
|
|
|
+ };
|
|
|
+ })
|
|
|
+ .sort((a, b) => toTimestamp(b.fillTime) - toTimestamp(a.fillTime));
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 生成当前时刻所有适用的通知(幂等,同 c_ywid 不会重复入库)
|
|
|
+ * 持久化通知:待审核 / 审核结果 / 逾期催办(不含填报期提醒)
|
|
|
+ * 同一账期 + 青浦集团整合为一条;再次提交/审核时更新条数与时间
|
|
|
*/
|
|
|
export const buildNotifications = ({
|
|
|
pendingList = [],
|
|
|
approvedList = [],
|
|
|
rejectedList = [],
|
|
|
-} = {}, {
|
|
|
- currentGroup = '',
|
|
|
} = {}) => {
|
|
|
const notifications = [];
|
|
|
const now = Date.now();
|
|
|
const currentPeriod = getCurrentPeriod();
|
|
|
- const deadline = getPeriodDeadlineDate(currentPeriod);
|
|
|
- const daysLeft = Math.ceil((deadline.getTime() - now) / (86400000));
|
|
|
- const reminderTbtx = getReminderTbtx(daysLeft);
|
|
|
-
|
|
|
- // ── 待审核 ────────────────────────────────────────────────
|
|
|
- aggregateReportRecords(pendingList, { fallbackPeriod: currentPeriod }).forEach((record) => {
|
|
|
- const group = notifyDisplayGroup(record);
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
|
|
|
- const latest = record.items?.reduce((best, item) => {
|
|
|
- const t = toTimestamp(getFillTime(item));
|
|
|
- return t >= toTimestamp(getFillTime(best)) ? item : best;
|
|
|
- }, record.items?.[0]);
|
|
|
- const title = `${group} ${record.period} 资产月报(${record.count} 条)待审核`;
|
|
|
+ // ── 待审核:同一账期 + 青浦集团一条(管理端)──────────────
|
|
|
+ groupItemsByQingpuGroup(pendingList, { fallbackPeriod: currentPeriod }).forEach((batch) => {
|
|
|
+ const group = batch.qingpuGroup;
|
|
|
+ const title = `${group} ${batch.period} 资产月报(${batch.count} 条)待审核`;
|
|
|
|
|
|
notifications.push({
|
|
|
- ywid: notifyAggregateYwid('pending', record),
|
|
|
- time: record.submittedAt,
|
|
|
+ ywid: notifyYwid('pending', batch.period, group),
|
|
|
+ time: batch.submittedAt,
|
|
|
type: '待审核',
|
|
|
title,
|
|
|
content: title,
|
|
|
- timestamp: toTimestamp(getFillTime(latest)),
|
|
|
+ timestamp: toTimestamp(batch.fillTime),
|
|
|
group,
|
|
|
tzlx: NOTIFY_TZLX.PENDING,
|
|
|
tbtx: 0,
|
|
|
shjg: 0,
|
|
|
route: buildRoute('audit', {
|
|
|
sub: 'pending',
|
|
|
- group: record.qingpuGroup || group,
|
|
|
- period: record.period,
|
|
|
+ group,
|
|
|
+ period: batch.period,
|
|
|
}),
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ── 已通过 ────────────────────────────────────────────────
|
|
|
- aggregateReportRecords(approvedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
|
|
|
- const group = notifyDisplayGroup(record);
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
-
|
|
|
- const latest = record.items?.reduce((best, item) => {
|
|
|
- const t = toTimestamp(getAuditTime(item));
|
|
|
- return t >= toTimestamp(getAuditTime(best)) ? item : best;
|
|
|
- }, record.items?.[0]);
|
|
|
- const title = `${record.period} 月报已通过(${record.group},${record.count} 条)`;
|
|
|
+ // ── 已通过:同一账期 + 青浦集团一条(企业端)──────────────
|
|
|
+ groupItemsByQingpuGroup(approvedList, { fallbackPeriod: currentPeriod }).forEach((batch) => {
|
|
|
+ const group = batch.qingpuGroup;
|
|
|
+ const latest = batch.latestAuditItem;
|
|
|
+ const title = `${batch.period} 月报已通过(${group},${batch.count} 条)`;
|
|
|
|
|
|
notifications.push({
|
|
|
- ywid: notifyAggregateYwid('approved', record),
|
|
|
+ ywid: notifyYwid('approved', batch.period, group),
|
|
|
time: formatDateTime(getAuditTime(latest)),
|
|
|
type: '审核结果',
|
|
|
title,
|
|
|
@@ -159,27 +147,22 @@ export const buildNotifications = ({
|
|
|
shjg: 1,
|
|
|
route: buildRoute('archive', {
|
|
|
sub: 'history',
|
|
|
- group: record.qingpuGroup || group,
|
|
|
- period: record.period,
|
|
|
+ group,
|
|
|
+ period: batch.period,
|
|
|
status: '已通过',
|
|
|
}),
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ── 已驳回 ────────────────────────────────────────────────
|
|
|
- aggregateReportRecords(rejectedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
|
|
|
- const group = notifyDisplayGroup(record);
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
-
|
|
|
- const latest = record.items?.reduce((best, item) => {
|
|
|
- const t = toTimestamp(getAuditTime(item));
|
|
|
- return t >= toTimestamp(getAuditTime(best)) ? item : best;
|
|
|
- }, record.items?.[0]);
|
|
|
+ // ── 已驳回:同一账期 + 青浦集团一条(企业端)──────────────
|
|
|
+ groupItemsByQingpuGroup(rejectedList, { fallbackPeriod: currentPeriod }).forEach((batch) => {
|
|
|
+ const group = batch.qingpuGroup;
|
|
|
+ const latest = batch.latestAuditItem;
|
|
|
const reason = getRejectReason(latest);
|
|
|
- const title = `${record.period} 月报已被驳回(${record.group})${reason ? `:${reason}` : ''}`;
|
|
|
+ const title = `${batch.period} 月报已被驳回(${group},${batch.count} 条)${reason ? `:${reason}` : ''}`;
|
|
|
|
|
|
notifications.push({
|
|
|
- ywid: notifyAggregateYwid('rejected', record),
|
|
|
+ ywid: notifyYwid('rejected', batch.period, group),
|
|
|
time: formatDateTime(getAuditTime(latest)),
|
|
|
type: '审核结果',
|
|
|
title,
|
|
|
@@ -191,116 +174,69 @@ export const buildNotifications = ({
|
|
|
shjg: 0,
|
|
|
route: buildRoute('archive', {
|
|
|
sub: 'history',
|
|
|
- group: record.qingpuGroup || group,
|
|
|
- period: record.period,
|
|
|
+ group,
|
|
|
+ period: batch.period,
|
|
|
status: '已驳回',
|
|
|
}),
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ── 填报提醒(截止前 7/3/1 天,尚未提交的集团)────────────
|
|
|
- if (reminderTbtx > 0) {
|
|
|
- const allLists = [pendingList, approvedList, rejectedList];
|
|
|
+ // ── 逾期催办:上月无已通过则一条(企业端,持久化)──────────
|
|
|
+ const prevPeriod = getPreviousPeriod(currentPeriod);
|
|
|
+ const prevDeadline = getPeriodDeadlineDate(prevPeriod);
|
|
|
+ if (now > prevDeadline.getTime()) {
|
|
|
GROUP_ORDER.forEach((group) => {
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
- if (groupHasAnySubmission(allLists, group, currentPeriod)) return;
|
|
|
+ if (groupHasApproved(approvedList, group, prevPeriod)) return;
|
|
|
|
|
|
- const title = reminderTitle(currentPeriod, group, daysLeft);
|
|
|
+ const title = `${prevPeriod} 月报已逾期,请补报(${group})`;
|
|
|
notifications.push({
|
|
|
- ywid: notifyYwid('reminder', currentPeriod, group, String(reminderTbtx)),
|
|
|
- time: formatDateTime(now),
|
|
|
- type: '填报提醒',
|
|
|
+ ywid: notifyYwid('overdue', prevPeriod, group),
|
|
|
+ time: formatDateTime(prevDeadline),
|
|
|
+ type: '逾期催办',
|
|
|
title,
|
|
|
content: title,
|
|
|
- timestamp: now,
|
|
|
+ timestamp: prevDeadline.getTime(),
|
|
|
group,
|
|
|
- tzlx: NOTIFY_TZLX.REMINDER,
|
|
|
- tbtx: reminderTbtx,
|
|
|
+ tzlx: NOTIFY_TZLX.OVERDUE,
|
|
|
+ tbtx: 0,
|
|
|
shjg: 0,
|
|
|
route: buildRoute('archive'),
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- // ── 逾期催办 ──────────────────────────────────────────────
|
|
|
- const prevPeriod = getPreviousPeriod(currentPeriod);
|
|
|
- const prevDeadline = getPeriodDeadlineDate(prevPeriod);
|
|
|
- if (now > prevDeadline.getTime()) {
|
|
|
- GROUP_ORDER.forEach((group) => {
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
-
|
|
|
- if (groupHasApproved(approvedList, group, prevPeriod)) return;
|
|
|
-
|
|
|
- const rejected = groupHasRejected(rejectedList, group, prevPeriod);
|
|
|
- const hasAny = groupHasAnySubmission(
|
|
|
- [pendingList, approvedList, rejectedList],
|
|
|
- group,
|
|
|
- prevPeriod
|
|
|
- );
|
|
|
-
|
|
|
- if (!hasAny) {
|
|
|
- const title = `${prevPeriod} 月报已逾期,请补报(${group})`;
|
|
|
- notifications.push({
|
|
|
- ywid: notifyYwid('overdue', prevPeriod, group),
|
|
|
- time: formatDateTime(prevDeadline),
|
|
|
- type: '逾期催办',
|
|
|
- title,
|
|
|
- content: title,
|
|
|
- timestamp: prevDeadline.getTime(),
|
|
|
- group,
|
|
|
- tzlx: NOTIFY_TZLX.OVERDUE,
|
|
|
- tbtx: 0,
|
|
|
- shjg: 0,
|
|
|
- route: buildRoute('archive'),
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (rejected) {
|
|
|
- const title = `${prevPeriod} 月报被驳回且未重报,请尽快处理(${group})`;
|
|
|
- notifications.push({
|
|
|
- ywid: notifyYwid('overdue-reject', prevPeriod, group),
|
|
|
- time: formatDateTime(now),
|
|
|
- type: '逾期催办',
|
|
|
- title,
|
|
|
- content: title,
|
|
|
- timestamp: now,
|
|
|
- group,
|
|
|
- tzlx: NOTIFY_TZLX.OVERDUE,
|
|
|
- tbtx: 0,
|
|
|
- shjg: 0,
|
|
|
- route: buildRoute('archive', { sub: 'history', group, period: prevPeriod, status: '已驳回' }),
|
|
|
- });
|
|
|
- }
|
|
|
- });
|
|
|
- }
|
|
|
+ return notifications.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
+};
|
|
|
|
|
|
- // ── 租约到期/预警 ─────────────────────────────────────────
|
|
|
- const expiryItems = approvedList
|
|
|
- .filter((item) => item.status === 'red' || item.status === 'orange')
|
|
|
- .slice(0, 8);
|
|
|
+/**
|
|
|
+ * 填报期提醒:仅前端展示,不入库;本月无已通过则提醒,通过后自动消失
|
|
|
+ */
|
|
|
+export const buildEphemeralReminders = ({
|
|
|
+ approvedList = [],
|
|
|
+} = {}) => {
|
|
|
+ const currentPeriod = getCurrentPeriod();
|
|
|
+ const now = Date.now();
|
|
|
|
|
|
- expiryItems.forEach((item) => {
|
|
|
- const label = item.c_bh || item.c_qymc || '资产';
|
|
|
- const withinOneMonth = item.status === 'red';
|
|
|
- const group = normalizeGroupName(item.c_ssqpjt) || '未知';
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
+ return GROUP_ORDER.flatMap((group) => {
|
|
|
+ if (groupHasApproved(approvedList, group, currentPeriod)) return [];
|
|
|
|
|
|
- const title = `${label} 租约${withinOneMonth ? '将于一个月内到期' : '将于三个月内到期'},请关注`;
|
|
|
- notifications.push({
|
|
|
- ywid: `expiry:${item.id}`,
|
|
|
- time: formatDateTime(item.update_time || now),
|
|
|
- type: withinOneMonth ? '租约到期' : '租约预警',
|
|
|
+ const title = `${currentPeriod} 月报尚未通过审核,请尽快填报(${group})`;
|
|
|
+ return [{
|
|
|
+ id: `ephemeral:reminder:${currentPeriod}:${group}`,
|
|
|
+ ywid: `ephemeral:reminder:${currentPeriod}:${group}`,
|
|
|
+ time: formatDateTime(now),
|
|
|
+ timestamp: now,
|
|
|
+ type: '填报提醒',
|
|
|
title,
|
|
|
content: title,
|
|
|
- timestamp: toTimestamp(item.update_time || now),
|
|
|
+ read: false,
|
|
|
+ deleted: false,
|
|
|
+ ephemeral: true,
|
|
|
group,
|
|
|
- tzlx: withinOneMonth ? NOTIFY_TZLX.LEASE_EXPIRY : NOTIFY_TZLX.LEASE_WARNING,
|
|
|
+ tzlx: NOTIFY_TZLX.REMINDER,
|
|
|
tbtx: 0,
|
|
|
shjg: 0,
|
|
|
- route: buildRoute('assets'),
|
|
|
- });
|
|
|
+ route: buildRoute('archive'),
|
|
|
+ }];
|
|
|
});
|
|
|
-
|
|
|
- return notifications.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
};
|