LFFE-10 Merge branch 'feature/LFFE-10_implemention_of_report_page' into 'develop'

This commit is contained in:
AmirHossein Mahmoodi
2023-11-18 07:42:59 +00:00
26 changed files with 1087 additions and 6 deletions

View File

@@ -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",

View File

@@ -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": "بروزرسانی مجدد"
}
}

View File

@@ -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 (
<Stack sx={{width: "95%", mx: "auto"}}>
<Accordion elevation={0} sx={{
width: "100%",
mb: 2,
borderBottom: 2,
borderBottomColor: "divider",
backgroundColor: "#f1f1f1"
}}>
<AccordionSummary>
<Box sx={{width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between"}}>
<Box sx={{display: "flex", alignItems: "center", gap: 0.5}}>
<AssignmentIcon sx={{color: "primary.main"}}/>
<Typography sx={{fontWeight: 500, color: "primary.main"}}>
{t("filters.excel_export")}
</Typography>
</Box>
<FileUploadIcon sx={{color: "primary.main"}}/>
</Box>
</AccordionSummary>
<AccordionDetails
sx={{
display: "flex",
flexDirection: {xs: "column", sm: "row"},
justifyContent: "space-between",
alignItems: "center",
mx: "auto",
}}>
<Stack sx={{flexDirection: {xs: "column", sm: "row"}, alignItems: "center", flexWrap: 'wrap'}}>
{filter_by_province ?
<ProvinceFilter formik={formik} multiple={filter_by_province.multiple}
setFilteredText={setFilteredText}/> : ""
}
{filter_by_year ?
<YearFilter formik={formik} multiple={filter_by_year.multiple}
setFilteredText={setFilteredText}/> : ""
}
</Stack>
<Stack>
<Button onClick={formik.handleSubmit} variant="contained"
disabled={formik.isSubmitting}
sx={{backgroundColor: "success.main"}}
startIcon={formik.isSubmitting ?
<CircularProgress size={20} color="inherit"/> :
<BackupIcon/>}
>
{t("filters.export")}
</Button>
</Stack>
</AccordionDetails>
</Accordion>
</Stack>
)
};
export default ExcelExport;

View File

@@ -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 (
<FormControl sx={{width: 300, mx: 1, my: {xs: 1, lg: 0}}}>
<InputLabel size="small">{t("filters.choose_province")}</InputLabel>
<Select
name="province_id"
multiple={multiple}
size="small"
variant="outlined"
value={formik.values.province_id}
onChange={provinceChange}
onBlur={formik.handleBlur("province_id")}
input={<OutlinedInput label={t("filters.choose_province")}/>}
disabled={isLoadingProvinceList || errorProvinceList}
renderValue={multiple ? (selected) => (
<Box sx={{
display: 'flex', gap: 0.7, overflow: "hidden",
width: '100%', pr: 4
}}>
{selected.map((value) => {
const selectedProvince = provinceList.find(province => province.id === value);
return (
<Typography key={value} variant="button">
{selectedProvince ? selectedProvince.name : ''}
</Typography>
);
})}
</Box>
) : undefined}
endAdornment={
!needAdorment ?
<InputAdornment position="end" sx={{mr: 2}}>
<IconButton
onClick={() => multiple
? formik.setFieldValue('province_id', [])
: formik.setFieldValue('province_id', "")
}
size="small">
<ClearIcon/>
</IconButton>
</InputAdornment> : null
}
>
{isLoadingProvinceList ? (
<MenuItem>
{t("filters.text_field_loading_provinces_list")}
</MenuItem>
) : errorProvinceList ? (
<MenuItem>
{t("filters.text_field_error_fetching_provinces")}
</MenuItem>
) : (
provinceList.map((item) => (
<MenuItem key={item.id} value={item.id}>
{item.name}
</MenuItem>
))
)}
</Select>
</FormControl>
)
};
export default ProvinceFilter;

View File

@@ -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 (
<FormControl sx={{width: 300, mx: 1, my: {xs: 1, lg: 0}}}>
<InputLabel size="small">{t("filters.choose_year")}</InputLabel>
<Select
name="date"
multiple={multiple}
size="small"
variant="outlined"
value={formik.values.date}
onChange={yearChange}
onBlur={formik.handleBlur("date")}
input={<OutlinedInput label={t("filters.choose_year")}/>}
renderValue={multiple ? (selected) => (
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 0.5}}>
{selected.map((value) => (
<Chip key={value} label={value}
sx={{color: "#fff", backgroundColor: "primary.dark"}}/>
))}
</Box>
) : undefined}
>
{
shamsiYearList.map((year) => (
<MenuItem
key={year}
value={year}
>
{year}
</MenuItem>
))
}
</Select>
</FormControl>
)
};
export default YearFilter;

View File

@@ -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 (
<Accordion elevation={0} expanded={expanded} onChange={() => {
}} sx={{
mb: 2,
borderBottom: 2,
borderBottomColor: "divider",
backgroundColor: "#f1f1f1",
}}>
<AccordionSummary sx={{cursor: "unset !important"}}>
<Box sx={{width: "100%", display: "flex", alignItems: "center", justifyContent: "space-between"}}>
<Box sx={{display: "flex", alignItems: "center", gap: 0.5}}>
<EqualizerIcon sx={{color: "primary.main"}}/>
<Typography sx={{fontWeight: 500, color: "primary.main"}}>{title}</Typography>
<Typography variant="caption" sx={{fontWeight: 500}} color="error">
{filteredText.province} {filteredText.year}
</Typography>
</Box>
<Box>
<Tooltip title={t("filters.update_again")} arrow>
<IconButton onClick={() => mutate()}
size="small">
<CachedIcon color="primary"/>
</IconButton>
</Tooltip>
<Tooltip title={t("filters.filter_btn")} arrow>
<IconButton onClick={() => setExpanded((prevExpanded) => !prevExpanded)}
size="small">
<FilterAltIcon color="primary"/>
</IconButton>
</Tooltip>
</Box>
</Box>
</AccordionSummary>
<AccordionDetails sx={{
display: "flex",
flexDirection: {xs: "column", sm: "row"},
justifyContent: "space-between",
alignItems: "center"
}}>
<Stack sx={{flexDirection: {xs: "column", sm: "row"}, alignItems: "center", flexWrap: 'wrap'}}>
{filter_by_province ?
<ProvinceFilter formik={formik} multiple={filter_by_province.multiple}
setFilteredText={setFilteredText}/> : ""
}
{filter_by_year ?
<YearFilter formik={formik} multiple={filter_by_year.multiple}
setFilteredText={setFilteredText}/> : ""
}
</Stack>
<Stack>
<Button onClick={formik.handleSubmit} variant="contained"
disabled={isLoadingReportList || !formik.dirty}
startIcon={isLoadingReportList ?
<CircularProgress size={20} color="inherit"/> :
<TroubleshootIcon/>}
>{t("filters.filter")}</Button>
</Stack>
</AccordionDetails>
</Accordion>
)
};
export default Filter;

View File

@@ -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 (
<Chart chartId="LoanProgressBar" type="radialBar" specialOption={specialOption} series={series}/>
)
};
export default RadialBarChart;

View File

@@ -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 (
<Chart chartId="LoanProgressBar" type="radialBar" specialOption={specialOption} series={series}/>
)
};
export default RadialBarSemiCircularChart;

View File

@@ -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 (
<Chart chartId="LoanProgressBar" type="radialBar" specialOption={specialOption} series={series}/>
)
};
export default RadialBarStrokedChart;

View File

@@ -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 (
<Paper elevation={2} sx={{width: "95%", mx: "auto", my: 2, pb: 2, background: "#f7f7f7e6"}}>
{/*<Filter title={t("reports.loan_details")}*/}
{/* filterItem={[*/}
{/* {type: "province", type_fa: "استان", multiple: false},*/}
{/* {type: "year", type_fa: "سال", multiple: false}*/}
{/* ]}*/}
{/* setFilterOption={setFilterOption}*/}
{/* isLoadingReportList={isLoadingReportList}*/}
{/*/>*/}
<Grid container sx={{justifyContent: {xs: "center", md: "space-between"}}}>
<Grid item xs={12} sm={10} md={5} sx={{aspectRatio: "1/0.75"}}>
<RadialBarSemiCircularChart data={20} label={t("reports.cost_of_loan_refer_to_bank")}/>
</Grid>
<Grid item xs={12} sm={10} md={5} sx={{aspectRatio: "1/0.75"}}>
<RadialBarSemiCircularChart data={60} label={t("reports.number_of_loan_refer_to_bank")}/>
</Grid>
<Grid item xs={12} sm={10} md={5} sx={{aspectRatio: "1/0.75"}}>
<RadialBarSemiCircularChart data={95} label={t("reports.cost_of_payed_loan")}/>
</Grid>
<Grid item xs={12} sm={10} md={5} sx={{aspectRatio: "1/0.75"}}>
<RadialBarStrokedChart data={15} label={t("reports.number_of_payed_loan")}/>
</Grid>
</Grid>
</Paper>
)
};
export default LoanDetails;

View File

@@ -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 (
<Chart chartId="LoanDistributionPie" type="pie" specialOption={specialOption} series={series}/>
)
};
export default Pie;

View File

@@ -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 (
<Chart chartId="LoanDistributionPolar" type="polarArea" specialOption={specialOption} series={series}/>
)
};
export default PolarChart;

View File

@@ -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 (
<Paper elevation={2} sx={{width: "95%", mx: "auto", my: 2, pb: 2, background: "#f7f7f7e6"}}>
<Filter title={t("reports.loan_distribution")}
filterItem={[
{type: "province", type_fa: "استان", multiple: true},
{type: "year", type_fa: "سال", multiple: false}
]}
setFilterOption={setFilterOption}
isLoadingReportList={isLoadingReportList}
mutate={mutate}
/>
<Grid container sx={{justifyContent: {xs: "center", md: "space-between", position: "relative"}}}>
{isLoadingReportList ? (
<Grid item xs={12} sx={{aspectRatio: "3/0.95"}}>
<LoadingHardPage
icon={<AnalyticsIcon sx={{width: "inherit", height: "inherit"}}/>}
loading={isLoadingReportList}
label={t("reports.loading_fetching_reports")}
width={100}
height={100}
sx={{position: "absolute", bgcolor: "#f7f7f7e6"}}
/>
</Grid>
) : errorReportList ? (
<Grid item xs={12}
sx={{aspectRatio: "3/0.95", display: "flex", justifyContent: "center", alignItems: "center"}}>
<Typography variant="h6" sx={{
color: "#858585",
letterSpacing: "0.1rem"
}}>{t("reports.error_fetching_reports")}</Typography>
</Grid>
) : (
<>
<Grid item xs={12} sm={10} md={6} xl={5} sx={{aspectRatio: "1/0.75"}}>
<PolarChart data={reportList}/>
</Grid>
<Grid item xs={12} sm={10} md={6} xl={5} sx={{aspectRatio: "1/0.75"}}>
<Pie data={reportList}/>
</Grid>
</>
)}
</Grid>
</Paper>
)
};
export default LoanDistribution;

View File

@@ -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 <Chart chartId="LoanProgressBar" type="bar" specialOption={specialOption} series={series}/>;
};
export default BarChart;

View File

@@ -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 (
<Paper elevation={2} sx={{width: "95%", mx: "auto", my: 2, pb: 2, background: "#f7f7f7e6"}}>
<Filter title={t("reports.loan_progress")}
filterItem={[
{type: "year", type_fa: "سال", multiple: false}
]}
setFilterOption={setFilterOption}
isLoadingReportList={isLoadingReportList}
mutate={mutate}
/>
<Grid container>
{isLoadingReportList ? (
<Grid item xs={12}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
aspectRatio: {xs: "1/1", sm: "1/1", md: "1/0.4", xl: "1/0.3", position: "relative"}
}}>
<LoadingHardPage
icon={<AnalyticsIcon sx={{width: "inherit", height: "inherit"}}/>}
loading={isLoadingReportList}
label={t("reports.loading_fetching_reports")}
width={100}
height={100}
sx={{position: "absolute", bgcolor: "#f7f7f7e6"}}
/>
</Grid>
) : errorReportList ? (
<Grid item xs={12}
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
aspectRatio: {xs: "1/1", sm: "1/1", md: "1/0.4", xl: "1/0.3", position: "relative"}
}}>
<Typography variant="h6" sx={{
color: "#858585",
letterSpacing: "0.1rem"
}}>{t("reports.error_fetching_reports")}</Typography>
</Grid>
) : (
<Grid item xs={12}
sx={{
aspectRatio: {xs: "1/1", sm: "1/1", md: "1/0.4", xl: "1/0.3", position: "relative"}
}}>
<BarChart data={reportList}/>
</Grid>
)}
</Grid>
</Paper>
)
};
export default LoanProgress;

View File

@@ -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 (
<>
<ExcelExport/>
<LoanProgress/>
<LoanDistribution/>
<LoanDetails/>
</>
)
};
export default DashboardReportsComponent;

View File

@@ -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(<WidgetsIcon/>);
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 : {})
}

View File

@@ -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

View File

@@ -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: <AssessmentIcon/>,
selected: false,
permission: "all",
},
{
key: "sidebar.passenger-office-chief",
secondary: "secondary.passenger-office",
@@ -146,7 +156,7 @@ const sidebarMenu = [
route: "/dashboard/expert-management",
icon: <ManageAccountsIcon sx={{width: 'inherit', height: 'inherit'}}/>,
selected: false,
permission: "manage_navgan_loan",
permission: "manage_experts",
},
{
key: "sidebar.user-management",

View File

@@ -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]})`;
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -0,0 +1,20 @@
import DashboardReportsComponent from "@/components/dashboard/reports";
import {parse} from "next-useragent";
export default function Reports() {
return (
<DashboardReportsComponent/>
);
}
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'}
},
};
}