feat: log staff sign-in and sign-out and force logout at a set time
This commit is contained in:
@@ -26,13 +26,14 @@ import {
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import { formatClockFa, formatJalaliNumeric } from '@/lib/jalali';
|
||||
|
||||
type LogTab = 'all' | 'creates' | 'edits' | 'deletes';
|
||||
type LogTab = 'all' | 'creates' | 'edits' | 'deletes' | 'attendance';
|
||||
|
||||
const TABS: Array<{ key: LogTab; label: string; action?: string }> = [
|
||||
{ key: 'all', label: 'همه' },
|
||||
{ key: 'creates', label: 'ایجاد', action: 'ایجاد' },
|
||||
{ key: 'edits', label: 'ویرایش', action: 'ویرایش' },
|
||||
{ key: 'deletes', label: 'حذف', action: 'حذف' },
|
||||
{ key: 'attendance', label: 'ورود و خروج' },
|
||||
];
|
||||
|
||||
const ENTITY_LABELS: Record<string, string> = {
|
||||
@@ -77,13 +78,28 @@ const page = ref(1);
|
||||
const pageSize = usePageSize('audit-logs');
|
||||
const data = ref<AuditLogListResponse | null>(null);
|
||||
const deletions = ref<DeletionArchiveListResponse | null>(null);
|
||||
const attendance = ref<{
|
||||
items: Array<{
|
||||
id: string;
|
||||
fullName: string;
|
||||
kind: 'login' | 'logout' | 'forced_logout';
|
||||
at: string;
|
||||
afterHours: boolean;
|
||||
}>;
|
||||
total: number;
|
||||
} | null>(null);
|
||||
const retention = ref<DeletionRetention>('never');
|
||||
const purgeId = ref<string | null>(null);
|
||||
|
||||
const showingArchive = computed(() => tab.value === 'deletes' && isOwner.value);
|
||||
const showingAttendance = computed(() => tab.value === 'attendance');
|
||||
|
||||
const listTotal = computed(() =>
|
||||
showingArchive.value ? deletions.value?.total : data.value?.total,
|
||||
showingAttendance.value
|
||||
? attendance.value?.total
|
||||
: showingArchive.value
|
||||
? deletions.value?.total
|
||||
: data.value?.total,
|
||||
);
|
||||
|
||||
const totalPages = computed(() => {
|
||||
@@ -202,7 +218,23 @@ async function load(resetPage = false) {
|
||||
const current = tab.value;
|
||||
const archive = current === 'deletes' && isOwner.value;
|
||||
try {
|
||||
if (archive) {
|
||||
if (current === 'attendance') {
|
||||
const { data: res } = await api.get<{
|
||||
items: NonNullable<typeof attendance.value>['items'];
|
||||
total: number;
|
||||
}>('/audit-logs/attendance', {
|
||||
params: {
|
||||
q: q.value.trim() || undefined,
|
||||
actorUserId: actorId.value || undefined,
|
||||
page: page.value,
|
||||
limit: pageSize.value,
|
||||
},
|
||||
});
|
||||
if (tab.value !== current) return;
|
||||
attendance.value = res;
|
||||
data.value = null;
|
||||
deletions.value = null;
|
||||
} else if (archive) {
|
||||
const { data: res } = await api.get<DeletionArchiveListResponse>('/deletions', {
|
||||
params: {
|
||||
q: q.value.trim() || undefined,
|
||||
@@ -229,6 +261,7 @@ async function load(resetPage = false) {
|
||||
if (tab.value !== current) return;
|
||||
data.value = res;
|
||||
deletions.value = null;
|
||||
attendance.value = null;
|
||||
}
|
||||
} catch (e) {
|
||||
if (tab.value !== current) return;
|
||||
@@ -238,12 +271,19 @@ async function load(resetPage = false) {
|
||||
}
|
||||
}
|
||||
|
||||
function attendanceLabel(kind: string, afterHours: boolean) {
|
||||
if (kind === 'login') return afterHours ? 'ورود بعد از ساعت خروج' : 'ورود';
|
||||
if (kind === 'forced_logout') return 'خروج در ساعت تعیینشده';
|
||||
return 'خروج';
|
||||
}
|
||||
|
||||
function selectTab(next: LogTab) {
|
||||
if (tab.value === next) return;
|
||||
tab.value = next;
|
||||
okMsg.value = '';
|
||||
data.value = null;
|
||||
deletions.value = null;
|
||||
attendance.value = null;
|
||||
void load(true);
|
||||
}
|
||||
|
||||
@@ -382,7 +422,13 @@ onMounted(() => {
|
||||
/>
|
||||
|
||||
<UiEmptyState
|
||||
v-else-if="!showingArchive && !loading && data && !data.items.length"
|
||||
v-else-if="showingAttendance && !loading && attendance && !attendance.items.length"
|
||||
title="ورود و خروجی ثبت نشده"
|
||||
description="با ورود یا خروج کارمندان، اینجا دیده میشود."
|
||||
/>
|
||||
|
||||
<UiEmptyState
|
||||
v-else-if="!showingArchive && !showingAttendance && !loading && data && !data.items.length"
|
||||
title="رویدادی در این بخش نیست"
|
||||
description="با انجام کار در سامانه، ایجاد و ویرایش اینجا دیده میشود."
|
||||
/>
|
||||
@@ -437,6 +483,39 @@ onMounted(() => {
|
||||
/>
|
||||
</UiCard>
|
||||
|
||||
<UiCard v-else-if="showingAttendance && attendance?.items.length" title="ورود و خروج">
|
||||
<div class="table-wrap">
|
||||
<table data-no-rownum>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="when-col">زمان</th>
|
||||
<th>کاربر</th>
|
||||
<th>رویداد</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item in attendance.items" :key="item.id">
|
||||
<td class="when" data-label="زمان">
|
||||
<span class="clock">{{ formatClockFa(new Date(item.at)) }}</span>
|
||||
<span>{{ formatJalaliNumeric(new Date(item.at)) }}</span>
|
||||
</td>
|
||||
<td data-label="کاربر">{{ item.fullName }}</td>
|
||||
<td data-label="رویداد">{{ attendanceLabel(item.kind, item.afterHours) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<UiPagination
|
||||
:page="page"
|
||||
:total-pages="totalPages"
|
||||
:total-items="attendance.total"
|
||||
:page-size="pageSize"
|
||||
:page-size-options="PAGE_SIZE_OPTIONS"
|
||||
@update:page="onPage"
|
||||
@update:page-size="onPageSize"
|
||||
/>
|
||||
</UiCard>
|
||||
|
||||
<UiCard v-else-if="data?.items.length" title="رویدادها">
|
||||
<div class="tc-row">
|
||||
<TableColumnsMenu table-key="audit-logs" align="end" />
|
||||
|
||||
Reference in New Issue
Block a user