diff --git a/src/api/paymentApi.js b/src/api/paymentApi.js index 6cb6fa9..552aba3 100644 --- a/src/api/paymentApi.js +++ b/src/api/paymentApi.js @@ -12,5 +12,7 @@ export const paymentApi = { delete: (id) => axiosInstance.delete(`/payments/admin/delete/${id}`), recordTransaction: (paymentId, data) => axiosInstance.post(`/payments/admin/transactions/${paymentId}`, data), updateTransaction: (transactionId, data) => axiosInstance.put(`/payments/admin/transactions/${transactionId}`, data), - cancelTransaction: (transactionId) => axiosInstance.post(`/payments/admin/transactions/${transactionId}/cancel`) + cancelTransaction: (transactionId) => axiosInstance.post(`/payments/admin/transactions/${transactionId}/cancel`), + revertTransaction: (transactionId) => axiosInstance.post(`/payments/admin/transactions/${transactionId}/revert`), + deleteTransaction: (transactionId) => axiosInstance.delete(`/payments/admin/transactions/${transactionId}`) }; diff --git a/src/api/waitlistApi.js b/src/api/waitlistApi.js new file mode 100644 index 0000000..42f9dac --- /dev/null +++ b/src/api/waitlistApi.js @@ -0,0 +1,14 @@ +// /src/api/waitlistApi.js +import axiosInstance from './axiosInstance'; + +export const waitlistApi = { + getAll: (params) => axiosInstance.get('/waitlist/admin/get-all', { params }), + getStats: () => axiosInstance.get('/waitlist/admin/stats'), + getOne: (id) => axiosInstance.get(`/waitlist/admin/get-one/${id}`), + create: (data) => axiosInstance.post('/waitlist/admin/create', data), + update: (id, data) => axiosInstance.put(`/waitlist/admin/update/${id}`, data), + assignClass: (id, data) => axiosInstance.post(`/waitlist/admin/${id}/assign-class`, data), + revert: (id, data) => axiosInstance.post(`/waitlist/admin/${id}/revert`, data), + cancel: (id, data) => axiosInstance.post(`/waitlist/admin/${id}/cancel`, data), + delete: (id) => axiosInstance.delete(`/waitlist/admin/delete/${id}`) +}; diff --git a/src/components/common/StatusTag.vue b/src/components/common/StatusTag.vue index f97be67..c7357cf 100644 --- a/src/components/common/StatusTag.vue +++ b/src/components/common/StatusTag.vue @@ -47,7 +47,8 @@ const PAYMENT_LABELS = { partial: 'پیش پرداخت', pending: 'در انتظار پرداخت', overdue: 'معوق', - cancelled: 'لغوشده' + cancelled: 'لغوشده', + reverted: 'مسترد شده' }; const PENDING_STUDENT_LABELS = { @@ -67,6 +68,7 @@ const severity = computed(() => { if (val === 'pending' || val === 'در انتظار پرداخت') return 'info'; if (val === 'overdue' || val === 'معوق شده' || val === 'معوق') return 'danger'; if (val === 'cancelled' || val === 'canceled' || val === 'لغوشده') return 'secondary'; + if (val === 'reverted' || val === 'مسترد شده' || val === 'مسترد') return 'warn'; } if (props.type === 'session') { diff --git a/src/components/layout/AppSidebar.vue b/src/components/layout/AppSidebar.vue index 3f6bb5f..0fd3ff8 100644 --- a/src/components/layout/AppSidebar.vue +++ b/src/components/layout/AppSidebar.vue @@ -122,6 +122,7 @@ const menuGroups = computed(() => { { label: 'مدیریت اساتید', icon: 'pi pi-id-card', to: '/professors' }, { label: 'دورههای آموزشی', icon: 'pi pi-book', to: '/courses' }, { label: 'کلاسها', icon: 'pi pi-desktop', to: '/classes' }, + { label: 'لیست انتظار', icon: 'pi pi-clock', to: '/waitlist', permission: PERMISSIONS.WAITLIST_READ }, { label: 'جلسات آموزشی', icon: 'pi pi-calendar', to: '/sessions' } ] }, diff --git a/src/components/payments/QuickEditPaymentDialog.vue b/src/components/payments/QuickEditPaymentDialog.vue new file mode 100644 index 0000000..b618375 --- /dev/null +++ b/src/components/payments/QuickEditPaymentDialog.vue @@ -0,0 +1,701 @@ + + + + + + + + diff --git a/src/constants/permissions.js b/src/constants/permissions.js index 8b6f19d..ecd1bb2 100644 --- a/src/constants/permissions.js +++ b/src/constants/permissions.js @@ -84,7 +84,12 @@ export const PERMISSIONS = { EXPENSES_UPDATE: 'expenses:update', EXPENSES_DELETE: 'expenses:delete', - FINANCIAL_REPORTS_READ: 'financial_reports:read' + FINANCIAL_REPORTS_READ: 'financial_reports:read', + + WAITLIST_CREATE: 'waitlist:create', + WAITLIST_READ: 'waitlist:read', + WAITLIST_UPDATE: 'waitlist:update', + WAITLIST_DELETE: 'waitlist:delete' }; export const PERMISSION_GROUPS = [ @@ -138,6 +143,17 @@ export const PERMISSION_GROUPS = [ { key: PERMISSIONS.CLASSES_REGISTER_USERS, label: 'ثبتنام کاربران در کلاس' } ] }, + { + key: 'waitlist', + label: 'لیست انتظار', + icon: 'pi pi-clock', + permissions: [ + { key: PERMISSIONS.WAITLIST_CREATE, label: 'افزودن به لیست انتظار' }, + { key: PERMISSIONS.WAITLIST_READ, label: 'مشاهده لیست انتظار' }, + { key: PERMISSIONS.WAITLIST_UPDATE, label: 'ویرایش / انتقال به کلاس' }, + { key: PERMISSIONS.WAITLIST_DELETE, label: 'حذف از لیست انتظار' } + ] + }, { key: 'sessions', label: 'جلسات آموزشی', diff --git a/src/router/routes.js b/src/router/routes.js index d797906..94a40bf 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -115,6 +115,14 @@ export const routes = [ component: () => import('@/views/classes/ClassDetailView.vue') }, + // Waitlist + { + path: 'waitlist', + name: 'Waitlist', + component: () => import('@/views/waitlist/WaitlistListView.vue'), + meta: { title: 'لیست انتظار', permission: 'waitlist:read' } + }, + // Sessions { path: 'sessions', diff --git a/src/utils/professorShare.js b/src/utils/professorShare.js index 02d258e..693a05e 100644 --- a/src/utils/professorShare.js +++ b/src/utils/professorShare.js @@ -35,8 +35,10 @@ export function resolveSessionDurationHours({ hoursPerSection, startTime, endTim return calculateSessionDurationHours(startTime, endTime); } -export function calculatePercentageShare({ payoutPercentage = 0, revenue = 0 } = {}) { - return (toPercentage(payoutPercentage) / 100) * toNonNegativeNumber(revenue); +export function calculatePercentageShare({ payoutPercentage = 0, revenue = 0, serviceFeePerPerson = 0, studentsCount = 0 } = {}) { + const totalServiceFee = toNonNegativeNumber(serviceFeePerPerson) * toNonNegativeNumber(studentsCount); + const netRevenue = Math.max(0, toNonNegativeNumber(revenue) - totalServiceFee); + return (toPercentage(payoutPercentage) / 100) * netRevenue; } export function calculateHourlyShare({ payoutHourlyRate = 0, sessionDurationHours = 0, sessionsCount = 0 } = {}) { diff --git a/src/utils/professorShare.test.js b/src/utils/professorShare.test.js index e4ff0a8..7800f74 100644 --- a/src/utils/professorShare.test.js +++ b/src/utils/professorShare.test.js @@ -47,6 +47,19 @@ describe('calculateProfessorPayout', () => { assert.equal(result.totalPayout, 4_500_000); }); + it('computes percentage-based payout deducting serviceFeePerPerson * studentsCount first', () => { + const result = calculateProfessorPayout({ + payoutType: 'percentage', + payoutPercentage: 50, + revenue: 20_000_000, + serviceFeePerPerson: 400_000, + studentsCount: 2 + }); + // 20M - (400k * 2) = 19.2M * 50% = 9.6M + assert.equal(result.baseShare, 9_600_000); + assert.equal(result.totalPayout, 9_600_000); + }); + it('computes hourly-based payout with extra expenses', () => { const result = calculateProfessorPayout({ payoutType: 'hourly', diff --git a/src/views/classes/ClassDetailView.vue b/src/views/classes/ClassDetailView.vue index be6c530..4d384c1 100644 --- a/src/views/classes/ClassDetailView.vue +++ b/src/views/classes/ClassDetailView.vue @@ -67,6 +67,10 @@ ساعت کلاس {{ formatClassTime(classData.startTime, classData.endTime) || '—' }} +
+ با استرداد، وضعیت این پیشثبتنام و تمام صورتحسابها و تراکنشهای مربوطه به وضعیت «مسترد شده» تغییر خواهند یافت. +
+