Merge branch 'release/v1.29.0'

This commit is contained in:
AmirHossein Mahmoodi
2024-01-15 16:24:20 +03:30
32 changed files with 741 additions and 150 deletions

View File

@@ -1,5 +1,5 @@
NEXT_PUBLIC_API_NAME = "Loan Facilities Dashboard"
NEXT_PUBLIC_API_VERSION = "1.28.6"
NEXT_PUBLIC_API_VERSION = "1.29.0"
NEXT_PUBLIC_DEFAULT_LANGUAGE = "fa"
NEXT_PUBLIC_DEFAULT_DIRECTION = "rtl"

View File

@@ -31,7 +31,7 @@
"dayjs": "^1.11.9",
"eslint": "8.36.0",
"file-saver": "^2.0.5",
"formik": "^2.2.9",
"formik": "^2.4.5",
"fs-extra": "^11.1.1",
"image-resize": "^1.3.2",
"image-to-file-converter": "^1.0.6",

View File

@@ -603,6 +603,15 @@
"loading_permissions_list": "درحال دریافت لیست دسترسی ها"
},
"reports": {
"dynamic_label_functionality": "{done} از {total} انجام شده",
"dynamic_total_budget": "ابلاغ تسهیلات: {total_budget} میلیون ریال",
"dynamic_label_referred": "{count} فقره در مجموع {total_amount} میلیون ریال",
"referred_loans_to_bank": "ارسال مدیرکل به بانک",
"paid_loans": "پرداخت شده بانک",
"expert": "عملکرد کارشناس",
"transportation_assistant": "عملکرد معاون",
"province_manager": "عملکرد مدیر",
"bank": "عملکرد بانک",
"loan_progress": "درصد پیشرفت تسویه وام",
"loan_distribution": "درصد توزیع وام ها در هر مرحله",
"filter_as": "فیلتر های اعمال شده",
@@ -663,6 +672,7 @@
"Table_head_position": "سمت",
"Table_head_action": "عملیات",
"Table_head_file": "فایل پیوست",
"Table_head_expert_description": "توضیحات",
"empty_history_detail": "سابقه ای برای این وام وجود ندارد"
}
}

View File

@@ -19,21 +19,17 @@ const PrintHistory = ({attachment}) => {
}
return (
<Button
color="primary"
variant="contained"
size="small"
disabled={loading}
sx={{textTransform: "unset", alignSelf: "center"}}
startIcon={loading ?
<CircularProgress size={18} color="inherit"/> :
<DescriptionIcon/>}
onClick={() => clickHandler(attachment)}
>
{t("LoanHistory.history_report")}
</Button>
)
return (<Button
color="primary"
variant="contained"
size="small"
disabled={loading}
sx={{textTransform: "unset", alignSelf: "center", mx: .25}}
startIcon={loading ? <CircularProgress size={18} color="inherit"/> : <DescriptionIcon/>}
onClick={() => clickHandler(attachment.file)}
>
{attachment.title}
</Button>)
}
export default PrintHistory

View File

@@ -33,77 +33,77 @@ const TableContent = ({rowId}) => {
})
}, []);
return (
<Box>
return (<Box>
{error ? (<Typography sx={{
padding: 2,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}>{t("LoanHistory.Table_history_error")}</Typography>) :
(isLoading ? (<LinearProgress sx={{width: "100%"}}/>) : (
historyDetails.latest_histories.length === 0 ? (
<Typography
sx={{
padding: 2,
display: "flex",
alignItems: "center",
justifyContent: "center"
}}
variant={'h6'}>{t("LoanHistory.empty_history_detail")}</Typography>
) : (
<Paper elevation={0} sx={{
width: '100%', overflow: 'hidden',
'*::-webkit-scrollbar': {
width: '0.5em'
},
'*::-webkit-scrollbar-thumb': {
backgroundColor: 'primary.main',
}
}}>
<TableContainer sx={{maxHeight: 600}}>
<Table stickyHeader sx={{minWidth: 650}}>
<TableHead>
<TableRow>
<TableCell>{t("LoanHistory.Table_head_name")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_date")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_position")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_action")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_file")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{
historyDetails.latest_histories.map((latest_history, index) => {
return (
<TableRow
key={index}
sx={{
'&:last-child td, &:last-child th': {border: 0}
}}
>
<TableCell component="th" scope="row">
{latest_history.expert.name}
</TableCell>
<TableCell>{moment(latest_history.created_at).locale("fa").format("HH:mm | yyyy/MM/DD")}</TableCell>
<TableCell>{latest_history.expert.position}</TableCell>
<TableCell>{latest_history.previous_state_name}</TableCell>
<TableCell>
{latest_history.attachment ? (
<PrintHistory attachment={latest_history.attachment}/>
) : null}
</TableCell>
</TableRow>
)
})
}
</TableBody>
</Table>
</TableContainer>
</Paper>
)
))
}
padding: 2, display: "flex", alignItems: "center", justifyContent: "center"
}}>{t("LoanHistory.Table_history_error")}</Typography>) : (isLoading ? (
<LinearProgress sx={{width: "100%"}}/>) : (historyDetails.latest_histories.length === 0 ? (<Typography
sx={{
padding: 2, display: "flex", alignItems: "center", justifyContent: "center"
}}
variant={'h6'}>{t("LoanHistory.empty_history_detail")}</Typography>) : (<Paper elevation={0} sx={{
width: '100%', overflow: 'hidden', '*::-webkit-scrollbar': {
width: '0.5em'
}, '*::-webkit-scrollbar-thumb': {
backgroundColor: 'primary.main',
}
}}>
<TableContainer sx={{maxHeight: 600}}>
<Table stickyHeader sx={{minWidth: 800}}>
<TableHead>
<TableRow>
<TableCell>{t("LoanHistory.Table_head_name")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_date")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_position")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_action")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_expert_description")}</TableCell>
<TableCell>{t("LoanHistory.Table_head_file")}</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow
sx={{
'&:last-child td, &:last-child th': {border: 0}
}}
>
<TableCell>
{historyDetails.latest_histories[0].expert.name}
</TableCell>
<TableCell>{moment(historyDetails.latest_histories[0].created_at).locale("fa").format("HH:mm | yyyy/MM/DD")}</TableCell>
<TableCell>{historyDetails.latest_histories[0].expert.position}</TableCell>
<TableCell>{historyDetails.state_name}</TableCell>
<TableCell>{historyDetails.latest_histories[0].expert_description}</TableCell>
<TableCell>
{historyDetails.latest_histories[0].attachments.map(attachment => <PrintHistory
attachment={attachment}/>)}
</TableCell>
</TableRow>
{historyDetails.latest_histories.map((latest_history, index) => {
const _name = historyDetails.latest_histories.length > (index + 1) ? historyDetails.latest_histories[index + 1].expert.name : '-'
const _created_at = historyDetails.latest_histories.length > (index + 1) ? moment(historyDetails.latest_histories[index + 1].created_at).locale("fa").format("HH:mm | yyyy/MM/DD") : '-'
const _position = historyDetails.latest_histories.length > (index + 1) ? historyDetails.latest_histories[index + 1].expert.position : '-'
const _expert_description = historyDetails.latest_histories.length > (index + 1) ? historyDetails.latest_histories[index + 1].expert_description : '-'
const _attachments = historyDetails.latest_histories.length > (index + 1) ? historyDetails.latest_histories[index + 1].attachments : []
return (<TableRow
key={index}
sx={{
'&:last-child td, &:last-child th': {border: 0}
}}
>
<TableCell> {_name}</TableCell>
<TableCell>{_created_at}</TableCell>
<TableCell>{_position}</TableCell>
<TableCell>{latest_history.previous_state_name}</TableCell>
<TableCell>{_expert_description}</TableCell>
<TableCell>
{_attachments.map(attachment => <PrintHistory attachment={attachment}/>)}
</TableCell>
</TableRow>)
})}
</TableBody>
</Table>
</TableContainer>
</Paper>)))}
</Box>
)

View File

@@ -19,7 +19,7 @@ const History = ({rowId, mutate}) => {
<HistoryIcon/>
</IconButton>
</Tooltip>
<Dialog scroll={'paper'} fullWidth maxWidth='lg' open={openConfirmDialog}
<Dialog scroll={'paper'} fullWidth maxWidth="xl" open={openConfirmDialog}
PaperProps={{sx: {boxShadow: 'rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px'}}}>
<DialogTitle>{t("LoanHistory.history")}</DialogTitle>
<HistoryContent mutate={mutate} rowId={rowId} setOpenConfirmDialog={setOpenConfirmDialog}/>

View File

@@ -12,8 +12,8 @@ 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 ProvinceFilter from "@/components/dashboard/old-reports/Filter/ProvinceFilter";
import YearFilter from "@/components/dashboard/old-reports/Filter/YearFilter";
import {useState} from "react";
import {useFormik} from "formik";
import {GET_EXPORT} from "@/core/data/apiRoutes";

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,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,37 @@
import {Grid, Paper} from "@mui/material";
import {useTranslations} from "next-intl";
import RadialBarSemiCircularChart from "@/components/dashboard/old-reports/LoanDetails/RadialBarSemiCircularChart";
import RadialBarStrokedChart from "@/components/dashboard/old-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

@@ -2,7 +2,7 @@ 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 Filter from "@/components/dashboard/old-reports/Filter";
import useChart from "@/lib/app/hooks/useChart";
import {GET_LOAN_DISTRIBUTION} from "@/core/data/apiRoutes";
import LoadingHardPage from "@/core/components/LoadingHardPage";

View File

@@ -0,0 +1,16 @@
import LoanProgress from "@/components/dashboard/old-reports/LoanProgress";
import LoanDistribution from "@/components/dashboard/old-reports/LoanDistribution";
import ExcelExport from "@/components/dashboard/old-reports/ExcelExport";
const DashboardReportsComponent = (props) => {
return (
<>
<ExcelExport/>
<LoanProgress/>
<LoanDistribution/>
{/*<LoanDetails/>*/}
</>
)
};
export default DashboardReportsComponent;

View File

@@ -20,13 +20,11 @@ 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({
@@ -36,11 +34,10 @@ const Filter = ({title, filterItem, setFilterOption, isLoadingReportList, mutate
...(filter_by_province && {
province_id: filter_by_province.multiple ? [] : ""
}),
from_date: periodOfYear(moment().format('jYYYY')).from,
to_date: periodOfYear(moment().format('jYYYY')).to
from_date: "",
to_date: ""
})
const formik = useFormik({
enableReinitialize: true,
initialValues: stepValue, onSubmit: (values, {setSubmitting}) => {
@@ -76,7 +73,6 @@ const Filter = ({title, filterItem, setFilterOption, isLoadingReportList, mutate
: []
),
].filter(Boolean);
setFilterOption(fields)
}, []);
return (

View File

@@ -0,0 +1,41 @@
import {Grid, Typography} from "@mui/material";
import {useTranslations} from "next-intl";
import RadialBarSemiCircularChart from "@/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart";
const FunctionalityCharts = ({reportList}) => {
const t = useTranslations();
const DataItemsName = [
{id: "expert", name: t("reports.expert")},
{id: "transportation_assistant", name: t("reports.transportation_assistant")},
{id: "province_manager", name: t("reports.province_manager")},
{id: "bank", name: t("reports.bank")}
]
return (
<Grid container sx={{width: "100%"}}>
{
Object.entries(reportList).map(([entryKey, innerObject]) => {
let percentage = innerObject.total !== 0 ? (innerObject.total - innerObject.pending) / innerObject.total * 100 : 0;
const ItemName = DataItemsName.find(item => item.id === entryKey).name;
return (
<Grid key={entryKey} item xs={12} sm={10} md={6} xl={3}
sx={{textAlign: "center"}}>
<Typography sx={{
fontSize: "1.3rem",
fontWeight: "600",
color: "primary.main"
}}>{ItemName}</Typography>
<RadialBarSemiCircularChart data={Math.round(percentage)}
formatter={(val) => `${val}%`}
label={t("reports.dynamic_label_functionality", {
total: (innerObject.total).toLocaleString("en"),
done: (innerObject.total - innerObject.pending).toLocaleString("en")
})}/>
</Grid>
)
})
}
</Grid>
)
};
export default FunctionalityCharts;

View File

@@ -1,33 +1,31 @@
import Chart from "@/core/components/Chart";
import {calculateGradientColor} from "@/core/utils/gradientColorHandler";
const RadialBarSemiCircularChart = ({data, label}) => {
const RadialBarSemiCircularChart = ({data, label, formatter}) => {
const specialOption = {
title: {
text: undefined,
},
plotOptions: {
radialBar: {
startAngle: -110,
endAngle: 110,
startAngle: -90,
endAngle: 90,
track: {
background: "#e7e7e7",
strokeWidth: '80%',
},
dataLabels: {
name: {
fontSize: '20px',
offsetY: 110,
fontSize: '1rem',
offsetY: 30,
color: "#8c8c8c"
},
value: {
offsetY: 50,
fontSize: '50px',
offsetY: -30,
fontSize: '30px',
fontWeight: 500,
color: "#8c8c8c",
formatter: function (val) {
return val + "%";
}
formatter: formatter
}
}
}

View File

@@ -0,0 +1,60 @@
import {Box, Grid, Typography} from "@mui/material";
import RadialBarSemiCircularChart from "@/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart";
import {useTranslations} from "next-intl"; // Make sure to import React if it's not already imported
const ReferredCharts = ({reportList}) => {
const t = useTranslations();
const DataItemsName = [
{id: "referred_loans_to_bank", name: t("reports.referred_loans_to_bank")},
{id: "paid_loans", name: t("reports.paid_loans")}
];
return (
<Grid container sx={{width: "100%", mt: 3}}>
{Object.entries(reportList).map(([entryKey, innerObject]) => {
const ItemName = DataItemsName.find(item => item.id === entryKey).name;
return (
<Grid key={entryKey} item xs={12} xl={6}
sx={{display: "flex", flexDirection: "column", alignItems: "center"}}>
<Box sx={{mb: 2}}>
<Typography sx={{
fontSize: "1.3rem",
fontWeight: "600",
color: "primary.main"
}}>{ItemName}</Typography>
</Box>
<Grid container sx={{width: "100%"}}>
{innerObject.map((item, index) => {
let percentage = item.total_amount !== 0 ? item.total_amount / innerObject.total_budget * 100 : 0;
return (
<Grid key={index} item xs={12} md={6} sx={{textAlign: "center"}}>
<Box>
<Typography sx={{
fontSize: "1.1rem",
fontWeight: "600",
color: "#8c8c8c"
}}>{item.vehicle_type}</Typography>
<Typography variant="caption">
{t("reports.dynamic_total_budget", {
total_budget: (item.total_budget / 1000000).toLocaleString("en"),
})}
</Typography>
</Box>
<RadialBarSemiCircularChart data={Math.round(percentage)}
formatter={(val) => `${val}%`}
label={t("reports.dynamic_label_referred", {
count: (item.count).toLocaleString("en"),
total_amount: (item.total_amount / 1000000).toLocaleString("en")
})}/>
</Grid>
);
})}
</Grid>
</Grid>
);
})}
</Grid>
);
};
export default ReferredCharts;

View File

@@ -1,37 +1,78 @@
import {Grid, Paper} from "@mui/material";
import {Divider, Grid, Paper, Stack, Typography} from "@mui/material";
import {useTranslations} from "next-intl";
import RadialBarSemiCircularChart from "@/components/dashboard/reports/LoanDetails/RadialBarSemiCircularChart";
import RadialBarStrokedChart from "@/components/dashboard/reports/LoanDetails/RadialBarStrokedChart";
import Filter from "@/components/dashboard/reports/Filter";
import useChart from "@/lib/app/hooks/useChart";
import {GET_LOAN_EXPERTPROGRESS, GET_LOAN_REFERREDLOANS} from "@/core/data/apiRoutes";
import LoadingHardPage from "@/core/components/LoadingHardPage";
import AnalyticsIcon from "@mui/icons-material/Analytics";
import FunctionalityCharts from "@/components/dashboard/reports/LoanDetails/FunctionalityCharts";
import ReferredCharts from "@/components/dashboard/reports/LoanDetails/ReferredCharts";
const LoanDetails = () => {
const t = useTranslations();
const {
errorReportList: errorReportList1,
isLoadingReportList: isLoadingReportList1,
reportList: reportList1,
setFilterOption: setFilterOption1,
mutate: mutate1
} = useChart(GET_LOAN_EXPERTPROGRESS);
// Use the useChart hook for the second API route
const {
errorReportList: errorReportList2,
isLoadingReportList: isLoadingReportList2,
reportList: reportList2,
setFilterOption: setFilterOption2,
mutate: mutate2
} = useChart(GET_LOAN_REFERREDLOANS);
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>
<Filter title={t("reports.loan_details")}
filterItem={[{type: "province", type_fa: "استان", multiple: false},]}
setFilterOption={(fields) => {
setFilterOption1(fields);
setFilterOption2(fields)
}}
isLoadingReportList={isLoadingReportList1 || isLoadingReportList2}
mutate={() => {
mutate1();
mutate2()
}}
/>
<Grid container sx={{justifyContent: {xs: "center", position: "relative"}}}>
{isLoadingReportList1 || isLoadingReportList2 ? (
<Grid item xs={12} sx={{aspectRatio: "3/0.95"}}>
<LoadingHardPage
icon={<AnalyticsIcon sx={{width: "inherit", height: "inherit"}}/>}
loading={isLoadingReportList1 || isLoadingReportList2}
label={t("reports.loading_fetching_reports")}
width={100}
height={100}
sx={{position: "absolute", bgcolor: "#f7f7f7e6"}}
/>
</Grid>
) : errorReportList1 || errorReportList2 ? (
<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>
) : (
<Stack sx={{width: "100%"}}>
<FunctionalityCharts reportList={reportList1}/>
<Divider/>
<ReferredCharts reportList={reportList2}/>
</Stack>
)
}
</Grid>
</Paper>
)
};
export default LoanDetails;
export default LoanDetails;

View File

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

View File

@@ -64,18 +64,21 @@ export default function SidebarList({handleDrawerToggle}) {
}, [selectedKey]);
return (<List dense={true} sx={{overflow: "scroll", p: 0}}>
{itemMenu.map((itemArr, index) => (<Fragment key={index}>
{itemMenu.map((itemArr, index) => {
let isDividerVisible = false;
return (<Fragment key={index}>
{itemArr.map((item) => <Fragment key={item.key}>
{(item.permissions.some((permission) => user?.permissions?.includes(permission)) || item.permissions.includes('all')) &&
{(item.permissions.some((permission) => user?.permissions?.includes(permission)) || item.permissions.includes('all')) && <>
<SidebarListItem
item={item}
dispatch={dispatch}
handleDrawerToggle={handleDrawerToggle}
/>}
/>
{isDividerVisible = true}
</>}
</Fragment>)}
<Divider/>
</Fragment>))}
</List>
);
{isDividerVisible && <Divider/>}
</Fragment>)
})}
</List>);
}

View File

@@ -13,6 +13,7 @@ const Chart = ({chartId, type, series, specialOption}) => {
const options = {
chart: {
id: chartId,
offsetY: -30,
locales: [chartLang],
defaultLocale: languageApp,
fontFamily: languageList[0].fontFamily,
@@ -26,7 +27,7 @@ const Chart = ({chartId, type, series, specialOption}) => {
},
...(specialOption ? specialOption : {})
}
return <ApexChart type={type} options={options} series={series} width="100%" height="100%"/>
return <ApexChart type={type} options={options} series={series}/>
};
export default Chart;

View File

@@ -225,6 +225,8 @@ export const GET_PERMISSIONS_LIST =
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"
export const GET_LOAN_EXPERTPROGRESS = BASE_URL + "/dashboard/reports/navgan/expert_progress"
export const GET_LOAN_REFERREDLOANS = BASE_URL + "/dashboard/reports/navgan/referred_loans"
// reports
//loan history

View File

@@ -20,13 +20,14 @@ const useChart = (chart_url) => {
data,
isValidating,
mutate
} = useSWR(filterOption.length !== 0 ? `${chart_url}${queryString}` : "", fetcher, {
} = useSWR(filterOption.length !== 0 ? `${chart_url}${queryString}` : chart_url, fetcher, {
revalidateIfStale: true,
revalidateOnFocus: false,
revalidateOnReconnect: false,
keepPreviousData: true
});
return {
reportList: data,
isLoadingReportList: isValidating,

View File

@@ -1,7 +1,14 @@
import DashboardFirstComponent from "@/components/dashboard/first";
import {parse} from "next-useragent";
import useUser from "@/lib/app/hooks/useUser";
export default function Dashboard() {
const {user} = useUser();
const hasPermission = user?.permissions?.includes("view_dashboard_stats")
if (!hasPermission) return null
return (
<DashboardFirstComponent/>
);

View File

@@ -0,0 +1,20 @@
import DashboardReportsComponent from "@/components/dashboard/old-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', props: {permissions: ["view_reports_navgan"]}}
},
};
}