|
|
@@ -1,14 +1,14 @@
|
|
|
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';
|
|
|
|
|
|
const pad2 = (n) => String(n).padStart(2, '0');
|
|
|
@@ -25,12 +25,12 @@ const getPreviousPeriod = (period) => {
|
|
|
return `${prevYear}-${pad2(prevMonth)}`;
|
|
|
};
|
|
|
|
|
|
-/** 账期截止:次月 5 日 23:59:59 */
|
|
|
+/** 账期截止:次月 1 日 23:59:59 */
|
|
|
const getPeriodDeadlineDate = (period) => {
|
|
|
const [year, month] = period.split('-').map(Number);
|
|
|
const nextMonth = month === 12 ? 1 : month + 1;
|
|
|
const nextYear = month === 12 ? year + 1 : year;
|
|
|
- return new Date(nextYear, nextMonth - 1, 5, 23, 59, 59);
|
|
|
+ return new Date(nextYear, nextMonth - 1, 1, 23, 59, 59);
|
|
|
};
|
|
|
|
|
|
const toTimestamp = (val) => {
|
|
|
@@ -41,208 +41,202 @@ 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 });
|
|
|
|
|
|
-/** 通知 id 仅依赖类型 + 账期 + 集团,保证同一条通知不会重复入库 */
|
|
|
-const notifyRecordId = (type, record) => {
|
|
|
- const qingpuGroup = (record.qingpuGroup ?? '').trim() || 'unknown';
|
|
|
- const group = (record.group ?? '').trim() || 'unknown';
|
|
|
- return `${type}:${record.period}:${qingpuGroup}:${group}`;
|
|
|
+const notifyYwid = (prefix, period, group) => `${prefix}:${period}:${group}`;
|
|
|
+
|
|
|
+/** 按账期 + 青浦集团整合(同一集团一条) */
|
|
|
+const groupItemsByQingpuGroup = (items, { fallbackPeriod = '2026-01' } = {}) => {
|
|
|
+ const map = new Map();
|
|
|
+
|
|
|
+ items.forEach((item) => {
|
|
|
+ const period = extractPeriod(item, fallbackPeriod);
|
|
|
+ const qingpuGroup = normalizeGroupName((item.c_ssqpjt ?? '').trim());
|
|
|
+ if (!qingpuGroup) return;
|
|
|
+ const key = `${period}|${qingpuGroup}`;
|
|
|
+
|
|
|
+ if (!map.has(key)) {
|
|
|
+ map.set(key, { period, qingpuGroup, items: [] });
|
|
|
+ }
|
|
|
+ map.get(key).items.push(item);
|
|
|
+ });
|
|
|
+
|
|
|
+ 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));
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
- * 生成当前时刻所有适用的通知(幂等,同 id 不会重复入库)
|
|
|
- * @param {{ pendingList: object[], approvedList: object[], rejectedList: object[] }} lists
|
|
|
- * @param {{ currentGroup?: string }} options - 可选,限定集团范围
|
|
|
- * @returns {Array<{ id: string, time: string, type: string, title: string, timestamp: number, route?: object, group: string }>}
|
|
|
+ * 持久化通知:待审核 / 审核结果 / 逾期催办(不含填报期提醒)
|
|
|
+ * 同一账期 + 青浦集团整合为一条;再次提交/审核时更新条数与时间
|
|
|
*/
|
|
|
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));
|
|
|
-
|
|
|
- // ── 待审核(来自 pendingList)─────────────────────────────
|
|
|
- aggregateReportRecords(pendingList, { fallbackPeriod: currentPeriod }).forEach((record) => {
|
|
|
- const group = record.qingpuGroup || record.group;
|
|
|
- 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]);
|
|
|
+
|
|
|
+ // ── 待审核:同一账期 + 青浦集团一条(管理端)──────────────
|
|
|
+ groupItemsByQingpuGroup(pendingList, { fallbackPeriod: currentPeriod }).forEach((batch) => {
|
|
|
+ const group = batch.qingpuGroup;
|
|
|
+ const title = `${group} ${batch.period} 资产月报(${batch.count} 条)待审核`;
|
|
|
+
|
|
|
notifications.push({
|
|
|
- id: notifyRecordId('pending', record),
|
|
|
- time: record.submittedAt,
|
|
|
+ ywid: notifyYwid('pending', batch.period, group),
|
|
|
+ time: batch.submittedAt,
|
|
|
type: '待审核',
|
|
|
- title: `${group} ${record.period} 资产月报(${record.count} 条)待审核`,
|
|
|
- timestamp: toTimestamp(getFillTime(latest)),
|
|
|
+ title,
|
|
|
+ content: title,
|
|
|
+ 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,
|
|
|
}),
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ── 已通过(来自 approvedList)────────────────────────────
|
|
|
- aggregateReportRecords(approvedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
|
|
|
- const group = record.qingpuGroup || record.group;
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
+ // ── 已通过:同一账期 + 青浦集团一条(企业端)──────────────
|
|
|
+ groupItemsByQingpuGroup(approvedList, { fallbackPeriod: currentPeriod }).forEach((batch) => {
|
|
|
+ const group = batch.qingpuGroup;
|
|
|
+ const latest = batch.latestAuditItem;
|
|
|
+ const title = `${batch.period} 月报已通过(${group},${batch.count} 条)`;
|
|
|
|
|
|
- const latest = record.items?.reduce((best, item) => {
|
|
|
- const t = toTimestamp(getAuditTime(item));
|
|
|
- return t >= toTimestamp(getAuditTime(best)) ? item : best;
|
|
|
- }, record.items?.[0]);
|
|
|
notifications.push({
|
|
|
- id: notifyRecordId('approved', record),
|
|
|
+ ywid: notifyYwid('approved', batch.period, group),
|
|
|
time: formatDateTime(getAuditTime(latest)),
|
|
|
type: '审核结果',
|
|
|
- title: `${record.period} 月报已通过(${group},${record.count} 条)`,
|
|
|
+ title,
|
|
|
+ content: title,
|
|
|
timestamp: toTimestamp(getAuditTime(latest)),
|
|
|
group,
|
|
|
+ tzlx: NOTIFY_TZLX.AUDIT_RESULT,
|
|
|
+ tbtx: 0,
|
|
|
+ shjg: 1,
|
|
|
route: buildRoute('archive', {
|
|
|
sub: 'history',
|
|
|
- group: record.qingpuGroup || group,
|
|
|
- period: record.period,
|
|
|
+ group,
|
|
|
+ period: batch.period,
|
|
|
status: '已通过',
|
|
|
}),
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ── 已驳回(来自 rejectedList)────────────────────────────
|
|
|
- aggregateReportRecords(rejectedList, { fallbackPeriod: currentPeriod }).forEach((record) => {
|
|
|
- const group = record.qingpuGroup || record.group;
|
|
|
- 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 = `${batch.period} 月报已被驳回(${group},${batch.count} 条)${reason ? `:${reason}` : ''}`;
|
|
|
+
|
|
|
notifications.push({
|
|
|
- id: notifyRecordId('rejected', record),
|
|
|
+ ywid: notifyYwid('rejected', batch.period, group),
|
|
|
time: formatDateTime(getAuditTime(latest)),
|
|
|
type: '审核结果',
|
|
|
- title: `${record.period} 月报已被驳回(${group})${reason ? `:${reason}` : ''}`,
|
|
|
+ title,
|
|
|
+ content: title,
|
|
|
timestamp: toTimestamp(getAuditTime(latest)),
|
|
|
group,
|
|
|
+ tzlx: NOTIFY_TZLX.AUDIT_RESULT,
|
|
|
+ tbtx: 0,
|
|
|
+ shjg: 0,
|
|
|
route: buildRoute('archive', {
|
|
|
sub: 'history',
|
|
|
- group: record.qingpuGroup || group,
|
|
|
- period: record.period,
|
|
|
+ group,
|
|
|
+ period: batch.period,
|
|
|
status: '已驳回',
|
|
|
}),
|
|
|
});
|
|
|
});
|
|
|
|
|
|
- // ── 填报提醒(当前账期截止前 7 天,尚未提交的集团)────────
|
|
|
- if (daysLeft > 0 && daysLeft <= 7) {
|
|
|
- const allLists = [pendingList, approvedList, rejectedList];
|
|
|
- GROUP_ORDER.forEach((group) => {
|
|
|
- if (currentGroup && group !== currentGroup) return;
|
|
|
- if (groupHasAnySubmission(allLists, group, currentPeriod)) return;
|
|
|
- notifications.push({
|
|
|
- id: `reminder:${currentPeriod}:${group}`,
|
|
|
- time: formatDateTime(now),
|
|
|
- type: '填报提醒',
|
|
|
- title: `${currentPeriod} 月报截止倒计时 ${daysLeft} 天(${group})`,
|
|
|
- timestamp: now,
|
|
|
- group,
|
|
|
- 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],
|
|
|
+ const title = `${prevPeriod} 月报已逾期,请补报(${group})`;
|
|
|
+ notifications.push({
|
|
|
+ ywid: notifyYwid('overdue', prevPeriod, group),
|
|
|
+ time: formatDateTime(prevDeadline),
|
|
|
+ type: '逾期催办',
|
|
|
+ title,
|
|
|
+ content: title,
|
|
|
+ timestamp: prevDeadline.getTime(),
|
|
|
group,
|
|
|
- prevPeriod
|
|
|
- );
|
|
|
-
|
|
|
- if (!hasAny) {
|
|
|
- notifications.push({
|
|
|
- id: `overdue:${prevPeriod}:${group}`,
|
|
|
- time: formatDateTime(prevDeadline),
|
|
|
- type: '逾期催办',
|
|
|
- title: `${prevPeriod} 月报已逾期,请补报(${group})`,
|
|
|
- timestamp: prevDeadline.getTime(),
|
|
|
- group,
|
|
|
- route: buildRoute('archive'),
|
|
|
- });
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (rejected) {
|
|
|
- notifications.push({
|
|
|
- id: `overdue-reject:${prevPeriod}:${group}`,
|
|
|
- time: formatDateTime(now),
|
|
|
- type: '逾期催办',
|
|
|
- title: `${prevPeriod} 月报被驳回且未重报,请尽快处理(${group})`,
|
|
|
- timestamp: now,
|
|
|
- group,
|
|
|
- route: buildRoute('archive', { sub: 'history', group, period: prevPeriod, status: '已驳回' }),
|
|
|
- });
|
|
|
- }
|
|
|
+ tzlx: NOTIFY_TZLX.OVERDUE,
|
|
|
+ tbtx: 0,
|
|
|
+ shjg: 0,
|
|
|
+ route: buildRoute('archive'),
|
|
|
+ });
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- // ── 租约到期/预警(来自已通过资产的 status 字段)──────────
|
|
|
- const expiryItems = approvedList
|
|
|
- .filter((item) => item.status === 'red' || item.status === 'orange')
|
|
|
- .slice(0, 8);
|
|
|
+ return notifications.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
+};
|
|
|
|
|
|
- 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;
|
|
|
+/**
|
|
|
+ * 填报期提醒:仅前端展示,不入库;本月无已通过则提醒,通过后自动消失
|
|
|
+ */
|
|
|
+export const buildEphemeralReminders = ({
|
|
|
+ approvedList = [],
|
|
|
+} = {}) => {
|
|
|
+ const currentPeriod = getCurrentPeriod();
|
|
|
+ const now = Date.now();
|
|
|
|
|
|
- notifications.push({
|
|
|
- id: `expiry:${item.id}`,
|
|
|
- time: formatDateTime(item.update_time || now),
|
|
|
- type: withinOneMonth ? '租约到期' : '租约预警',
|
|
|
- title: `${label} 租约${withinOneMonth ? '将于一个月内到期' : '将于三个月内到期'},请关注`,
|
|
|
- timestamp: toTimestamp(item.update_time || now),
|
|
|
+ return GROUP_ORDER.flatMap((group) => {
|
|
|
+ if (groupHasApproved(approvedList, group, currentPeriod)) return [];
|
|
|
+
|
|
|
+ const title = `${currentPeriod} 月报尚未通过审核,请尽快填报(${group})`;
|
|
|
+ return [{
|
|
|
+ id: `ephemeral:reminder:${currentPeriod}:${group}`,
|
|
|
+ ywid: `ephemeral:reminder:${currentPeriod}:${group}`,
|
|
|
+ time: formatDateTime(now),
|
|
|
+ timestamp: now,
|
|
|
+ type: '填报提醒',
|
|
|
+ title,
|
|
|
+ content: title,
|
|
|
+ read: false,
|
|
|
+ deleted: false,
|
|
|
+ ephemeral: true,
|
|
|
group,
|
|
|
- route: buildRoute('assets'),
|
|
|
- });
|
|
|
+ tzlx: NOTIFY_TZLX.REMINDER,
|
|
|
+ tbtx: 0,
|
|
|
+ shjg: 0,
|
|
|
+ route: buildRoute('archive'),
|
|
|
+ }];
|
|
|
});
|
|
|
-
|
|
|
- return notifications.sort((a, b) => b.timestamp - a.timestamp);
|
|
|
};
|