Add attendance hub, categorized sidebar, and UI preferences page.

Improve dashboard and navigation access to attendance, group the right-side menu by category, and let each account save default theme and font size.
This commit is contained in:
2026-08-16 07:28:58 +03:30
parent 9c4182ea5a
commit ba509734ae
12 changed files with 731 additions and 39 deletions
+94
View File
@@ -109,6 +109,58 @@
</div>
</div>
<!-- Attendance -->
<PermissionGate :permission="PERMISSIONS.SESSIONS_ATTENDANCE">
<div class="surface-card p-4 border-round-xl border-1 border-color shadow-sm mb-4">
<div class="flex align-items-center justify-content-between mb-3">
<div>
<h3 class="text-base font-bold text-color m-0">حضور و غیاب</h3>
<p class="text-xs text-muted m-0 mt-1">جلسات اخیر که نیاز به ثبت حضور دارند</p>
</div>
<Button label="مشاهده همه" icon="pi pi-arrow-left" text size="small" @click="$router.push('/attendance')" />
</div>
<DataTable :value="attendanceSessions" class="p-datatable-sm text-sm" :loading="isAttendanceLoading">
<template #empty>
<div class="text-center text-muted p-4">جلسهای برای ثبت حضور وجود ندارد</div>
</template>
<Column field="topic" header="موضوع">
<template #body="{ data }">
<span class="font-semibold text-color">{{ data.topic || '—' }}</span>
</template>
</Column>
<Column field="course" header="دوره">
<template #body="{ data }">
{{ data.course?.title || data.courseTitle || '—' }}
</template>
</Column>
<Column field="day" header="تاریخ">
<template #body="{ data }">
{{ formatJalali(data.day || data.date) }}
</template>
</Column>
<Column field="attendanceStatus" header="وضعیت">
<template #body="{ data }">
<Tag
:value="hasAttendance(data) ? 'ثبت شده' : 'در انتظار'"
:severity="hasAttendance(data) ? 'success' : 'warn'"
/>
</template>
</Column>
<Column header="عملیات" style="width: 100px">
<template #body="{ data }">
<Button
icon="pi pi-check-square"
:label="hasAttendance(data) ? 'ویرایش' : 'ثبت'"
size="small"
text
@click="$router.push(`/sessions/attendance/${data._id || data.id}`)"
/>
</template>
</Column>
</DataTable>
</div>
</PermissionGate>
<!-- Recent Sessions -->
<div class="surface-card p-4 border-round-xl border-1 border-color shadow-sm">
<div class="flex align-items-center justify-content-between mb-3">
@@ -167,8 +219,12 @@
import { ref, computed, onMounted } from 'vue';
import { usePersianDate } from '@/composables/usePersianDate';
import { dashboardApi } from '@/api/dashboardApi';
import { sessionApi } from '@/api/sessionApi';
import { usePermissionStore } from '@/stores/permissionStore';
import { PERMISSIONS } from '@/constants/permissions';
import PageHeader from '@/components/common/PageHeader.vue';
import StatusTag from '@/components/common/StatusTag.vue';
import PermissionGate from '@/components/common/PermissionGate.vue';
import Button from 'primevue/button';
import Tag from 'primevue/tag';
import DataTable from 'primevue/datatable';
@@ -176,9 +232,12 @@ import Column from 'primevue/column';
import ProgressSpinner from 'primevue/progressspinner';
const { toPersianDigits, formatJalali } = usePersianDate();
const permissionStore = usePermissionStore();
// ── State ─────────────────────────────────────────────────────────────────────
const isLoading = ref(false);
const isAttendanceLoading = ref(false);
const attendanceSessions = ref([]);
const stats = ref({
totals: { users: 0, activeUsers: 0, professors: 0, activeProfessors: 0, courses: 0, sessions: 0 },
recentSessions: [],
@@ -321,6 +380,16 @@ const quickLinks = computed(() => [
badge: stats.value.totals.sessions,
badgeSeverity: 'warning'
},
...(permissionStore.hasPermission(PERMISSIONS.SESSIONS_ATTENDANCE)
? [{
to: '/attendance',
label: 'حضور و غیاب',
icon: 'pi pi-check-square',
iconBg: 'bg-amber-100',
iconColor: 'text-amber-600',
cardClass: 'quick-link-amber'
}]
: []),
{
to: '/payments',
label: 'امور مالی',
@@ -339,7 +408,31 @@ const quickLinks = computed(() => [
}
]);
const hasAttendance = (session) => Array.isArray(session.attendanceList) && session.attendanceList.length > 0;
// ── Load Data ─────────────────────────────────────────────────────────────────
const loadAttendanceSessions = async () => {
if (!permissionStore.hasPermission(PERMISSIONS.SESSIONS_ATTENDANCE)) return;
isAttendanceLoading.value = true;
try {
const res = await sessionApi.getAll({ page: 1, limit: 8, sortBy: 'day', sortOrder: 'desc' });
const data = res.data || res;
const sessions = Array.isArray(data) ? data : (data.data || []);
attendanceSessions.value = sessions
.filter((s) => !hasAttendance(s))
.slice(0, 5);
if (attendanceSessions.value.length < 5) {
const recorded = sessions.filter((s) => hasAttendance(s)).slice(0, 5 - attendanceSessions.value.length);
attendanceSessions.value = [...attendanceSessions.value, ...recorded];
}
} catch (err) {
console.warn('Attendance sessions not available:', err.message);
attendanceSessions.value = [];
} finally {
isAttendanceLoading.value = false;
}
};
const loadStats = async () => {
isLoading.value = true;
try {
@@ -357,6 +450,7 @@ const loadStats = async () => {
onMounted(() => {
loadStats();
loadAttendanceSessions();
});
</script>