feat(payments): auto-populate payment due date with middle session date upon class selection

This commit is contained in:
2026-08-22 00:15:03 +03:30
parent 2d124eb0eb
commit 60b21fd94a
3 changed files with 105 additions and 17 deletions
+50 -14
View File
@@ -42,6 +42,30 @@ export function formatClassSchedule(cls) {
return days || time || ''; return days || time || '';
} }
/**
* Parses any date format (Jalali string, ISO string, Date object, Persian/Latin digits) into a valid moment object
*/
export function parseDateSmart(input) {
if (!input) return null;
if (input instanceof Date) {
const m = moment(input);
return m.isValid() ? m : null;
}
const latin = String(input)
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d))
.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d))
.trim();
const isJalaliPattern = /^(1[34]\d{2})[/-](\d{1,2})[/-](\d{1,2})/.test(latin);
if (isJalaliPattern) {
const m = moment(latin, ['jYYYY/jMM/jDD', 'jYYYY-jMM-jDD', 'jYYYY/jM/jD', 'jYYYY-jM-jD']);
if (m.isValid()) return m;
}
const m = moment(latin);
if (m.isValid()) return m;
const mJalali = moment(latin, ['jYYYY/jMM/jDD', 'jYYYY-jMM-jDD']);
return mJalali.isValid() ? mJalali : null;
}
/** /**
* Calculates the class end date in Jalali (jYYYY/jMM/jDD) based on start date, selected days, and number of sessions. * Calculates the class end date in Jalali (jYYYY/jMM/jDD) based on start date, selected days, and number of sessions.
* @param {string|Date} startDate - Jalali string (e.g. 1403/01/01) or ISO string / Date * @param {string|Date} startDate - Jalali string (e.g. 1403/01/01) or ISO string / Date
@@ -58,13 +82,8 @@ export function calculateClassEndDate(startDate, days = [], numberOfSessions = 0
: []; : [];
if (!validDays.length) return ''; if (!validDays.length) return '';
const latinStartDate = String(startDate) let current = parseDateSmart(startDate);
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d)) if (!current || !current.isValid()) return '';
.replace(/[٠-٩]/g, (d) => '٠١٢٣٤٥٦٧٨٩'.indexOf(d))
.trim();
let current = moment(latinStartDate, ['jYYYY/jMM/jDD', 'YYYY-MM-DD', 'YYYY/MM/DD', moment.ISO_8601]);
if (!current.isValid()) return '';
let count = 0; let count = 0;
let lastMatchingDate = null; let lastMatchingDate = null;
@@ -85,11 +104,28 @@ export function calculateClassEndDate(startDate, days = [], numberOfSessions = 0
/** /**
* Calculates the halfway due date for a class in Jalali (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. * If class has 10 sessions, due date is calculated as the date of the 5th session.
* @param {Object} cls - Class object * @param {Object} cls - Class object
* @param {Array} [sessions] - Optional array of session objects
* @returns {string} Jalali formatted date string, or today's date if not calculable * @returns {string} Jalali formatted date string, or today's date if not calculable
*/ */
export function calculateClassMidDate(cls) { export function calculateClassMidDate(cls, sessions = []) {
if (Array.isArray(sessions) && sessions.length > 0) {
const validSessions = sessions
.filter((s) => s && (s.day || s.date))
.map((s) => ({
...s,
momentDate: parseDateSmart(s.day || s.date)
}))
.filter((s) => s.momentDate && s.momentDate.isValid());
if (validSessions.length > 0) {
validSessions.sort((a, b) => a.momentDate.valueOf() - b.momentDate.valueOf());
const midIndex = Math.ceil(validSessions.length / 2) - 1;
return validSessions[midIndex].momentDate.locale('fa').format('jYYYY/jMM/jDD');
}
}
if (!cls) return moment().locale('fa').format('jYYYY/jMM/jDD'); if (!cls) return moment().locale('fa').format('jYYYY/jMM/jDD');
const totalSessions = parseInt(cls.numberOfSessions || cls.course?.sectionCount || 0, 10); const totalSessions = parseInt(cls.numberOfSessions || cls.course?.sectionCount || 0, 10);
@@ -101,17 +137,17 @@ export function calculateClassMidDate(cls) {
} }
if (cls.startDate && cls.endDate) { if (cls.startDate && cls.endDate) {
const startM = moment(cls.startDate); const startM = parseDateSmart(cls.startDate);
const endM = moment(cls.endDate); const endM = parseDateSmart(cls.endDate);
if (startM.isValid() && endM.isValid()) { if (startM && startM.isValid() && endM && endM.isValid()) {
const midTime = startM.valueOf() + (endM.valueOf() - startM.valueOf()) / 2; const midTime = startM.valueOf() + (endM.valueOf() - startM.valueOf()) / 2;
return moment(midTime).locale('fa').format('jYYYY/jMM/jDD'); return moment(midTime).locale('fa').format('jYYYY/jMM/jDD');
} }
} }
if (cls.startDate) { if (cls.startDate) {
const startM = moment(cls.startDate); const startM = parseDateSmart(cls.startDate);
if (startM.isValid()) return startM.locale('fa').format('jYYYY/jMM/jDD'); if (startM && startM.isValid()) return startM.locale('fa').format('jYYYY/jMM/jDD');
} }
return moment().locale('fa').format('jYYYY/jMM/jDD'); return moment().locale('fa').format('jYYYY/jMM/jDD');
+35
View File
@@ -44,5 +44,40 @@ describe('class schedule display', () => {
// Session 5: 1403/01/18 (Sat) // Session 5: 1403/01/18 (Sat)
assert.equal(midDate, '1403/01/18'); assert.equal(midDate, '1403/01/18');
}); });
it('calculates class mid date from an array of existing sessions', () => {
const sessions10 = [
{ day: '2024-04-01' },
{ day: '2024-04-03' },
{ day: '2024-04-05' },
{ day: '2024-04-08' },
{ day: '2024-04-10' }, // 5th session (index 4) -> 1403/01/22
{ day: '2024-04-12' },
{ day: '2024-04-15' },
{ day: '2024-04-17' },
{ day: '2024-04-19' },
{ day: '2024-04-22' }
];
const midDate10 = calculateClassMidDate({ name: 'Class A' }, sessions10);
assert.equal(midDate10, '1403/01/22');
const sessions9 = sessions10.slice(0, 9); // 5th session is still index 4
const midDate9 = calculateClassMidDate({ name: 'Class A' }, sessions9);
assert.equal(midDate9, '1403/01/22');
const sessions4 = sessions10.slice(0, 4); // 2nd session is index 1 -> 2024-04-03 -> 1403/01/15
const midDate4 = calculateClassMidDate({ name: 'Class A' }, sessions4);
assert.equal(midDate4, '1403/01/15');
});
it('calculates class mid date with Gregorian ISO string startDate', () => {
// 2024-03-20 is 1403/01/01 (Wednesday)
const midDate = calculateClassMidDate({
startDate: '2024-03-20T00:00:00.000Z',
days: [6, 2],
numberOfSessions: 10
});
assert.equal(midDate, '1403/01/18');
});
}); });
+20 -3
View File
@@ -180,6 +180,7 @@ import { useToast } from '@/composables/useToast';
import { paymentApi } from '@/api/paymentApi'; import { paymentApi } from '@/api/paymentApi';
import { userApi } from '@/api/userApi'; import { userApi } from '@/api/userApi';
import { classApi } from '@/api/classApi'; import { classApi } from '@/api/classApi';
import { sessionApi } from '@/api/sessionApi';
import PageHeader from '@/components/common/PageHeader.vue'; import PageHeader from '@/components/common/PageHeader.vue';
import DataTableWrapper from '@/components/common/DataTableWrapper.vue'; import DataTableWrapper from '@/components/common/DataTableWrapper.vue';
import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue'; import ConfirmDeleteDialog from '@/components/common/ConfirmDeleteDialog.vue';
@@ -263,7 +264,7 @@ const eligibleUsers = computed(() => {
return usersList.value.filter((u) => eligibleIds?.has(String(u._id || u.id))); return usersList.value.filter((u) => eligibleIds?.has(String(u._id || u.id)));
}); });
const onClassesSelected = () => { const onClassesSelected = async () => {
if (createForm.user && !eligibleUsers.value.some((u) => (u._id || u.id) === createForm.user)) { if (createForm.user && !eligibleUsers.value.some((u) => (u._id || u.id) === createForm.user)) {
createForm.user = null; createForm.user = null;
} }
@@ -287,8 +288,24 @@ 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;
} }
if (!createForm.dueDate) {
createForm.dueDate = getTodayJalali(); const selectedClassId = createForm.classes[createForm.classes.length - 1];
const selectedClass = classesList.value.find((item) => String(item._id || item.id) === String(selectedClassId));
if (selectedClass) {
try {
const res = await sessionApi.getAll({
class: selectedClassId,
sortBy: 'day',
sortOrder: 'asc',
limit: 100
});
const data = res.data || res;
const rawSessions = data.items || data.sessions || data.data || data || [];
const sessionList = Array.isArray(rawSessions) ? rawSessions : [];
createForm.dueDate = calculateClassMidDate(selectedClass, sessionList);
} catch (e) {
createForm.dueDate = calculateClassMidDate(selectedClass);
}
} }
}; };