fix: jalali date defaults, calendar arrow direction, payment status label, edit page load, and session holding notification

This commit is contained in:
2026-08-21 20:17:37 +03:30
parent 8cd6e38c59
commit 78c6fe4529
9 changed files with 221 additions and 14 deletions
+2 -1
View File
@@ -10,5 +10,6 @@ export const sessionApi = {
delete: (id) => axiosInstance.delete(`/sessions/admin/delete/${id}`),
bulkDelete: (ids) => axiosInstance.post('/sessions/admin/bulk-delete', { ids }),
bulkUpdateStatus: (ids, status) => axiosInstance.post('/sessions/admin/bulk-status', { ids, status }),
recordAttendance: (sessionId, data) => axiosInstance.put(`/sessions/admin/${sessionId}/attendance`, data)
recordAttendance: (sessionId, data) => axiosInstance.put(`/sessions/admin/${sessionId}/attendance`, data),
notifyHolding: (id) => axiosInstance.post(`/sessions/admin/${id}/notify-holding`)
};
+2
View File
@@ -130,6 +130,7 @@ a {
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
transform: none !important;
width: 32px !important;
height: 32px !important;
border-radius: 8px !important;
@@ -143,6 +144,7 @@ a {
width: 14px !important;
height: 14px !important;
fill: var(--text-primary) !important;
transform: none !important;
path {
fill: var(--text-primary) !important;
@@ -74,7 +74,7 @@
<StatusTag :status="data.status || 'scheduled'" type="session" />
</template>
</Column>
<Column header="عملیات" style="width: 140px">
<Column header="عملیات" style="width: 170px">
<template #body="{ data }">
<div class="flex align-items-center gap-1">
<Button
@@ -86,6 +86,16 @@
v-tooltip.top="'حضور و غیاب'"
@click="$router.push(`/sessions/attendance/${data._id || data.id}`)"
/>
<Button
icon="pi pi-send"
text
rounded
size="small"
severity="info"
v-tooltip.top="'اطلاع‌رسانی برگزاری جلسه'"
:loading="notifyingSessionId === (data._id || data.id)"
@click="openNotifyHoldingDialog(data)"
/>
<Button
icon="pi pi-pencil"
text
@@ -118,6 +128,34 @@
@confirm="handleDeleteConfirm"
/>
<Dialog
v-model:visible="notifyDialogVisible"
header="اطلاع‌رسانی برگزاری جلسه"
modal
:style="{ width: '450px' }"
>
<div class="flex flex-column gap-3 py-2">
<p class="m-0 line-height-3 text-sm">
آیا از ارسال پیامک برگزاری جلسه طبق برنامه به تمامی دانشجویان این کلاس اطمینان دارید؟
</p>
<div v-if="selectedSessionForNotify" class="surface-100 p-3 border-round text-xs flex flex-column gap-1">
<div><strong>موضوع جلسه:</strong> {{ selectedSessionForNotify.topic || 'طبق برنامه' }}</div>
<div><strong>تاریخ:</strong> {{ formatJalali(selectedSessionForNotify.day || selectedSessionForNotify.date) }}</div>
<div><strong>ساعت:</strong> {{ selectedSessionForNotify.startTime || '—' }} تا {{ selectedSessionForNotify.endTime || '—' }}</div>
</div>
</div>
<template #footer>
<Button label="انصراف" text severity="secondary" @click="notifyDialogVisible = false" />
<Button
label="ارسال پیامک"
icon="pi pi-send"
severity="info"
:loading="isNotifying"
@click="handleSendSessionHolding"
/>
</template>
</Dialog>
<!-- Bulk auto-create: only when no sessions yet -->
<div v-if="!hasSessions" class="surface-card p-4 sm:p-5 border-round border-1 border-color shadow-sm">
<h2 class="text-lg font-bold text-color mb-1">ایجاد جلسات کلاس</h2>
@@ -249,6 +287,7 @@ import SelectButton from 'primevue/selectbutton';
import DatePicker from 'vue3-persian-datetime-picker';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Dialog from 'primevue/dialog';
const props = defineProps({
classId: { type: [String, Object], required: true },
@@ -260,7 +299,7 @@ const props = defineProps({
endTime: { type: String, default: '' }
});
const { toPersianDigits, toLatinDigits, formatJalali } = usePersianDate();
const { toPersianDigits, toLatinDigits, formatJalali, getTodayJalali } = usePersianDate();
const { showSuccess, showError } = useToast();
const todayJalali = () => moment().locale('fa').format('jYYYY/jMM/jDD');
@@ -278,6 +317,35 @@ const bulkStatus = ref(null);
const generatedSessions = ref([]);
const manualSessions = ref([{ date: todayJalali(), startTime: '19:00', endTime: '20:30', topic: '' }]);
const notifyDialogVisible = ref(false);
const selectedSessionForNotify = ref(null);
const notifyingSessionId = ref(null);
const isNotifying = ref(false);
const openNotifyHoldingDialog = (session) => {
selectedSessionForNotify.value = session;
notifyDialogVisible.value = true;
};
const handleSendSessionHolding = async () => {
if (!selectedSessionForNotify.value) return;
const id = selectedSessionForNotify.value._id || selectedSessionForNotify.value.id;
notifyingSessionId.value = id;
isNotifying.value = true;
try {
const res = await sessionApi.notifyHolding(id);
const count = res.data?.sentCount || res.data?.totalStudents || '';
showSuccess(`پیامک برگزاری جلسه با موفقیت به ${count ? toPersianDigits(count) + ' نفر از ' : ''}دانشجویان ارسال شد`);
notifyDialogVisible.value = false;
selectedSessionForNotify.value = null;
} catch (err) {
showError(err);
} finally {
notifyingSessionId.value = null;
isNotifying.value = false;
}
};
const statusOptions = [
{ label: 'طبق برنامه', value: 'scheduled' },
{ label: 'برگزار شده', value: 'held' },
+2 -2
View File
@@ -44,7 +44,7 @@ const NOTIFICATION_LABELS = {
const PAYMENT_LABELS = {
paid: 'تسویه‌شده',
partial: 'پرداخت جزئی',
partial: یش پرداخت',
pending: 'در انتظار پرداخت',
overdue: 'معوق',
cancelled: 'لغوشده'
@@ -63,7 +63,7 @@ const severity = computed(() => {
if (props.type === 'payment') {
if (val === 'paid' || val === 'تسویه شده' || val === 'تسویه‌شده') return 'success';
if (val === 'partial' || val === 'پرداخت جزئی') return 'contrast';
if (val === 'partial' || val === 'پرداخت جزئی' || val === 'پیش پرداخت') return 'contrast';
if (val === 'pending' || val === 'در انتظار پرداخت') return 'info';
if (val === 'overdue' || val === 'معوق شده' || val === 'معوق') return 'danger';
if (val === 'cancelled' || val === 'canceled' || val === 'لغوشده') return 'secondary';
@@ -5,9 +5,48 @@
:title="$t('attendances.title')"
:subtitle="`دوره: ${session.course?.title || '-'} | کلاس: ${session.class?.name || '-'} | تاریخ: ${formatJalali(session.day)} (${session.startTime || ''} تا ${session.endTime || ''})`"
>
<div class="flex align-items-center gap-2">
<Button
label="ارسال پیامک برگزاری جلسه"
icon="pi pi-send"
severity="info"
outlined
size="small"
:loading="isNotifying"
@click="notifyDialogVisible = true"
/>
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/attendance')" />
</div>
</PageHeader>
<Dialog
v-model:visible="notifyDialogVisible"
header="اطلاع‌رسانی برگزاری جلسه"
modal
:style="{ width: '450px' }"
>
<div class="flex flex-column gap-3 py-2">
<p class="m-0 line-height-3 text-sm">
آیا از ارسال پیامک برگزاری جلسه طبق برنامه به تمامی دانشجویان کلاس «{{ session?.class?.name || session?.course?.title || '' }}» اطمینان دارید؟
</p>
<div class="surface-100 p-3 border-round text-xs flex flex-column gap-1">
<div><strong>موضوع جلسه:</strong> {{ session.topic || 'طبق برنامه' }}</div>
<div><strong>تاریخ:</strong> {{ formatJalali(session.day || session.date) }}</div>
<div><strong>ساعت:</strong> {{ session.startTime || '—' }} تا {{ session.endTime || '—' }}</div>
</div>
</div>
<template #footer>
<Button label="انصراف" text severity="secondary" @click="notifyDialogVisible = false" />
<Button
label="ارسال پیامک"
icon="pi pi-send"
severity="info"
:loading="isNotifying"
@click="handleSendSessionHolding"
/>
</template>
</Dialog>
<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"
@@ -129,6 +168,7 @@ import Column from 'primevue/column';
import SelectButton from 'primevue/selectbutton';
import InputText from 'primevue/inputtext';
import Tag from 'primevue/tag';
import Dialog from 'primevue/dialog';
const route = useRoute();
const router = useRouter();
@@ -140,6 +180,23 @@ const { showSuccess, showError } = useToast();
const session = ref(null);
const studentList = ref([]);
const isSubmitting = ref(false);
const notifyDialogVisible = ref(false);
const isNotifying = ref(false);
const handleSendSessionHolding = async () => {
if (!sessionId) return;
isNotifying.value = true;
try {
const res = await sessionApi.notifyHolding(sessionId);
const count = res.data?.sentCount || res.data?.totalStudents || '';
showSuccess(`پیامک برگزاری جلسه با موفقیت به ${count ? toPersianDigits(count) + ' نفر از ' : ''}دانشجویان ارسال شد`);
notifyDialogVisible.value = false;
} catch (err) {
showError(err);
} finally {
isNotifying.value = false;
}
};
const statusOptions = [
{ label: 'حاضر', value: 'present' },
+11 -1
View File
@@ -63,7 +63,7 @@
<div class="flex align-items-center justify-content-between mb-3">
<h3 class="text-lg font-bold m-0">تاریخچه تراکنشهای دریافتی</h3>
<PermissionGate permission="payments:update">
<Button label="ثبت تراکنش جدید" icon="pi pi-plus" size="small" severity="success" @click="showTransactionModal = true" />
<Button label="ثبت تراکنش جدید" icon="pi pi-plus" size="small" severity="success" @click="openTransactionModal" />
</PermissionGate>
</div>
@@ -316,6 +316,16 @@ const defaultTrxForm = () => ({
const trxForm = reactive(defaultTrxForm());
const openTransactionModal = () => {
Object.assign(trxForm, defaultTrxForm());
if (remainingAmount.value > 0) {
trxForm.amount = remainingAmount.value;
}
trxForm.dueDate = getTodayJalali();
trxForm.date = getTodayJalali();
showTransactionModal.value = true;
};
const editForm = reactive({
amount: 0,
status: 'paid',
+3 -5
View File
@@ -225,7 +225,7 @@ const createForm = reactive({
amount: 0,
discount: 0,
notes: '',
dueDate: ''
dueDate: getTodayJalali()
});
const hasDiscount = ref(false);
const createNotify = reactive({ sms: true, email: true, bot: true });
@@ -287,10 +287,8 @@ const onClassesSelected = () => {
if ((createForm.discount || 0) > (createForm.amount || 0)) {
createForm.discount = createForm.amount || 0;
}
const firstSelectedClass = classesList.value.find((item) => String(item._id || item.id) === String(createForm.classes[0]));
if (firstSelectedClass) {
createForm.dueDate = calculateClassMidDate(firstSelectedClass);
if (!createForm.dueDate) {
createForm.dueDate = getTodayJalali();
}
};
+1 -1
View File
@@ -112,7 +112,7 @@ import Button from 'primevue/button';
const route = useRoute();
const router = useRouter();
const { showSuccess, showError } = useToast();
const { toLatinDigits } = usePersianDate();
const { toLatinDigits, getTodayJalali } = usePersianDate();
const sessionId = route.params.id;
const isEditMode = computed(() => !!sessionId);
+72 -1
View File
@@ -116,7 +116,7 @@
</template>
</Column>
<Column header="عملیات" style="width: 170px">
<Column header="عملیات" style="width: 200px">
<template #body="{ data }">
<div class="flex align-items-center gap-1">
<PermissionGate permission="sessions:attendance">
@@ -131,6 +131,19 @@
/>
</PermissionGate>
<PermissionGate permission="sessions:read">
<Button
icon="pi pi-send"
text
rounded
size="small"
severity="info"
v-tooltip.top="'اطلاع‌رسانی برگزاری جلسه'"
:loading="notifyingSessionId === (data._id || data.id)"
@click="openNotifyHoldingDialog(data)"
/>
</PermissionGate>
<PermissionGate permission="sessions:update">
<Button icon="pi pi-pencil" text rounded size="small" severity="warning" v-tooltip.top="'ویرایش'" @click="$router.push(`/sessions/edit/${data._id || data.id}`)" />
</PermissionGate>
@@ -150,6 +163,34 @@
:message="bulkDeleteMessage"
@confirm="handleDeleteConfirm"
/>
<Dialog
v-model:visible="notifyDialogVisible"
header="اطلاع‌رسانی برگزاری جلسه"
modal
:style="{ width: '450px' }"
>
<div class="flex flex-column gap-3 py-2">
<p class="m-0 line-height-3 text-sm">
آیا از ارسال پیامک برگزاری جلسه طبق برنامه به دانشجویان کلاس «{{ selectedSessionForNotify?.class?.name || selectedSessionForNotify?.course?.title || '' }}» اطمینان دارید؟
</p>
<div v-if="selectedSessionForNotify" class="surface-100 p-3 border-round text-xs flex flex-column gap-1">
<div><strong>موضوع جلسه:</strong> {{ selectedSessionForNotify.topic || 'طبق برنامه' }}</div>
<div><strong>تاریخ:</strong> {{ formatJalali(selectedSessionForNotify.day || selectedSessionForNotify.date) }}</div>
<div><strong>ساعت:</strong> {{ selectedSessionForNotify.startTime || '—' }} تا {{ selectedSessionForNotify.endTime || '—' }}</div>
</div>
</div>
<template #footer>
<Button label="انصراف" text severity="secondary" @click="notifyDialogVisible = false" />
<Button
label="ارسال پیامک"
icon="pi pi-send"
severity="info"
:loading="isNotifying"
@click="handleSendSessionHolding"
/>
</template>
</Dialog>
</div>
</template>
@@ -170,6 +211,7 @@ import Column from 'primevue/column';
import Tag from 'primevue/tag';
import Select from 'primevue/select';
import Dropdown from 'primevue/select';
import Dialog from 'primevue/dialog';
const { formatJalali, toPersianDigits } = usePersianDate();
const { showSuccess, showError } = useToast();
@@ -200,6 +242,35 @@ const selectedSession = ref(null);
const bulkDeleteMode = ref(false);
const isDeleting = ref(false);
const notifyDialogVisible = ref(false);
const selectedSessionForNotify = ref(null);
const notifyingSessionId = ref(null);
const isNotifying = ref(false);
const openNotifyHoldingDialog = (session) => {
selectedSessionForNotify.value = session;
notifyDialogVisible.value = true;
};
const handleSendSessionHolding = async () => {
if (!selectedSessionForNotify.value) return;
const id = selectedSessionForNotify.value._id || selectedSessionForNotify.value.id;
notifyingSessionId.value = id;
isNotifying.value = true;
try {
const res = await sessionApi.notifyHolding(id);
const count = res.data?.sentCount || res.data?.totalStudents || '';
showSuccess(`پیامک برگزاری جلسه با موفقیت به ${count ? toPersianDigits(count) + ' نفر از ' : ''}دانشجویان ارسال شد`);
notifyDialogVisible.value = false;
selectedSessionForNotify.value = null;
} catch (err) {
showError(err);
} finally {
notifyingSessionId.value = null;
isNotifying.value = false;
}
};
const statusOptions = [
{ label: 'طبق برنامه', value: 'scheduled' },
{ label: 'برگزار شده', value: 'held' },