diff --git a/package-lock.json b/package-lock.json
index 60f8d2b..894eacb 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@fontsource/vazirmatn": "^5.3.0",
"@primevue/themes": "4.0.7",
"axios": "^1.19.0",
+ "chart.js": "^4.5.1",
"filepond": "^4.32.12",
"filepond-plugin-file-validate-type": "^1.2.9",
"filepond-plugin-image-preview": "^4.6.12",
@@ -24,6 +25,7 @@
"sass": "^1.102.0",
"vee-validate": "^4.15.1",
"vue": "^3.5.40",
+ "vue-chartjs": "^5.3.4",
"vue-filepond": "^8.0.0",
"vue-i18n": "^11.4.8",
"vue-router": "^5.2.0",
@@ -280,6 +282,12 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@kurkle/color": {
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
+ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
+ "license": "MIT"
+ },
"node_modules/@oxc-project/types": {
"version": "0.143.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
@@ -1277,6 +1285,18 @@
"integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==",
"license": "MIT"
},
+ "node_modules/chart.js": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
+ "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "@kurkle/color": "^0.3.0"
+ },
+ "engines": {
+ "pnpm": ">=8"
+ }
+ },
"node_modules/chokidar": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
@@ -3265,6 +3285,16 @@
}
}
},
+ "node_modules/vue-chartjs": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/vue-chartjs/-/vue-chartjs-5.3.4.tgz",
+ "integrity": "sha512-x3Fqob8RQvrTdssfi9ecsCzEkFOd8JPmNwSkSQzdfKj/uBsRJs/Y88cZcZIEcPsTVfMGwMo4MOoihoDG2DoE/g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "chart.js": "^4.1.1",
+ "vue": "^3.0.0-0 || ^2.7.0"
+ }
+ },
"node_modules/vue-filepond": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/vue-filepond/-/vue-filepond-8.0.0.tgz",
diff --git a/package.json b/package.json
index 950f385..1fbe860 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@fontsource/vazirmatn": "^5.3.0",
"@primevue/themes": "4.0.7",
"axios": "^1.19.0",
+ "chart.js": "^4.5.1",
"filepond": "^4.32.12",
"filepond-plugin-file-validate-type": "^1.2.9",
"filepond-plugin-image-preview": "^4.6.12",
@@ -26,6 +27,7 @@
"sass": "^1.102.0",
"vee-validate": "^4.15.1",
"vue": "^3.5.40",
+ "vue-chartjs": "^5.3.4",
"vue-filepond": "^8.0.0",
"vue-i18n": "^11.4.8",
"vue-router": "^5.2.0",
diff --git a/src/api/financialReportApi.js b/src/api/financialReportApi.js
index ca329b9..41a4a67 100644
--- a/src/api/financialReportApi.js
+++ b/src/api/financialReportApi.js
@@ -4,5 +4,6 @@ import axiosInstance from './axiosInstance';
export const financialReportApi = {
getClassReport: (classId) => axiosInstance.get(`/financial-reports/admin/classes/${classId}`),
getSessionReport: (sessionId) => axiosInstance.get(`/financial-reports/admin/sessions/${sessionId}`),
- getRangeReport: (params) => axiosInstance.get('/financial-reports/admin/range', { params })
+ getRangeReport: (params) => axiosInstance.get('/financial-reports/admin/range', { params }),
+ getAnalytics: (params) => axiosInstance.get('/financial-reports/admin/analytics', { params })
};
diff --git a/src/components/financialReports/FinancialStatCard.vue b/src/components/financialReports/FinancialStatCard.vue
index 7b6e546..c13d5c7 100644
--- a/src/components/financialReports/FinancialStatCard.vue
+++ b/src/components/financialReports/FinancialStatCard.vue
@@ -4,6 +4,7 @@
@@ -20,10 +21,19 @@ const props = defineProps({
value: { type: Number, default: 0 },
icon: { type: String, default: 'pi pi-wallet' },
bgClass: { type: String, default: 'bg-blue-100' },
- iconClass: { type: String, default: 'text-blue-600' }
+ iconClass: { type: String, default: 'text-blue-600' },
+ /** 'currency' (default, appends تومان) | 'number' (plain count) | 'text' (uses displayValue verbatim) */
+ format: { type: String, default: 'currency' },
+ /** Overrides the computed value entirely, e.g. a "12 / 20" ratio string. */
+ displayValue: { type: String, default: '' },
+ hint: { type: String, default: '' }
});
const { toPersianDigits } = usePersianDate();
-const formattedValue = computed(() => `${toPersianDigits(Math.round(props.value || 0).toLocaleString())} تومان`);
+const formattedValue = computed(() => {
+ if (props.displayValue) return props.displayValue;
+ if (props.format === 'number') return toPersianDigits(Math.round(props.value || 0).toLocaleString());
+ return `${toPersianDigits(Math.round(props.value || 0).toLocaleString())} تومان`;
+});
diff --git a/src/components/financialReports/charts/ForecastChart.vue b/src/components/financialReports/charts/ForecastChart.vue
new file mode 100644
index 0000000..bffb0c8
--- /dev/null
+++ b/src/components/financialReports/charts/ForecastChart.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/financialReports/charts/IncomeByClassChart.vue b/src/components/financialReports/charts/IncomeByClassChart.vue
new file mode 100644
index 0000000..73de680
--- /dev/null
+++ b/src/components/financialReports/charts/IncomeByClassChart.vue
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+ دادهای برای نمایش وجود ندارد
+
+
+
+
+
+
+
diff --git a/src/components/financialReports/charts/IncomeTrendChart.vue b/src/components/financialReports/charts/IncomeTrendChart.vue
new file mode 100644
index 0000000..d8ade22
--- /dev/null
+++ b/src/components/financialReports/charts/IncomeTrendChart.vue
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/financialReports/charts/MonthlyBreakdownChart.vue b/src/components/financialReports/charts/MonthlyBreakdownChart.vue
new file mode 100644
index 0000000..3d281ed
--- /dev/null
+++ b/src/components/financialReports/charts/MonthlyBreakdownChart.vue
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/financialReports/charts/PaymentStatusChart.vue b/src/components/financialReports/charts/PaymentStatusChart.vue
new file mode 100644
index 0000000..d771f4b
--- /dev/null
+++ b/src/components/financialReports/charts/PaymentStatusChart.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ دادهای برای نمایش وجود ندارد
+
+
+
+
+
+
+
diff --git a/src/utils/chartTheme.js b/src/utils/chartTheme.js
new file mode 100644
index 0000000..adf66a4
--- /dev/null
+++ b/src/utils/chartTheme.js
@@ -0,0 +1,70 @@
+// /src/utils/chartTheme.js
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ LineElement,
+ PointElement,
+ ArcElement,
+ Tooltip,
+ Legend,
+ Filler
+} from 'chart.js';
+
+ChartJS.register(
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ LineElement,
+ PointElement,
+ ArcElement,
+ Tooltip,
+ Legend,
+ Filler
+);
+
+ChartJS.defaults.font.family = "'Vazirmatn', sans-serif";
+ChartJS.defaults.font.size = 12;
+ChartJS.defaults.color = '#94a3b8';
+
+/** Shared semantic palette — kept consistent with the stat-card icon colors used across the dashboard. */
+export const CHART_COLORS = {
+ received: '#10b981',
+ receivedSoft: 'rgba(16, 185, 129, 0.15)',
+ sessionIncome: '#3b82f6',
+ sessionIncomeSoft: 'rgba(59, 130, 246, 0.15)',
+ outstanding: '#f59e0b',
+ outstandingSoft: 'rgba(245, 158, 11, 0.15)',
+ overdue: '#ef4444',
+ overdueSoft: 'rgba(239, 68, 68, 0.15)',
+ payout: '#8b5cf6',
+ payoutSoft: 'rgba(139, 92, 246, 0.15)',
+ expense: '#f43f5e',
+ expenseSoft: 'rgba(244, 63, 94, 0.15)',
+ netProfit: '#14b8a6',
+ netProfitSoft: 'rgba(20, 184, 166, 0.15)',
+ gridLine: 'rgba(148, 163, 184, 0.15)',
+ muted: '#94a3b8'
+};
+
+export const STATUS_COLORS = {
+ paid: '#10b981',
+ partial: '#3b82f6',
+ pending: '#f59e0b',
+ overdue: '#ef4444'
+};
+
+export const STATUS_LABELS_FA = {
+ paid: 'تسویهشده',
+ partial: 'پرداخت جزئی',
+ pending: 'در انتظار پرداخت',
+ overdue: 'معوق'
+};
+
+export const baseGridOptions = () => ({
+ grid: { color: CHART_COLORS.gridLine, drawBorder: false },
+ ticks: { color: CHART_COLORS.muted, font: { family: "'Vazirmatn', sans-serif" } }
+});
+
+export default ChartJS;
diff --git a/src/views/financialReports/FinancialOverviewTab.vue b/src/views/financialReports/FinancialOverviewTab.vue
new file mode 100644
index 0000000..eabd9a5
--- /dev/null
+++ b/src/views/financialReports/FinancialOverviewTab.vue
@@ -0,0 +1,339 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
روند درآمد بر اساس جلسات
+
مقایسه وجوه نقداً دریافتی با درآمد تعهدی حاصل از جلسات برگزارشده
+
+
+
+
+
+
+
+
+
+
+
گزارش کامل درآمد مؤسسه ({{ toPersianDigits(analytics.monthly.length) }} ماه اخیر)
+
دریافتی، سهم اساتید، هزینههای عمومی و سود خالص در هر ماه
+
+
+
+
+
+
+
+
+
+
وضعیت مطالبات (بر اساس مبلغ)
+
+
+
+
+
+
پیشبینی وصولی بر اساس سررسید اقساط
+
۸ هفته پیشرو — بر اساس تاریخ سررسید تراکنشهای در انتظار پرداخت
+
+
+
+
۷ روز آینده
+
{{ formatToman(analytics.forecast.next7DaysAmount) }}
+
+
+
+
+
۳۰ روز آینده
+
{{ formatToman(analytics.forecast.next30DaysAmount) }}
+
+
+
+
+
۶۰ روز آینده
+
{{ formatToman(analytics.forecast.next60DaysAmount) }}
+
+
+
+
+
+
+
+
+
+
+
عملکرد مالی به تفکیک کلاس (درآمد هر جلسه)
+
+
+
+
+
جدول کامل درآمد به تفکیک کلاس
+
+
+
+ {{ data.className }}
+
+
+
+ {{ data.professorName }}
+
+
+ {{ toPersianDigits(data.studentsCount) }}
+
+
+ {{ toPersianDigits(data.sessionsHeld) }} / {{ toPersianDigits(data.sessionsPlanned) }}
+
+
+ {{ formatToman(data.actualReceivedRevenue) }}
+
+
+ {{ formatToman(data.pendingReceivables) }}
+
+
+ {{ formatToman(data.incomePerSessionActual) }}
+
+
+ {{ formatToman(data.incomePerSessionExpected) }}
+
+
+
+
+
+
+
+
جدول سررسید پرداختها (اقساط دانشجویان)
+
+
+
+
+ {{ data.studentName }}
+
+
+ {{ data.className }}
+
+
+ {{ formatToman(data.amount) }}
+
+
+ {{ formatJalali(data.dueDate) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/financialReports/FinancialReportsView.vue b/src/views/financialReports/FinancialReportsView.vue
index f88f04f..43c3512 100644
--- a/src/views/financialReports/FinancialReportsView.vue
+++ b/src/views/financialReports/FinancialReportsView.vue
@@ -7,14 +7,22 @@
-
+
- گزارش کلاسها
- گزارش بازه زمانی / ماهانه
+ نمای کلی و نمودارها
+ گزارش کلاسها
+ گزارش بازه زمانی / ماهانه
-
+
+
+
+
+
+
+
+
@@ -166,7 +174,7 @@
-
+
@@ -326,6 +334,7 @@ import TableSkeleton from '@/components/common/TableSkeleton.vue';
import StatusTag from '@/components/common/StatusTag.vue';
import PermissionGate from '@/components/common/PermissionGate.vue';
import FinancialStatCard from '@/components/financialReports/FinancialStatCard.vue';
+import FinancialOverviewTab from './FinancialOverviewTab.vue';
import Button from 'primevue/button';
import Dropdown from 'primevue/select';
import DataTable from 'primevue/datatable';
@@ -343,6 +352,8 @@ const route = useRoute();
const { toPersianDigits, formatJalali, toGregorianIso, toJalaliPickerValue, getTodayJalali, getStartOfJalaliMonth } = usePersianDate();
const { showError } = useToast();
+const activeTab = ref('0');
+
const formatToman = (value) => `${toPersianDigits(Math.round(value || 0).toLocaleString())} تومان`;
// ── Per-class report ──────────────────────────────────────────────────────
@@ -441,6 +452,7 @@ onMounted(async () => {
await loadClasses();
setThisMonth();
if (route.query.classId) {
+ activeTab.value = '1';
selectedClassId.value = String(route.query.classId);
loadClassReport();
}