diff --git a/src/utils/classSchedule.js b/src/utils/classSchedule.js index f106d1e..0500dca 100644 --- a/src/utils/classSchedule.js +++ b/src/utils/classSchedule.js @@ -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'); diff --git a/src/utils/classSchedule.test.js b/src/utils/classSchedule.test.js index 275bd85..b17a4fe 100644 --- a/src/utils/classSchedule.test.js +++ b/src/utils/classSchedule.test.js @@ -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'); + }); }); diff --git a/src/views/payments/PaymentListView.vue b/src/views/payments/PaymentListView.vue index 5152b4a..b954bf7 100644 --- a/src/views/payments/PaymentListView.vue +++ b/src/views/payments/PaymentListView.vue @@ -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); + } } };