feat(uat-batch-13): activity feed resolves user UUIDs to display names
Audit-log rows with user-FK diffs (assignedTo, ownerId, reassignedTo, createdBy, addedBy, changedBy, transferredBy) previously rendered the raw user UUID in the activity feed (e.g. "→ mEcsLxo5kyFMyhbOSehxJjY…"). Same gap on the row's actor — the rep had no idea who did what. - getRecentActivity collects all userIds referenced by either the row's actor (auditLogs.userId) or by user-FK diff values, then bulk-fetches user_profiles in a single query. Output rows now carry an `actorName` field and have their `oldValue`/`newValue` swapped for display names on user-FK fields. - Unknown / deleted users fall back to "Unknown user (#short-uuid)" so the audit trail stays useful for forensics. - ActivityItem client type extended with `actorName`. Existing consumers still read the raw `userId` for forensics + deep-link. tsc clean. 1419/1419 vitest pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ import { documents } from '@/lib/db/schema/documents';
|
||||
import { reminders } from '@/lib/db/schema/operations';
|
||||
import { ports } from '@/lib/db/schema/ports';
|
||||
import { systemSettings, auditLogs } from '@/lib/db/schema/system';
|
||||
import { userProfiles } from '@/lib/db/schema/users';
|
||||
import { PIPELINE_STAGES, STAGE_WEIGHTS } from '@/lib/constants';
|
||||
import { activeInterestsWhere } from '@/lib/services/active-interest';
|
||||
import { convert as convertCurrency } from '@/lib/services/currency';
|
||||
@@ -454,8 +455,61 @@ export async function getRecentActivity(portId: string, limit = 20) {
|
||||
),
|
||||
]);
|
||||
|
||||
return rows.map((r) => ({
|
||||
...r,
|
||||
label: r.entityId ? (labels.get(`${r.entityType}:${r.entityId}`) ?? null) : null,
|
||||
}));
|
||||
// Resolve user UUIDs that appear as the actor (auditLogs.userId) and
|
||||
// as oldValue/newValue on user-FK diff rows (assignedTo, ownerId,
|
||||
// reassignedTo, createdBy). Activity-feed audit-log rows previously
|
||||
// rendered the raw UUID prefix, which was unreadable.
|
||||
const USER_FK_FIELDS = new Set([
|
||||
'assignedTo',
|
||||
'ownerId',
|
||||
'reassignedTo',
|
||||
'createdBy',
|
||||
'addedBy',
|
||||
'changedBy',
|
||||
'transferredBy',
|
||||
]);
|
||||
const userIds = new Set<string>();
|
||||
for (const r of rows) {
|
||||
if (r.userId) userIds.add(r.userId);
|
||||
if (r.fieldChanged && USER_FK_FIELDS.has(r.fieldChanged)) {
|
||||
if (typeof r.oldValue === 'string') userIds.add(r.oldValue);
|
||||
if (typeof r.newValue === 'string') userIds.add(r.newValue);
|
||||
}
|
||||
}
|
||||
const userNames = new Map<string, string>();
|
||||
if (userIds.size > 0) {
|
||||
const profiles = await db
|
||||
.select({
|
||||
userId: userProfiles.userId,
|
||||
displayName: userProfiles.displayName,
|
||||
firstName: userProfiles.firstName,
|
||||
lastName: userProfiles.lastName,
|
||||
})
|
||||
.from(userProfiles)
|
||||
.where(inArray(userProfiles.userId, Array.from(userIds)));
|
||||
for (const p of profiles) {
|
||||
const name = [p.firstName, p.lastName].filter(Boolean).join(' ').trim() || p.displayName;
|
||||
userNames.set(p.userId, name);
|
||||
}
|
||||
}
|
||||
function resolveUser(id: unknown): unknown {
|
||||
if (typeof id !== 'string') return id;
|
||||
const name = userNames.get(id);
|
||||
if (name) return name;
|
||||
return `Unknown user (#${id.slice(0, 8)})`;
|
||||
}
|
||||
|
||||
return rows.map((r) => {
|
||||
const isUserFk = r.fieldChanged && USER_FK_FIELDS.has(r.fieldChanged);
|
||||
return {
|
||||
...r,
|
||||
label: r.entityId ? (labels.get(`${r.entityType}:${r.entityId}`) ?? null) : null,
|
||||
// Replace user UUIDs with display names; non-user-FK rows pass through.
|
||||
oldValue: isUserFk ? resolveUser(r.oldValue) : r.oldValue,
|
||||
newValue: isUserFk ? resolveUser(r.newValue) : r.newValue,
|
||||
// Surfaces the actor's name to the renderer; original userId stays
|
||||
// available for forensics / deep-link if a later UI needs it.
|
||||
actorName: r.userId ? (userNames.get(r.userId) ?? null) : null,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user