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:
@@ -2,6 +2,15 @@
|
||||
<template>
|
||||
<div class="class-detail-view" v-if="classData">
|
||||
<PageHeader :title="classData.name" :subtitle="`دوره: ${classData.course?.title || '-'}`">
|
||||
<PermissionGate permission="financial_reports:read">
|
||||
<Button
|
||||
label="گزارش مالی کلاس"
|
||||
icon="pi pi-chart-line"
|
||||
text
|
||||
severity="info"
|
||||
@click="$router.push(`/financial-reports?classId=${classId}`)"
|
||||
/>
|
||||
</PermissionGate>
|
||||
<Button label="ویرایش" icon="pi pi-pencil" class="ml-2" @click="$router.push(`/classes/edit/${classId}`)" />
|
||||
<Button label="بازگشت" icon="pi pi-arrow-left" text severity="secondary" @click="$router.push('/classes')" />
|
||||
</PageHeader>
|
||||
|
||||
@@ -70,6 +70,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12">
|
||||
<div class="payout-fields p-3 border-1 border-color border-round">
|
||||
<h3 class="text-base font-bold text-color m-0 mb-3">سهم و هزینه استاد</h3>
|
||||
<div class="grid">
|
||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">مدل محاسبه سهم استاد</label>
|
||||
<Dropdown
|
||||
v-model="form.payoutType"
|
||||
:options="payoutTypeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="form.payoutType === 'percentage'" class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">درصد سهم استاد از درآمد کلاس</label>
|
||||
<InputNumber v-model="form.payoutPercentage" :min="0" :max="100" class="w-full text-sm" suffix=" %" />
|
||||
</div>
|
||||
|
||||
<div v-else class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">نرخ ساعتی استاد</label>
|
||||
<InputNumber v-model="form.payoutHourlyRate" :min="0" class="w-full text-sm" suffix=" تومان" />
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">هزینه جانبی هر جلسه</label>
|
||||
<InputNumber v-model="form.extraExpensePerSession" :min="0" class="w-full text-sm" suffix=" تومان" placeholder="مثلا: رفتوآمد، پذیرایی" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 p-2 border-round surface-100 flex flex-column gap-1">
|
||||
<span class="text-xs text-muted">
|
||||
مدت هر جلسه برای محاسبه ساعتی:
|
||||
<strong class="text-color">{{ toPersianDigits(sessionDurationHoursPreview) }} ساعت</strong>
|
||||
<template v-if="!sessionDurationHoursPreview"> (از ساعت شروع/پایان کلاس یا دوره تعیین میشود)</template>
|
||||
</span>
|
||||
<span class="text-xs text-muted" v-if="form.payoutType === 'hourly'">
|
||||
سهم تخمینی هر جلسه: <strong class="text-color">{{ toPersianDigits(sessionShareEstimate.toLocaleString()) }} تومان</strong>
|
||||
(نرخ ساعتی × مدت جلسه)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 md:col-4 flex flex-column gap-2">
|
||||
<label class="font-semibold text-sm">تاریخ شروع</label>
|
||||
<DatePicker v-model="form.startDate" class="w-full text-sm" placeholder="1403/01/01" />
|
||||
@@ -240,7 +285,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue';
|
||||
import { ref, reactive, computed, onMounted, watch } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import moment from 'jalali-moment';
|
||||
import { classApi } from '@/api/classApi';
|
||||
@@ -268,6 +313,7 @@ import DataTable from 'primevue/datatable';
|
||||
import Column from 'primevue/column';
|
||||
import DatePicker from 'vue3-persian-datetime-picker';
|
||||
import { WEEKDAYS } from '@/utils/classSchedule';
|
||||
import { resolveSessionDurationHours, calculateHourlyShare } from '@/utils/professorShare';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -309,10 +355,19 @@ const form = reactive({
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
numberOfSessions: null,
|
||||
payoutType: 'percentage',
|
||||
payoutPercentage: 0,
|
||||
payoutHourlyRate: 0,
|
||||
extraExpensePerSession: 0,
|
||||
isActive: true,
|
||||
adminNotes: []
|
||||
});
|
||||
const weekdays = WEEKDAYS;
|
||||
const payoutTypeOptions = [
|
||||
{ label: 'درصدی از درآمد کلاس', value: 'percentage' },
|
||||
{ label: 'نرخ ساعتی', value: 'hourly' }
|
||||
];
|
||||
const selectedCourseHoursPerSection = ref(null);
|
||||
|
||||
const finalTuitionFee = computed(() => {
|
||||
const tuition = Number(form.tuitionFee) || 0;
|
||||
@@ -320,6 +375,18 @@ const finalTuitionFee = computed(() => {
|
||||
return Math.max(0, tuition - (Number(form.discount) || 0));
|
||||
});
|
||||
|
||||
const sessionDurationHoursPreview = computed(() => resolveSessionDurationHours({
|
||||
hoursPerSection: selectedCourseHoursPerSection.value,
|
||||
startTime: form.startTime,
|
||||
endTime: form.endTime
|
||||
}));
|
||||
|
||||
const sessionShareEstimate = computed(() => calculateHourlyShare({
|
||||
payoutHourlyRate: form.payoutHourlyRate,
|
||||
sessionDurationHours: sessionDurationHoursPreview.value,
|
||||
sessionsCount: 1
|
||||
}));
|
||||
|
||||
const removeDialogMessage = computed(() => {
|
||||
const name = studentToRemove.value?.name;
|
||||
return name
|
||||
@@ -369,6 +436,7 @@ const applyCourseDefaults = (course) => {
|
||||
form.numberOfSessions = course.sectionCount;
|
||||
}
|
||||
if (course.sectionCount) defaultSessionCount.value = course.sectionCount;
|
||||
selectedCourseHoursPerSection.value = course.hoursPerSection ?? null;
|
||||
};
|
||||
|
||||
const fetchData = async () => {
|
||||
@@ -410,10 +478,15 @@ const fetchData = async () => {
|
||||
startTime: data.startTime || '',
|
||||
endTime: data.endTime || '',
|
||||
numberOfSessions: data.numberOfSessions ?? null,
|
||||
payoutType: data.payoutType === 'hourly' ? 'hourly' : 'percentage',
|
||||
payoutPercentage: data.payoutPercentage || 0,
|
||||
payoutHourlyRate: data.payoutHourlyRate || 0,
|
||||
extraExpensePerSession: data.extraExpensePerSession || 0,
|
||||
isActive: data.isActive !== false,
|
||||
adminNotes: Array.isArray(data.adminNotes) ? [...data.adminNotes] : []
|
||||
});
|
||||
students.value = data.students || [];
|
||||
selectedCourseHoursPerSection.value = data.course?.hoursPerSection ?? null;
|
||||
if (data.numberOfSessions) {
|
||||
defaultSessionCount.value = data.numberOfSessions;
|
||||
} else if (data.course?.sectionCount) {
|
||||
@@ -458,6 +531,10 @@ const handleSubmit = async () => {
|
||||
startTime: form.startTime || '',
|
||||
endTime: form.endTime || '',
|
||||
numberOfSessions: form.numberOfSessions,
|
||||
payoutType: form.payoutType,
|
||||
payoutPercentage: form.payoutType === 'percentage' ? form.payoutPercentage : 0,
|
||||
payoutHourlyRate: form.payoutType === 'hourly' ? form.payoutHourlyRate : 0,
|
||||
extraExpensePerSession: form.extraExpensePerSession,
|
||||
isActive: form.isActive,
|
||||
adminNotes: (form.adminNotes || []).map((n) => String(n).trim()).filter(Boolean)
|
||||
};
|
||||
@@ -517,5 +594,10 @@ const handleRemoveStudent = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => form.course, (courseId) => {
|
||||
const course = courses.value.find((c) => String(c._id) === String(courseId));
|
||||
selectedCourseHoursPerSection.value = course?.hoursPerSection ?? null;
|
||||
});
|
||||
|
||||
onMounted(fetchData);
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user