fix(dashboard): fix date conversion offset, calendar arrow directions, payment duplicate warning, attendance class filter, and session auto-generation
This commit is contained in:
@@ -119,18 +119,40 @@ a {
|
||||
|
||||
.vpd-controls {
|
||||
color: var(--text-primary) !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
justify-content: space-between !important;
|
||||
direction: rtl !important;
|
||||
padding: 6px 10px;
|
||||
|
||||
.vpd-prev {
|
||||
order: 1 !important;
|
||||
|
||||
svg {
|
||||
transform: rotate(180deg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vpd-month-label {
|
||||
order: 2 !important;
|
||||
flex: 1 !important;
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.vpd-next {
|
||||
order: 3 !important;
|
||||
|
||||
svg {
|
||||
transform: rotate(0deg) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.vpd-next,
|
||||
.vpd-prev,
|
||||
button {
|
||||
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;
|
||||
@@ -144,7 +166,6 @@ a {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
fill: var(--text-primary) !important;
|
||||
transform: none !important;
|
||||
|
||||
path {
|
||||
fill: var(--text-primary) !important;
|
||||
|
||||
@@ -294,12 +294,13 @@ const props = defineProps({
|
||||
courseId: { type: [String, Object], required: true },
|
||||
professorId: { type: [String, Object], default: null },
|
||||
defaultSessionCount: { type: Number, default: 12 },
|
||||
startDate: { type: [String, Date], default: '' },
|
||||
days: { type: Array, default: () => [] },
|
||||
startTime: { type: String, default: '' },
|
||||
endTime: { type: String, default: '' }
|
||||
});
|
||||
|
||||
const { toPersianDigits, toLatinDigits, toGregorianIso, formatJalali, getTodayJalali } = usePersianDate();
|
||||
const { toPersianDigits, toLatinDigits, toGregorianIso, formatJalali, getTodayJalali, parseToMoment, toJalaliPickerValue } = usePersianDate();
|
||||
const { showSuccess, showError } = useToast();
|
||||
|
||||
const sessions = ref([]);
|
||||
@@ -407,16 +408,17 @@ const weekdays = [
|
||||
|
||||
const autoForm = reactive({
|
||||
sessionCount: props.defaultSessionCount || 12,
|
||||
startDate: getTodayJalali(),
|
||||
startDate: toJalaliPickerValue(props.startDate) || getTodayJalali(),
|
||||
startTime: props.startTime || '19:00',
|
||||
endTime: props.endTime || '20:30',
|
||||
selectedDays: props.days?.length ? [...props.days] : [6, 2]
|
||||
selectedDays: props.days?.length ? props.days.map(Number) : [6, 2]
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [props.days, props.startTime, props.endTime, props.defaultSessionCount],
|
||||
() => [props.startDate, props.days, props.startTime, props.endTime, props.defaultSessionCount],
|
||||
() => {
|
||||
if (props.days?.length) autoForm.selectedDays = [...props.days];
|
||||
if (props.startDate) autoForm.startDate = toJalaliPickerValue(props.startDate) || autoForm.startDate;
|
||||
if (props.days?.length) autoForm.selectedDays = props.days.map(Number);
|
||||
if (props.startTime) autoForm.startTime = props.startTime;
|
||||
if (props.endTime) autoForm.endTime = props.endTime;
|
||||
if (props.defaultSessionCount) autoForm.sessionCount = props.defaultSessionCount;
|
||||
@@ -519,9 +521,8 @@ const generateSessionsPreview = () => {
|
||||
}
|
||||
|
||||
const list = [];
|
||||
const latinStartDate = toLatinDigits(autoForm.startDate);
|
||||
let currentMoment = moment(latinStartDate, ['jYYYY/jMM/jDD', 'YYYY-MM-DD', 'YYYY/MM/DD', moment.ISO_8601]);
|
||||
if (!currentMoment.isValid()) {
|
||||
const currentMoment = parseToMoment(autoForm.startDate);
|
||||
if (!currentMoment || !currentMoment.isValid()) {
|
||||
showError('تاریخ شروع نامعتبر است');
|
||||
return;
|
||||
}
|
||||
@@ -532,7 +533,7 @@ const generateSessionsPreview = () => {
|
||||
|
||||
while (count < targetCount && safetyLimit > 0) {
|
||||
safetyLimit--;
|
||||
if (autoForm.selectedDays.includes(currentMoment.day())) {
|
||||
if (autoForm.selectedDays.map(Number).includes(currentMoment.day())) {
|
||||
count++;
|
||||
list.push({
|
||||
sessionNumber: count,
|
||||
|
||||
@@ -19,23 +19,71 @@ export function usePersianDate() {
|
||||
return result;
|
||||
};
|
||||
|
||||
const parseToMoment = (value) => {
|
||||
if (!value) return null;
|
||||
if (value instanceof Date) {
|
||||
if (Number.isNaN(value.getTime())) return null;
|
||||
return moment.utc(value.toISOString());
|
||||
}
|
||||
const latin = toLatinDigits(String(value)).trim();
|
||||
if (!latin) return null;
|
||||
|
||||
if (/^\d{4}-\d{2}-\d{2}T/.test(latin)) {
|
||||
const m = moment.utc(latin);
|
||||
return m.isValid() ? m : null;
|
||||
}
|
||||
|
||||
const match = latin.match(/^(\d{3,4})[./\-](\d{1,2})[./\-](\d{1,2})/);
|
||||
if (match) {
|
||||
const year = parseInt(match[1], 10);
|
||||
const month = parseInt(match[2], 10);
|
||||
const day = parseInt(match[3], 10);
|
||||
if (year >= 1200 && year <= 1599) {
|
||||
const normalized = `${year}/${String(month).padStart(2, '0')}/${String(day).padStart(2, '0')}`;
|
||||
const m = moment(normalized, 'jYYYY/jMM/jDD');
|
||||
return m.isValid() ? m : null;
|
||||
}
|
||||
if (year >= 1900 && year <= 2200) {
|
||||
const normalized = `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
|
||||
const m = moment.utc(normalized, 'YYYY-MM-DD');
|
||||
return m.isValid() ? m : null;
|
||||
}
|
||||
}
|
||||
|
||||
const m = moment.utc(latin);
|
||||
return m.isValid() ? m : null;
|
||||
};
|
||||
|
||||
const formatJalali = (date, formatStr = 'jYYYY/jMM/jDD') => {
|
||||
if (!date) return '-';
|
||||
try {
|
||||
return toPersianDigits(moment(date).locale('fa').format(formatStr));
|
||||
const m = parseToMoment(date);
|
||||
if (!m || !m.isValid()) return '-';
|
||||
return toPersianDigits(m.locale('fa').format(formatStr));
|
||||
} catch (e) {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
const formatJalaliWithTime = (date) => {
|
||||
return formatJalali(date, 'jYYYY/jMM/jDD - HH:mm');
|
||||
if (!date) return '-';
|
||||
try {
|
||||
const latin = toLatinDigits(String(date)).trim();
|
||||
if (/^\d{4}-\d{2}-\d{2}T/.test(latin) || date instanceof Date) {
|
||||
return toPersianDigits(moment(date).locale('fa').format('jYYYY/jMM/jDD - HH:mm'));
|
||||
}
|
||||
return formatJalali(date, 'jYYYY/jMM/jDD - HH:mm');
|
||||
} catch (e) {
|
||||
return '-';
|
||||
}
|
||||
};
|
||||
|
||||
const toJalaliPickerValue = (value) => {
|
||||
if (!value) return '';
|
||||
try {
|
||||
return toLatinDigits(moment(value).locale('fa').format('jYYYY/jMM/jDD'));
|
||||
const m = parseToMoment(value);
|
||||
if (!m || !m.isValid()) return '';
|
||||
return toLatinDigits(m.locale('fa').format('jYYYY/jMM/jDD'));
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
@@ -43,11 +91,10 @@ export function usePersianDate() {
|
||||
|
||||
const toGregorianIso = (jalaliValue) => {
|
||||
if (!jalaliValue) return undefined;
|
||||
if (jalaliValue instanceof Date) return jalaliValue.toISOString();
|
||||
try {
|
||||
const latin = toLatinDigits(String(jalaliValue));
|
||||
const m = moment(latin, 'jYYYY/jMM/jDD');
|
||||
return m.isValid() ? m.toDate().toISOString() : undefined;
|
||||
const m = parseToMoment(jalaliValue);
|
||||
if (!m || !m.isValid()) return undefined;
|
||||
return `${m.format('YYYY-MM-DD')}T00:00:00.000Z`;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -66,6 +113,7 @@ export function usePersianDate() {
|
||||
return {
|
||||
toPersianDigits,
|
||||
toLatinDigits,
|
||||
parseToMoment,
|
||||
formatJalali,
|
||||
formatJalaliWithTime,
|
||||
toJalaliPickerValue,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// /src/composables/usePersianDate.test.js
|
||||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { usePersianDate } from './usePersianDate.js';
|
||||
|
||||
describe('usePersianDate', () => {
|
||||
const {
|
||||
toPersianDigits,
|
||||
toLatinDigits,
|
||||
toGregorianIso,
|
||||
toJalaliPickerValue,
|
||||
formatJalali,
|
||||
getTodayJalali
|
||||
} = usePersianDate();
|
||||
|
||||
it('converts Persian digits and Latin digits back and forth', () => {
|
||||
assert.equal(toPersianDigits('1405/05/26'), '۱۴۰۵/۰۵/۲۶');
|
||||
assert.equal(toLatinDigits('۱۴۰۵/۰۵/۲۶'), '1405/05/26');
|
||||
});
|
||||
|
||||
it('converts Jalali date string to Gregorian ISO without shifting day', () => {
|
||||
assert.equal(toGregorianIso('1405/05/26'), '2026-08-17T00:00:00.000Z');
|
||||
assert.equal(toGregorianIso('1405-05-26'), '2026-08-17T00:00:00.000Z');
|
||||
assert.equal(toGregorianIso('۱۴۰۵/۰۵/۲۶'), '2026-08-17T00:00:00.000Z');
|
||||
assert.equal(toGregorianIso('2026-08-17'), '2026-08-17T00:00:00.000Z');
|
||||
assert.equal(toGregorianIso('2026-08-17T00:00:00.000Z'), '2026-08-17T00:00:00.000Z');
|
||||
});
|
||||
|
||||
it('converts ISO or Gregorian date to Jalali picker value without 1-day offset', () => {
|
||||
assert.equal(toJalaliPickerValue('2026-08-17T00:00:00.000Z'), '1405/05/26');
|
||||
assert.equal(toJalaliPickerValue('2026-08-17'), '1405/05/26');
|
||||
assert.equal(toJalaliPickerValue('1405/05/26'), '1405/05/26');
|
||||
assert.equal(toJalaliPickerValue('۱۴۰۵/۰۵/۲۶'), '1405/05/26');
|
||||
});
|
||||
|
||||
it('formats Jalali date for display', () => {
|
||||
assert.equal(formatJalali('2026-08-17T00:00:00.000Z'), '۱۴۰۵/۰۵/۲۶');
|
||||
assert.equal(formatJalali('1405/05/26'), '۱۴۰۵/۰۵/۲۶');
|
||||
});
|
||||
});
|
||||
@@ -47,14 +47,27 @@
|
||||
@search-change="onSearch"
|
||||
>
|
||||
<template #toolbar>
|
||||
<SelectButton
|
||||
v-model="attendanceFilter"
|
||||
:options="filterOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="text-sm"
|
||||
@change="onAttendanceFilterChange"
|
||||
/>
|
||||
<div class="flex align-items-center justify-content-between flex-wrap gap-2 w-full">
|
||||
<SelectButton
|
||||
v-model="attendanceFilter"
|
||||
:options="filterOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="text-sm"
|
||||
@change="onAttendanceFilterChange"
|
||||
/>
|
||||
<Dropdown
|
||||
v-model="selectedClassId"
|
||||
:options="classesList"
|
||||
optionLabel="name"
|
||||
optionValue="_id"
|
||||
placeholder="همه کلاسها"
|
||||
showClear
|
||||
filter
|
||||
class="w-16rem text-sm"
|
||||
@change="onClassFilterChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<Column field="topic" header="موضوع جلسه">
|
||||
@@ -130,6 +143,7 @@ import { ref, onMounted } from 'vue';
|
||||
import { useDataTable } from '@/composables/useDataTable';
|
||||
import { usePersianDate } from '@/composables/usePersianDate';
|
||||
import { sessionApi } from '@/api/sessionApi';
|
||||
import { classApi } from '@/api/classApi';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import DataTableWrapper from '@/components/common/DataTableWrapper.vue';
|
||||
import StatusTag from '@/components/common/StatusTag.vue';
|
||||
@@ -137,6 +151,7 @@ import Button from 'primevue/button';
|
||||
import Column from 'primevue/column';
|
||||
import Tag from 'primevue/tag';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import Dropdown from 'primevue/dropdown';
|
||||
|
||||
const { formatJalali, toPersianDigits } = usePersianDate();
|
||||
|
||||
@@ -146,11 +161,14 @@ const filterOptions = [
|
||||
{ label: 'ثبت شده', value: 'recorded' }
|
||||
];
|
||||
|
||||
const selectedClassId = ref(null);
|
||||
const classesList = ref([]);
|
||||
const summary = ref({ pending: 0, recorded: 0, total: 0 });
|
||||
|
||||
const fetchAttendanceSessions = (params) =>
|
||||
sessionApi.getAll({
|
||||
...params,
|
||||
class: selectedClassId.value || undefined,
|
||||
attendanceScope: params.attendanceScope || attendanceFilter.value
|
||||
});
|
||||
|
||||
@@ -185,11 +203,29 @@ const onAttendanceFilterChange = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
const onClassFilterChange = () => {
|
||||
queryParams.class = selectedClassId.value || undefined;
|
||||
queryParams.page = 1;
|
||||
loadData();
|
||||
loadSummary();
|
||||
};
|
||||
|
||||
const loadClasses = async () => {
|
||||
try {
|
||||
const res = await classApi.getAll({ limit: 500, sortBy: 'name', sortOrder: 'asc' });
|
||||
const data = res.data || res;
|
||||
classesList.value = data.items || data.classes || data.data || data || [];
|
||||
} catch (err) {
|
||||
classesList.value = [];
|
||||
}
|
||||
};
|
||||
|
||||
const loadSummary = async () => {
|
||||
try {
|
||||
const classParam = selectedClassId.value || undefined;
|
||||
const [pendingRes, recordedRes] = await Promise.all([
|
||||
sessionApi.getAll({ page: 1, limit: 1, attendanceScope: 'pending', sortBy: 'day', sortOrder: 'desc' }),
|
||||
sessionApi.getAll({ page: 1, limit: 1, attendanceScope: 'recorded', sortBy: 'day', sortOrder: 'desc' })
|
||||
sessionApi.getAll({ page: 1, limit: 1, attendanceScope: 'pending', class: classParam, sortBy: 'day', sortOrder: 'desc' }),
|
||||
sessionApi.getAll({ page: 1, limit: 1, attendanceScope: 'recorded', class: classParam, sortBy: 'day', sortOrder: 'desc' })
|
||||
]);
|
||||
const pending = pendingRes.meta?.totalCount ?? 0;
|
||||
const recorded = recordedRes.meta?.totalCount ?? 0;
|
||||
@@ -204,6 +240,6 @@ const loadSummary = async () => {
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadData(), loadSummary()]);
|
||||
await Promise.all([loadData(), loadSummary(), loadClasses()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -171,7 +171,10 @@
|
||||
|
||||
<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" />
|
||||
<InputGroup>
|
||||
<InputNumber inputId="class-bulk-amount" v-model="bulkForm.amount" class="w-full text-sm" :min="0" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-2">
|
||||
@@ -181,14 +184,16 @@
|
||||
|
||||
<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"
|
||||
/>
|
||||
<InputGroup>
|
||||
<InputNumber
|
||||
inputId="class-bulk-discount-amount"
|
||||
v-model="bulkForm.discount"
|
||||
class="w-full text-sm"
|
||||
:min="0"
|
||||
:max="bulkForm.amount || 0"
|
||||
/>
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<small class="font-semibold text-color">
|
||||
مبلغ نهایی هر صورتحساب: {{ toPersianDigits(bulkPayableAmount.toLocaleString()) }} تومان
|
||||
</small>
|
||||
@@ -257,6 +262,8 @@ import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputGroup from 'primevue/inputgroup';
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import DatePicker from 'vue3-persian-datetime-picker';
|
||||
|
||||
@@ -37,7 +37,10 @@
|
||||
|
||||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">شهریه کلاس</label>
|
||||
<InputNumber v-model="form.tuitionFee" :min="0" class="w-full text-sm" suffix=" تومان" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="form.tuitionFee" :min="0" class="w-full text-sm" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||||
@@ -59,7 +62,10 @@
|
||||
<div v-if="form.hasDiscount" class="grid mt-1">
|
||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مبلغ تخفیف (تومان)</label>
|
||||
<InputNumber v-model="form.discount" :min="0" :max="form.tuitionFee || 0" class="w-full text-sm" suffix=" تومان" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="form.discount" :min="0" :max="form.tuitionFee || 0" class="w-full text-sm" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div class="col-12 md:col-8 flex align-items-end">
|
||||
<span class="font-semibold text-color text-sm">
|
||||
@@ -92,12 +98,18 @@
|
||||
|
||||
<div v-else class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">نرخ ساعتی استاد</label>
|
||||
<InputNumber v-model="form.payoutHourlyRate" :min="0" class="w-full text-sm" suffix=" تومان" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="form.payoutHourlyRate" :min="0" class="w-full text-sm" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">هزینه جانبی هر جلسه</label>
|
||||
<InputNumber v-model="form.extraExpensePerSession" :min="0" class="w-full text-sm" suffix=" تومان" placeholder="مثلا: رفتوآمد، پذیرایی" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="form.extraExpensePerSession" :min="0" class="w-full text-sm" placeholder="مثلا: رفتوآمد، پذیرایی" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -273,6 +285,7 @@
|
||||
:course-id="form.course ? String(form.course) : ''"
|
||||
:professor-id="form.professor ? String(form.professor) : null"
|
||||
:default-session-count="defaultSessionCount"
|
||||
:start-date="form.startDate"
|
||||
:days="form.days"
|
||||
:start-time="form.startTime"
|
||||
:end-time="form.endTime"
|
||||
@@ -308,6 +321,8 @@ import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
|
||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputGroup from 'primevue/inputgroup';
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon';
|
||||
import Dropdown from 'primevue/select';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import InputSwitch from 'primevue/toggleswitch';
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
|
||||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">{{ $t('courses.price') }} *</label>
|
||||
<InputNumber v-model="form.price" class="w-full text-sm" :class="{ 'p-invalid': errors.price }" suffix=" تومان" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="form.price" class="w-full text-sm" :class="{ 'p-invalid': errors.price }" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<small v-if="errors.price" class="text-red-500 text-xs">{{ errors.price }}</small>
|
||||
</div>
|
||||
|
||||
@@ -158,6 +161,8 @@ import StatusTag from '@/components/common/StatusTag.vue';
|
||||
import TableSkeleton from '@/components/common/TableSkeleton.vue';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputGroup from 'primevue/inputgroup';
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon';
|
||||
import Dropdown from 'primevue/select';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import InputSwitch from 'primevue/toggleswitch';
|
||||
|
||||
@@ -152,7 +152,10 @@
|
||||
<div class="flex flex-column gap-3 py-2">
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مبلغ واریزی (تومان) *</label>
|
||||
<InputNumber v-model="trxForm.amount" class="w-full text-sm" suffix=" تومان" :min="1" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="trxForm.amount" class="w-full text-sm" :min="1" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">وضعیت تراکنش *</label>
|
||||
@@ -198,7 +201,10 @@
|
||||
<div class="flex flex-column gap-3 py-2">
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مبلغ (تومان) *</label>
|
||||
<InputNumber v-model="editForm.amount" class="w-full text-sm" suffix=" تومان" :min="0" />
|
||||
<InputGroup>
|
||||
<InputNumber v-model="editForm.amount" class="w-full text-sm" :min="0" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">وضعیت تراکنش *</label>
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
:placeholder="createForm.classes?.length ? 'دانشجوی ثبتنامشده را انتخاب کنید' : 'ابتدا کلاس را انتخاب کنید'"
|
||||
class="w-full text-sm"
|
||||
:disabled="!createForm.classes?.length"
|
||||
@change="checkDuplicateInvoice"
|
||||
@change="() => checkDuplicateInvoice()"
|
||||
/>
|
||||
<small v-if="createForm.classes?.length && !eligibleUsers.length" class="text-orange-500">
|
||||
هیچ دانشجوی ثبتنامشدهای در کلاسهای انتخابشده یافت نشد
|
||||
@@ -132,7 +132,10 @@
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="invoice-amount">مبلغ کل صورتحساب (تومان) *</label>
|
||||
<InputNumber inputId="invoice-amount" v-model="createForm.amount" class="w-full text-sm" suffix=" تومان" :min="0" />
|
||||
<InputGroup>
|
||||
<InputNumber inputId="invoice-amount" v-model="createForm.amount" class="w-full text-sm" :min="0" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-2">
|
||||
@@ -142,14 +145,16 @@
|
||||
|
||||
<div v-if="hasDiscount" class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="invoice-discount-amount">مبلغ تخفیف (تومان)</label>
|
||||
<InputNumber
|
||||
inputId="invoice-discount-amount"
|
||||
v-model="createForm.discount"
|
||||
class="w-full text-sm"
|
||||
suffix=" تومان"
|
||||
:min="0"
|
||||
:max="createForm.amount || 0"
|
||||
/>
|
||||
<InputGroup>
|
||||
<InputNumber
|
||||
inputId="invoice-discount-amount"
|
||||
v-model="createForm.discount"
|
||||
class="w-full text-sm"
|
||||
:min="0"
|
||||
:max="createForm.amount || 0"
|
||||
/>
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<small class="font-semibold text-color">
|
||||
مبلغ نهایی: {{ toPersianDigits(createPayableAmount.toLocaleString()) }} تومان
|
||||
</small>
|
||||
@@ -204,7 +209,10 @@
|
||||
|
||||
<div class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="bulk-invoice-amount">مبلغ شهریه هر دانشجو (تومان) *</label>
|
||||
<InputNumber inputId="bulk-invoice-amount" v-model="bulkForm.amount" class="w-full text-sm" suffix=" تومان" :min="0" />
|
||||
<InputGroup>
|
||||
<InputNumber inputId="bulk-invoice-amount" v-model="bulkForm.amount" class="w-full text-sm" :min="0" />
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
|
||||
<div class="flex align-items-center gap-2">
|
||||
@@ -214,14 +222,16 @@
|
||||
|
||||
<div v-if="hasBulkDiscount" class="flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm" for="bulk-invoice-discount-amount">مبلغ تخفیف (تومان)</label>
|
||||
<InputNumber
|
||||
inputId="bulk-invoice-discount-amount"
|
||||
v-model="bulkForm.discount"
|
||||
class="w-full text-sm"
|
||||
suffix=" تومان"
|
||||
:min="0"
|
||||
:max="bulkForm.amount || 0"
|
||||
/>
|
||||
<InputGroup>
|
||||
<InputNumber
|
||||
inputId="bulk-invoice-discount-amount"
|
||||
v-model="bulkForm.discount"
|
||||
class="w-full text-sm"
|
||||
:min="0"
|
||||
:max="bulkForm.amount || 0"
|
||||
/>
|
||||
<InputGroupAddon>تومان</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<small class="font-semibold text-color">
|
||||
مبلغ نهایی هر صورتحساب: {{ toPersianDigits(bulkPayableAmount.toLocaleString()) }} تومان
|
||||
</small>
|
||||
@@ -298,6 +308,8 @@ import Dialog from 'primevue/dialog';
|
||||
import Dropdown from 'primevue/select';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import InputGroup from 'primevue/inputgroup';
|
||||
import InputGroupAddon from 'primevue/inputgroupaddon';
|
||||
import Checkbox from 'primevue/checkbox';
|
||||
import Textarea from 'primevue/textarea';
|
||||
import DatePicker from 'vue3-persian-datetime-picker';
|
||||
@@ -391,18 +403,33 @@ const eligibleUsers = computed(() => {
|
||||
return usersList.value.filter((u) => eligibleIds?.has(String(u._id || u.id)));
|
||||
});
|
||||
|
||||
const extractId = (val) => {
|
||||
if (!val) return null;
|
||||
if (typeof val === 'object') {
|
||||
if (val.value !== undefined && (typeof val.value === 'string' || typeof val.value === 'number')) {
|
||||
return String(val.value);
|
||||
}
|
||||
return val._id ? String(val._id) : (val.id ? String(val.id) : null);
|
||||
}
|
||||
return String(val);
|
||||
};
|
||||
|
||||
const checkDuplicateInvoice = async (userIdVal = null) => {
|
||||
const targetUser = userIdVal || createForm.user;
|
||||
if (!targetUser || !createForm.classes || !createForm.classes.length) {
|
||||
const targetUserId = extractId(userIdVal) || extractId(createForm.user);
|
||||
if (!targetUserId || !createForm.classes || !createForm.classes.length) {
|
||||
duplicateWarning.value = null;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const classIds = Array.isArray(createForm.classes)
|
||||
? createForm.classes.map((c) => c?._id || c?.id || c).join(',')
|
||||
: String(createForm.classes);
|
||||
? createForm.classes.map(extractId).filter(Boolean).join(',')
|
||||
: (extractId(createForm.classes) || '');
|
||||
if (!classIds) {
|
||||
duplicateWarning.value = null;
|
||||
return;
|
||||
}
|
||||
const res = await paymentApi.checkDuplicate({
|
||||
userId: String(targetUser?._id || targetUser?.id || targetUser),
|
||||
userId: targetUserId,
|
||||
classes: classIds
|
||||
});
|
||||
duplicateWarning.value = res.data?.data || res.data || res;
|
||||
|
||||
Reference in New Issue
Block a user