Add professor share & financial reports UI, expense management page

Adds payout configuration fields (percentage/hourly + per-session
expense allowance) with a live payout preview to the class form,
a new Financial Reports page (per-class and date-range/monthly tabs,
per-session drill-down dialog) with reusable stat cards, and an
Institutional Expenses management page. Registers routes, sidebar
navigation, and permission constants for the new financial_reports
and expenses permissions.
This commit is contained in:
2026-08-21 12:19:18 +03:30
parent ee3bc2b856
commit 26762e4dac
13 changed files with 1055 additions and 3 deletions
+72
View File
@@ -0,0 +1,72 @@
// /src/utils/professorShare.js
// Mirrors gameno-api/utils/professorShare.js so the dashboard can preview payout
// calculations client-side before saving a class.
const toNonNegativeNumber = (value) => {
const n = Number(value);
if (!Number.isFinite(n) || n < 0) return 0;
return n;
};
const toPercentage = (value) => Math.min(100, toNonNegativeNumber(value));
const parseClockTime = (value) => {
const raw = String(value || '').trim();
const match = raw.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const hours = Number(match[1]);
const minutes = Number(match[2]);
if (hours > 23 || minutes > 59) return null;
return hours * 60 + minutes;
};
export function calculateSessionDurationHours(startTime, endTime) {
const start = parseClockTime(startTime);
const end = parseClockTime(endTime);
if (start == null || end == null) return 0;
let diffMinutes = end - start;
if (diffMinutes <= 0) diffMinutes += 24 * 60;
return diffMinutes / 60;
}
export function resolveSessionDurationHours({ hoursPerSection, startTime, endTime } = {}) {
const hours = toNonNegativeNumber(hoursPerSection);
if (hours > 0) return hours;
return calculateSessionDurationHours(startTime, endTime);
}
export function calculatePercentageShare({ payoutPercentage = 0, revenue = 0 } = {}) {
return (toPercentage(payoutPercentage) / 100) * toNonNegativeNumber(revenue);
}
export function calculateHourlyShare({ payoutHourlyRate = 0, sessionDurationHours = 0, sessionsCount = 0 } = {}) {
return toNonNegativeNumber(payoutHourlyRate) * toNonNegativeNumber(sessionDurationHours) * toNonNegativeNumber(sessionsCount);
}
export function calculateExtraExpenses({ extraExpensePerSession = 0, sessionsCount = 0 } = {}) {
return toNonNegativeNumber(extraExpensePerSession) * toNonNegativeNumber(sessionsCount);
}
export function calculateProfessorPayout(params = {}) {
const baseShare = params.payoutType === 'hourly'
? calculateHourlyShare(params)
: calculatePercentageShare(params);
const extraExpenses = calculateExtraExpenses(params);
return {
baseShare,
extraExpenses,
totalPayout: baseShare + extraExpenses
};
}
export function calculateNetProfit({ totalIncome = 0, professorTotalPayout = 0 } = {}) {
return toNonNegativeNumber(totalIncome) - toNonNegativeNumber(professorTotalPayout);
}
export function calculatePendingReceivables(expectedRevenue = 0, actualReceivedRevenue = 0) {
return Math.max(0, toNonNegativeNumber(expectedRevenue) - toNonNegativeNumber(actualReceivedRevenue));
}
export function calculateOverpaidAmount(expectedRevenue = 0, actualReceivedRevenue = 0) {
return Math.max(0, toNonNegativeNumber(actualReceivedRevenue) - toNonNegativeNumber(expectedRevenue));
}