feat(payments): auto-populate payment due date with middle session date upon class selection
This commit is contained in:
+50
-14
@@ -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');
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user