diff --git a/artifacts/tx-os/public/opengraph.jpg b/artifacts/tx-os/public/opengraph.jpg index 335bec3c..3c74bc77 100644 Binary files a/artifacts/tx-os/public/opengraph.jpg and b/artifacts/tx-os/public/opengraph.jpg differ diff --git a/artifacts/tx-os/src/locales/ar.json b/artifacts/tx-os/src/locales/ar.json index 3bcdc313..67309177 100644 --- a/artifacts/tx-os/src/locales/ar.json +++ b/artifacts/tx-os/src/locales/ar.json @@ -761,6 +761,13 @@ "chipAriaTargetType": "{{label}} — فتح سجل التدقيق مفلتراً حسب نوع الهدف \"{{type}}\"", "chipAriaParent": "{{label}} — فتح سجل التدقيق لـ {{type}} #{{id}}" }, + "recent": { + "title": "آخر النشاطات", + "loading": "جارٍ تحميل آخر النشاطات…", + "empty": "لا توجد نشاطات حديثة لهذا السجل بعد.", + "viewAll": "عرض السجل الكامل", + "viewAllAria": "فتح سجل التدقيق مفلتراً على هذا الـ {{type}}" + }, "summary": { "app": { "create": "تم إنشاء التطبيق '{{name}}'", diff --git a/artifacts/tx-os/src/locales/en.json b/artifacts/tx-os/src/locales/en.json index d85f4410..685ab033 100644 --- a/artifacts/tx-os/src/locales/en.json +++ b/artifacts/tx-os/src/locales/en.json @@ -662,6 +662,13 @@ "chipAriaTargetType": "{{label}} — open audit log filtered by target type \"{{type}}\"", "chipAriaParent": "{{label}} — open audit log for {{type}} #{{id}}" }, + "recent": { + "title": "Recent activity", + "loading": "Loading recent activity…", + "empty": "No recent activity for this record yet.", + "viewAll": "View full history", + "viewAllAria": "Open the audit log filtered to this {{type}}" + }, "summary": { "app": { "create": "Created app '{{name}}'", diff --git a/artifacts/tx-os/src/pages/admin.tsx b/artifacts/tx-os/src/pages/admin.tsx index bb316d25..bb9c8f9d 100644 --- a/artifacts/tx-os/src/pages/admin.tsx +++ b/artifacts/tx-os/src/pages/admin.tsx @@ -1023,6 +1023,30 @@ export default function AdminPage() { const emptyAppForm: AppForm = { nameAr: "", nameEn: "", slug: "", iconName: "Grid2X2", route: "/", color: "#6366f1", sortOrder: 0, permissionIds: [] }; const emptyServiceForm: ServiceForm = { nameAr: "", nameEn: "", descriptionAr: "", descriptionEn: "", price: "0.00", imageUrl: "", isAvailable: true }; + // #196: Switch to the audit-log section pre-filtered by the given + // targetType/targetId. Writes the hash BEFORE the section change so the + // section-sync effect (which would otherwise drop targetType/targetId + // when the previous section differs) sees a matching previousSection and + // preserves the deep-link params. + const openAuditLogForTarget = ( + targetType: "app" | "service" | "user" | "group" | "role", + targetId: number, + ) => { + if (typeof window !== "undefined") { + const params = new URLSearchParams( + window.location.hash.replace(/^#/, ""), + ); + params.set("section", "audit-log"); + params.set("targetType", targetType); + params.set("targetId", String(targetId)); + window.history.replaceState(null, "", `#${params.toString()}`); + } + // Close any open editor modals so the audit-log view is unobstructed. + setEditingApp(null); + setEditingService(null); + setSection("audit-log"); + }; + const handleSaveApp = () => { if (!editingApp) return; if (editingApp.id) { @@ -1193,6 +1217,17 @@ export default function AdminPage() { }} /> )} + {editingApp.id !== undefined && ( + + openAuditLogForTarget("app", editingApp.id!) + } + /> + )}
@@ -1244,6 +1279,17 @@ export default function AdminPage() { onCheckedChange={(v) => setEditingService({ ...editingService, form: { ...editingService.form, isAvailable: v } })} />
+ {editingService.id !== undefined && ( + + openAuditLogForTarget("service", editingService.id!) + } + /> + )}
@@ -1586,10 +1632,15 @@ export default function AdminPage() { }, ) } + onOpenAuditLogForTarget={openAuditLogForTarget} /> )} - {section === "groups" && } - {section === "roles" && } + {section === "groups" && ( + + )} + {section === "roles" && ( + + )} {section === "audit-log" && } {section === "settings" && ( @@ -3323,16 +3374,23 @@ function DependencyRow({ // ===== Users Management Panel ===== type UserSortKey = "username" | "email" | "createdAt"; +type OpenAuditLogForTarget = ( + targetType: "app" | "service" | "user" | "group" | "role", + targetId: number, +) => void; + function UsersPanel({ currentUserId, highlightedUserId, userRowRefs, onIssueResetLink, + onOpenAuditLogForTarget, }: { currentUserId: number; highlightedUserId: number | null; userRowRefs: React.MutableRefObject>; onIssueResetLink: (userId: number, username: string) => void; + onOpenAuditLogForTarget: OpenAuditLogForTarget; }) { const { t, i18n } = useTranslation(); const lang = i18n.language; @@ -3742,6 +3800,7 @@ function UsersPanel({ groups={groups ?? []} onClose={() => setEditingUser(null)} onSaved={() => { invalidateUsers(); setEditingUser(null); }} + onOpenAuditLogForTarget={onOpenAuditLogForTarget} /> )} @@ -3798,11 +3857,13 @@ function UserGroupsEditor({ groups, onClose, onSaved, + onOpenAuditLogForTarget, }: { user: UserProfile; groups: Group[]; onClose: () => void; onSaved: () => void; + onOpenAuditLogForTarget: OpenAuditLogForTarget; }) { const { t, i18n } = useTranslation(); const { toast } = useToast(); @@ -4047,6 +4108,13 @@ function UserGroupsEditor({ return r ? r.name : `#${id}`; }} /> + onOpenAuditLogForTarget("user", user.id)} + /> +
+ ); +} + // Parses targetType / targetId values out of the admin page URL hash so the // audit log panel can deep-link to a pre-filtered view (e.g. when an admin // clicks a dependency chip on a forced-delete row).