diff --git a/app/requestHandler/use-admin-hooks.ts b/app/requestHandler/use-admin-hooks.ts index 408c12b..c07f7ea 100644 --- a/app/requestHandler/use-admin-hooks.ts +++ b/app/requestHandler/use-admin-hooks.ts @@ -318,6 +318,23 @@ export const useLedgerSellers = (params: AdminListParams) => export const useLedgerJournal = (params: AdminListParams) => useAdminList("ledger/journal", params); +/* ------------------------------ audit log ------------------------------ */ + +export interface AuditLogRow { + id: string; + createdAt: string; + actorPhone: string | null; + action: string; + targetType: string; + targetId: string | null; + before: Record | null; + after: Record | null; + note: string; +} + +export const useAuditLog = (params: AdminListParams) => + useAdminList("audit", params); + export function useLedgerSellerDetail(id: string | undefined) { const token = useAuthToken(); return useQuery({ diff --git a/app/routes/admin.audit.tsx b/app/routes/admin.audit.tsx new file mode 100644 index 0000000..05be1c8 --- /dev/null +++ b/app/routes/admin.audit.tsx @@ -0,0 +1,166 @@ +import { useState } from "react"; +import { useAuditLog, type AuditLogRow } from "~/requestHandler/use-admin-hooks"; +import { + AdminTable, + AdminPagination, + AdminToolbar, + AdminSearch, + AdminSelect, + AdminPageTitle, + StatusBadge, + faDate, + useDebouncedValue, + type Column, +} from "~/components/admin/DataTable"; + +const PAGE_SIZE = 25; + +const ACTION: Record = { + "seller.update": { label: "ویرایش فروشنده", tone: "blue" }, + "user.update": { label: "ویرایش کاربر", tone: "blue" }, + "product.update": { label: "ویرایش محصول", tone: "blue" }, + "payout.complete": { label: "تکمیل تسویه", tone: "green" }, + "payout.reject": { label: "رد تسویه", tone: "red" }, + "ledger.backfill": { label: "بازسازی دفتر", tone: "gray" }, +}; + +const TARGET_LABEL: Record = { + seller: "فروشنده", + user: "کاربر", + product: "محصول", + payout: "تسویه", + ledger: "دفتر مالی", +}; + +/** Compact before→after diff of the touched fields. */ +function Changes({ before, after }: Pick) { + const keys = Array.from( + new Set([...Object.keys(before || {}), ...Object.keys(after || {})]) + ); + const changed = keys.filter( + (k) => JSON.stringify(before?.[k]) !== JSON.stringify(after?.[k]) + ); + if (changed.length === 0) + return ; + return ( +
+ {changed.map((k) => ( + + {k}: + {fmt(before?.[k])} + + {fmt(after?.[k])} + + ))} +
+ ); +} + +function fmt(v: unknown) { + if (v === null || v === undefined) return "∅"; + if (typeof v === "boolean") return v ? "true" : "false"; + return String(v); +} + +export default function AdminAudit() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(""); + const [action, setAction] = useState(""); + const [targetType, setTargetType] = useState(""); + const debouncedSearch = useDebouncedValue(search); + + const { data, isLoading } = useAuditLog({ + page, + page_size: PAGE_SIZE, + search: debouncedSearch, + action, + target_type: targetType, + }); + + const reset = (fn: () => void) => { + fn(); + setPage(1); + }; + + const columns: Column[] = [ + { + header: "عملیات", + render: (r) => { + const a = ACTION[r.action] || { label: r.action, tone: "gray" as const }; + return ; + }, + }, + { + header: "هدف", + render: (r) => ( + + {TARGET_LABEL[r.targetType] || r.targetType} + {r.targetId && ( + · {r.targetId.slice(0, 8)} + )} + + ), + }, + { + header: "تغییرات", + render: (r) => , + }, + { + header: "مدیر", + render: (r) => ( + + {r.actorPhone || "سیستم"} + + ), + }, + { + header: "تاریخ", + render: (r) => {faDate(r.createdAt)}, + }, + ]; + + return ( +
+ گزارش فعالیت مدیران + + reset(() => setSearch(v))} + placeholder="جستجوی تلفن مدیر / شناسه هدف…" + /> + reset(() => setAction(v))} + options={[ + { value: "", label: "همه عملیات‌ها" }, + { value: "seller.update", label: "ویرایش فروشنده" }, + { value: "user.update", label: "ویرایش کاربر" }, + { value: "product.update", label: "ویرایش محصول" }, + { value: "payout.complete", label: "تکمیل تسویه" }, + { value: "payout.reject", label: "رد تسویه" }, + { value: "ledger.backfill", label: "بازسازی دفتر" }, + ]} + /> + reset(() => setTargetType(v))} + options={[ + { value: "", label: "همه هدف‌ها" }, + { value: "seller", label: "فروشنده" }, + { value: "user", label: "کاربر" }, + { value: "product", label: "محصول" }, + { value: "payout", label: "تسویه" }, + ]} + /> + + r.id} + emptyText="فعالیتی ثبت نشده است" + /> + +
+ ); +} diff --git a/app/routes/admin.tsx b/app/routes/admin.tsx index 1012234..4cccf10 100644 --- a/app/routes/admin.tsx +++ b/app/routes/admin.tsx @@ -6,6 +6,7 @@ import { Users, Package, Landmark, + ScrollText, ArrowRight, ShieldCheck, Loader2, @@ -28,6 +29,7 @@ const NAV: NavItem[] = [ { label: "کاربران", to: "/admin/users", icon: Users }, { label: "محصولات", to: "/admin/products", icon: Package }, { label: "دفتر مالی", to: "/admin/ledger", icon: Landmark }, + { label: "گزارش فعالیت", to: "/admin/audit", icon: ScrollText }, ]; export default function AdminLayout() {