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.
This commit is contained in:
2026-08-16 09:22:41 +03:30
parent 8d29a5d858
commit 26c53d57e4
2 changed files with 96 additions and 16 deletions
+84 -12
View File
@@ -8,6 +8,33 @@
<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>
@@ -24,7 +51,12 @@
</div>
</div>
<DataTable :value="studentList" class="p-datatable-sm text-sm" emptyMessage="دانشجویی در این کلاس ثبت‌نام نشده است">
<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>
@@ -33,6 +65,18 @@
<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>
@@ -43,8 +87,9 @@
:options="statusOptions"
optionLabel="label"
optionValue="value"
class="text-xs"
:class="['text-xs', { 'attendance-select-pending': !data.status }]"
/>
<small v-if="!data.status" class="text-orange-600 block mt-1">یک وضعیت انتخاب کنید</small>
</template>
</Column>
@@ -62,7 +107,7 @@
icon="pi pi-save"
severity="success"
:loading="isSubmitting"
:disabled="!studentList.length"
:disabled="!canSubmit"
@click="handleSubmit"
/>
</div>
@@ -71,7 +116,7 @@
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { ref, computed, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { sessionApi } from '@/api/sessionApi';
import { attendanceApi } from '@/api/attendanceApi';
@@ -83,6 +128,7 @@ 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();
@@ -102,6 +148,15 @@ const statusOptions = [
{ 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);
@@ -121,11 +176,11 @@ const fetchSessionAttendance = async () => {
return {
userId: user._id,
name: user.name || '',
name: user.name || '',
gender: user.gender || null,
nationalIdCode: user.nationalIdCode || '',
status: existing?.status || 'present',
note: existing?.note || ''
status: existing?.status ?? null,
note: existing?.note || '',
isRecorded: Boolean(existing)
};
});
} catch (err) {
@@ -134,16 +189,23 @@ const fetchSessionAttendance = async () => {
};
const markAllPresent = () => {
studentList.value.forEach((s) => { s.status = 'present'; });
studentList.value.forEach((student) => {
student.status = 'present';
});
};
const handleSubmit = async () => {
if (!canSubmit.value) {
showError('برای همه دانشجویان باید وضعیت حضور انتخاب شود');
return;
}
isSubmitting.value = true;
try {
const payloadList = studentList.value.map((s) => ({
user: s.userId,
status: s.status,
note: s.note
const payloadList = studentList.value.map((student) => ({
user: student.userId,
status: student.status,
note: student.note
}));
await attendanceApi.recordSessionAttendance(sessionId, payloadList);
showSuccess('حضور و غیاب جلسه با موفقیت ثبت شد');
@@ -157,3 +219,13 @@ const handleSubmit = async () => {
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>
+12 -4
View File
@@ -89,10 +89,18 @@
<Column field="attendanceStatus" header="وضعیت حضور">
<template #body="{ data }">
<Tag
:value="getAttendanceStatus(data).label"
:severity="getAttendanceStatus(data).severity"
/>
<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>