fix(ui): payment due date calculation, datepicker colors, scrollable tabs, optional receipt, default today date
This commit is contained in:
@@ -119,25 +119,84 @@ a {
|
|||||||
|
|
||||||
.vpd-controls {
|
.vpd-controls {
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 6px 10px;
|
||||||
|
|
||||||
button, span, i, svg, .vpd-month-label {
|
.vpd-next,
|
||||||
|
.vpd-prev,
|
||||||
|
button {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
border-radius: 8px !important;
|
||||||
|
background-color: var(--surface-ground) !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
color: var(--text-primary) !important;
|
color: var(--text-primary) !important;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 14px !important;
|
||||||
|
height: 14px !important;
|
||||||
|
fill: var(--text-primary) !important;
|
||||||
|
|
||||||
|
path {
|
||||||
fill: var(--text-primary) !important;
|
fill: var(--text-primary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
|
||||||
background-color: var(--surface-hover) !important;
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.vpd-month-label span {
|
&:hover:not([disabled="true"]) {
|
||||||
border-color: var(--border-color) !important;
|
background-color: var(--primary-color) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
|
||||||
|
svg,
|
||||||
|
svg path {
|
||||||
|
fill: #ffffff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[disabled="true"] {
|
||||||
|
opacity: 0.3 !important;
|
||||||
|
cursor: not-allowed !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpd-month-label {
|
||||||
|
span {
|
||||||
|
color: var(--text-primary) !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 4px 10px !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
border: 1px solid var(--border-color) !important;
|
||||||
|
background-color: var(--surface-ground) !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--surface-hover) !important;
|
background-color: var(--surface-hover) !important;
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.vpd-up-arrow-btn,
|
||||||
|
.vpd-down-arrow-btn {
|
||||||
|
svg,
|
||||||
|
svg path {
|
||||||
|
fill: var(--text-primary) !important;
|
||||||
|
}
|
||||||
|
&:hover {
|
||||||
|
svg,
|
||||||
|
svg path {
|
||||||
|
fill: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.vpd-week {
|
.vpd-week {
|
||||||
.vpd-weekday {
|
.vpd-weekday {
|
||||||
@@ -272,3 +331,20 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ==========================================
|
||||||
|
Responsive Tabs & TabList Horizontal Scroll
|
||||||
|
========================================== */
|
||||||
|
.p-tablist {
|
||||||
|
max-width: 100% !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
-webkit-overflow-scrolling: touch !important;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-tablist-tab-list {
|
||||||
|
flex-wrap: nowrap !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
min-width: max-content !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-12 sm:col-6 md:col-3 flex flex-column gap-2">
|
<div class="col-12 sm:col-6 md:col-3 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ شروع</label>
|
<label class="font-semibold text-sm">تاریخ شروع</label>
|
||||||
<DatePicker v-model="autoForm.startDate" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="autoForm.startDate" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 sm:col-6 md:col-3 flex flex-column gap-2">
|
<div class="col-12 sm:col-6 md:col-3 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">ساعت شروع</label>
|
<label class="font-semibold text-sm">ساعت شروع</label>
|
||||||
@@ -194,7 +194,7 @@
|
|||||||
<div v-for="(session, index) in manualSessions" :key="index" class="grid align-items-center mb-2 p-2 border-round surface-ground">
|
<div v-for="(session, index) in manualSessions" :key="index" class="grid align-items-center mb-2 p-2 border-round surface-ground">
|
||||||
<div class="col-12 sm:col-1 text-center font-bold">#{{ toPersianDigits(index + 1) }}</div>
|
<div class="col-12 sm:col-1 text-center font-bold">#{{ toPersianDigits(index + 1) }}</div>
|
||||||
<div class="col-12 sm:col-3">
|
<div class="col-12 sm:col-3">
|
||||||
<InputText v-model="session.date" placeholder="تاریخ (1403/01/01)" class="w-full text-sm" />
|
<InputText v-model="session.date" :placeholder="`تاریخ (${todayJalali()})`" class="w-full text-sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 sm:col-2">
|
<div class="col-6 sm:col-2">
|
||||||
<InputText v-model="session.startTime" placeholder="ساعت شروع" class="w-full text-sm" dir="ltr" />
|
<InputText v-model="session.startTime" placeholder="ساعت شروع" class="w-full text-sm" dir="ltr" />
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ export function usePersianDate() {
|
|||||||
|
|
||||||
const jalaliToIso = (jalaliStr) => toGregorianIso(jalaliStr) || null;
|
const jalaliToIso = (jalaliStr) => toGregorianIso(jalaliStr) || null;
|
||||||
|
|
||||||
|
const getTodayJalali = (formatStr = 'jYYYY/jMM/jDD') => {
|
||||||
|
return moment().locale('fa').format(formatStr);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toPersianDigits,
|
toPersianDigits,
|
||||||
toLatinDigits,
|
toLatinDigits,
|
||||||
@@ -62,6 +66,8 @@ export function usePersianDate() {
|
|||||||
formatJalaliWithTime,
|
formatJalaliWithTime,
|
||||||
toJalaliPickerValue,
|
toJalaliPickerValue,
|
||||||
toGregorianIso,
|
toGregorianIso,
|
||||||
jalaliToIso
|
jalaliToIso,
|
||||||
|
getTodayJalali
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,3 +82,38 @@ export function calculateClassEndDate(startDate, days = [], numberOfSessions = 0
|
|||||||
|
|
||||||
return lastMatchingDate ? lastMatchingDate.locale('fa').format('jYYYY/jMM/jDD') : '';
|
return lastMatchingDate ? lastMatchingDate.locale('fa').format('jYYYY/jMM/jDD') : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the halfway due date for a class in Jalali (jYYYY/jMM/jDD).
|
||||||
|
* If class has 10 sessions, due date is calculated until the 5th session.
|
||||||
|
* @param {Object} cls - Class object
|
||||||
|
* @returns {string} Jalali formatted date string, or today's date if not calculable
|
||||||
|
*/
|
||||||
|
export function calculateClassMidDate(cls) {
|
||||||
|
if (!cls) return moment().locale('fa').format('jYYYY/jMM/jDD');
|
||||||
|
|
||||||
|
const totalSessions = parseInt(cls.numberOfSessions || cls.course?.sectionCount || 0, 10);
|
||||||
|
const midSessions = totalSessions > 0 ? Math.ceil(totalSessions / 2) : 0;
|
||||||
|
|
||||||
|
if (cls.startDate && Array.isArray(cls.days) && cls.days.length > 0 && midSessions >= 1) {
|
||||||
|
const computed = calculateClassEndDate(cls.startDate, cls.days, midSessions);
|
||||||
|
if (computed) return computed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cls.startDate && cls.endDate) {
|
||||||
|
const startM = moment(cls.startDate);
|
||||||
|
const endM = moment(cls.endDate);
|
||||||
|
if (startM.isValid() && endM.isValid()) {
|
||||||
|
const midTime = startM.valueOf() + (endM.valueOf() - startM.valueOf()) / 2;
|
||||||
|
return moment(midTime).locale('fa').format('jYYYY/jMM/jDD');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cls.startDate) {
|
||||||
|
const startM = moment(cls.startDate);
|
||||||
|
if (startM.isValid()) return startM.locale('fa').format('jYYYY/jMM/jDD');
|
||||||
|
}
|
||||||
|
|
||||||
|
return moment().locale('fa').format('jYYYY/jMM/jDD');
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { formatClassDays, formatClassTime, formatClassSchedule, calculateClassEndDate } from './classSchedule.js';
|
import { formatClassDays, formatClassTime, formatClassSchedule, calculateClassEndDate, calculateClassMidDate } from './classSchedule.js';
|
||||||
|
|
||||||
describe('class schedule display', () => {
|
describe('class schedule display', () => {
|
||||||
it('formats weekdays in Iran week order', () => {
|
it('formats weekdays in Iran week order', () => {
|
||||||
@@ -28,4 +28,21 @@ describe('class schedule display', () => {
|
|||||||
const endDate = calculateClassEndDate('1403/01/01', [6, 2], 2);
|
const endDate = calculateClassEndDate('1403/01/01', [6, 2], 2);
|
||||||
assert.equal(endDate, '1403/01/07');
|
assert.equal(endDate, '1403/01/07');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('calculates class mid date (halfway through class sessions)', () => {
|
||||||
|
// For 10 sessions on [6, 2] starting 1403/01/01:
|
||||||
|
// Half is 5th session
|
||||||
|
const midDate = calculateClassMidDate({
|
||||||
|
startDate: '1403/01/01',
|
||||||
|
days: [6, 2],
|
||||||
|
numberOfSessions: 10
|
||||||
|
});
|
||||||
|
// Session 1: 1403/01/04 (Sat)
|
||||||
|
// Session 2: 1403/01/07 (Tue)
|
||||||
|
// Session 3: 1403/01/11 (Sat)
|
||||||
|
// Session 4: 1403/01/14 (Tue)
|
||||||
|
// Session 5: 1403/01/18 (Sat)
|
||||||
|
assert.equal(midDate, '1403/01/18');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
|
|
||||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ شروع *</label>
|
<label class="font-semibold text-sm">تاریخ شروع *</label>
|
||||||
<DatePicker v-model="form.startDate" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="form.startDate" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||||
@@ -324,7 +324,7 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
const { hasPermission } = usePermission();
|
const { hasPermission } = usePermission();
|
||||||
const { toPersianDigits, toLatinDigits } = usePersianDate();
|
const { toPersianDigits, toLatinDigits, getTodayJalali } = usePersianDate();
|
||||||
|
|
||||||
const classId = route.params.id;
|
const classId = route.params.id;
|
||||||
const queryCourseId = route.query.courseId ? String(route.query.courseId) : null;
|
const queryCourseId = route.query.courseId ? String(route.query.courseId) : null;
|
||||||
@@ -354,9 +354,10 @@ const form = reactive({
|
|||||||
discount: 0,
|
discount: 0,
|
||||||
freeSpots: null,
|
freeSpots: null,
|
||||||
showOnFrontend: true,
|
showOnFrontend: true,
|
||||||
startDate: '',
|
startDate: isEditMode.value ? '' : getTodayJalali(),
|
||||||
endDate: '',
|
endDate: '',
|
||||||
days: [],
|
days: [],
|
||||||
|
|
||||||
startTime: '',
|
startTime: '',
|
||||||
endTime: '',
|
endTime: '',
|
||||||
numberOfSessions: null,
|
numberOfSessions: null,
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ *</label>
|
<label class="font-semibold text-sm">تاریخ *</label>
|
||||||
<DatePicker v-model="form.date" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="form.date" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">یادداشت</label>
|
<label class="font-semibold text-sm">یادداشت</label>
|
||||||
@@ -126,7 +126,8 @@ import InputNumber from 'primevue/inputnumber';
|
|||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import DatePicker from 'vue3-persian-datetime-picker';
|
import DatePicker from 'vue3-persian-datetime-picker';
|
||||||
|
|
||||||
const { toPersianDigits, formatJalali, toGregorianIso, toJalaliPickerValue } = usePersianDate();
|
const { toPersianDigits, formatJalali, toGregorianIso, toJalaliPickerValue, getTodayJalali } = usePersianDate();
|
||||||
|
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -171,11 +171,11 @@
|
|||||||
<div class="flex flex-column md:flex-row gap-3 mb-4 align-items-end">
|
<div class="flex flex-column md:flex-row gap-3 mb-4 align-items-end">
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">از تاریخ</label>
|
<label class="font-semibold text-sm">از تاریخ</label>
|
||||||
<DatePicker v-model="rangeForm.startDate" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="rangeForm.startDate" class="w-full text-sm" :placeholder="rangeForm.startDate || getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تا تاریخ</label>
|
<label class="font-semibold text-sm">تا تاریخ</label>
|
||||||
<DatePicker v-model="rangeForm.endDate" class="w-full text-sm" placeholder="1403/01/30" />
|
<DatePicker v-model="rangeForm.endDate" class="w-full text-sm" :placeholder="rangeForm.endDate || getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<Button label="این ماه" text size="small" @click="setThisMonth" />
|
<Button label="این ماه" text size="small" @click="setThisMonth" />
|
||||||
<Button label="نمایش گزارش" icon="pi pi-search" :loading="isRangeReportLoading" @click="loadRangeReport" />
|
<Button label="نمایش گزارش" icon="pi pi-search" :loading="isRangeReportLoading" @click="loadRangeReport" />
|
||||||
@@ -340,7 +340,7 @@ import TabPanel from 'primevue/tabpanel';
|
|||||||
import DatePicker from 'vue3-persian-datetime-picker';
|
import DatePicker from 'vue3-persian-datetime-picker';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const { toPersianDigits, formatJalali, toGregorianIso, toJalaliPickerValue } = usePersianDate();
|
const { toPersianDigits, formatJalali, toGregorianIso, toJalaliPickerValue, getTodayJalali } = usePersianDate();
|
||||||
const { showError } = useToast();
|
const { showError } = useToast();
|
||||||
|
|
||||||
const formatToman = (value) => `${toPersianDigits(Math.round(value || 0).toLocaleString())} تومان`;
|
const formatToman = (value) => `${toPersianDigits(Math.round(value || 0).toLocaleString())} تومان`;
|
||||||
|
|||||||
@@ -164,11 +164,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
||||||
<DatePicker v-model="trxForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="trxForm.dueDate" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="trxForm.status === 'paid'" class="flex flex-column gap-2">
|
<div v-if="trxForm.status === 'paid'" class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ پرداخت</label>
|
<label class="font-semibold text-sm">تاریخ پرداخت</label>
|
||||||
<DatePicker v-model="trxForm.date" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="trxForm.date" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="trxForm.status === 'paid'" class="flex flex-column gap-2">
|
<div v-if="trxForm.status === 'paid'" class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">شماره فیش / پیگیری</label>
|
<label class="font-semibold text-sm">شماره فیش / پیگیری</label>
|
||||||
@@ -185,6 +185,7 @@
|
|||||||
placeholder="یادداشت داخلی درباره این تراکنش…"
|
placeholder="یادداشت داخلی درباره این تراکنش…"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<NotifyChannelsField :notify="trxNotify" />
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="انصراف" text severity="secondary" @click="showTransactionModal = false" />
|
<Button label="انصراف" text severity="secondary" @click="showTransactionModal = false" />
|
||||||
@@ -209,11 +210,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
||||||
<DatePicker v-model="editForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="editForm.dueDate" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
|
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ پرداخت</label>
|
<label class="font-semibold text-sm">تاریخ پرداخت</label>
|
||||||
<DatePicker v-model="editForm.date" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="editForm.date" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
|
<div v-if="editForm.status === 'paid'" class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">شماره فیش / پیگیری</label>
|
<label class="font-semibold text-sm">شماره فیش / پیگیری</label>
|
||||||
@@ -231,6 +232,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<Button label="انصراف" text severity="secondary" @click="showEditModal = false" />
|
<Button label="انصراف" text severity="secondary" @click="showEditModal = false" />
|
||||||
<Button label="ذخیره تغییرات" icon="pi pi-check" severity="success" :loading="isSubmittingEdit" @click="handleUpdateTransaction" />
|
<Button label="ذخیره تغییرات" icon="pi pi-check" severity="success" :loading="isSubmittingEdit" @click="handleUpdateTransaction" />
|
||||||
@@ -263,6 +265,7 @@ import { getPayableAmount } from '@/utils/paymentAmount';
|
|||||||
import PageHeader from '@/components/common/PageHeader.vue';
|
import PageHeader from '@/components/common/PageHeader.vue';
|
||||||
import PermissionGate from '@/components/common/PermissionGate.vue';
|
import PermissionGate from '@/components/common/PermissionGate.vue';
|
||||||
import StatusTag from '@/components/common/StatusTag.vue';
|
import StatusTag from '@/components/common/StatusTag.vue';
|
||||||
|
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import Tag from 'primevue/tag';
|
import Tag from 'primevue/tag';
|
||||||
import DataTable from 'primevue/datatable';
|
import DataTable from 'primevue/datatable';
|
||||||
@@ -276,7 +279,7 @@ import DatePicker from 'vue3-persian-datetime-picker';
|
|||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const paymentId = route.params.id;
|
const paymentId = route.params.id;
|
||||||
const { toPersianDigits, formatJalali, toJalaliPickerValue, toGregorianIso } = usePersianDate();
|
const { toPersianDigits, formatJalali, toJalaliPickerValue, toGregorianIso, getTodayJalali } = usePersianDate();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
|
||||||
const payment = ref(null);
|
const payment = ref(null);
|
||||||
@@ -288,6 +291,7 @@ const isSubmittingEdit = ref(false);
|
|||||||
const isSubmittingCancel = ref(false);
|
const isSubmittingCancel = ref(false);
|
||||||
const editingTransactionId = ref(null);
|
const editingTransactionId = ref(null);
|
||||||
const cancellingTransactionId = ref(null);
|
const cancellingTransactionId = ref(null);
|
||||||
|
const trxNotify = reactive({ sms: true, email: true, bot: true });
|
||||||
|
|
||||||
const methodOptions = [
|
const methodOptions = [
|
||||||
{ label: 'کارت به کارت', value: 'card' },
|
{ label: 'کارت به کارت', value: 'card' },
|
||||||
@@ -305,8 +309,8 @@ const defaultTrxForm = () => ({
|
|||||||
status: 'paid',
|
status: 'paid',
|
||||||
method: 'card',
|
method: 'card',
|
||||||
receiptNumber: '',
|
receiptNumber: '',
|
||||||
dueDate: '',
|
dueDate: getTodayJalali(),
|
||||||
date: '',
|
date: getTodayJalali(),
|
||||||
notes: ''
|
notes: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -348,10 +352,6 @@ const handleRecordTransaction = async () => {
|
|||||||
if (!trxForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
if (!trxForm.dueDate) { showError('لطفا تاریخ سررسید را وارد کنید'); return; }
|
||||||
const dueDate = toGregorianIso(trxForm.dueDate);
|
const dueDate = toGregorianIso(trxForm.dueDate);
|
||||||
if (!dueDate) { showError('تاریخ سررسید نامعتبر است'); return; }
|
if (!dueDate) { showError('تاریخ سررسید نامعتبر است'); return; }
|
||||||
if (trxForm.status === 'paid' && !trxForm.receiptNumber) {
|
|
||||||
showError('لطفا شماره فیش یا پیگیری را وارد کنید');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isSubmittingTrx.value = true;
|
isSubmittingTrx.value = true;
|
||||||
try {
|
try {
|
||||||
@@ -359,11 +359,12 @@ const handleRecordTransaction = async () => {
|
|||||||
amount: trxForm.amount,
|
amount: trxForm.amount,
|
||||||
status: trxForm.status,
|
status: trxForm.status,
|
||||||
dueDate,
|
dueDate,
|
||||||
notes: trxForm.notes || undefined
|
notes: trxForm.notes || undefined,
|
||||||
|
notify: { ...trxNotify }
|
||||||
};
|
};
|
||||||
if (trxForm.status === 'paid') {
|
if (trxForm.status === 'paid') {
|
||||||
payload.method = trxForm.method;
|
payload.method = trxForm.method;
|
||||||
payload.receiptNumber = trxForm.receiptNumber;
|
payload.receiptNumber = trxForm.receiptNumber || '';
|
||||||
const paymentDate = toGregorianIso(trxForm.date);
|
const paymentDate = toGregorianIso(trxForm.date);
|
||||||
if (paymentDate) payload.date = paymentDate;
|
if (paymentDate) payload.date = paymentDate;
|
||||||
}
|
}
|
||||||
@@ -373,6 +374,9 @@ const handleRecordTransaction = async () => {
|
|||||||
showSuccess('تراکنش جدید با موفقیت ثبت شد');
|
showSuccess('تراکنش جدید با موفقیت ثبت شد');
|
||||||
showTransactionModal.value = false;
|
showTransactionModal.value = false;
|
||||||
Object.assign(trxForm, defaultTrxForm());
|
Object.assign(trxForm, defaultTrxForm());
|
||||||
|
trxNotify.sms = true;
|
||||||
|
trxNotify.email = true;
|
||||||
|
trxNotify.bot = true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showError(err);
|
showError(err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -380,6 +384,7 @@ const handleRecordTransaction = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const openEditTransaction = (trx) => {
|
const openEditTransaction = (trx) => {
|
||||||
if (isTransactionCancelled(trx)) return;
|
if (isTransactionCancelled(trx)) return;
|
||||||
editingTransactionId.value = trx._id || trx.id;
|
editingTransactionId.value = trx._id || trx.id;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
<label class="font-semibold text-sm">تاریخ سررسید *</label>
|
||||||
<DatePicker v-model="createForm.dueDate" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="createForm.dueDate" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-column gap-2">
|
<div class="flex flex-column gap-2">
|
||||||
@@ -187,6 +187,7 @@ import PermissionGate from '@/components/common/PermissionGate.vue';
|
|||||||
import StatusTag from '@/components/common/StatusTag.vue';
|
import StatusTag from '@/components/common/StatusTag.vue';
|
||||||
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
|
import NotifyChannelsField from '@/components/common/NotifyChannelsField.vue';
|
||||||
import { getPayableAmount } from '@/utils/paymentAmount';
|
import { getPayableAmount } from '@/utils/paymentAmount';
|
||||||
|
import { calculateClassMidDate } from '@/utils/classSchedule';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import Column from 'primevue/column';
|
import Column from 'primevue/column';
|
||||||
import Tag from 'primevue/tag';
|
import Tag from 'primevue/tag';
|
||||||
@@ -198,7 +199,8 @@ import Checkbox from 'primevue/checkbox';
|
|||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import DatePicker from 'vue3-persian-datetime-picker';
|
import DatePicker from 'vue3-persian-datetime-picker';
|
||||||
|
|
||||||
const { toPersianDigits, formatJalali, toGregorianIso } = usePersianDate();
|
const { toPersianDigits, formatJalali, toGregorianIso, getTodayJalali } = usePersianDate();
|
||||||
|
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -269,11 +271,12 @@ const onClassesSelected = () => {
|
|||||||
if (!createForm.classes || createForm.classes.length === 0) {
|
if (!createForm.classes || createForm.classes.length === 0) {
|
||||||
createForm.amount = 0;
|
createForm.amount = 0;
|
||||||
createForm.discount = 0;
|
createForm.discount = 0;
|
||||||
|
createForm.dueDate = getTodayJalali();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let totalFee = 0;
|
let totalFee = 0;
|
||||||
createForm.classes.forEach((classId) => {
|
createForm.classes.forEach((classId) => {
|
||||||
const c = classesList.value.find((item) => (item._id || item.id) === classId);
|
const c = classesList.value.find((item) => String(item._id || item.id) === String(classId));
|
||||||
if (c) {
|
if (c) {
|
||||||
totalFee += (c.tuitionFee || c.course?.price || 0);
|
totalFee += (c.tuitionFee || c.course?.price || 0);
|
||||||
}
|
}
|
||||||
@@ -284,6 +287,11 @@ const onClassesSelected = () => {
|
|||||||
if ((createForm.discount || 0) > (createForm.amount || 0)) {
|
if ((createForm.discount || 0) > (createForm.amount || 0)) {
|
||||||
createForm.discount = 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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetCreateForm = () => {
|
const resetCreateForm = () => {
|
||||||
@@ -292,13 +300,14 @@ const resetCreateForm = () => {
|
|||||||
createForm.amount = 0;
|
createForm.amount = 0;
|
||||||
createForm.discount = 0;
|
createForm.discount = 0;
|
||||||
createForm.notes = '';
|
createForm.notes = '';
|
||||||
createForm.dueDate = '';
|
createForm.dueDate = getTodayJalali();
|
||||||
hasDiscount.value = false;
|
hasDiscount.value = false;
|
||||||
createNotify.sms = true;
|
createNotify.sms = true;
|
||||||
createNotify.email = true;
|
createNotify.email = true;
|
||||||
createNotify.bot = true;
|
createNotify.bot = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const openCreateModal = () => {
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
showCreateModal.value = true;
|
showCreateModal.value = true;
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
|
|
||||||
<div class="col-12 md:col-6 flex flex-column gap-2">
|
<div class="col-12 md:col-6 flex flex-column gap-2">
|
||||||
<label class="font-semibold text-sm">{{ $t('sessions.date') }} *</label>
|
<label class="font-semibold text-sm">{{ $t('sessions.date') }} *</label>
|
||||||
<DatePicker v-model="form.day" class="w-full text-sm" placeholder="1403/01/01" />
|
<DatePicker v-model="form.day" class="w-full text-sm" :placeholder="getTodayJalali()" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 md:col-3 flex flex-column gap-2">
|
<div class="col-12 md:col-3 flex flex-column gap-2">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Tabs value="0" class="surface-card border-round-xl border-1 border-color shadow-sm">
|
<Tabs value="0" class="surface-card border-round-xl border-1 border-color shadow-sm">
|
||||||
<TabList>
|
<TabList :scrollable="true">
|
||||||
<Tab value="0">کانالها</Tab>
|
<Tab value="0">کانالها</Tab>
|
||||||
<Tab value="1">اطلاعرسانی</Tab>
|
<Tab value="1">اطلاعرسانی</Tab>
|
||||||
<Tab value="2">قالبهای پیامک</Tab>
|
<Tab value="2">قالبهای پیامک</Tab>
|
||||||
|
|||||||
Reference in New Issue
Block a user