feat: improve bill flow, user columns, and admin notes UI
Select class before user when creating bills, fix national ID and registered classes display, remove course ratings, fix notification badge, and show Persian datetime in the topbar.
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
<div class="payment-list-view">
|
||||
<PageHeader :title="$t('payments.title')" :subtitle="$t('payments.subtitle')">
|
||||
<PermissionGate permission="payments:create">
|
||||
<Button :label="$t('payments.addPayment')" icon="pi pi-plus" severity="success" @click="showCreateModal = true" />
|
||||
<Button :label="$t('payments.addPayment')" icon="pi pi-plus" severity="success" @click="openCreateModal" />
|
||||
</PermissionGate>
|
||||
</PageHeader>
|
||||
|
||||
@@ -80,24 +80,37 @@
|
||||
<Dialog v-model:visible="showCreateModal" header="ایجاد صورتحساب جدید" modal :style="{ width: '480px' }">
|
||||
<div class="flex flex-column gap-3 py-2">
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب کاربر / دانشجو *</label>
|
||||
<Dropdown v-model="createForm.user" :options="usersList" optionLabel="fullName" optionValue="_id" filter placeholder="کاربر را انتخاب کنید" class="w-full text-sm" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب یک یا چند کلاس مربوطه *</label>
|
||||
<label class="font-semibold text-sm">انتخاب یک یا چند کلاس *</label>
|
||||
<MultiSelect
|
||||
v-model="createForm.classes"
|
||||
:options="classesList"
|
||||
optionLabel="name"
|
||||
optionValue="_id"
|
||||
display="chip"
|
||||
placeholder="کلاسها را انتخاب کنید"
|
||||
filter
|
||||
placeholder="ابتدا کلاسها را انتخاب کنید"
|
||||
class="w-full text-sm"
|
||||
@change="onClassesSelected"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">انتخاب کاربر / دانشجو *</label>
|
||||
<Dropdown
|
||||
v-model="createForm.user"
|
||||
:options="eligibleUsers"
|
||||
optionLabel="fullName"
|
||||
optionValue="_id"
|
||||
filter
|
||||
:placeholder="createForm.classes?.length ? 'دانشجوی ثبتنامشده را انتخاب کنید' : 'ابتدا کلاس را انتخاب کنید'"
|
||||
class="w-full text-sm"
|
||||
:disabled="!createForm.classes?.length"
|
||||
/>
|
||||
<small v-if="createForm.classes?.length && !eligibleUsers.length" class="text-orange-500">
|
||||
هیچ دانشجوی ثبتنامشدهای در کلاسهای انتخابشده یافت نشد
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مبلغ کل صورتحساب (تومان) *</label>
|
||||
<InputNumber v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" />
|
||||
@@ -123,7 +136,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { useDataTable } from '@/composables/useDataTable';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
@@ -174,11 +187,43 @@ const deleteDialogVisible = ref(false);
|
||||
const selectedPayment = ref(null);
|
||||
const isDeleting = ref(false);
|
||||
|
||||
const studentIdOf = (student) => String(student?._id || student?.id || student);
|
||||
|
||||
const eligibleUsers = computed(() => {
|
||||
if (!createForm.classes?.length) return [];
|
||||
|
||||
const selectedIds = new Set(createForm.classes.map(String));
|
||||
const selectedClasses = classesList.value.filter((c) =>
|
||||
selectedIds.has(String(c._id || c.id))
|
||||
);
|
||||
if (!selectedClasses.length) return [];
|
||||
|
||||
// Intersection: user must be registered in every selected class
|
||||
let eligibleIds = null;
|
||||
selectedClasses.forEach((cls) => {
|
||||
const ids = new Set((cls.students || []).map(studentIdOf));
|
||||
if (eligibleIds === null) {
|
||||
eligibleIds = ids;
|
||||
} else {
|
||||
eligibleIds = new Set([...eligibleIds].filter((id) => ids.has(id)));
|
||||
}
|
||||
});
|
||||
|
||||
return usersList.value.filter((u) => eligibleIds?.has(String(u._id || u.id)));
|
||||
});
|
||||
|
||||
const onClassesSelected = () => {
|
||||
if (!createForm.classes || createForm.classes.length === 0) return;
|
||||
if (createForm.user && !eligibleUsers.value.some((u) => (u._id || u.id) === createForm.user)) {
|
||||
createForm.user = null;
|
||||
}
|
||||
|
||||
if (!createForm.classes || createForm.classes.length === 0) {
|
||||
createForm.amount = 0;
|
||||
return;
|
||||
}
|
||||
let totalFee = 0;
|
||||
createForm.classes.forEach(classId => {
|
||||
const c = classesList.value.find(item => (item._id || item.id) === classId);
|
||||
createForm.classes.forEach((classId) => {
|
||||
const c = classesList.value.find((item) => (item._id || item.id) === classId);
|
||||
if (c) {
|
||||
totalFee += (c.tuitionFee || c.course?.price || 0);
|
||||
}
|
||||
@@ -188,15 +233,27 @@ const onClassesSelected = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const resetCreateForm = () => {
|
||||
createForm.user = null;
|
||||
createForm.classes = [];
|
||||
createForm.amount = 0;
|
||||
createForm.dueDate = '';
|
||||
};
|
||||
|
||||
const openCreateModal = () => {
|
||||
resetCreateForm();
|
||||
showCreateModal.value = true;
|
||||
};
|
||||
|
||||
const fetchDropdownData = async () => {
|
||||
try {
|
||||
const [uRes, cRes] = await Promise.all([
|
||||
userApi.getAll({ limit: 150 }),
|
||||
userApi.getAll({ limit: 200 }),
|
||||
classApi.getAll({ limit: 100 })
|
||||
]);
|
||||
const uData = uRes.data || uRes;
|
||||
const rawUsers = uData.items || uData.users || uData || [];
|
||||
usersList.value = rawUsers.map(u => ({ ...u, fullName: u.name || '' }));
|
||||
usersList.value = rawUsers.map((u) => ({ ...u, fullName: u.name || '' }));
|
||||
|
||||
const cData = cRes.data || cRes;
|
||||
classesList.value = cData.items || cData.classes || cData || [];
|
||||
@@ -206,6 +263,7 @@ const fetchDropdownData = async () => {
|
||||
};
|
||||
|
||||
const handleCreatePayment = async () => {
|
||||
if (!createForm.classes?.length) { showError('لطفا کلاس را انتخاب کنید'); return; }
|
||||
if (!createForm.user) { showError('لطفا کاربر را انتخاب کنید'); return; }
|
||||
if (!createForm.amount) { showError('لطفا مبلغ را وارد کنید'); return; }
|
||||
if (!createForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
||||
|
||||
Reference in New Issue
Block a user