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 || '';
}
/**
* 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.
* @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 '';
const latinStartDate = String(startDate)
.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d))
.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 current = parseDateSmart(startDate);
if (!current || !current.isValid()) return '';
let count = 0;
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).
* 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 {Array} [sessions] - Optional array of session objects
* @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');
const totalSessions = parseInt(cls.numberOfSessions || cls.course?.sectionCount || 0, 10);
@@ -101,17 +137,17 @@ export function calculateClassMidDate(cls) {
}
if (cls.startDate && cls.endDate) {
const startM = moment(cls.startDate);
const endM = moment(cls.endDate);
if (startM.isValid() && endM.isValid()) {
const startM = parseDateSmart(cls.startDate);
const endM = parseDateSmart(cls.endDate);
if (startM && startM.isValid() && endM && 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');
const startM = parseDateSmart(cls.startDate);
if (startM && startM.isValid()) return startM.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)
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 { userApi } from '@/api/userApi';
import { classApi } from '@/api/classApi';
import { sessionApi } from '@/api/sessionApi';
import PageHeader from '@/components/common/PageHeader.vue';
import DataTableWrapper from '@/components/common/DataTableWrapper.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)));
});
const onClassesSelected = () => {
const onClassesSelected = async () => {
if (createForm.user && !eligibleUsers.value.some((u) => (u._id || u.id) === createForm.user)) {
createForm.user = null;
}
@@ -287,8 +288,24 @@ const onClassesSelected = () => {
if ((createForm.discount || 0) > (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);
}
}
};