feat(payments): add bulk payment modal and duplicate payment warning in dialogs
This commit is contained in:
@@ -11,6 +11,15 @@
|
||||
@click="$router.push(`/financial-reports?classId=${classId}`)"
|
||||
/>
|
||||
</PermissionGate>
|
||||
<PermissionGate permission="payments:create">
|
||||
<Button
|
||||
label="صدور صورتحساب گروهی"
|
||||
icon="pi pi-wallet"
|
||||
severity="success"
|
||||
class="ml-2"
|
||||
@click="openBulkModal"
|
||||
/>
|
||||
</PermissionGate>
|
||||
<Button
|
||||
label="حذف کلاس"
|
||||
icon="pi pi-trash"
|
||||
@@ -151,31 +160,111 @@
|
||||
:loading="isDeletingClass"
|
||||
@confirm="handleDeleteClass"
|
||||
/>
|
||||
|
||||
<!-- Bulk Class Payment Modal -->
|
||||
<Dialog v-model:visible="bulkModalVisible" header="صدور صورتحساب گروهی برای این کلاس" modal :style="{ width: '540px' }">
|
||||
<div class="flex flex-column gap-3 py-2" v-if="classData">
|
||||
<div class="p-3 bg-blue-50 border-round border-1 border-blue-200 text-blue-900 text-sm flex align-items-center justify-content-between">
|
||||
<span>تعداد دانشجویان ثبتنامشده:</span>
|
||||
<span class="font-bold">{{ toPersianDigits((classData.students || []).length) }} نفر</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="class-bulk-amount">مبلغ شهریه هر دانشجو (تومان) *</label>
|
||||
<InputNumber inputId="class-bulk-amount" v-model="bulkForm.amount" class="w-full text-sm" suffix=" تومان" :min="0" />
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-2">
|
||||
<Checkbox v-model="hasBulkDiscount" binary inputId="class-bulk-discount" />
|
||||
<label for="class-bulk-discount" class="font-semibold text-sm cursor-pointer">تخفیف همگانی ؟</label>
|
||||
</div>
|
||||
|
||||
<div v-if="hasBulkDiscount" class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="class-bulk-discount-amount">مبلغ تخفیف (تومان)</label>
|
||||
<InputNumber
|
||||
inputId="class-bulk-discount-amount"
|
||||
v-model="bulkForm.discount"
|
||||
class="w-full text-sm"
|
||||
suffix=" تومان"
|
||||
:min="0"
|
||||
:max="bulkForm.amount || 0"
|
||||
/>
|
||||
<small class="font-semibold text-color">
|
||||
مبلغ نهایی هر صورتحساب: {{ toPersianDigits(bulkPayableAmount.toLocaleString()) }} تومان
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
||||
<DatePicker v-model="bulkForm.dueDate" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-2">
|
||||
<Checkbox v-model="bulkForm.skipExisting" binary inputId="class-bulk-skip-existing" />
|
||||
<label for="class-bulk-skip-existing" class="text-sm cursor-pointer font-medium">
|
||||
عدم صدور مجدد برای دانشجویانی که قبلاً در این کلاس صورتحساب دارند
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="class-bulk-notes">یادداشت</label>
|
||||
<Textarea
|
||||
id="class-bulk-notes"
|
||||
v-model="bulkForm.notes"
|
||||
rows="2"
|
||||
class="w-full text-sm"
|
||||
maxlength="5000"
|
||||
placeholder="یادداشت برای تمام صورتحسابهای صادره…"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NotifyChannelsField :notify="bulkNotify" />
|
||||
</div>
|
||||
<template #footer>
|
||||
<Button label="انصراف" text severity="secondary" @click="bulkModalVisible = false" />
|
||||
<Button
|
||||
label="صدور صورتحسابها"
|
||||
icon="pi pi-check"
|
||||
severity="success"
|
||||
:loading="isBulkCreating"
|
||||
:disabled="!(classData?.students?.length)"
|
||||
@click="handleBulkCreatePayment"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import { formatClassDays, formatClassTime } from '@/utils/classSchedule';
|
||||
import { paymentApi } from '@/api/paymentApi';
|
||||
import { formatClassDays, formatClassTime, calculateClassMidDate } from '@/utils/classSchedule';
|
||||
import { sessionApi } from '@/api/sessionApi';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { usePermission } from '@/composables/usePermission';
|
||||
import { getPayableAmount } from '@/utils/paymentAmount';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import StatusTag from '@/components/common/StatusTag.vue';
|
||||
import TableSkeleton from '@/components/common/TableSkeleton.vue';
|
||||
import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
|
||||
import Button from 'primevue/button';
|
||||
import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import DatePicker from 'vue3-persian-datetime-picker';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const classId = route.params.id;
|
||||
const { toPersianDigits, formatJalali } = usePersianDate();
|
||||
const { toPersianDigits, formatJalali, toGregorianIso, getTodayJalali } = usePersianDate();
|
||||
const { showError, showSuccess } = useToast();
|
||||
const { hasPermission } = usePermission();
|
||||
const canManageStudents = computed(() => hasPermission('classes:register_users'));
|
||||
@@ -190,6 +279,86 @@ const studentToRemove = ref(null);
|
||||
const deleteClassDialogVisible = ref(false);
|
||||
const isDeletingClass = ref(false);
|
||||
|
||||
// Bulk Payment State
|
||||
const bulkModalVisible = ref(false);
|
||||
const isBulkCreating = ref(false);
|
||||
const hasBulkDiscount = ref(false);
|
||||
const bulkNotify = reactive({ sms: true, email: true, bot: true });
|
||||
const bulkForm = reactive({
|
||||
amount: 0,
|
||||
discount: 0,
|
||||
notes: '',
|
||||
dueDate: getTodayJalali(),
|
||||
skipExisting: true
|
||||
});
|
||||
|
||||
const bulkPayableAmount = computed(() => getPayableAmount({
|
||||
amount: bulkForm.amount,
|
||||
discount: hasBulkDiscount.value ? bulkForm.discount : 0
|
||||
}));
|
||||
|
||||
const openBulkModal = () => {
|
||||
if (!classData.value) return;
|
||||
bulkForm.amount = classData.value.tuitionFee || classData.value.course?.price || 0;
|
||||
if (classData.value.hasDiscount && classData.value.discount) {
|
||||
hasBulkDiscount.value = true;
|
||||
bulkForm.discount = classData.value.discount;
|
||||
} else {
|
||||
hasBulkDiscount.value = false;
|
||||
bulkForm.discount = 0;
|
||||
}
|
||||
bulkForm.notes = '';
|
||||
bulkForm.skipExisting = true;
|
||||
bulkNotify.sms = true;
|
||||
bulkNotify.email = true;
|
||||
bulkNotify.bot = true;
|
||||
bulkForm.dueDate = calculateClassMidDate(classData.value, sessions.value);
|
||||
bulkModalVisible.value = true;
|
||||
};
|
||||
|
||||
const handleBulkCreatePayment = async () => {
|
||||
if (!classData.value?.students?.length) {
|
||||
showError('هیچ دانشجویی در این کلاس ثبتنام نشده است');
|
||||
return;
|
||||
}
|
||||
if (!bulkForm.amount) { showError('لطفا مبلغ شهریه را وارد کنید'); return; }
|
||||
if (!bulkForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
||||
const dueDate = toGregorianIso(bulkForm.dueDate);
|
||||
if (!dueDate) { showError('تاریخ سررسید نامعتبر است'); return; }
|
||||
|
||||
if (hasBulkDiscount.value && (bulkForm.discount || 0) > bulkForm.amount) {
|
||||
showError('مبلغ تخفیف نمیتواند بیشتر از مبلغ کل باشد');
|
||||
return;
|
||||
}
|
||||
|
||||
isBulkCreating.value = true;
|
||||
try {
|
||||
const res = await paymentApi.createBulkClass({
|
||||
classId,
|
||||
amount: bulkForm.amount,
|
||||
discount: hasBulkDiscount.value ? (bulkForm.discount || 0) : 0,
|
||||
dueDate,
|
||||
notes: bulkForm.notes,
|
||||
skipExisting: bulkForm.skipExisting,
|
||||
notify: { ...bulkNotify }
|
||||
});
|
||||
const result = res.data?.data || res.data || res;
|
||||
const createdCount = result.createdCount ?? 0;
|
||||
const skippedCount = result.skippedCount ?? 0;
|
||||
|
||||
let msg = `صورتحساب برای ${toPersianDigits(createdCount)} دانشجو با موفقیت ایجاد شد`;
|
||||
if (skippedCount > 0) {
|
||||
msg += ` (${toPersianDigits(skippedCount)} دانشجو به دلیل داشتن صورتحساب قبلی رد شدند)`;
|
||||
}
|
||||
showSuccess(msg);
|
||||
bulkModalVisible.value = false;
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
} finally {
|
||||
isBulkCreating.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteClassDialogMessage = computed(() => {
|
||||
const name = classData.value?.name;
|
||||
return name
|
||||
|
||||
Reference in New Issue
Block a user