LFFE-10 bring radial chart and complete search part of province and year

This commit is contained in:
2023-11-08 15:59:57 +03:30
parent 734bb153a0
commit ef03dea5cf
14 changed files with 197 additions and 71 deletions

View File

@@ -472,7 +472,16 @@
}, },
"reports": { "reports": {
"loan_progress": "درصد پیشرفت تسویه وام", "loan_progress": "درصد پیشرفت تسویه وام",
"loan_distribution": "درصد توضیع وام ها در هر مرحله", "loan_distribution": "درصد توزیع وام ها در هر مرحله",
"filter_as": "فیلتر های اعمال شده" "filter_as": "فیلتر های اعمال شده",
"loan_details": "اطلاعات وام ها"
},
"filters": {
"provinces": "استان های",
"province": "استان",
"year": "سال",
"years": "سال های",
"text_field_loading_provinces_list": "درحال دریافت استان ها",
"text_field_error_fetching_provinces": "خطا در دریافت استان ها"
} }
} }

View File

@@ -1,4 +1,6 @@
import {Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select} from "@mui/material"; import {Box, FormControl, InputLabel, MenuItem, OutlinedInput, Select, Typography} from "@mui/material";
import {useTranslations} from "next-intl";
import useProvince from "@/lib/app/hooks/useProvince";
const provinceList = [ const provinceList = [
{name: "قزوین", id: 1}, {name: "قزوین", id: 1},
@@ -6,11 +8,29 @@ const provinceList = [
{name: "مشهد", id: 3} {name: "مشهد", id: 3}
] ]
const ProvinceFilter = ({formik, multiple}) => { const ProvinceFilter = ({formik, multiple, setFilteredText}) => {
const {errorProvinceList, isLoadingProvinceList, provinceList} = useProvince();
const t = useTranslations();
const provinceChange = (event) => { const provinceChange = (event) => {
const {target: {value}} = event; const {target: {value}} = event;
formik.handleChange(event); formik.handleChange(event);
formik.setFieldValue('province_id', value); 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}`
}));
}
}; };
return ( return (
<FormControl sx={{width: 300, mx: 1}}> <FormControl sx={{width: 300, mx: 1}}>
@@ -24,29 +44,38 @@ const ProvinceFilter = ({formik, multiple}) => {
onChange={provinceChange} onChange={provinceChange}
onBlur={formik.handleBlur("province_id")} onBlur={formik.handleBlur("province_id")}
input={<OutlinedInput label="انتخاب استان"/>} input={<OutlinedInput label="انتخاب استان"/>}
disabled={isLoadingProvinceList || errorProvinceList}
renderValue={multiple ? (selected) => ( renderValue={multiple ? (selected) => (
<Box sx={{display: 'flex', flexWrap: 'wrap', gap: 0.5}}> <Box sx={{
display: 'flex', gap: 0.7, overflow: "hidden",
width: '100%',
}}>
{selected.map((value) => { {selected.map((value) => {
const selectedProvince = provinceList.find(province => province.id === value); const selectedProvince = provinceList.find(province => province.id === value);
return ( return (
<Chip <Typography key={value} variant="button">
key={value} {selectedProvince ? selectedProvince.name : ''}
label={selectedProvince ? selectedProvince.name : ''} </Typography>
sx={{color: "#fff", backgroundColor: "primary.dark"}}
/>
); );
})} })}
</Box> </Box>
) : undefined} ) : undefined}
> >
{provinceList.map((province) => ( {isLoadingProvinceList ? (
<MenuItem <MenuItem>
key={province.id} {t("filters.text_field_loading_provinces_list")}
value={province.id}
>
{province.name}
</MenuItem> </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> </Select>
</FormControl> </FormControl>
) )

View File

@@ -1,14 +1,27 @@
import {Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select} from "@mui/material"; import {Box, Chip, FormControl, InputLabel, MenuItem, OutlinedInput, Select} from "@mui/material";
import moment from "jalali-moment"; import moment from "jalali-moment";
import {useTranslations} from "next-intl";
const currentShamsiYear = moment().format('jYYYY'); const currentShamsiYear = moment().format('jYYYY');
const shamsiYearList = Array.from({length: currentShamsiYear - 1399}, (_, index) => (currentShamsiYear - index).toString()); const shamsiYearList = Array.from({length: currentShamsiYear - 1399}, (_, index) => (currentShamsiYear - index).toString());
const YearFilter = ({formik, multiple}) => { const YearFilter = ({formik, multiple, setFilteredText}) => {
const t = useTranslations();
const yearChange = (event) => { const yearChange = (event) => {
const {target: {value}} = event; const {target: {value}} = event;
formik.handleChange(event); formik.handleChange(event);
formik.setFieldValue('date', value); formik.setFieldValue('date', value);
if (Array.isArray(value)) {
setFilteredText(prevState => ({
...prevState,
year: `| ${t("filters.years")}: ${value.join(' , ')}`
}));
} else {
setFilteredText(prevState => ({
...prevState,
year: `| ${t("filters.year")}: ${value}`
}));
}
}; };
return ( return (
<FormControl sx={{width: 300, mx: 1}}> <FormControl sx={{width: 300, mx: 1}}>

View File

@@ -6,6 +6,7 @@ import FilterAltIcon from '@mui/icons-material/FilterAlt';
import {useTranslations} from "next-intl"; import {useTranslations} from "next-intl";
import TroubleshootIcon from '@mui/icons-material/Troubleshoot'; import TroubleshootIcon from '@mui/icons-material/Troubleshoot';
import moment from "jalali-moment"; import moment from "jalali-moment";
import {useState} from "react";
const periodOfYear = (year) => { const periodOfYear = (year) => {
const isLeapYear = moment.jIsLeapYear(year); const isLeapYear = moment.jIsLeapYear(year);
@@ -19,10 +20,11 @@ const periodOfYear = (year) => {
const Filter = ({title, filterItem, expanded}) => { const Filter = ({title, filterItem, expanded}) => {
const t = useTranslations(); const t = useTranslations();
const filter_by_province = filterItem.find((item) => item.type === "province"); const filter_by_province = filterItem.find((item) => item.type === "province");
const filter_by_year = filterItem.find((item) => item.type === "year"); const filter_by_year = filterItem.find((item) => item.type === "year");
const [filteredText, setFilteredText] = useState({province: "", year: ""});
const formik = useFormik({ const formik = useFormik({
initialValues: { initialValues: {
...(filter_by_year && { ...(filter_by_year && {
@@ -32,11 +34,6 @@ const Filter = ({title, filterItem, expanded}) => {
province_id: filter_by_province.multiple ? [] : "" province_id: filter_by_province.multiple ? [] : ""
}) })
}, onSubmit: (values, {setSubmitting}) => { }, onSubmit: (values, {setSubmitting}) => {
const dates = values.date
dates.map((date) => {
periodOfYear(date);
})
console.log("val", values);
}, },
}); });
return ( return (
@@ -51,7 +48,7 @@ const Filter = ({title, filterItem, expanded}) => {
<Box sx={{display: "flex", alignItems: "center", gap: 0.5}}> <Box sx={{display: "flex", alignItems: "center", gap: 0.5}}>
<Typography sx={{fontWeight: 500, color: "primary.main"}}>{title}</Typography> <Typography sx={{fontWeight: 500, color: "primary.main"}}>{title}</Typography>
<Typography variant="caption" sx={{fontWeight: 500}} color="error"> <Typography variant="caption" sx={{fontWeight: 500}} color="error">
| {t("reports.filter_as")} : {filteredText.province} {filteredText.year}
</Typography> </Typography>
</Box> </Box>
<FilterAltIcon sx={{color: "primary.main"}}/> <FilterAltIcon sx={{color: "primary.main"}}/>
@@ -60,10 +57,12 @@ const Filter = ({title, filterItem, expanded}) => {
<AccordionDetails sx={{display: "flex", justifyContent: "space-between", alignItems: "center"}}> <AccordionDetails sx={{display: "flex", justifyContent: "space-between", alignItems: "center"}}>
<Stack sx={{flexDirection: {xs: "column", sm: "row"}, alignItems: "center", flexWrap: 'wrap'}}> <Stack sx={{flexDirection: {xs: "column", sm: "row"}, alignItems: "center", flexWrap: 'wrap'}}>
{filter_by_province ? {filter_by_province ?
<ProvinceFilter formik={formik} multiple={filter_by_province.multiple}/> : "" <ProvinceFilter formik={formik} multiple={filter_by_province.multiple}
setFilteredText={setFilteredText}/> : ""
} }
{filter_by_year ? {filter_by_year ?
<YearFilter formik={formik} multiple={filter_by_year.multiple}/> : "" <YearFilter formik={formik} multiple={filter_by_year.multiple}
setFilteredText={setFilteredText}/> : ""
} }
</Stack> </Stack>
<Stack> <Stack>

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,31 @@
import {Grid, Paper} from "@mui/material";
import {useTranslations} from "next-intl";
import Filter from "@/components/dashboard/reports/Filter";
import RadialBarChart from "@/components/dashboard/reports/LoanDetails/RadialBarChart";
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}
]}
/>
<Grid container spacing={2} sx={{justifyContent: "space-between"}}>
<Grid item xs={4} sx={{height: "300px"}}>
<RadialBarChart data={90}/>
</Grid>
<Grid item xs={4} sx={{height: "300px"}}>
<RadialBarChart data={50}/>
</Grid>
<Grid item xs={4} sx={{height: "300px"}}>
<RadialBarChart data={10}/>
</Grid>
</Grid>
</Paper>
)
};
export default LoanDetails;

View File

@@ -7,19 +7,15 @@ const Pie = ({data}) => {
}, },
plotOptions: { plotOptions: {
pie: { pie: {
rings: {
strokeWidth: 3,
strokeColor: '#fff',
},
fill: { fill: {
colors: ['red', 'blue', 'green', 'yellow', 'pink'] colors: ['#F99417', '#363062', '#005B41', '#C70039', '#EE9322']
} }
}, },
}, },
fill: { fill: {
opacity: 0.9, opacity: 0.9,
}, },
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'], labels: [' آیتم 1', ' آیتم 2', ' آیتم 3', ' آیتم 4', ' آیتم 5'],
dataLabels: { dataLabels: {
enabled: true, enabled: true,
style: { style: {

View File

@@ -12,14 +12,14 @@ const PolarChart = ({data}) => {
strokeColor: '#e3e3e3', strokeColor: '#e3e3e3',
}, },
fill: { fill: {
colors: ['red', 'blue', 'green', 'yellow', 'pink'] colors: ['#F99417', '#363062', '#005B41', '#C70039', '#EE9322']
} }
}, },
}, },
fill: { fill: {
opacity: 0.9, opacity: 0.9,
}, },
labels: ['Team A', 'Team B', 'Team C', 'Team D', 'Team E'], labels: [' آیتم 1', ' آیتم 2', ' آیتم 3', ' آیتم 4', ' آیتم 5'],
dataLabels: { dataLabels: {
enabled: true, enabled: true,
style: { style: {

View File

@@ -45,7 +45,7 @@ const LoanDistribution = () => {
<Filter title={t("reports.loan_distribution")} <Filter title={t("reports.loan_distribution")}
filterItem={[ filterItem={[
{type: "province", type_fa: "استان", multiple: false}, {type: "province", type_fa: "استان", multiple: false},
{type: "year", type_fa: "سال", multiple: true} {type: "year", type_fa: "سال", multiple: false}
]} ]}
/> />
<Grid container spacing={2} sx={{justifyContent: "space-between"}}> <Grid container spacing={2} sx={{justifyContent: "space-between"}}>

View File

@@ -1,36 +1,5 @@
import Chart from "@/core/components/Chart"; import Chart from "@/core/components/Chart";
import {calculateGradientColor} from "@/core/utils/gradientColorHandler";
function calculateGradientColor(percentage) {
let colorStops = [
{percent: 0, color: [199, 18, 5]}, // 0%: #3D0C11
{percent: 33, color: [240, 59, 47]}, // 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]})`;
}
const BarChart = ({data}) => { const BarChart = ({data}) => {
const usableArray = data.map(({name, data, ...rest}) => ({ const usableArray = data.map(({name, data, ...rest}) => ({
@@ -83,7 +52,7 @@ const BarChart = ({data}) => {
}, },
}; };
const series = [{ const series = [{
name: 'اطلاعی ندارم', name: 'درصد پیشرفت',
data: usableArray data: usableArray
}] }]
return ( return (

View File

@@ -43,8 +43,8 @@ const LoanProgress = () => {
<Paper elevation={2} sx={{width: "95%", mx: "auto", my: 2, pb: 2, background: "#f7f7f7e6"}}> <Paper elevation={2} sx={{width: "95%", mx: "auto", my: 2, pb: 2, background: "#f7f7f7e6"}}>
<Filter title={t("reports.loan_progress")} <Filter title={t("reports.loan_progress")}
filterItem={[ filterItem={[
{type: "province", type_fa: "استان", multiple: false}, {type: "province", type_fa: "استان", multiple: true},
{type: "year", type_fa: "سال", multiple: true} {type: "year", type_fa: "سال", multiple: false}
]} ]}
/> />
<Grid container spacing={2}> <Grid container spacing={2}>

View File

@@ -1,11 +1,13 @@
import LoanProgress from "@/components/dashboard/reports/LoanProgress"; import LoanProgress from "@/components/dashboard/reports/LoanProgress";
import LoanDistribution from "@/components/dashboard/reports/LoanDistribution"; import LoanDistribution from "@/components/dashboard/reports/LoanDistribution";
import LoanDetails from "@/components/dashboard/reports/LoanDetails";
const DashboardReportsComponent = (props) => { const DashboardReportsComponent = (props) => {
return ( return (
<> <>
<LoanProgress/> <LoanProgress/>
<LoanDistribution/> <LoanDistribution/>
<LoanDetails/>
</> </>
) )
}; };

View File

@@ -0,0 +1,31 @@
export const calculateGradientColor = (percentage) => {
let colorStops = [
{percent: 0, color: [199, 18, 5]}, // 0%: #3D0C11
{percent: 33, color: [240, 59, 47]}, // 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]})`;
}