feat: log staff sign-in and sign-out and force logout at a set time

This commit is contained in:
2026-10-19 13:34:55 +03:30
parent 3863c5e7d1
commit 8590579605
25 changed files with 588 additions and 15 deletions
+83 -4
View File
@@ -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" />