mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat: add user filter chips and resource types to audit log
- Replace user ID text input with checkbox-style user chips extracted from log entries - Add sales_stats and landings to resource type filter - Fix export not respecting status filter - Remove dead auto-refresh interval code - Add aria-pressed and focus-visible styles for accessibility
This commit is contained in:
@@ -3097,6 +3097,7 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"title": "Filters",
|
"title": "Filters",
|
||||||
"active": "Active",
|
"active": "Active",
|
||||||
|
"user": "User",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"actionPlaceholder": "Search by action...",
|
"actionPlaceholder": "Search by action...",
|
||||||
"resource": "Resource type",
|
"resource": "Resource type",
|
||||||
|
|||||||
@@ -2826,6 +2826,7 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"title": "فیلترها",
|
"title": "فیلترها",
|
||||||
"active": "فعال",
|
"active": "فعال",
|
||||||
|
"user": "کاربر",
|
||||||
"action": "عملیات",
|
"action": "عملیات",
|
||||||
"actionPlaceholder": "جستجو بر اساس عملیات...",
|
"actionPlaceholder": "جستجو بر اساس عملیات...",
|
||||||
"resource": "نوع منبع",
|
"resource": "نوع منبع",
|
||||||
|
|||||||
@@ -3645,6 +3645,7 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"title": "Фильтры",
|
"title": "Фильтры",
|
||||||
"active": "Активны",
|
"active": "Активны",
|
||||||
|
"user": "Пользователь",
|
||||||
"action": "Действие",
|
"action": "Действие",
|
||||||
"actionPlaceholder": "Поиск по действию...",
|
"actionPlaceholder": "Поиск по действию...",
|
||||||
"resource": "Тип ресурса",
|
"resource": "Тип ресурса",
|
||||||
|
|||||||
@@ -2825,6 +2825,7 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"title": "筛选",
|
"title": "筛选",
|
||||||
"active": "已激活",
|
"active": "已激活",
|
||||||
|
"user": "用户",
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
"actionPlaceholder": "按操作搜索...",
|
"actionPlaceholder": "按操作搜索...",
|
||||||
"resource": "资源类型",
|
"resource": "资源类型",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
import { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
@@ -84,6 +84,7 @@ const RESOURCE_TYPES = [
|
|||||||
'users',
|
'users',
|
||||||
'tickets',
|
'tickets',
|
||||||
'stats',
|
'stats',
|
||||||
|
'sales_stats',
|
||||||
'broadcasts',
|
'broadcasts',
|
||||||
'tariffs',
|
'tariffs',
|
||||||
'promocodes',
|
'promocodes',
|
||||||
@@ -106,6 +107,7 @@ const RESOURCE_TYPES = [
|
|||||||
'apps',
|
'apps',
|
||||||
'email_templates',
|
'email_templates',
|
||||||
'pinned_messages',
|
'pinned_messages',
|
||||||
|
'landings',
|
||||||
'updates',
|
'updates',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -116,6 +118,7 @@ const PAGE_SIZE_OPTIONS = [20, 50, 100] as const;
|
|||||||
const AUTO_REFRESH_INTERVAL = 30_000;
|
const AUTO_REFRESH_INTERVAL = 30_000;
|
||||||
|
|
||||||
interface FiltersState {
|
interface FiltersState {
|
||||||
|
userId: string;
|
||||||
action: string;
|
action: string;
|
||||||
resource: string;
|
resource: string;
|
||||||
status: string;
|
status: string;
|
||||||
@@ -124,6 +127,7 @@ interface FiltersState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const INITIAL_FILTERS: FiltersState = {
|
const INITIAL_FILTERS: FiltersState = {
|
||||||
|
userId: '',
|
||||||
action: '',
|
action: '',
|
||||||
resource: '',
|
resource: '',
|
||||||
status: '',
|
status: '',
|
||||||
@@ -411,6 +415,11 @@ function LogEntryCard({ entry, isExpanded, onToggle }: LogEntryCardProps) {
|
|||||||
|
|
||||||
// === Main Page ===
|
// === Main Page ===
|
||||||
|
|
||||||
|
interface KnownUser {
|
||||||
|
name: string;
|
||||||
|
email: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
export default function AdminAuditLog() {
|
export default function AdminAuditLog() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -431,8 +440,8 @@ export default function AdminAuditLog() {
|
|||||||
const [exporting, setExporting] = useState(false);
|
const [exporting, setExporting] = useState(false);
|
||||||
const [exportError, setExportError] = useState<string | null>(null);
|
const [exportError, setExportError] = useState<string | null>(null);
|
||||||
|
|
||||||
// Auto-refresh interval ref
|
// Known users accumulated from audit log entries
|
||||||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
const [knownUsers, setKnownUsers] = useState<Map<number, KnownUser>>(new Map());
|
||||||
|
|
||||||
// Build query params
|
// Build query params
|
||||||
const queryParams = useMemo((): AuditLogFilters => {
|
const queryParams = useMemo((): AuditLogFilters => {
|
||||||
@@ -441,6 +450,10 @@ export default function AdminAuditLog() {
|
|||||||
offset: page * pageSize,
|
offset: page * pageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parsedUserId = parseInt(appliedFilters.userId, 10);
|
||||||
|
if (!isNaN(parsedUserId) && parsedUserId > 0) {
|
||||||
|
params.user_id = parsedUserId;
|
||||||
|
}
|
||||||
if (appliedFilters.action.trim()) {
|
if (appliedFilters.action.trim()) {
|
||||||
params.action = appliedFilters.action.trim();
|
params.action = appliedFilters.action.trim();
|
||||||
}
|
}
|
||||||
@@ -467,21 +480,23 @@ export default function AdminAuditLog() {
|
|||||||
refetchInterval: autoRefresh ? AUTO_REFRESH_INTERVAL : false,
|
refetchInterval: autoRefresh ? AUTO_REFRESH_INTERVAL : false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-refresh visual indicator
|
// Accumulate unique users from audit log entries
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoRefresh) {
|
if (!data?.items?.length) return;
|
||||||
intervalRef.current = setInterval(() => {
|
setKnownUsers((prev) => {
|
||||||
// The visual indicator updates are driven by isFetching from react-query
|
const next = new Map(prev);
|
||||||
}, AUTO_REFRESH_INTERVAL);
|
let changed = false;
|
||||||
}
|
for (const entry of data.items) {
|
||||||
|
const name = entry.user_first_name || entry.user_email || `#${entry.user_id}`;
|
||||||
return () => {
|
const existing = next.get(entry.user_id);
|
||||||
if (intervalRef.current) {
|
if (!existing || existing.name !== name || existing.email !== entry.user_email) {
|
||||||
clearInterval(intervalRef.current);
|
next.set(entry.user_id, { name, email: entry.user_email });
|
||||||
intervalRef.current = null;
|
changed = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
return changed ? next : prev;
|
||||||
}, [autoRefresh]);
|
});
|
||||||
|
}, [data?.items]);
|
||||||
|
|
||||||
const entries = data?.items ?? [];
|
const entries = data?.items ?? [];
|
||||||
const total = data?.total ?? 0;
|
const total = data?.total ?? 0;
|
||||||
@@ -517,8 +532,11 @@ export default function AdminAuditLog() {
|
|||||||
setExporting(true);
|
setExporting(true);
|
||||||
try {
|
try {
|
||||||
const exportParams: AuditLogFilters = {};
|
const exportParams: AuditLogFilters = {};
|
||||||
|
const exportUserId = parseInt(appliedFilters.userId, 10);
|
||||||
|
if (!isNaN(exportUserId) && exportUserId > 0) exportParams.user_id = exportUserId;
|
||||||
if (appliedFilters.action.trim()) exportParams.action = appliedFilters.action.trim();
|
if (appliedFilters.action.trim()) exportParams.action = appliedFilters.action.trim();
|
||||||
if (appliedFilters.resource) exportParams.resource_type = appliedFilters.resource;
|
if (appliedFilters.resource) exportParams.resource_type = appliedFilters.resource;
|
||||||
|
if (appliedFilters.status) exportParams.status = appliedFilters.status;
|
||||||
if (appliedFilters.dateFrom) exportParams.date_from = appliedFilters.dateFrom;
|
if (appliedFilters.dateFrom) exportParams.date_from = appliedFilters.dateFrom;
|
||||||
if (appliedFilters.dateTo) exportParams.date_to = appliedFilters.dateTo;
|
if (appliedFilters.dateTo) exportParams.date_to = appliedFilters.dateTo;
|
||||||
|
|
||||||
@@ -548,6 +566,7 @@ export default function AdminAuditLog() {
|
|||||||
|
|
||||||
const hasActiveFilters = useMemo(() => {
|
const hasActiveFilters = useMemo(() => {
|
||||||
return (
|
return (
|
||||||
|
appliedFilters.userId.trim() !== '' ||
|
||||||
appliedFilters.action.trim() !== '' ||
|
appliedFilters.action.trim() !== '' ||
|
||||||
appliedFilters.resource !== '' ||
|
appliedFilters.resource !== '' ||
|
||||||
appliedFilters.status !== '' ||
|
appliedFilters.status !== '' ||
|
||||||
@@ -649,6 +668,49 @@ export default function AdminAuditLog() {
|
|||||||
{filtersOpen && (
|
{filtersOpen && (
|
||||||
<div className="border-t border-dark-700 p-4">
|
<div className="border-t border-dark-700 p-4">
|
||||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{/* User filter */}
|
||||||
|
{knownUsers.size > 0 && (
|
||||||
|
<div className="sm:col-span-2 lg:col-span-3">
|
||||||
|
<label className="mb-1 block text-sm font-medium text-dark-300">
|
||||||
|
{t('admin.auditLog.filters.user')}
|
||||||
|
</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Array.from(knownUsers.entries()).map(([uid, user]) => {
|
||||||
|
const isSelected = filters.userId === String(uid);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={uid}
|
||||||
|
type="button"
|
||||||
|
aria-pressed={isSelected}
|
||||||
|
onClick={() =>
|
||||||
|
setFilters((prev) => ({
|
||||||
|
...prev,
|
||||||
|
userId: isSelected ? '' : String(uid),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
className={`flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500 focus-visible:ring-offset-1 focus-visible:ring-offset-dark-900 ${
|
||||||
|
isSelected
|
||||||
|
? 'border-accent-500 bg-accent-500/20 text-accent-300'
|
||||||
|
: 'border-dark-600 bg-dark-900 text-dark-300 hover:border-dark-500 hover:text-dark-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex h-4 w-4 items-center justify-center rounded border text-xs ${
|
||||||
|
isSelected
|
||||||
|
? 'border-accent-500 bg-accent-500 text-white'
|
||||||
|
: 'border-dark-500 bg-dark-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isSelected && '✓'}
|
||||||
|
</span>
|
||||||
|
<span>{user.name}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Action search */}
|
{/* Action search */}
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
|
|||||||
Reference in New Issue
Block a user