234 lines
5.7 KiB
Vue
234 lines
5.7 KiB
Vue
<script setup lang="ts">
|
|
import { computed, onMounted, ref } from 'vue';
|
|
import axios from 'axios';
|
|
import { api } from '@/lib/api';
|
|
import type { AuditLogListResponse } from '@mento/shared';
|
|
import {
|
|
UiButton,
|
|
UiCard,
|
|
UiEmptyState,
|
|
UiField,
|
|
UiInput,
|
|
UiPageHeader,
|
|
} from '@/components/ui';
|
|
import UiIcon from '@/components/UiIcon.vue';
|
|
import { formatJalaliNumeric, toFaDigits } from '@/lib/jalali';
|
|
|
|
const loading = ref(false);
|
|
const error = ref('');
|
|
const q = ref('');
|
|
const entityType = ref('');
|
|
const page = ref(1);
|
|
const data = ref<AuditLogListResponse | null>(null);
|
|
|
|
const totalPages = computed(() => {
|
|
if (!data.value) return 1;
|
|
return Math.max(1, Math.ceil(data.value.total / data.value.limit));
|
|
});
|
|
|
|
async function load(resetPage = false) {
|
|
if (resetPage) page.value = 1;
|
|
loading.value = true;
|
|
error.value = '';
|
|
try {
|
|
const { data: res } = await api.get<AuditLogListResponse>('/audit-logs', {
|
|
params: {
|
|
q: q.value.trim() || undefined,
|
|
entityType: entityType.value.trim() || undefined,
|
|
page: page.value,
|
|
limit: 30,
|
|
},
|
|
});
|
|
data.value = res;
|
|
} catch (e) {
|
|
if (axios.isAxiosError(e) && e.response?.status === 403) {
|
|
error.value = 'دسترسی به لاگ فعالیت برای این محیط فعال نیست.';
|
|
} else if (axios.isAxiosError(e) && !e.response) {
|
|
error.value = 'اتصال به API برقرار نشد.';
|
|
} else {
|
|
error.value = 'بارگذاری لاگ ناموفق بود.';
|
|
}
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
function go(delta: number) {
|
|
page.value = Math.min(totalPages.value, Math.max(1, page.value + delta));
|
|
void load();
|
|
}
|
|
|
|
onMounted(() => {
|
|
void load();
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="audit-page">
|
|
<UiPageHeader
|
|
title="لاگ فعالیت کاربران"
|
|
description="ثبت تغییرات ایجاد، ویرایش و حذف در سامانه"
|
|
>
|
|
<template #actions>
|
|
<UiButton variant="secondary" :disabled="loading" @click="load()">
|
|
<UiIcon name="refresh" />
|
|
بروزرسانی
|
|
</UiButton>
|
|
</template>
|
|
</UiPageHeader>
|
|
|
|
<UiCard class="filters">
|
|
<div class="filters-grid">
|
|
<UiField label="جستجو">
|
|
<UiInput
|
|
v-model="q"
|
|
placeholder="خلاصه، مسیر یا نوع عملیات…"
|
|
@keydown.enter="load(true)"
|
|
/>
|
|
</UiField>
|
|
<UiField label="نوع منبع">
|
|
<UiInput
|
|
v-model="entityType"
|
|
placeholder="مثلاً deals یا people"
|
|
@keydown.enter="load(true)"
|
|
/>
|
|
</UiField>
|
|
<div class="filters-actions">
|
|
<UiButton :disabled="loading" @click="load(true)">اعمال فیلتر</UiButton>
|
|
</div>
|
|
</div>
|
|
</UiCard>
|
|
|
|
<p v-if="error" class="error">{{ error }}</p>
|
|
|
|
<UiCard v-if="loading && !data" title="در حال بارگذاری…">
|
|
<p class="muted">لطفاً صبر کنید.</p>
|
|
</UiCard>
|
|
|
|
<UiEmptyState
|
|
v-else-if="!loading && data && !data.items.length"
|
|
title="لاگی ثبت نشده"
|
|
description="پس از ایجاد یا ویرایش رکوردها، رویدادها اینجا دیده میشوند."
|
|
/>
|
|
|
|
<UiCard v-else-if="data?.items.length" title="رویدادها">
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>زمان</th>
|
|
<th>کاربر</th>
|
|
<th>عملیات</th>
|
|
<th>منبع</th>
|
|
<th>خلاصه</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="row in data.items" :key="row.id">
|
|
<td class="nowrap">{{ formatJalaliNumeric(new Date(row.createdAt)) }}</td>
|
|
<td>{{ row.actor?.fullName ?? '—' }}</td>
|
|
<td>
|
|
<span class="pill">{{ row.action }}</span>
|
|
</td>
|
|
<td class="muted">{{ row.entityType ?? '—' }}</td>
|
|
<td>{{ row.summary }}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="pager">
|
|
<UiButton
|
|
variant="secondary"
|
|
:disabled="loading || page <= 1"
|
|
@click="go(-1)"
|
|
>
|
|
قبلی
|
|
</UiButton>
|
|
<span class="muted">
|
|
صفحه {{ toFaDigits(page) }} از {{ toFaDigits(totalPages) }}
|
|
· {{ toFaDigits(data.total) }} مورد
|
|
</span>
|
|
<UiButton
|
|
variant="secondary"
|
|
:disabled="loading || page >= totalPages"
|
|
@click="go(1)"
|
|
>
|
|
بعدی
|
|
</UiButton>
|
|
</div>
|
|
</UiCard>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.audit-page {
|
|
display: grid;
|
|
gap: 1rem;
|
|
}
|
|
.filters-grid {
|
|
display: grid;
|
|
grid-template-columns: 1.4fr 1fr auto;
|
|
gap: 0.75rem;
|
|
align-items: end;
|
|
}
|
|
.filters-actions {
|
|
display: flex;
|
|
align-items: end;
|
|
}
|
|
.error {
|
|
color: #b91c1c;
|
|
margin: 0;
|
|
font-weight: 600;
|
|
}
|
|
.muted {
|
|
color: #64748b;
|
|
margin: 0;
|
|
}
|
|
.table-wrap {
|
|
overflow: auto;
|
|
}
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.9rem;
|
|
}
|
|
th,
|
|
td {
|
|
text-align: right;
|
|
padding: 0.65rem 0.55rem;
|
|
border-bottom: 1px solid #eef2f7;
|
|
vertical-align: top;
|
|
}
|
|
th {
|
|
color: #64748b;
|
|
font-weight: 700;
|
|
font-size: 0.8rem;
|
|
}
|
|
.nowrap {
|
|
white-space: nowrap;
|
|
}
|
|
.pill {
|
|
display: inline-flex;
|
|
padding: 0.15rem 0.55rem;
|
|
border-radius: var(--radius);
|
|
background: #f0fdfa;
|
|
color: #0f766e;
|
|
font-weight: 700;
|
|
font-size: 0.8rem;
|
|
}
|
|
.pager {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 0.75rem;
|
|
margin-top: 0.85rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
@media (max-width: 860px) {
|
|
.filters-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|