Files
gameno-dashboard/src/views/attendances/AttendanceListView.vue
T

246 lines
7.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- /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>