diff --git a/package.json b/package.json
index 6459d69..4583a4c 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"date-fns-jalali": "^2.13.0-0",
"dayjs": "^1.11.9",
"eslint": "8.36.0",
+ "file-saver": "^2.0.5",
"formik": "^2.2.9",
"fs-extra": "^11.1.1",
"image-resize": "^1.3.2",
diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json
index fa27cd6..12a1600 100644
--- a/public/locales/fa/app.json
+++ b/public/locales/fa/app.json
@@ -40,6 +40,7 @@
},
"sidebar": {
"dashboard": "داشبورد",
+ "reports": "گزارشات",
"passenger-office-chief": "اداره مسافر",
"machinery-expert": "ماشین آلات",
"passenger-boss": "رئیس مسافر ",
@@ -99,6 +100,7 @@
},
"Dashboard": {
"dashboard_page": "داشبورد",
+ "reports": "گزارشات",
"passenger_boss_page": "رئیس مسافر",
"transportation_assistance": "معاونت حمل و نقل",
"change_password": "تغییر رمز عبور",
@@ -499,5 +501,32 @@
"button-cancel": "انصراف",
"button-add": "ثبت",
"loading_permissions_list": "درحال دریافت لیست دسترسی ها"
+ },
+ "reports": {
+ "loan_progress": "درصد پیشرفت تسویه وام",
+ "loan_distribution": "درصد توزیع وام ها در هر مرحله",
+ "filter_as": "فیلتر های اعمال شده",
+ "loan_details": "جزئیات وام ها",
+ "cost_of_loan_refer_to_bank": "مبلغ وام های معرفی به بانک",
+ "number_of_loan_refer_to_bank": "تعداد وام های معرفی به بانک",
+ "cost_of_payed_loan": "مبلغ وام های پرداخت شده",
+ "number_of_payed_loan": "تعداد وام های پرداخت شده",
+ "loading_fetching_reports": "درحال دریافت گزارشات",
+ "error_fetching_reports": "خطا در دریافت گزارشات"
+ },
+ "filters": {
+ "choose_province": "انتخاب استان",
+ "choose_year": "انتخاب سال",
+ "provinces": "استان های",
+ "province": "استان",
+ "year": "سال",
+ "years": "سال های",
+ "text_field_loading_provinces_list": "درحال دریافت استان ها",
+ "text_field_error_fetching_provinces": "خطا در دریافت استان ها",
+ "filter_btn": "فیلتر",
+ "filter": "اعمال فیلتر",
+ "excel_export": "خروجی به اکسل",
+ "export": "دریافت",
+ "update_again": "بروزرسانی مجدد"
}
}
diff --git a/src/components/dashboard/reports/ExcelExport/index.jsx b/src/components/dashboard/reports/ExcelExport/index.jsx
new file mode 100644
index 0000000..472d8b8
--- /dev/null
+++ b/src/components/dashboard/reports/ExcelExport/index.jsx
@@ -0,0 +1,135 @@
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Button,
+ CircularProgress,
+ Stack,
+ Typography
+} from "@mui/material";
+import FileUploadIcon from '@mui/icons-material/FileUpload';
+import AssignmentIcon from '@mui/icons-material/Assignment';
+import BackupIcon from '@mui/icons-material/Backup';
+import {useTranslations} from "next-intl";
+import ProvinceFilter from "@/components/dashboard/reports/Filter/ProvinceFilter";
+import YearFilter from "@/components/dashboard/reports/Filter/YearFilter";
+import {useState} from "react";
+import {useFormik} from "formik";
+import {GET_EXPORT} from "@/core/data/apiRoutes";
+import moment from "jalali-moment";
+import FileSaver from 'file-saver';
+import useRequest from "@/lib/app/hooks/useRequest";
+import {periodOfYear} from "@/core/utils/yearPeriodFinder";
+
+const ExcelExport = () => {
+ const t = useTranslations();
+ const requestServer = useRequest()
+ const filterItem = [
+ {type: "province", type_fa: "استان", multiple: false},
+ {type: "year", type_fa: "سال", multiple: false}
+ ];
+ const filter_by_province = filterItem.find((item) => item.type === "province");
+ const filter_by_year = filterItem.find((item) => item.type === "year");
+
+ const [filteredText, setFilteredText] = useState({province: "", year: ""});
+
+ const formik = useFormik({
+ initialValues: {
+ ...(filter_by_year && {
+ date: filter_by_year.multiple ? [] : `${moment().format('jYYYY')}`
+ }),
+ ...(filter_by_province && {
+ province_id: filter_by_province.multiple ? [] : ""
+ }),
+ from_date: periodOfYear(moment().format('jYYYY')).from,
+ to_date: periodOfYear(moment().format('jYYYY')).to
+ }, onSubmit: (values, {setSubmitting}) => {
+ const province = values.province_id;
+ const fields = [
+ values.from_date ? {key: "from_date", value: values.from_date} : null,
+ values.to_date ? {key: "to_date", value: values.to_date} : null,
+ ...(Array.isArray(province)
+ ? province.length !== 0
+ ? province.map((item, index) => ({key: `province_id[${index}]`, value: item}))
+ : []
+ : province
+ ? [{key: "province_id", value: province}]
+ : []
+ ),
+ ].filter(Boolean);
+ const queryString = fields.length !== 0
+ ? `?${fields.map((option) => `${option.key}=${option.value}`).join('&')}`
+ : '';
+ setSubmitting(true)
+ requestServer(`${GET_EXPORT}${queryString}`, 'GET', {
+ auth: true,
+ notification: false,
+ requestOptions: {responseType: 'blob'}
+ })
+ .then((response) => {
+ const filename = `گزارش درخواست های وام ناوگان تاریخ_${moment().format('jYYYY_jMM_jDD')}.xlsx`;
+ FileSaver.saveAs(response.data, filename);
+ }).catch(() => {
+ }).finally(() => {
+ setSubmitting(false);
+ });
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+ {t("filters.excel_export")}
+
+
+
+
+
+
+
+ {filter_by_province ?
+ : ""
+ }
+ {filter_by_year ?
+ : ""
+ }
+
+
+ :
+ }
+ >
+ {t("filters.export")}
+
+
+
+
+
+ )
+};
+
+export default ExcelExport;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/Filter/ProvinceFilter.jsx b/src/components/dashboard/reports/Filter/ProvinceFilter.jsx
new file mode 100644
index 0000000..7dacacc
--- /dev/null
+++ b/src/components/dashboard/reports/Filter/ProvinceFilter.jsx
@@ -0,0 +1,111 @@
+import {
+ Box,
+ FormControl,
+ IconButton,
+ InputAdornment,
+ InputLabel,
+ MenuItem,
+ OutlinedInput,
+ Select,
+ Typography
+} from "@mui/material";
+import {useTranslations} from "next-intl";
+import useProvince from "@/lib/app/hooks/useProvince";
+import ClearIcon from '@mui/icons-material/Clear';
+
+const provinceList = [
+ {name: "قزوین", id: 1},
+ {name: "تبریز", id: 2},
+ {name: "مشهد", id: 3}
+]
+
+const ProvinceFilter = ({formik, multiple, setFilteredText}) => {
+ const {errorProvinceList, isLoadingProvinceList, provinceList} = useProvince();
+ const t = useTranslations();
+ const provinceChange = (event) => {
+ const {target: {value}} = event;
+ formik.handleChange(event);
+ formik.setFieldValue('province_id', value);
+ if (Array.isArray(value)) {
+ const province_names = []
+ value.map((item) => {
+ province_names.push(provinceList.find(province => province.id === item).name)
+ })
+ setFilteredText(prevState => ({
+ ...prevState,
+ province: province_names.length !== 0 ? `| ${t("filters.provinces")}: ${province_names.join(' , ')}` : ""
+ }));
+ } else {
+ const selectedProvince = provinceList.find(province => province.id === value);
+ setFilteredText(prevState => ({
+ ...prevState,
+ province: `| ${t("filters.province")}: ${selectedProvince.name}`
+ }));
+ }
+ };
+ const needAdorment = multiple
+ ? formik.values.province_id.length === 0 :
+ formik.values.province_id === "";
+ return (
+
+ {t("filters.choose_province")}
+
+
+ )
+};
+
+export default ProvinceFilter;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/Filter/YearFilter.jsx b/src/components/dashboard/reports/Filter/YearFilter.jsx
new file mode 100644
index 0000000..a18e9db
--- /dev/null
+++ b/src/components/dashboard/reports/Filter/YearFilter.jsx
@@ -0,0 +1,66 @@
+import {Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select} from "@mui/material";
+import moment from "jalali-moment";
+import {useTranslations} from "next-intl";
+import {periodOfYear} from "@/core/utils/yearPeriodFinder";
+
+const currentShamsiYear = moment().format('jYYYY');
+const shamsiYearList = Array.from({length: currentShamsiYear - 1399}, (_, index) => (currentShamsiYear - index).toString());
+
+const YearFilter = ({formik, multiple, setFilteredText}) => {
+ const t = useTranslations();
+ const yearChange = (event) => {
+ const {target: {value}} = event;
+ formik.handleChange(event);
+ formik.setFieldValue('date', value);
+ const {from, to} = periodOfYear(value);
+ formik.setFieldValue('from_date', from);
+ formik.setFieldValue('to_date', to);
+ if (Array.isArray(value)) {
+ setFilteredText(prevState => ({
+ ...prevState,
+ year: `| ${t("filters.years")}: ${value.join(' , ')}`
+ }));
+ } else {
+ setFilteredText(prevState => ({
+ ...prevState,
+ year: `| ${t("filters.year")}: ${value}`
+ }));
+ }
+ };
+ return (
+
+ {t("filters.choose_year")}
+
+
+ )
+};
+
+export default YearFilter;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/Filter/index.jsx b/src/components/dashboard/reports/Filter/index.jsx
new file mode 100644
index 0000000..f42d2aa
--- /dev/null
+++ b/src/components/dashboard/reports/Filter/index.jsx
@@ -0,0 +1,144 @@
+import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
+ Box,
+ Button,
+ CircularProgress,
+ IconButton,
+ Stack,
+ Tooltip,
+ Typography
+} from "@mui/material";
+import {useFormik} from "formik";
+import ProvinceFilter from "../Filter/ProvinceFilter";
+import YearFilter from "../Filter/YearFilter";
+import FilterAltIcon from '@mui/icons-material/FilterAlt';
+import EqualizerIcon from '@mui/icons-material/Equalizer';
+import {useTranslations} from "next-intl";
+import TroubleshootIcon from '@mui/icons-material/Troubleshoot';
+import CachedIcon from '@mui/icons-material/Cached';
+import {useEffect, useState} from "react";
+import moment from "jalali-moment";
+import {periodOfYear} from "@/core/utils/yearPeriodFinder";
+
+const Filter = ({title, filterItem, setFilterOption, isLoadingReportList, mutate}) => {
+ const t = useTranslations();
+ const filter_by_province = filterItem.find((item) => item.type === "province");
+ const filter_by_year = filterItem.find((item) => item.type === "year");
+
+ const [expanded, setExpanded] = useState(false);
+ const [filteredText, setFilteredText] = useState({province: "", year: ""});
+ const [stepValue, setStepValue] = useState({
+ ...(filter_by_year && {
+ date: filter_by_year.multiple ? [] : `${moment().format('jYYYY')}`
+ }),
+ ...(filter_by_province && {
+ province_id: filter_by_province.multiple ? [] : ""
+ }),
+ from_date: periodOfYear(moment().format('jYYYY')).from,
+ to_date: periodOfYear(moment().format('jYYYY')).to
+ })
+
+
+ const formik = useFormik({
+ enableReinitialize: true,
+ initialValues: stepValue, onSubmit: (values, {setSubmitting}) => {
+ setStepValue(values)
+ const province = values.province_id;
+ const fields = [
+ values.from_date ? {key: "from_date", value: values.from_date} : null,
+ values.to_date ? {key: "to_date", value: values.to_date} : null,
+ ...(Array.isArray(province)
+ ? province.length !== 0
+ ? province.map((item, index) => ({key: `province_id[${index}]`, value: item}))
+ : []
+ : province
+ ? [{key: "province_id", value: province}]
+ : []
+ ),
+ ].filter(Boolean);
+ setFilterOption(fields)
+ },
+ });
+
+ useEffect(() => {
+ const province = formik.values.province_id;
+ const fields = [
+ formik.values.from_date ? {key: "from_date", value: formik.values.from_date} : null,
+ formik.values.to_date ? {key: "to_date", value: formik.values.to_date} : null,
+ ...(Array.isArray(province)
+ ? province.length !== 0
+ ? province.map((item, index) => ({key: `province_id[${index}]`, value: item}))
+ : []
+ : province
+ ? [{key: "province_id", value: province}]
+ : []
+ ),
+ ].filter(Boolean);
+ setFilterOption(fields)
+ }, []);
+
+ return (
+ {
+ }} sx={{
+ mb: 2,
+ borderBottom: 2,
+ borderBottomColor: "divider",
+ backgroundColor: "#f1f1f1",
+ }}>
+
+
+
+
+ {title}
+
+ {filteredText.province} {filteredText.year}
+
+
+
+
+ mutate()}
+ size="small">
+
+
+
+
+ setExpanded((prevExpanded) => !prevExpanded)}
+ size="small">
+
+
+
+
+
+
+
+
+ {filter_by_province ?
+ : ""
+ }
+ {filter_by_year ?
+ : ""
+ }
+
+
+ :
+ }
+ >{t("filters.filter")}
+
+
+
+ )
+};
+
+export default Filter;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDetails/RadialBarChart.jsx b/src/components/dashboard/reports/LoanDetails/RadialBarChart.jsx
new file mode 100644
index 0000000..69e0bc0
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDetails/RadialBarChart.jsx
@@ -0,0 +1,47 @@
+import Chart from "@/core/components/Chart";
+import {calculateGradientColor} from "@/core/utils/gradientColorHandler";
+
+const RadialBarChart = ({data}) => {
+ const specialOption = {
+ title: {
+ text: undefined,
+ },
+ plotOptions: {
+ radialBar: {
+ startAngle: -90,
+ endAngle: 90,
+ track: {
+ background: "#e7e7e7",
+ strokeWidth: '90%',
+ margin: 5, // margin is in pixels
+ dropShadow: {
+ enabled: true,
+ top: 2,
+ left: 0,
+ color: '#999',
+ opacity: 1,
+ blur: 2
+ }
+ },
+ dataLabels: {
+ name: {
+ show: false
+ },
+ value: {
+ offsetY: -2,
+ fontSize: '22px'
+ }
+ }
+ }
+ },
+ fill: {
+ colors: calculateGradientColor(data)
+ },
+ };
+ const series = [data]
+ return (
+
+ )
+};
+
+export default RadialBarChart;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart.jsx b/src/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart.jsx
new file mode 100644
index 0000000..4a8fd11
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart.jsx
@@ -0,0 +1,46 @@
+import Chart from "@/core/components/Chart";
+import {calculateGradientColor} from "@/core/utils/gradientColorHandler";
+
+const RadialBarSemiCircularChart = ({data, label}) => {
+ const specialOption = {
+ title: {
+ text: undefined,
+ },
+ plotOptions: {
+ radialBar: {
+ startAngle: -110,
+ endAngle: 110,
+ track: {
+ background: "#e7e7e7",
+ strokeWidth: '80%',
+ },
+ dataLabels: {
+ name: {
+ fontSize: '20px',
+ offsetY: 110,
+ color: "#8c8c8c"
+ },
+ value: {
+ offsetY: 50,
+ fontSize: '50px',
+ fontWeight: 500,
+ color: "#8c8c8c",
+ formatter: function (val) {
+ return val + "%";
+ }
+ }
+ }
+ }
+ },
+ fill: {
+ colors: calculateGradientColor(data)
+ },
+ labels: [label],
+ };
+ const series = [data]
+ return (
+
+ )
+};
+
+export default RadialBarSemiCircularChart;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDetails/RadialBarStrokedChart.jsx b/src/components/dashboard/reports/LoanDetails/RadialBarStrokedChart.jsx
new file mode 100644
index 0000000..6482994
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDetails/RadialBarStrokedChart.jsx
@@ -0,0 +1,51 @@
+import Chart from "@/core/components/Chart";
+import {calculateGradientColor} from "@/core/utils/gradientColorHandler";
+import {useTranslations} from "next-intl";
+
+const RadialBarStrokedChart = ({data, label}) => {
+ const t = useTranslations();
+ const specialOption = {
+ title: {
+ text: undefined,
+ },
+ plotOptions: {
+ radialBar: {
+ startAngle: -110,
+ endAngle: 110,
+ track: {
+ background: "#e7e7e7",
+ strokeWidth: '80%',
+ },
+ dataLabels: {
+ name: {
+ fontSize: '20px',
+ offsetY: 110,
+ color: "#8c8c8c"
+ },
+ value: {
+ offsetY: 50,
+ fontSize: '50px',
+ fontWeight: 500,
+ color: "#8c8c8c",
+ formatter: function (val) {
+ return val + "%";
+ }
+ }
+ }
+ }
+ },
+ stroke: {
+ dashArray: 4
+ },
+ fill: {
+ colors: calculateGradientColor(data)
+ },
+ labels: [label],
+ };
+ const series = [data]
+ return (
+
+ )
+};
+
+export default RadialBarStrokedChart;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDetails/index.jsx b/src/components/dashboard/reports/LoanDetails/index.jsx
new file mode 100644
index 0000000..aaf5090
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDetails/index.jsx
@@ -0,0 +1,37 @@
+import {Grid, Paper} from "@mui/material";
+import {useTranslations} from "next-intl";
+import RadialBarSemiCircularChart from "@/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart";
+import RadialBarStrokedChart from "@/components/dashboard/reports/LoanDetails/RadialBarStrokedChart";
+
+
+const LoanDetails = () => {
+ const t = useTranslations();
+ return (
+
+ {/**/}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+};
+
+export default LoanDetails;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDistribution/PieChart.jsx b/src/components/dashboard/reports/LoanDistribution/PieChart.jsx
new file mode 100644
index 0000000..1ce3242
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDistribution/PieChart.jsx
@@ -0,0 +1,34 @@
+import Chart from "@/core/components/Chart";
+import {useTheme} from "@mui/material/styles";
+import {useMediaQuery} from "@mui/material";
+
+
+const Pie = ({data}) => {
+ const theme = useTheme();
+ const upperMd = useMediaQuery((theme.breakpoints.up("md")))
+ const specialOption = {
+ title: {
+ text: undefined,
+ },
+ fill: {
+ opacity: 0.9,
+ },
+ labels: data.map((item) => ` ${item.state_name}`),
+ dataLabels: {
+ enabled: true,
+ style: {
+ fontSize: '14px',
+ fontWeight: 300,
+ },
+ },
+ legend: {
+ show: upperMd
+ }
+ };
+ const series = data.map((item) => +item.percentage)
+ return (
+
+ )
+};
+
+export default Pie;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDistribution/PolarChart.jsx b/src/components/dashboard/reports/LoanDistribution/PolarChart.jsx
new file mode 100644
index 0000000..d6b73ff
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDistribution/PolarChart.jsx
@@ -0,0 +1,41 @@
+import Chart from "@/core/components/Chart";
+import {useTheme} from "@mui/material/styles";
+import {useMediaQuery} from "@mui/material";
+
+const PolarChart = ({data}) => {
+ const theme = useTheme();
+ const upperMd = useMediaQuery((theme.breakpoints.up("md")))
+ const specialOption = {
+ title: {
+ text: undefined,
+ },
+ plotOptions: {
+ polarArea: {
+ rings: {
+ strokeWidth: 2,
+ strokeColor: '#e3e3e3',
+ },
+ },
+ },
+ fill: {
+ opacity: 0.9,
+ },
+ labels: data.map((item) => ` ${item.state_name}`),
+ dataLabels: {
+ enabled: true,
+ style: {
+ fontSize: '14px',
+ fontWeight: 400,
+ },
+ },
+ legend: {
+ show: upperMd
+ }
+ };
+ const series = data.map((item) => +item.percentage)
+ return (
+
+ )
+};
+
+export default PolarChart;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanDistribution/index.jsx b/src/components/dashboard/reports/LoanDistribution/index.jsx
new file mode 100644
index 0000000..ff65cda
--- /dev/null
+++ b/src/components/dashboard/reports/LoanDistribution/index.jsx
@@ -0,0 +1,60 @@
+import {Grid, Paper, Typography} from "@mui/material";
+import PolarChart from "./PolarChart";
+import Pie from "./PieChart";
+import {useTranslations} from "next-intl";
+import Filter from "@/components/dashboard/reports/Filter";
+import useChart from "@/lib/app/hooks/useChart";
+import {GET_LOAN_DISTRIBUTION} from "@/core/data/apiRoutes";
+import LoadingHardPage from "@/core/components/LoadingHardPage";
+import AnalyticsIcon from "@mui/icons-material/Analytics";
+
+const LoanDistribution = () => {
+ const t = useTranslations();
+ const {errorReportList, isLoadingReportList, reportList, setFilterOption, mutate} = useChart(GET_LOAN_DISTRIBUTION);
+ return (
+
+
+
+ {isLoadingReportList ? (
+
+ }
+ loading={isLoadingReportList}
+ label={t("reports.loading_fetching_reports")}
+ width={100}
+ height={100}
+ sx={{position: "absolute", bgcolor: "#f7f7f7e6"}}
+ />
+
+ ) : errorReportList ? (
+
+ {t("reports.error_fetching_reports")}
+
+ ) : (
+ <>
+
+
+
+
+
+
+ >
+ )}
+
+
+ )
+};
+
+export default LoanDistribution;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanProgress/BarChart.jsx b/src/components/dashboard/reports/LoanProgress/BarChart.jsx
new file mode 100644
index 0000000..745257c
--- /dev/null
+++ b/src/components/dashboard/reports/LoanProgress/BarChart.jsx
@@ -0,0 +1,64 @@
+import {calculateGradientColor} from "@/core/utils/gradientColorHandler";
+import Chart from "@/core/components/Chart";
+
+const BarChart = ({data}) => {
+ const usableArray = data.map(({province_name, percentage, ...rest}) => ({
+ x: province_name,
+ y: +percentage,
+ ...rest,
+ }));
+ let colorArray = data.map((obj) => calculateGradientColor(obj.percentage));
+ const specialOption = {
+ title: {
+ text: undefined,
+ },
+ plotOptions: {
+ bar: {
+ distributed: true,
+ horizontal: false,
+ },
+ },
+ legend: {
+ show: false,
+ },
+ colors: colorArray,
+ xaxis: {
+ labels: {
+ rotate: 45,
+ rotateAlways: false,
+ trim: false,
+ style: {
+ fontWeight: 600,
+ fontSize: '11px',
+ colors: '#7e7e7e',
+ },
+ },
+ },
+ yaxis: {
+ min: 0,
+ max: 100,
+ labels: {
+ formatter: function (val) {
+ return val + '%';
+ },
+ },
+ },
+ dataLabels: {
+ enabled: true,
+ style: {
+ fontSize: '14px',
+ fontWeight: 400,
+ },
+ },
+ };
+ const series = [
+ {
+ name: 'درصد پیشرفت',
+ data: usableArray,
+ },
+ ];
+
+ return ;
+};
+
+export default BarChart;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/LoanProgress/index.jsx b/src/components/dashboard/reports/LoanProgress/index.jsx
new file mode 100644
index 0000000..dadac1e
--- /dev/null
+++ b/src/components/dashboard/reports/LoanProgress/index.jsx
@@ -0,0 +1,67 @@
+import {Grid, Paper, Typography} from "@mui/material";
+import BarChart from "./BarChart";
+import Filter from "../Filter";
+import {useTranslations} from "next-intl";
+import useChart from "@/lib/app/hooks/useChart";
+import {GET_PROVINCE_PROGRESS} from "@/core/data/apiRoutes";
+import LoadingHardPage from "@/core/components/LoadingHardPage";
+import AnalyticsIcon from '@mui/icons-material/Analytics';
+
+const LoanProgress = () => {
+ const t = useTranslations();
+ const {errorReportList, isLoadingReportList, reportList, setFilterOption, mutate} = useChart(GET_PROVINCE_PROGRESS);
+ return (
+
+
+
+ {isLoadingReportList ? (
+
+ }
+ loading={isLoadingReportList}
+ label={t("reports.loading_fetching_reports")}
+ width={100}
+ height={100}
+ sx={{position: "absolute", bgcolor: "#f7f7f7e6"}}
+ />
+
+ ) : errorReportList ? (
+
+ {t("reports.error_fetching_reports")}
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+};
+
+export default LoanProgress;
\ No newline at end of file
diff --git a/src/components/dashboard/reports/index.jsx b/src/components/dashboard/reports/index.jsx
new file mode 100644
index 0000000..56f513c
--- /dev/null
+++ b/src/components/dashboard/reports/index.jsx
@@ -0,0 +1,17 @@
+import LoanProgress from "@/components/dashboard/reports/LoanProgress";
+import LoanDistribution from "@/components/dashboard/reports/LoanDistribution";
+import LoanDetails from "@/components/dashboard/reports/LoanDetails";
+import ExcelExport from "@/components/dashboard/reports/ExcelExport";
+
+const DashboardReportsComponent = (props) => {
+ return (
+ <>
+
+
+
+
+ >
+ )
+};
+
+export default DashboardReportsComponent;
\ No newline at end of file
diff --git a/src/core/components/Chart.jsx b/src/core/components/Chart.jsx
index d0796ab..86e5d85 100644
--- a/src/core/components/Chart.jsx
+++ b/src/core/components/Chart.jsx
@@ -1,16 +1,28 @@
import dynamic from "next/dynamic";
import useLanguage from "@/lib/app/hooks/useLanguage";
+import ReactDOMServer from 'react-dom/server';
+import WidgetsIcon from '@mui/icons-material/Widgets';
+
+const widgetIconSvgString = ReactDOMServer.renderToString();
const ApexChart = dynamic(() => import("react-apexcharts"), {ssr: false});
-const Chart = ({type, series, specialOption}) => {
+const Chart = ({chartId, type, series, specialOption}) => {
const {languageApp, languageList} = useLanguage();
const chartLang = languageList.find((item) => item.key == languageApp).chartLocalization
const options = {
chart: {
+ id: chartId,
locales: [chartLang],
defaultLocale: languageApp,
- fontFamily: languageList[0].fontFamily
+ fontFamily: languageList[0].fontFamily,
+ toolbar: {
+ tools: {
+ zoomIn: false,
+ zoomOut: false,
+ download: widgetIconSvgString,
+ }
+ }
},
...(specialOption ? specialOption : {})
}
diff --git a/src/core/data/apiRoutes.js b/src/core/data/apiRoutes.js
index dba065e..7ca5fca 100644
--- a/src/core/data/apiRoutes.js
+++ b/src/core/data/apiRoutes.js
@@ -197,3 +197,9 @@ export const DELETE_ROLE_MANAGEMENT =
export const GET_PERMISSIONS_LIST =
BASE_URL + "/dashboard/permissions/list"
//role management
+
+// reports
+export const GET_EXPORT = BASE_URL + "/dashboard/exports/navgan"
+export const GET_PROVINCE_PROGRESS = BASE_URL + "/dashboard/reports/navgan/province_progress"
+export const GET_LOAN_DISTRIBUTION = BASE_URL + "/dashboard/reports/navgan/loan_distribution"
+// reports
diff --git a/src/core/data/sidebarMenu.jsx b/src/core/data/sidebarMenu.jsx
index c54bbca..09b81da 100644
--- a/src/core/data/sidebarMenu.jsx
+++ b/src/core/data/sidebarMenu.jsx
@@ -13,6 +13,7 @@ import PaidIcon from '@mui/icons-material/Paid';
import ManageAccountsIcon from '@mui/icons-material/ManageAccounts';
import PersonIcon from '@mui/icons-material/Person';
import AccessibilityIcon from '@mui/icons-material/Accessibility';
+import AssessmentIcon from '@mui/icons-material/Assessment';
const sidebarMenu = [
[
@@ -24,6 +25,15 @@ const sidebarMenu = [
selected: false,
permission: "all",
},
+ {
+ key: "sidebar.reports",
+ name: "reports",
+ type: "page",
+ route: "/dashboard/reports",
+ icon: ,
+ selected: false,
+ permission: "all",
+ },
{
key: "sidebar.passenger-office-chief",
secondary: "secondary.passenger-office",
@@ -146,7 +156,7 @@ const sidebarMenu = [
route: "/dashboard/expert-management",
icon: ,
selected: false,
- permission: "manage_navgan_loan",
+ permission: "manage_experts",
},
{
key: "sidebar.user-management",
diff --git a/src/core/utils/gradientColorHandler.js b/src/core/utils/gradientColorHandler.js
new file mode 100644
index 0000000..e16b42a
--- /dev/null
+++ b/src/core/utils/gradientColorHandler.js
@@ -0,0 +1,31 @@
+export const calculateGradientColor = (percentage) => {
+ let colorStops = [
+ {percent: 0, color: [173, 3, 23]}, // 0%: #3D0C11
+ {percent: 33, color: [245, 81, 81]}, // 33%: #D80032
+ {percent: 34, color: [237, 204, 101]}, // 34%: #F2F7A1
+ {percent: 66, color: [240, 184, 7]}, // 66%: #E9B824
+ {percent: 67, color: [167, 211, 151]}, // 67%: #B0D9B1
+ {percent: 100, color: [0, 91, 65]} // 100%: #005B41
+ ];
+
+ let segment;
+ for (let i = 0; i < colorStops.length - 1; i++) {
+ if (percentage >= colorStops[i].percent && percentage <= colorStops[i + 1].percent) {
+ segment = i;
+ break;
+ }
+ }
+
+ let color1 = colorStops[segment].color;
+ let color2 = colorStops[segment + 1].color;
+ let ratio = (percentage - colorStops[segment].percent) / (colorStops[segment + 1].percent - colorStops[segment].percent);
+
+ let color = [
+ Math.round(color1[0] + ratio * (color2[0] - color1[0])),
+ Math.round(color1[1] + ratio * (color2[1] - color1[1])),
+ Math.round(color1[2] + ratio * (color2[2] - color1[2]))
+ ];
+
+ return `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
+
+}
\ No newline at end of file
diff --git a/src/core/utils/yearPeriodFinder.js b/src/core/utils/yearPeriodFinder.js
new file mode 100644
index 0000000..73c01b9
--- /dev/null
+++ b/src/core/utils/yearPeriodFinder.js
@@ -0,0 +1,11 @@
+import moment from "jalali-moment";
+
+export const periodOfYear = (year) => {
+ const isLeapYear = moment.jIsLeapYear(year);
+ const firstDayGeorgian = moment.from(`${year}/01/01`, 'fa').format('YYYY-MM-DD');
+ const lastDayGeorgian = moment.from(`${year}/12/${isLeapYear ? '30' : '29'}`, 'fa').format('YYYY-MM-DD');
+ return {
+ from: firstDayGeorgian,
+ to: lastDayGeorgian
+ }
+}
\ No newline at end of file
diff --git a/src/lib/app/contexts/language.jsx b/src/lib/app/contexts/language.jsx
index 03e3bc3..243d7e8 100644
--- a/src/lib/app/contexts/language.jsx
+++ b/src/lib/app/contexts/language.jsx
@@ -13,7 +13,7 @@ export const LanguageProvider = ({children}) => {
key: "fa",
dir: "rtl",
name: "فارسی",
- fontFamily: `IRANSans, sans-serif`,
+ fontFamily: `IRANSansFaNum, sans-serif`,
tableLocalization: FA_DATATABLE_LOCALIZATION,
chartLocalization: FA_CHART_LOCALIZATION
}
diff --git a/src/lib/app/hooks/useChart.jsx b/src/lib/app/hooks/useChart.jsx
new file mode 100644
index 0000000..d1dd454
--- /dev/null
+++ b/src/lib/app/hooks/useChart.jsx
@@ -0,0 +1,39 @@
+import useRequest from "@/lib/app/hooks/useRequest";
+import useSWR from "swr";
+import {useState} from "react";
+
+const useChart = (chart_url) => {
+ const requestServer = useRequest({auth: true, notification: false})
+ const [filterOption, setFilterOption] = useState([]);
+ const queryString = filterOption.length !== 0
+ ? `?${filterOption.map((option) => `${option.key}=${option.value}`).join('&')}`
+ : '';
+ //swr config
+ const fetcher = (...args) => {
+ return requestServer(args, 'get').then(({data}) => {
+ return data.data;
+ }).catch(() => {
+ })
+ };
+
+ const {
+ data,
+ isValidating,
+ mutate
+ } = useSWR(filterOption.length !== 0 ? `${chart_url}${queryString}` : "", fetcher, {
+ revalidateIfStale: true,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ keepPreviousData: true
+ });
+
+ return {
+ reportList: data,
+ isLoadingReportList: isValidating,
+ errorReportList: !data,
+ setFilterOption: setFilterOption,
+ mutate: mutate
+ }
+};
+
+export default useChart;
\ No newline at end of file
diff --git a/src/lib/app/hooks/useProvince.jsx b/src/lib/app/hooks/useProvince.jsx
index d0fbda0..3443a6e 100644
--- a/src/lib/app/hooks/useProvince.jsx
+++ b/src/lib/app/hooks/useProvince.jsx
@@ -16,7 +16,8 @@ const useProvince = () => {
const {data, isLoading} = useSWR(GET_PROVINCE_LIST, fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
- revalidateOnReconnect: false
+ revalidateOnReconnect: false,
+ keepPreviousData: true
});
return {
diff --git a/src/lib/app/hooks/useRole.jsx b/src/lib/app/hooks/useRole.jsx
index fc3dd18..b4c231c 100644
--- a/src/lib/app/hooks/useRole.jsx
+++ b/src/lib/app/hooks/useRole.jsx
@@ -16,7 +16,8 @@ const useRole = () => {
const {data, isLoading} = useSWR(GET_ROLE_LIST, fetcher, {
revalidateIfStale: false,
revalidateOnFocus: false,
- revalidateOnReconnect: false
+ revalidateOnReconnect: false,
+ keepPreviousData: true
});
return {
diff --git a/src/pages/dashboard/reports/index.jsx b/src/pages/dashboard/reports/index.jsx
new file mode 100644
index 0000000..9dba421
--- /dev/null
+++ b/src/pages/dashboard/reports/index.jsx
@@ -0,0 +1,20 @@
+import DashboardReportsComponent from "@/components/dashboard/reports";
+import {parse} from "next-useragent";
+
+export default function Reports() {
+ return (
+
+ );
+}
+
+export async function getServerSideProps({req, locale}) {
+ const {isBot} = parse(req.headers["user-agent"]);
+ return {
+ props: {
+ messages: (await import(`&/locales/${locale}/app.json`)).default,
+ title: "Dashboard.reports",
+ isBot,
+ layout: {name: 'DashboardLayout'}
+ },
+ };
+}