246 lines
7.9 KiB
Vue
246 lines
7.9 KiB
Vue
<!-- /src/views/attendances/AttendanceListView.vue -->
|
||
<template>
|
||
<div class="attendance-list-view">
|
||
<PageHeader
|
||
title="حضور و غیاب"
|
||
subtitle="جلسات سررسید گذشته که ثبت حضور دانشجویان آنها ناقص است"
|
||
>
|
||
<Button
|
||
label="مشاهده جلسات"
|
||
icon="pi pi-calendar"
|
||
text
|
||
@click="$router.push('/sessions')"
|
||
/>
|
||
</PageHeader>
|
||
|
||
<div class="grid mb-4">
|
||
<div class="col-12 sm:col-4">
|
||
<div class="surface-card p-4 border-round-xl border-1 border-color shadow-sm">
|
||
<span class="text-muted text-xs block mb-1">در انتظار ثبت حضور</span>
|
||
<span class="text-3xl font-bold text-orange-500">{{ toPersianDigits(summary.pending) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-12 sm:col-4">
|
||
<div class="surface-card p-4 border-round-xl border-1 border-color shadow-sm">
|
||
<span class="text-muted text-xs block mb-1">ثبت کامل شده</span>
|
||
<span class="text-3xl font-bold text-green-500">{{ toPersianDigits(summary.recorded) }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="col-12 sm:col-4">
|
||
<div class="surface-card p-4 border-round-xl border-1 border-color shadow-sm">
|
||
<span class="text-muted text-xs block mb-1">جلسات سررسید گذشته</span>
|
||
<span class="text-3xl font-bold text-color">{{ toPersianDigits(summary.total) }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<DataTableWrapper
|
||
:items="items"
|
||
:totalCount="totalCount"
|
||
:page="queryParams.page"
|
||
:limit="queryParams.limit"
|
||
:sortBy="queryParams.sortBy"
|
||
:sortOrder="queryParams.sortOrder"
|
||
:loading="isLoading"
|
||
@page-change="onPageChange"
|
||
@sort-change="onSort"
|
||
@search-change="onSearch"
|
||
>
|
||
<template #toolbar>
|
||
<div class="flex align-items-center justify-content-between flex-wrap gap-2 w-full">
|
||
<SelectButton
|
||
v-model="attendanceFilter"
|
||
:options="filterOptions"
|
||
optionLabel="label"
|
||
optionValue="value"
|
||
class="text-sm"
|
||
@change="onAttendanceFilterChange"
|
||
/>
|
||
<Dropdown
|
||
v-model="selectedClassId"
|
||
:options="classesList"
|
||
optionLabel="name"
|
||
optionValue="_id"
|
||
placeholder="همه کلاسها"
|
||
showClear
|
||
filter
|
||
class="w-16rem text-sm"
|
||
@change="onClassFilterChange"
|
||
/>
|
||
</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="class" header="کلاس">
|
||
<template #body="{ data }">
|
||
{{ data.class?.name || '—' }}
|
||
</template>
|
||
</Column>
|
||
|
||
<Column field="day" header="تاریخ" sortable sortField="day">
|
||
<template #body="{ data }">
|
||
{{ formatJalali(data.day || data.date) }}
|
||
</template>
|
||
</Column>
|
||
|
||
<Column field="timeRange" header="ساعت">
|
||
<template #body="{ data }">
|
||
<span dir="ltr">{{ data.startTime || '—' }} – {{ data.endTime || '—' }}</span>
|
||
</template>
|
||
</Column>
|
||
|
||
<Column field="attendanceStatus" header="وضعیت حضور">
|
||
<template #body="{ data }">
|
||
<div class="flex flex-column gap-1">
|
||
<Tag
|
||
:value="getAttendanceStatus(data).label"
|
||
:severity="getAttendanceStatus(data).severity"
|
||
/>
|
||
<span
|
||
v-if="data.attendanceTotalCount"
|
||
class="text-xs text-muted"
|
||
>
|
||
{{ toPersianDigits(data.attendanceRecordedCount || 0) }} از {{ toPersianDigits(data.attendanceTotalCount) }} دانشجو
|
||
</span>
|
||
</div>
|
||
</template>
|
||
</Column>
|
||
|
||
<Column field="status" header="وضعیت جلسه">
|
||
<template #body="{ data }">
|
||
<StatusTag :status="data.status || 'scheduled'" type="session" />
|
||
</template>
|
||
</Column>
|
||
|
||
<Column header="عملیات" style="width: 120px">
|
||
<template #body="{ data }">
|
||
<Button
|
||
icon="pi pi-check-square"
|
||
:label="data.attendanceComplete ? 'ویرایش' : 'ثبت'"
|
||
size="small"
|
||
:severity="data.attendanceComplete ? 'secondary' : 'success'"
|
||
@click="$router.push(`/sessions/attendance/${data._id || data.id}`)"
|
||
/>
|
||
</template>
|
||
</Column>
|
||
</DataTableWrapper>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, onMounted } from 'vue';
|
||
import { useDataTable } from '@/composables/useDataTable';
|
||
import { usePersianDate } from '@/composables/usePersianDate';
|
||
import { sessionApi } from '@/api/sessionApi';
|
||
import { classApi } from '@/api/classApi';
|
||
import PageHeader from '@/components/common/PageHeader.vue';
|
||
import DataTableWrapper from '@/components/common/DataTableWrapper.vue';
|
||
import StatusTag from '@/components/common/StatusTag.vue';
|
||
import Button from 'primevue/button';
|
||
import Column from 'primevue/column';
|
||
import Tag from 'primevue/tag';
|
||
import SelectButton from 'primevue/selectbutton';
|
||
import Dropdown from 'primevue/dropdown';
|
||
|
||
const { formatJalali, toPersianDigits } = usePersianDate();
|
||
|
||
const attendanceFilter = ref('pending');
|
||
const filterOptions = [
|
||
{ label: 'در انتظار ثبت', value: 'pending' },
|
||
{ label: 'ثبت شده', value: 'recorded' }
|
||
];
|
||
|
||
const selectedClassId = ref(null);
|
||
const classesList = ref([]);
|
||
const summary = ref({ pending: 0, recorded: 0, total: 0 });
|
||
|
||
const fetchAttendanceSessions = (params) =>
|
||
sessionApi.getAll({
|
||
...params,
|
||
class: selectedClassId.value || undefined,
|
||
attendanceScope: params.attendanceScope || attendanceFilter.value
|
||
});
|
||
|
||
const {
|
||
items,
|
||
totalCount,
|
||
isLoading,
|
||
queryParams,
|
||
loadData,
|
||
onPageChange,
|
||
onSort,
|
||
onSearch
|
||
} = useDataTable(fetchAttendanceSessions, {
|
||
sortBy: 'day',
|
||
sortOrder: 'desc',
|
||
attendanceScope: 'pending'
|
||
});
|
||
|
||
const getAttendanceStatus = (session) => {
|
||
if (session.attendanceComplete) {
|
||
return { label: 'ثبت شده', severity: 'success' };
|
||
}
|
||
if (session.attendancePending) {
|
||
return { label: 'در انتظار ثبت', severity: 'warn' };
|
||
}
|
||
return { label: 'برگزار نشده', severity: 'secondary' };
|
||
};
|
||
|
||
const onAttendanceFilterChange = () => {
|
||
queryParams.attendanceScope = attendanceFilter.value;
|
||
queryParams.page = 1;
|
||
loadData();
|
||
};
|
||
|
||
const onClassFilterChange = () => {
|
||
queryParams.class = selectedClassId.value || undefined;
|
||
queryParams.page = 1;
|
||
loadData();
|
||
loadSummary();
|
||
};
|
||
|
||
const loadClasses = async () => {
|
||
try {
|
||
const res = await classApi.getAll({ limit: 500, sortBy: 'name', sortOrder: 'asc' });
|
||
const data = res.data || res;
|
||
classesList.value = data.items || data.classes || data.data || data || [];
|
||
} catch (err) {
|
||
classesList.value = [];
|
||
}
|
||
};
|
||
|
||
const loadSummary = async () => {
|
||
try {
|
||
const classParam = selectedClassId.value || undefined;
|
||
const [pendingRes, recordedRes] = await Promise.all([
|
||
sessionApi.getAll({ page: 1, limit: 1, attendanceScope: 'pending', class: classParam, sortBy: 'day', sortOrder: 'desc' }),
|
||
sessionApi.getAll({ page: 1, limit: 1, attendanceScope: 'recorded', class: classParam, sortBy: 'day', sortOrder: 'desc' })
|
||
]);
|
||
const pending = pendingRes.meta?.totalCount ?? 0;
|
||
const recorded = recordedRes.meta?.totalCount ?? 0;
|
||
summary.value = {
|
||
pending,
|
||
recorded,
|
||
total: pending + recorded
|
||
};
|
||
} catch {
|
||
summary.value = { pending: 0, recorded: 0, total: 0 };
|
||
}
|
||
};
|
||
|
||
onMounted(async () => {
|
||
await Promise.all([loadData(), loadSummary(), loadClasses()]);
|
||
});
|
||
</script>
|