Task #348: Arabic-friendly Executive Meetings audit log

- Reorder audit table columns to RTL order: المستخدم، الوقت، الإجراء،
  النوع، التغييرات. Drop the entityId ("المعرف") column entirely. Update
  loading/empty colSpan from 6 to 5 to match the new column count.
- Rename audit.col.actor copy: "المنفّذ" → "المستخدم" (ar.json) and
  "Actor" → "User" (en.json). Underlying data field name unchanged.
- Localize the Changes cell in AuditDiffSummary:
  - Field-name labels (titleAr, titleEn, meetingDate, dailyNumber,
    startTime, endTime, location, meetingUrl, platform, status,
    isHighlighted, notes, assignedTo, taskType, dueAt, reviewDecision,
    reviewNotes) now resolve via executiveMeetings.audit.field.* with a
    raw-key fallback so unknown future fields don't crash the row.
  - Enum values render via existing label namespaces:
    `platform` → executiveMeetings.platform.*, `status` →
    executiveMeetings.status.*, `isHighlighted` → executiveMeetings.common.yes/no.
  - Both the diff view and the single-side created/removed view use the
    same labelForKey/fmtValue helpers.
- Added matching audit.field.* keys to ar.json and en.json with sensible
  Arabic and English labels.
- No test changes needed: no existing test asserts on the old column
  count, the "Actor"/"المنفّذ" header, the entityId column, or raw
  English field names in Changes.
- tsc passes for @workspace/tx-os.
This commit is contained in:
riyadhafraa
2026-05-03 13:57:22 +00:00
parent 0a18cf2abe
commit 3d2d80c250
3 changed files with 77 additions and 13 deletions
+20 -1
View File
@@ -1343,7 +1343,7 @@
"all": "الكل",
"col": {
"when": "الوقت",
"actor": "المنفّذ",
"actor": "المستخدم",
"action": "الإجراء",
"entity": "النوع",
"entityId": "المعرف",
@@ -1353,6 +1353,25 @@
"created": "تم إنشاء",
"removed": "تم حذف",
"moreChanges": "تغييرات أخرى",
"field": {
"titleAr": "العنوان",
"titleEn": "العنوان (إنجليزي)",
"meetingDate": "التاريخ",
"dailyNumber": "الرقم اليومي",
"startTime": "وقت البدء",
"endTime": "وقت الانتهاء",
"location": "المكان",
"meetingUrl": "رابط الاجتماع",
"platform": "المنصة",
"status": "الحالة",
"isHighlighted": "مميّز",
"notes": "ملاحظات",
"assignedTo": "المُسند إليه",
"taskType": "نوع المهمة",
"dueAt": "موعد الاستحقاق",
"reviewDecision": "قرار المراجعة",
"reviewNotes": "ملاحظات المراجعة"
},
"entity": {
"meeting": "اجتماع",
"attendee": "حاضر",
+20 -1
View File
@@ -1209,7 +1209,7 @@
"all": "All",
"col": {
"when": "When",
"actor": "Actor",
"actor": "User",
"action": "Action",
"entity": "Entity",
"entityId": "ID",
@@ -1219,6 +1219,25 @@
"created": "Created",
"removed": "Removed",
"moreChanges": "more changes",
"field": {
"titleAr": "Title (Arabic)",
"titleEn": "Title (English)",
"meetingDate": "Date",
"dailyNumber": "Daily number",
"startTime": "Start time",
"endTime": "End time",
"location": "Location",
"meetingUrl": "Meeting URL",
"platform": "Platform",
"status": "Status",
"isHighlighted": "Highlighted",
"notes": "Notes",
"assignedTo": "Assigned to",
"taskType": "Task type",
"dueAt": "Due at",
"reviewDecision": "Review decision",
"reviewNotes": "Review notes"
},
"entity": {
"meeting": "Meeting",
"attendee": "Attendee",
@@ -6566,8 +6566,30 @@ function AuditDiffSummary({
? (newValue as Record<string, unknown>)
: null;
const fmt = (v: unknown): string => {
// Translate a field name (e.g. "titleAr") into the user's locale,
// falling back to the raw key so a missing label never breaks the row.
const labelForKey = (k: string): string =>
tWithFallback(t, `executiveMeetings.audit.field.${k}`, k);
// Translate enum-style values inside the diff (platform/status tokens,
// booleans for the highlighted flag) so an Arabic UI doesn't surface
// raw English database tokens like "scheduled" or "webex".
const fmtValue = (key: string, v: unknown): string => {
if (v === null || v === undefined || v === "") return "—";
if (key === "platform" && typeof v === "string") {
return tWithFallback(t, `executiveMeetings.platform.${v}`, v);
}
if (key === "status" && typeof v === "string") {
return tWithFallback(t, `executiveMeetings.status.${v}`, v);
}
if (key === "isHighlighted") {
const truthy = v === true || v === 1 || v === "1" || v === "true";
return tWithFallback(
t,
truthy ? "executiveMeetings.common.yes" : "executiveMeetings.common.no",
truthy ? "Yes" : "No",
);
}
if (typeof v === "string") return v.length > 40 ? `${v.slice(0, 40)}` : v;
if (typeof v === "number" || typeof v === "boolean") return String(v);
try {
@@ -6596,10 +6618,15 @@ function AuditDiffSummary({
<ul className="space-y-0.5">
{diffs.slice(0, 6).map((d) => (
<li key={d.key} className="leading-snug">
<span className="font-semibold text-[#0B1E3F]">{d.key}</span>:{" "}
<span className="text-red-700 line-through">{fmt(d.from)}</span>
<span className="font-semibold text-[#0B1E3F]">
{labelForKey(d.key)}
</span>
:{" "}
<span className="text-red-700 line-through">
{fmtValue(d.key, d.from)}
</span>
<span className="mx-1 text-gray-400"></span>
<span className="text-green-700">{fmt(d.to)}</span>
<span className="text-green-700">{fmtValue(d.key, d.to)}</span>
</li>
))}
{diffs.length > 6 && (
@@ -6624,7 +6651,8 @@ function AuditDiffSummary({
<ul className="space-y-0.5">
{items.map((k) => (
<li key={k} className="leading-snug">
<span className="font-semibold">{k}</span>: {fmt(single[k])}
<span className="font-semibold">{labelForKey(k)}</span>:{" "}
{fmtValue(k, single[k])}
</li>
))}
</ul>
@@ -6757,25 +6785,24 @@ function AuditSection({
<table className="w-full text-sm" dir={isRtl ? "rtl" : "ltr"}>
<thead className="bg-gray-50 text-[#0B1E3F]">
<tr>
<th className="px-3 py-2 text-start w-44">{t("executiveMeetings.audit.col.when")}</th>
<th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.actor")}</th>
<th className="px-3 py-2 text-start w-44">{t("executiveMeetings.audit.col.when")}</th>
<th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.action")}</th>
<th className="px-3 py-2 text-start w-28">{t("executiveMeetings.audit.col.entity")}</th>
<th className="px-3 py-2 text-start w-20">{t("executiveMeetings.audit.col.entityId")}</th>
<th className="px-3 py-2 text-start">{t("executiveMeetings.audit.col.changes")}</th>
</tr>
</thead>
<tbody>
{isLoading && (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
<td colSpan={5} className="py-8 text-center text-gray-500">
{t("common.loading")}
</td>
</tr>
)}
{!isLoading && entries.length === 0 && (
<tr>
<td colSpan={6} className="py-8 text-center text-gray-500">
<td colSpan={5} className="py-8 text-center text-gray-500">
{t("executiveMeetings.audit.empty")}
</td>
</tr>
@@ -6792,10 +6819,10 @@ function AuditSection({
className="border-t border-gray-100 align-top"
data-testid={`em-audit-row-${e.id}`}
>
<td className="px-3 py-2">{actor}</td>
<td className="px-3 py-2 text-xs text-gray-600 whitespace-nowrap">
{new Date(e.performedAt).toLocaleString()}
</td>
<td className="px-3 py-2">{actor}</td>
<td className="px-3 py-2 text-xs">
{tWithFallback(
t,
@@ -6810,7 +6837,6 @@ function AuditSection({
e.entityType,
)}
</td>
<td className="px-3 py-2 text-xs">{e.entityId ?? "—"}</td>
<td className="px-3 py-2 text-xs">
<AuditDiffSummary
oldValue={e.oldValue}