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:
@@ -8,6 +8,33 @@
|
|||||||
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/attendance')" />
|
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/attendance')" />
|
||||||
</PageHeader>
|
</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">
|
<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>
|
<span class="text-muted text-xs block mb-1">یادداشت جلسه</span>
|
||||||
<p class="m-0 white-space-pre-wrap">{{ session.note }}</p>
|
<p class="m-0 white-space-pre-wrap">{{ session.note }}</p>
|
||||||
@@ -24,7 +51,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<Column header="#" style="width: 50px">
|
||||||
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
<template #body="{ index }">{{ toPersianDigits(index + 1) }}</template>
|
||||||
</Column>
|
</Column>
|
||||||
@@ -33,6 +65,18 @@
|
|||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<span class="font-bold text-color">{{ data.name }}</span>
|
<span class="font-bold text-color">{{ data.name }}</span>
|
||||||
<span class="text-xs text-muted block">کد ملی: {{ toPersianDigits(data.nationalIdCode || '—') }}</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>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
@@ -43,8 +87,9 @@
|
|||||||
:options="statusOptions"
|
:options="statusOptions"
|
||||||
optionLabel="label"
|
optionLabel="label"
|
||||||
optionValue="value"
|
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>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
@@ -62,7 +107,7 @@
|
|||||||
icon="pi pi-save"
|
icon="pi pi-save"
|
||||||
severity="success"
|
severity="success"
|
||||||
:loading="isSubmitting"
|
:loading="isSubmitting"
|
||||||
:disabled="!studentList.length"
|
:disabled="!canSubmit"
|
||||||
@click="handleSubmit"
|
@click="handleSubmit"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +116,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { sessionApi } from '@/api/sessionApi';
|
import { sessionApi } from '@/api/sessionApi';
|
||||||
import { attendanceApi } from '@/api/attendanceApi';
|
import { attendanceApi } from '@/api/attendanceApi';
|
||||||
@@ -83,6 +128,7 @@ import DataTable from 'primevue/datatable';
|
|||||||
import Column from 'primevue/column';
|
import Column from 'primevue/column';
|
||||||
import SelectButton from 'primevue/selectbutton';
|
import SelectButton from 'primevue/selectbutton';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -102,6 +148,15 @@ const statusOptions = [
|
|||||||
{ label: 'موجه', value: 'excused' }
|
{ 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 () => {
|
const fetchSessionAttendance = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await sessionApi.getOne(sessionId);
|
const res = await sessionApi.getOne(sessionId);
|
||||||
@@ -121,11 +176,11 @@ const fetchSessionAttendance = async () => {
|
|||||||
return {
|
return {
|
||||||
userId: user._id,
|
userId: user._id,
|
||||||
name: user.name || '',
|
name: user.name || '',
|
||||||
name: user.name || '',
|
|
||||||
gender: user.gender || null,
|
gender: user.gender || null,
|
||||||
nationalIdCode: user.nationalIdCode || '',
|
nationalIdCode: user.nationalIdCode || '',
|
||||||
status: existing?.status || 'present',
|
status: existing?.status ?? null,
|
||||||
note: existing?.note || ''
|
note: existing?.note || '',
|
||||||
|
isRecorded: Boolean(existing)
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -134,16 +189,23 @@ const fetchSessionAttendance = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const markAllPresent = () => {
|
const markAllPresent = () => {
|
||||||
studentList.value.forEach((s) => { s.status = 'present'; });
|
studentList.value.forEach((student) => {
|
||||||
|
student.status = 'present';
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (!canSubmit.value) {
|
||||||
|
showError('برای همه دانشجویان باید وضعیت حضور انتخاب شود');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
isSubmitting.value = true;
|
isSubmitting.value = true;
|
||||||
try {
|
try {
|
||||||
const payloadList = studentList.value.map((s) => ({
|
const payloadList = studentList.value.map((student) => ({
|
||||||
user: s.userId,
|
user: student.userId,
|
||||||
status: s.status,
|
status: student.status,
|
||||||
note: s.note
|
note: student.note
|
||||||
}));
|
}));
|
||||||
await attendanceApi.recordSessionAttendance(sessionId, payloadList);
|
await attendanceApi.recordSessionAttendance(sessionId, payloadList);
|
||||||
showSuccess('حضور و غیاب جلسه با موفقیت ثبت شد');
|
showSuccess('حضور و غیاب جلسه با موفقیت ثبت شد');
|
||||||
@@ -157,3 +219,13 @@ const handleSubmit = async () => {
|
|||||||
|
|
||||||
onMounted(fetchSessionAttendance);
|
onMounted(fetchSessionAttendance);
|
||||||
</script>
|
</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>
|
||||||
|
|||||||
@@ -89,10 +89,18 @@
|
|||||||
|
|
||||||
<Column field="attendanceStatus" header="وضعیت حضور">
|
<Column field="attendanceStatus" header="وضعیت حضور">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Tag
|
<div class="flex flex-column gap-1">
|
||||||
:value="getAttendanceStatus(data).label"
|
<Tag
|
||||||
:severity="getAttendanceStatus(data).severity"
|
: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>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user