Files
gameno-dashboard/src/views/attendances/AttendanceEntryView.vue
T
kavehhn 26c53d57e4 Require explicit attendance status selection before submit.
Do not prefill present for unsaved students, highlight pending rows, and show recorded counts on the attendance list.
2026-08-16 09:22:41 +03:30

232 lines
8.5 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- /src/views/attendances/AttendanceEntryView.vue -->
<template>
<div class="attendance-entry-view" v-if="session">
<PageHeader
:title="$t('attendances.title')"
:subtitle="`دوره: ${session.course?.title || '-'} | کلاس: ${session.class?.name || '-'} | تاریخ: ${formatJalali(session.day)} (${session.startTime || ''} تا ${session.endTime || ''})`"
>
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/attendance')" />
</PageHeader>
<div
v-if="pendingStudents.length"
class="surface-card p-3 sm:p-4 border-round border-1 border-orange-300 bg-orange-50 mb-3 text-sm text-orange-900"
>
<strong>{{ toPersianDigits(pendingStudents.length) }} دانشجو هنوز وضعیت حضور ندارند</strong>
<span class="block mt-1">برای هر دانشجوی زیر یکی از گزینههای حضور، غیبت، تاخیر یا موجه را انتخاب کنید.</span>
<ul class="mt-2 mb-0 pr-3">
<li v-for="student in pendingStudents" :key="student.userId">
{{ student.name || '—' }}
<span v-if="student.nationalIdCode" class="text-xs">({{ toPersianDigits(student.nationalIdCode) }})</span>
</li>
</ul>
</div>
<div
v-else-if="!attendanceIsComplete"
class="surface-card p-3 sm:p-4 border-round border-1 border-blue-300 bg-blue-50 mb-3 text-sm text-blue-900"
>
<strong>وضعیت همه دانشجویان انتخاب شده است.</strong>
<span class="block mt-1">
برای ذخیره نهایی، دکمه «{{ $t('attendances.submitAttendance') }}» را بزنید.
</span>
<span class="block mt-2 font-semibold">
{{ toPersianDigits(recordedCount) }} از {{ toPersianDigits(studentList.length) }} دانشجو در سیستم ثبت شده
</span>
</div>
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm mb-3" v-if="session.note">
<span class="text-muted text-xs block mb-1">یادداشت جلسه</span>
<p class="m-0 white-space-pre-wrap">{{ session.note }}</p>
</div>
<div class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
<div class="flex align-items-center justify-content-between mb-4 pb-3 border-bottom-1 border-color">
<div>
<h2 class="text-lg font-bold text-color m-0">لیست دانشجویان کلاس</h2>
<span class="text-muted text-xs">وضعیت حضور هر دانشجو را مشخص کنید</span>
</div>
<div class="flex align-items-center gap-2">
<Button label="علامت‌گذاری همه به عنوان حاضر" icon="pi pi-check-circle" size="small" text severity="success" @click="markAllPresent" />
</div>
</div>
<DataTable
:value="studentList"
:rowClass="getRowClass"
class="p-datatable-sm text-sm"
emptyMessage="دانشجویی در این کلاس ثبت‌نام نشده است"
>
<Column header="#" style="width: 50px">
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
</Column>
<Column field="name" header="نام و نام خانوادگی دانشجو">
<template #body="{ data }">
<span class="font-bold text-color">{{ data.name }}</span>
<span class="text-xs text-muted block">کد ملی: {{ toPersianDigits(data.nationalIdCode || '—') }}</span>
<Tag
v-if="!data.status"
value="نیاز به ثبت وضعیت"
severity="warn"
class="mt-1 text-xs"
/>
<Tag
v-else-if="!data.isRecorded"
value="ذخیره نشده"
severity="info"
class="mt-1 text-xs"
/>
</template>
</Column>
<Column field="status" header="وضعیت حضور">
<template #body="{ data }">
<SelectButton
v-model="data.status"
:options="statusOptions"
optionLabel="label"
optionValue="value"
:class="['text-xs', { 'attendance-select-pending': !data.status }]"
/>
<small v-if="!data.status" class="text-orange-600 block mt-1">یک وضعیت انتخاب کنید</small>
</template>
</Column>
<Column field="note" header="یادداشت / توضیحات">
<template #body="{ data }">
<InputText v-model="data.note" class="w-full text-xs" placeholder="دلیل غیبت یا تاخیر..." />
</template>
</Column>
</DataTable>
<div class="flex justify-content-end gap-2 mt-4 pt-3 border-top-1 border-color">
<Button label="انصراف" text severity="secondary" @click="$router.push('/attendance')" />
<Button
:label="$t('attendances.submitAttendance')"
icon="pi pi-save"
severity="success"
:loading="isSubmitting"
:disabled="!canSubmit"
@click="handleSubmit"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { sessionApi } from '@/api/sessionApi';
import { attendanceApi } from '@/api/attendanceApi';
import { usePersianDate } from '@/composables/usePersianDate';
import { useToast } from '@/composables/useToast';
import PageHeader from '@/components/common/PageHeader.vue';
import Button from 'primevue/button';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import SelectButton from 'primevue/selectbutton';
import InputText from 'primevue/inputtext';
import Tag from 'primevue/tag';
const route = useRoute();
const router = useRouter();
const sessionId = route.params.id;
const { toPersianDigits, formatJalali } = usePersianDate();
const { showSuccess, showError } = useToast();
const session = ref(null);
const studentList = ref([]);
const isSubmitting = ref(false);
const statusOptions = [
{ label: 'حاضر', value: 'present' },
{ label: 'غایب', value: 'absent' },
{ label: 'تاخیر', value: 'late' },
{ label: 'موجه', value: 'excused' }
];
const recordedCount = computed(() => studentList.value.filter((student) => student.isRecorded).length);
const pendingStudents = computed(() => studentList.value.filter((student) => !student.status));
const canSubmit = computed(() => studentList.value.length > 0 && pendingStudents.value.length === 0);
const attendanceIsComplete = computed(() =>
studentList.value.length > 0 && recordedCount.value === studentList.value.length
);
const getRowClass = (data) => (!data.status ? 'attendance-row-pending' : '');
const fetchSessionAttendance = async () => {
try {
const res = await sessionApi.getOne(sessionId);
const data = res.data || res;
session.value = data;
const roster = data.class?.students || [];
const attendanceByUser = new Map(
(data.attendanceList || []).map((row) => [
String(row.user?._id || row.user),
row
])
);
studentList.value = roster.map((user) => {
const existing = attendanceByUser.get(String(user._id));
return {
userId: user._id,
name: user.name || '',
gender: user.gender || null,
nationalIdCode: user.nationalIdCode || '',
status: existing?.status ?? null,
note: existing?.note || '',
isRecorded: Boolean(existing)
};
});
} catch (err) {
showError(err);
}
};
const markAllPresent = () => {
studentList.value.forEach((student) => {
student.status = 'present';
});
};
const handleSubmit = async () => {
if (!canSubmit.value) {
showError('برای همه دانشجویان باید وضعیت حضور انتخاب شود');
return;
}
isSubmitting.value = true;
try {
const payloadList = studentList.value.map((student) => ({
user: student.userId,
status: student.status,
note: student.note
}));
await attendanceApi.recordSessionAttendance(sessionId, payloadList);
showSuccess('حضور و غیاب جلسه با موفقیت ثبت شد');
router.push('/attendance');
} catch (err) {
showError(err);
} finally {
isSubmitting.value = false;
}
};
onMounted(fetchSessionAttendance);
</script>
<style scoped>
:deep(.attendance-row-pending) {
background: rgba(249, 115, 22, 0.08);
}
:deep(.attendance-select-pending .p-selectbutton .p-button) {
border-color: rgba(249, 115, 22, 0.45);
}
</style>