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:
@@ -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": "حاضر",
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user