mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Violation report: clean up Activity History formatting
The "Violation Updated" entries rendered raw change diffs: a "→" arrow the PDF font can't draw (showed as "!'"), snake_case field names, "—" for empty, and a dense single-paragraph wall. - Render-time (fixes existing reports): normalizeTimelineText now maps "→"→"to" and smart quotes/dashes/ellipsis to ASCII, prettifies snake_case field labels (violation_type → "Violation Type", etc.), and puts each change on its own line for proper spacing. - Logger (future entries): logViolationUpdated/logBulkUpdate now write professional notes — "Description / Notes changed from "X" to "Y".", "… set to …", "… cleared." — with Title-Case labels, long values truncated to 80 chars, and one change per line. No arrows or raw field names. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -45,8 +45,32 @@ const safeStr = (val) => {
|
||||
return cleanText(String(val));
|
||||
};
|
||||
|
||||
// Field-name → human label for change logs stored as snake_case.
|
||||
const TIMELINE_FIELD_LABELS = {
|
||||
violation_type: 'Violation Type', article_section: 'Article / Section',
|
||||
requested_action: 'Description / Notes', citation: 'Citation', address: 'Address',
|
||||
due_date: 'Due Date', violation_date: 'Violation Date', status: 'Status',
|
||||
stage: 'Notice Level', notice_level: 'Notice Level', priority: 'Priority',
|
||||
notes: 'Notes', assigned_to: 'Assigned To', title: 'Title',
|
||||
};
|
||||
|
||||
const normalizeTimelineText = (val) => {
|
||||
const text = safeStr(val);
|
||||
let text = safeStr(val);
|
||||
// Map glyphs the PDF font (Helvetica/WinAnsi) can't render to ASCII so they
|
||||
// don't show as artifacts (e.g. the "→" arrow rendering as "!'").
|
||||
text = text
|
||||
.replace(/\s*→\s*/g, ' to ') // → arrow
|
||||
.replace(/[‘’′]/g, "'") // ' ' ′ smart single quotes
|
||||
.replace(/[“”″]/g, '"') // " " ″ smart double quotes
|
||||
.replace(/–/g, '-') // – en dash
|
||||
.replace(/…/g, '...'); // … ellipsis
|
||||
// Prettify snake_case field labels left in older change logs.
|
||||
for (const [key, label] of Object.entries(TIMELINE_FIELD_LABELS)) {
|
||||
text = text.replace(new RegExp(`(^|[\\s;(])${key}:`, 'g'), `$1${label}:`);
|
||||
}
|
||||
// Put each change on its own line for readable paragraph spacing.
|
||||
text = text.replace(/;\s+/g, '\n');
|
||||
// Repair stray letter-spacing artifacts on a per-line basis.
|
||||
return text.split('\n').map((line) => {
|
||||
const singleLetterMatches = line.match(/(?:^|\s)[A-Za-z](?=\s)/g) || [];
|
||||
if (singleLetterMatches.length < 8) return line;
|
||||
|
||||
@@ -11,6 +11,37 @@ import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
// Human-readable labels for change logs (avoids raw snake_case in reports).
|
||||
const FIELD_LABELS: Record<string, string> = {
|
||||
violation_type: "Violation Type",
|
||||
article_section: "Article / Section",
|
||||
requested_action: "Description / Notes",
|
||||
citation: "Citation",
|
||||
address: "Address",
|
||||
due_date: "Due Date",
|
||||
violation_date: "Violation Date",
|
||||
status: "Status",
|
||||
stage: "Notice Level",
|
||||
notice_level: "Notice Level",
|
||||
priority: "Priority",
|
||||
notes: "Notes",
|
||||
assigned_to: "Assigned To",
|
||||
title: "Title",
|
||||
};
|
||||
|
||||
const fieldLabel = (field: string): string =>
|
||||
FIELD_LABELS[field] ??
|
||||
field.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
|
||||
// Trim a value for an audit note: collapse whitespace, cap long text so a long
|
||||
// citation doesn't become a wall of text. Empty/none -> "".
|
||||
const fmtChangeValue = (v: unknown): string => {
|
||||
if (v === null || v === undefined) return "";
|
||||
const s = String(v).replace(/\s+/g, " ").trim();
|
||||
if (!s || s === "—") return "";
|
||||
return s.length > 80 ? `${s.slice(0, 80).trimEnd()}...` : s;
|
||||
};
|
||||
|
||||
const estNow = () =>
|
||||
new Date().toLocaleString("en-US", { timeZone: "America/New_York" });
|
||||
|
||||
@@ -254,14 +285,14 @@ export async function logBulkUpdate(
|
||||
const userId = await currentUserId();
|
||||
|
||||
const fieldDescriptions: string[] = [];
|
||||
if (changes.status) fieldDescriptions.push(`status → "${changes.status}"`);
|
||||
if (changes.stage) fieldDescriptions.push(`stage → "${changes.stage}"`);
|
||||
if (changes.priority) fieldDescriptions.push(`priority → "${changes.priority}"`);
|
||||
if (changes.due_date) fieldDescriptions.push(`due date → ${changes.due_date}`);
|
||||
if (changes.violation_date) fieldDescriptions.push(`violation date → ${changes.violation_date}`);
|
||||
if (changes.notes) fieldDescriptions.push("notes updated");
|
||||
if (changes.status) fieldDescriptions.push(`Status set to "${changes.status}"`);
|
||||
if (changes.stage) fieldDescriptions.push(`Notice Level set to "${changes.stage}"`);
|
||||
if (changes.priority) fieldDescriptions.push(`Priority set to "${changes.priority}"`);
|
||||
if (changes.due_date) fieldDescriptions.push(`Due Date set to ${changes.due_date}`);
|
||||
if (changes.violation_date) fieldDescriptions.push(`Violation Date set to ${changes.violation_date}`);
|
||||
if (changes.notes) fieldDescriptions.push("Notes updated");
|
||||
|
||||
const details = `Bulk update by ${userName}: ${fieldDescriptions.join(", ")}.`;
|
||||
const details = `Bulk update by ${userName}. ${fieldDescriptions.join("; ")}.`;
|
||||
|
||||
await appendTimelineEntry(violationId, {
|
||||
action: "Bulk Update Applied",
|
||||
@@ -285,12 +316,18 @@ export async function logViolationUpdated(
|
||||
const userName = await currentUserName();
|
||||
const userId = await currentUserId();
|
||||
|
||||
const fieldDescriptions = Object.entries(changes).map(
|
||||
([field, { from, to }]) =>
|
||||
`${field}: "${from || "—"}" → "${to || "—"}"`
|
||||
);
|
||||
const fieldDescriptions = Object.entries(changes).map(([field, { from, to }]) => {
|
||||
const label = fieldLabel(field);
|
||||
const f = fmtChangeValue(from);
|
||||
const t = fmtChangeValue(to);
|
||||
if (!f && !t) return `${label} updated.`;
|
||||
if (!f) return `${label} set to "${t}".`;
|
||||
if (!t) return `${label} cleared.`;
|
||||
return `${label} changed from "${f}" to "${t}".`;
|
||||
});
|
||||
|
||||
const details = `Violation updated by ${userName}. Changes: ${fieldDescriptions.join("; ")}.`;
|
||||
// One change per line for readable paragraph spacing in the PDF/report.
|
||||
const details = `Violation updated by ${userName}.\n${fieldDescriptions.join("\n")}`;
|
||||
|
||||
await appendTimelineEntry(violationId, {
|
||||
action: "Violation Updated",
|
||||
|
||||
Reference in New Issue
Block a user