diff --git a/example.env.local b/example.env.local index f6cb8f0..e8913b3 100644 --- a/example.env.local +++ b/example.env.local @@ -1,5 +1,5 @@ NEXT_PUBLIC_API_NAME = "Loan Facilities Dashboard" -NEXT_PUBLIC_API_VERSION = "1.18.2" +NEXT_PUBLIC_API_VERSION = "1.22.3" NEXT_PUBLIC_DEFAULT_LANGUAGE = "fa" NEXT_PUBLIC_DEFAULT_DIRECTION = "rtl" diff --git a/package.json b/package.json index 6459d69..c03278c 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@emotion/styled": "^11.10.6", "@mui/icons-material": "^5.11.16", "@mui/material": "^5.12.0", + "@mui/x-data-grid": "^6.18.1", "@mui/x-date-pickers": "^6.9.2", "apexcharts": "^3.44.0", "axios": "^1.4.0", @@ -29,6 +30,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..5088829 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -7,7 +7,7 @@ "login": "ورود", "login_expert": "ورود کارشناس", "expert": "کارشناس", - "last_updated_at": "اخرین بروز رسانی", + "last_updated_at": "آخرین بروزرسانی", "filter_mode": "حالت فیلتر", "date_picker_label": "فیلتر بر اساس تاریخ", "lessThan": "کوچکتر از", @@ -40,6 +40,7 @@ }, "sidebar": { "dashboard": "داشبورد", + "reports": "گزارشات", "passenger-office-chief": "اداره مسافر", "machinery-expert": "ماشین آلات", "passenger-boss": "رئیس مسافر ", @@ -56,7 +57,8 @@ "expert-management": "مدیریت کارشناسان", "user-management": "مدیریت کاربر", "role-management": "مدیریت نقش ها", - "edit-profile": "ویرایش پروفایل" + "edit-profile": "ویرایش پروفایل", + "loan-history": "کارتابل نظارت" }, "secondary": { "passenger-office": "توزیع درخواست", @@ -99,6 +101,7 @@ }, "Dashboard": { "dashboard_page": "داشبورد", + "reports": "گزارشات", "passenger_boss_page": "رئیس مسافر", "transportation_assistance": "معاونت حمل و نقل", "change_password": "تغییر رمز عبور", @@ -113,7 +116,8 @@ "role_management_page": "مدیریت نقش ها", "edit_profile": "ویرایش پروفایل", "expert_management": "مدیریت کارشناسان", - "user_management_page": "مدیریت کاربر" + "user_management_page": "مدیریت کاربر", + "loan_history": "کارتابل نظارت" }, "MuiDatePicker": { "date_picker_birthday": "تاریخ" @@ -168,7 +172,8 @@ "state_name": "وضعیت درخواست", "confirm": "ارجاع به کارشناس ماشین آلات", "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", - "upload_file_format": "فرمت قابل قبول : png,jpg,pdf" + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", + "excel_report": "گزارش اکسل" }, "CommercialChief": { "name": "نام", @@ -193,7 +198,8 @@ "state_name": "وضعیت درخواست", "confirm": "تایید نهایی و ارجاع به بانک", "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", - "upload_file_format": "فرمت قابل قبول : png,jpg,pdf" + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", + "excel_report": "گزارش اکسل" }, "PassengerBoss": { "name": "نام", @@ -211,7 +217,8 @@ "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", "print_final_credit_amount": "چاپ صورت جلسه تعیین مبلغ نهایی", - "max_amount": "حداکثر مبلغ تصویب شده {value} میلیون ریال می باشد" + "max_amount": "حداکثر مبلغ تصویب شده {value} میلیون ریال می باشد", + "excel_report": "گزارش اکسل" }, "RefahiProvinceManager": { "name": "نام", @@ -282,7 +289,8 @@ "state_name": "وضعیت درخواست", "confirm": "ارجاع به مدیر کل استانی", "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", - "upload_file_format": "فرمت قابل قبول : png,jpg,pdf" + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", + "excel_report": "گزارش اکسل" }, "InspectorExpert": { "name": "نام", @@ -311,7 +319,8 @@ "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", "print_report": "چاپ گزارش کارشناسی ماشین آلات", - "max_amount": "حداکثر مبلغ پیشنهادی {value} میلیون ریال می باشد" + "max_amount": "حداکثر مبلغ پیشنهادی {value} میلیون ریال می باشد", + "excel_report": "گزارش اکسل" }, "UserManagement": { "name": "نام", @@ -499,5 +508,43 @@ "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": "بروزرسانی مجدد" + }, + "LoanHistory": { + "history": "تاریخچه", + "Table_history_error": "خطا در دریافت اطلاعات", + "history_report": "فایل ضمیمه", + "Table_head_name": "نام", + "Table_head_date": "تاریخ", + "Table_head_position": "سمت", + "Table_head_action": "عملیات", + "Table_head_file": "فایل پیوست", + "empty_history_detail": "سابقه ای برای این وام وجود ندارد" } } diff --git a/src/components/dashboard/loan-history/Form/HistoryForm/HistoryContent.jsx b/src/components/dashboard/loan-history/Form/HistoryForm/HistoryContent.jsx new file mode 100644 index 0000000..d4d214c --- /dev/null +++ b/src/components/dashboard/loan-history/Form/HistoryForm/HistoryContent.jsx @@ -0,0 +1,26 @@ +import {Button, DialogActions, DialogContent,} from "@mui/material" +import {useTranslations} from "next-intl"; +import {useFormik} from "formik"; +import TableContent from "@/components/dashboard/loan-history/Form/HistoryForm/TableContent"; + +const HistoryContent = ({mutate, setOpenConfirmDialog, rowId}) => { + const t = useTranslations(); + + const formik = useFormik({}); + + return ( + <> + + + + + + + + ) + +} +export default HistoryContent \ No newline at end of file diff --git a/src/components/dashboard/loan-history/Form/HistoryForm/PrintHistory.jsx b/src/components/dashboard/loan-history/Form/HistoryForm/PrintHistory.jsx new file mode 100644 index 0000000..79af47c --- /dev/null +++ b/src/components/dashboard/loan-history/Form/HistoryForm/PrintHistory.jsx @@ -0,0 +1,39 @@ +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import DescriptionIcon from '@mui/icons-material/Description'; + +const PrintHistory = ({attachment}) => { + const t = useTranslations(); + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + + const clickHandler = (attachment) => { + const anchor = document.createElement('a'); + anchor.href = attachment; + anchor.setAttribute('download', ''); + document.body.appendChild(anchor); + anchor.click(); + document.body.removeChild(anchor); + } + + + return ( + + ) +} + +export default PrintHistory \ No newline at end of file diff --git a/src/components/dashboard/loan-history/Form/HistoryForm/TableContent.jsx b/src/components/dashboard/loan-history/Form/HistoryForm/TableContent.jsx new file mode 100644 index 0000000..34b1529 --- /dev/null +++ b/src/components/dashboard/loan-history/Form/HistoryForm/TableContent.jsx @@ -0,0 +1,111 @@ +import { + Box, + LinearProgress, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography +} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {GET_HISTORY_DETAIL} from "@/core/data/apiRoutes"; +import moment from "jalali-moment"; +import PrintHistory from "@/components/dashboard/loan-history/Form/HistoryForm/PrintHistory"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {useEffect, useState} from "react"; + +const TableContent = ({rowId}) => { + const t = useTranslations(); + const requestServer = useRequest({auth: true, notification: false}) + const [historyDetails, setHistoryDetails] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(false); + + useEffect(() => { + requestServer(`${GET_HISTORY_DETAIL}/${rowId}`, 'get').then((response) => { + setIsLoading(false); + setHistoryDetails(response.data.data); + }).catch(() => { + setError(true); + }) + }, []); + + return ( + + {error ? ({t("LoanHistory.Table_history_error")}) : + (isLoading ? () : ( + historyDetails.latest_histories.length === 0 ? ( + {t("LoanHistory.empty_history_detail")} + ) : ( + + + + + + {t("LoanHistory.Table_head_name")} + {t("LoanHistory.Table_head_date")} + {t("LoanHistory.Table_head_position")} + {t("LoanHistory.Table_head_action")} + {t("LoanHistory.Table_head_file")} + + + + { + historyDetails.latest_histories.map((latest_history, index) => { + return ( + + + {latest_history.expert.name} + + {moment(latest_history.created_at).locale("fa").format("HH:mm | yyyy/MM/DD")} + {latest_history.expert.position} + {latest_history.previous_state_name} + + {latest_history.attachment ? ( + + ) : null} + + + ) + }) + } + +
+
+
+ ) + )) + } +
+ + ) +} +export default TableContent \ No newline at end of file diff --git a/src/components/dashboard/loan-history/Form/HistoryForm/index.jsx b/src/components/dashboard/loan-history/Form/HistoryForm/index.jsx new file mode 100644 index 0000000..de20f60 --- /dev/null +++ b/src/components/dashboard/loan-history/Form/HistoryForm/index.jsx @@ -0,0 +1,31 @@ +import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useState} from "react"; +import HistoryIcon from '@mui/icons-material/History'; +import HistoryContent from "@/components/dashboard/loan-history/Form/HistoryForm/HistoryContent"; + +const History = ({rowId, mutate}) => { + const t = useTranslations(); + const [openConfirmDialog, setOpenConfirmDialog] = useState(false); + return ( + <> + + { + setOpenConfirmDialog(true) + }} + > + + + + + {t("LoanHistory.history")} + + + + ) + +} +export default History; \ No newline at end of file diff --git a/src/components/dashboard/loan-history/TableRowActions.jsx b/src/components/dashboard/loan-history/TableRowActions.jsx new file mode 100644 index 0000000..d0a967f --- /dev/null +++ b/src/components/dashboard/loan-history/TableRowActions.jsx @@ -0,0 +1,16 @@ +import {Box} from "@mui/material"; +import History from "@/components/dashboard/loan-history/Form/HistoryForm"; + +const TableRow = ({row, mutate}) => { + return ( + + + + ); +}; + +export default TableRow; diff --git a/src/components/dashboard/loan-history/index.jsx b/src/components/dashboard/loan-history/index.jsx new file mode 100644 index 0000000..c2bb293 --- /dev/null +++ b/src/components/dashboard/loan-history/index.jsx @@ -0,0 +1,183 @@ +import {Box, Typography} from "@mui/material"; +import {useMemo} from "react"; +import {GET_LOAN_HISTORY} from "@/core/data/apiRoutes"; +import {useTranslations} from "next-intl"; +import TableRowActions from "./TableRowActions"; +import moment from "jalali-moment"; +import DataTable from "@/core/components/DataTable"; +import MuiDatePicker from "@/core/components/MuiDatePicker"; + +function DashboardLoanHistoryComponent() { + const t = useTranslations(); + const columns = useMemo( + () => [ + { + accessorFn: (row) => row.id, + id: "id", + header: t("PassengerOffice.id"), + enableColumnFilter: true, + datatype: "numeric", + filterFn: "equals", + columnFilterModeOptions: [ + "equals", + "notEquals", + "contains", + "lessThan", + "greaterThan", + "between", + ], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => row.name, + id: "name", + header: t("MachinaryOffice.name"), + enableColumnFilter: true, + datatype: "text", + filterFn: "contains", + columnFilterModeOptions: ["contains", "equals", "notEquals"], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => row.national_id, + id: "national_id", + header: t("MachinaryOffice.national_id"), + enableColumnFilter: true, + datatype: "numeric", + filterFn: "equals", + columnFilterModeOptions: [ + "equals", + "notEquals", + "contains", + "lessThan", + "greaterThan", + "between", + ], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => row.phone_number, + id: "phone_number", + header: t("MachinaryOffice.phone_number"), + enableColumnFilter: true, + datatype: "numeric", + filterFn: "equals", + columnFilterModeOptions: [ + "equals", + "notEquals", + "contains", + "lessThan", + "greaterThan", + "between", + ], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => + moment(row.created_at).locale("fa").format("HH:mm | yyyy/MM/DD"), + id: "created_at", + header: t("MachinaryOffice.created_at"), + enableColumnFilter: true, + datatype: "date", + filterFn: "lessThan", + columnFilterModeOptions: ["lessThan", "greaterThan"], + Cell: ({renderedCellValue}) => { + return {renderedCellValue}; + }, + Header: ({column}) => <>{column.columnDef.header}, + Filter: ({column}) => { + return ( + + ); + }, + }, + { + accessorFn: (row) => + moment(row.updated_at).locale("fa").format("HH:mm | yyyy/MM/DD"), + id: "updated_at", + header: t("MachinaryOffice.updated_at"), + enableColumnFilter: false, + datatype: "numeric", + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => row.navgan_id, + id: "navgan_id", + header: t("MachinaryOffice.navgan_id"), + enableColumnFilter: true, + datatype: "numeric", + filterFn: "equals", + columnFilterModeOptions: [ + "equals", + "notEquals", + "contains", + "lessThan", + "greaterThan", + "between", + ], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => row.vehicle_type, + id: "vehicle_type", + header: t("MachinaryOffice.vehicle_type"), + enableColumnFilter: true, + datatype: "text", + filterFn: "contains", + columnFilterModeOptions: ["contains", "equals", "notEquals"], + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + { + accessorFn: (row) => row.state_name, + id: "state_id", + header: t("MachinaryOffice.state_name"), + enableColumnFilter: false, + datatype: "numeric", + Cell: ({renderedCellValue}) => ( + {renderedCellValue} + ), + }, + ], + [] + ); + return ( + + + + ); +} + +export default DashboardLoanHistoryComponent; diff --git a/src/components/dashboard/machinary-office/Buttons/printExcel.jsx b/src/components/dashboard/machinary-office/Buttons/printExcel.jsx new file mode 100644 index 0000000..cba05a0 --- /dev/null +++ b/src/components/dashboard/machinary-office/Buttons/printExcel.jsx @@ -0,0 +1,68 @@ +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useMemo, useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {EXPORT_MACHINARY_OFFICE} from "@/core/data/apiRoutes"; +import moment from "jalali-moment"; +import FileSaver from 'file-saver'; +import DescriptionIcon from '@mui/icons-material/Description'; + +const PrintExcel = ({table}) => { + const t = useTranslations(); + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {columnFilters, columnFilterFns, sorting} = table.getState() + const columns = table.getAllColumns() + + const filterParams = useMemo(() => { + const params = new URLSearchParams(); + const filters = columnFilters.map((filter) => { + let datatype; + for (const i in columns) { + if (columns[i].id == filter.id) { + datatype = columns[i].columnDef.datatype; + } + } + return { + ...filter, fn: columnFilterFns[filter.id], datatype: datatype, + }; + }); + params.set("start", '0'); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return params; + }, [columnFilters, columnFilterFns, sorting, columns,]); + + + const clickHandler = () => { + setLoading(true) + requestServer(`${EXPORT_MACHINARY_OFFICE}?${filterParams}`, '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(() => { + setLoading(false) + }) + } + + + return ( + + ) +} + +export default PrintExcel \ No newline at end of file diff --git a/src/components/dashboard/machinary-office/TableToolbar.jsx b/src/components/dashboard/machinary-office/TableToolbar.jsx index 9704f95..cdf1562 100644 --- a/src/components/dashboard/machinary-office/TableToolbar.jsx +++ b/src/components/dashboard/machinary-office/TableToolbar.jsx @@ -1,10 +1,12 @@ import {Stack} from "@mui/material"; import PrintReport from "@/components/dashboard/machinary-office/Buttons/printReport"; +import PrintExcel from "@/components/dashboard/machinary-office/Buttons/printExcel"; -const TableToolbar = () => { +const TableToolbar = ({table}) => { return ( + ) } diff --git a/src/components/dashboard/navgan-province-manager/Buttons/printExcel.jsx b/src/components/dashboard/navgan-province-manager/Buttons/printExcel.jsx new file mode 100644 index 0000000..6c0aac3 --- /dev/null +++ b/src/components/dashboard/navgan-province-manager/Buttons/printExcel.jsx @@ -0,0 +1,68 @@ +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useMemo, useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {EXPORT_NAVGAN_PROVINCE_MANAGER} from "@/core/data/apiRoutes"; +import moment from "jalali-moment"; +import FileSaver from 'file-saver'; +import DescriptionIcon from '@mui/icons-material/Description'; + +const PrintExcel = ({table}) => { + const t = useTranslations(); + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {columnFilters, columnFilterFns, sorting} = table.getState() + const columns = table.getAllColumns() + + const filterParams = useMemo(() => { + const params = new URLSearchParams(); + const filters = columnFilters.map((filter) => { + let datatype; + for (const i in columns) { + if (columns[i].id == filter.id) { + datatype = columns[i].columnDef.datatype; + } + } + return { + ...filter, fn: columnFilterFns[filter.id], datatype: datatype, + }; + }); + params.set("start", '0'); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return params; + }, [columnFilters, columnFilterFns, sorting, columns,]); + + + const clickHandler = () => { + setLoading(true) + requestServer(`${EXPORT_NAVGAN_PROVINCE_MANAGER}?${filterParams}`, '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(() => { + setLoading(false) + }) + } + + + return ( + + ) +} + +export default PrintExcel \ No newline at end of file diff --git a/src/components/dashboard/navgan-province-manager/TableToolbar.jsx b/src/components/dashboard/navgan-province-manager/TableToolbar.jsx new file mode 100644 index 0000000..44a75d9 --- /dev/null +++ b/src/components/dashboard/navgan-province-manager/TableToolbar.jsx @@ -0,0 +1,13 @@ +import {Stack} from "@mui/material"; +import PrintExcel from "@/components/dashboard/navgan-province-manager/Buttons/printExcel"; + +const TableToolbar = ({table}) => { + + return ( + + + + ) +} + +export default TableToolbar \ No newline at end of file diff --git a/src/components/dashboard/navgan-province-manager/index.jsx b/src/components/dashboard/navgan-province-manager/index.jsx index e0e6a39..0397fc5 100644 --- a/src/components/dashboard/navgan-province-manager/index.jsx +++ b/src/components/dashboard/navgan-province-manager/index.jsx @@ -6,6 +6,7 @@ import TableRowActions from "./TableRowActions"; import moment from "jalali-moment"; import DataTable from "@/core/components/DataTable"; import MuiDatePicker from "@/core/components/MuiDatePicker"; +import TableToolbar from "@/components/dashboard/navgan-province-manager/TableToolbar"; function DashboardNavganProvinceManagerComponent() { const t = useTranslations(); @@ -162,7 +163,8 @@ function DashboardNavganProvinceManagerComponent() { tableUrl={GET_NAVGAN_PROVINCE_MANAGER} columns={columns} selectableRow={false} - enableCustomToolbar={false} + enableCustomToolbar={true} + CustomToolbar={TableToolbar} enableLastUpdate={true} sorting={[{ id: 'id', desc: false diff --git a/src/components/dashboard/passenger-boss/Buttons/printExcel.jsx b/src/components/dashboard/passenger-boss/Buttons/printExcel.jsx new file mode 100644 index 0000000..d04f129 --- /dev/null +++ b/src/components/dashboard/passenger-boss/Buttons/printExcel.jsx @@ -0,0 +1,68 @@ +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useMemo, useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {EXPORT_PASSENGER_BOSS} from "@/core/data/apiRoutes"; +import moment from "jalali-moment"; +import FileSaver from 'file-saver'; +import DescriptionIcon from '@mui/icons-material/Description'; + +const PrintExcel = ({table}) => { + const t = useTranslations(); + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {columnFilters, columnFilterFns, sorting} = table.getState() + const columns = table.getAllColumns() + + const filterParams = useMemo(() => { + const params = new URLSearchParams(); + const filters = columnFilters.map((filter) => { + let datatype; + for (const i in columns) { + if (columns[i].id == filter.id) { + datatype = columns[i].columnDef.datatype; + } + } + return { + ...filter, fn: columnFilterFns[filter.id], datatype: datatype, + }; + }); + params.set("start", '0'); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return params; + }, [columnFilters, columnFilterFns, sorting, columns,]); + + + const clickHandler = () => { + setLoading(true) + requestServer(`${EXPORT_PASSENGER_BOSS}?${filterParams}`, '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(() => { + setLoading(false) + }) + } + + + return ( + + ) +} + +export default PrintExcel \ No newline at end of file diff --git a/src/components/dashboard/passenger-boss/TableToolbar.jsx b/src/components/dashboard/passenger-boss/TableToolbar.jsx index ed224f4..91db1f3 100644 --- a/src/components/dashboard/passenger-boss/TableToolbar.jsx +++ b/src/components/dashboard/passenger-boss/TableToolbar.jsx @@ -1,10 +1,12 @@ import {Stack} from "@mui/material"; import PrintFinalCreditAmount from "@/components/dashboard/passenger-boss/Buttons/printFinalCreditAmount"; +import PrintExcel from "@/components/dashboard/passenger-boss/Buttons/printExcel"; -const TableToolbar = () => { +const TableToolbar = ({table}) => { return ( + ) } diff --git a/src/components/dashboard/passenger-office/Buttons/printExcel.jsx b/src/components/dashboard/passenger-office/Buttons/printExcel.jsx new file mode 100644 index 0000000..c78075a --- /dev/null +++ b/src/components/dashboard/passenger-office/Buttons/printExcel.jsx @@ -0,0 +1,68 @@ +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useMemo, useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {EXPORT_PASSENGER_OFFICE} from "@/core/data/apiRoutes"; +import moment from "jalali-moment"; +import FileSaver from 'file-saver'; +import DescriptionIcon from '@mui/icons-material/Description'; + +const PrintExcel = ({table}) => { + const t = useTranslations(); + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {columnFilters, columnFilterFns, sorting} = table.getState() + const columns = table.getAllColumns() + + const filterParams = useMemo(() => { + const params = new URLSearchParams(); + const filters = columnFilters.map((filter) => { + let datatype; + for (const i in columns) { + if (columns[i].id == filter.id) { + datatype = columns[i].columnDef.datatype; + } + } + return { + ...filter, fn: columnFilterFns[filter.id], datatype: datatype, + }; + }); + params.set("start", '0'); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return params; + }, [columnFilters, columnFilterFns, sorting, columns,]); + + + const clickHandler = () => { + setLoading(true) + requestServer(`${EXPORT_PASSENGER_OFFICE}?${filterParams}`, '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(() => { + setLoading(false) + }) + } + + + return ( + + ) +} + +export default PrintExcel \ No newline at end of file diff --git a/src/components/dashboard/passenger-office/TableToolbar.jsx b/src/components/dashboard/passenger-office/TableToolbar.jsx new file mode 100644 index 0000000..23f3c20 --- /dev/null +++ b/src/components/dashboard/passenger-office/TableToolbar.jsx @@ -0,0 +1,13 @@ +import {Stack} from "@mui/material"; +import PrintExcel from "@/components/dashboard/passenger-office/Buttons/printExcel"; + +const TableToolbar = ({table}) => { + + return ( + + + + ) +} + +export default TableToolbar \ No newline at end of file diff --git a/src/components/dashboard/passenger-office/index.jsx b/src/components/dashboard/passenger-office/index.jsx index b8292a0..1e4f705 100644 --- a/src/components/dashboard/passenger-office/index.jsx +++ b/src/components/dashboard/passenger-office/index.jsx @@ -6,6 +6,7 @@ import TableRowActions from "./TableRowActions"; import moment from "jalali-moment"; import DataTable from "@/core/components/DataTable"; import MuiDatePicker from "@/core/components/MuiDatePicker"; +import TableToolbar from "@/components/dashboard/passenger-office/TableToolbar"; function DashboardPassengerOfficeComponent() { const t = useTranslations(); @@ -161,7 +162,8 @@ function DashboardPassengerOfficeComponent() { tableUrl={GET_PASSENGER_OFFICE} columns={columns} selectableRow={false} - enableCustomToolbar={false} + enableCustomToolbar={true} + CustomToolbar={TableToolbar} enableLastUpdate={true} sorting={[{ id: 'id', desc: false 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 ? + : "" + } + + + + + + + + ) +}; + +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 ? + : "" + } + + + + + + + ) +}; + +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/components/dashboard/transportation-assistance/Buttons/printExcel.jsx b/src/components/dashboard/transportation-assistance/Buttons/printExcel.jsx new file mode 100644 index 0000000..b74eecc --- /dev/null +++ b/src/components/dashboard/transportation-assistance/Buttons/printExcel.jsx @@ -0,0 +1,68 @@ +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useMemo, useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {EXPORT_TRANSPORTATION_ASSISTANCE} from "@/core/data/apiRoutes"; +import moment from "jalali-moment"; +import FileSaver from 'file-saver'; +import DescriptionIcon from '@mui/icons-material/Description'; + +const PrintExcel = ({table}) => { + const t = useTranslations(); + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {columnFilters, columnFilterFns, sorting} = table.getState() + const columns = table.getAllColumns() + + const filterParams = useMemo(() => { + const params = new URLSearchParams(); + const filters = columnFilters.map((filter) => { + let datatype; + for (const i in columns) { + if (columns[i].id == filter.id) { + datatype = columns[i].columnDef.datatype; + } + } + return { + ...filter, fn: columnFilterFns[filter.id], datatype: datatype, + }; + }); + params.set("start", '0'); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return params; + }, [columnFilters, columnFilterFns, sorting, columns,]); + + + const clickHandler = () => { + setLoading(true) + requestServer(`${EXPORT_TRANSPORTATION_ASSISTANCE}?${filterParams}`, '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(() => { + setLoading(false) + }) + } + + + return ( + + ) +} + +export default PrintExcel \ No newline at end of file diff --git a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx index 0dcec2a..fa9f438 100644 --- a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx +++ b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx @@ -1,21 +1,88 @@ import UploadSystem from "@/core/components/UploadSystem"; import useNotification from "@/lib/app/hooks/useNotification"; import useRequest from "@/lib/app/hooks/useRequest"; -import {Button, DialogActions, DialogContent, FormHelperText, Stack, TextField, Typography} from "@mui/material" +import { + Box, + Button, + DialogActions, + DialogContent, + FormHelperText, Grid, LinearProgress, + Stack, + TextField, + Typography +} from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; -import {CONFIRM_TRANSPORTATION_ASSISTANCE} from "@/core/data/apiRoutes"; -import {useState} from "react"; +import {CONFIRM_TRANSPORTATION_ASSISTANCE, GET_TRANSPORTATION_ASSISTANCE_DETAILS} from "@/core/data/apiRoutes"; +import {useEffect, useState} from "react"; import * as Yup from "yup"; - -const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { +import CustomAccordion from "@/core/components/CustomAccordion"; +import ImageContent from "@/components/dashboard/transportation-assistance/Form/ConfirmForm/ImageContent"; +const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog ,handleSizeChangeClick}) => { const t = useTranslations(); const [selectedImage, setSelectedImage] = useState(""); const [fileType, setfileType] = useState(null); const [fileName, setfileName] = useState(null); const [showAddIcon, setShowAddIcon] = useState(true); - const requestServer = useRequest({auth: true}) - const {update_notification} = useNotification() + const requestServer = useRequest({auth: true , notification: false}) + const [accordionStates, setAccordionStates] = useState([]); + const [hasAccordionOpened, setHasAccordionOpened] = useState([]); + const {update_notification} = useNotification(); + const [hasImageShown, sethasImageShown] = useState(false); + const [imageLink, setimageLink] = useState(null); + const [detailsList, setDetailsData] = useState(null); + const [loading, setLoading] = useState(true); + const [isSkeleton, setIsSkeleton] = useState(false); + const [isButtonDisabled, setIsButtonDisabled] = useState(true); + + useEffect(() => { + requestServer(`${GET_TRANSPORTATION_ASSISTANCE_DETAILS}/${rowId}`, 'get') + .then((response) => { + const booleanArray = Array.from({ length: response.data.data.attachment_files.length }, () => false); + setAccordionStates(booleanArray); + setHasAccordionOpened(booleanArray); + setDetailsData(response.data) + }) + .catch((error) => { + + }).finally(() => { + setLoading(false); + }); + }, []); + + useEffect(() => { + if (detailsList && detailsList.data && detailsList.data.attachment_files && detailsList.data.attachment_files.length !== 0) { + setIsButtonDisabled(true); + } else { + setIsButtonDisabled(false); + } + }, [detailsList]); + + const handleAccordionChange = (index) => { + const newAccordionStates = accordionStates.map((state, i) => i === index ? !state : false); + setAccordionStates(newAccordionStates); + + if (newAccordionStates[index]===false) + { + handleSizeChangeClick(index , false); + sethasImageShown(false); + setIsSkeleton(false) + } + else { + handleSizeChangeClick(index , true); + sethasImageShown(true); + setimageLink(detailsList.data.attachment_files[index]) + setIsSkeleton(true) + const newHasAccordionOpened = [...hasAccordionOpened]; + newHasAccordionOpened[index] = true; + setHasAccordionOpened(newHasAccordionOpened); + + if (newHasAccordionOpened.every(opened => opened === true)) { + setIsButtonDisabled(false) + } + } + }; + const validationSchema = Yup.object().shape({ confirm_img: Yup.mixed().notRequired(t("ConfirmDialog.approved_amount_error")) @@ -72,51 +139,96 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { } }; + const renderCustomAccordions = () => { + if (detailsList && detailsList.data && detailsList.data.attachment_files) { + return detailsList.data.attachment_files.map((file, index) => ( + handleAccordionChange(index)} + accordionIndex={index} + handleSizeChange={() => handleSizeChange(index)} + /> + )); + } + return null; + }; + return ( <> - - - - {t("ConfirmDialog.description")}{t("ConfirmDialog.optional")}} - value={formik.values.description} - onChange={handleDescriptionChange} - fullWidth - variant="outlined" - sx={{mt: 1}} - /> - - - {t("UploadSystem.upload_file")}{t("UploadSystem.optional")} - formik.handleBlur("confirm_img")} - error={ - formik.touched.confirm_img && - Boolean(formik.errors.confirm_img) - } - /> - - {formik.touched.confirm_img && formik.errors.confirm_img} - - - + + + + {t("ConfirmDialog.approved_amount")} {t("sidebar.machinery-expert")} + + {isNaN((detailsList?.data?.proposed_amount) / 10) + ? "" + : ((detailsList?.data?.proposed_amount) / 10).toLocaleString()}{" "} + {t("ConfirmDialog.unit")} + + + + {t("ConfirmDialog.approved_amount")} {t("secondary.passenger-boss")} + + {isNaN((detailsList?.data?.approved_amount) / 10) + ? "" + : ((detailsList?.data?.approved_amount) / 10).toLocaleString()}{" "} + {t("ConfirmDialog.unit")} + + + {loading ? : renderCustomAccordions()} + + + + {t("ConfirmDialog.description")}{t("ConfirmDialog.optional")}} + value={formik.values.description} + onChange={handleDescriptionChange} + fullWidth + variant="outlined" + sx={{mt: 1}} + /> + + + {t("UploadSystem.upload_file")}{t("UploadSystem.optional")} + formik.handleBlur("confirm_img")} + error={ + formik.touched.confirm_img && + Boolean(formik.errors.confirm_img) + } + /> + + {formik.touched.confirm_img && formik.errors.confirm_img} + + + + + + + + + + diff --git a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ImageContent.jsx b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ImageContent.jsx new file mode 100644 index 0000000..9d3afa9 --- /dev/null +++ b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ImageContent.jsx @@ -0,0 +1,35 @@ +import {Box, Skeleton, Stack} from "@mui/material"; +import {useEffect} from "react"; +const ImageContent = (link) => { + useEffect(() => { + const timer = setTimeout(() => { + link.setIsSkeleton(false); + }, 1000); + + return () => clearTimeout(timer); + }, [link.isSkeleton]); + + const isPDF = link && link.imageLink && link.imageLink.attachment && link.imageLink.attachment.endsWith('.pdf'); + return ( + <> + + {link.isSkeleton && ( + + + + )} + {isPDF ? ( + + ) : ( + link && link.imageLink && link.imageLink.attachment && ( + + + + ) + )} + + + ) + +} +export default ImageContent; \ No newline at end of file diff --git a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx index 2ef7d25..b33f30d 100644 --- a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx +++ b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx @@ -1,27 +1,45 @@ -import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; +import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; import {useTranslations} from "next-intl"; -import {useState} from "react"; +import {useState , useEffect} from "react"; import ConfirmContent from "./ConfirmContent"; import CallMadeIcon from '@mui/icons-material/CallMade'; const Confirm = ({rowId, mutate}) => { const t = useTranslations(); const [openConfirmDialog, setOpenConfirmDialog] = useState(false); + const [dialogSize, setDialogSize] = useState('sm'); + + useEffect(() => { + if (!openConfirmDialog) { + setDialogSize('sm'); + } + }, [openConfirmDialog]); + + const handleSizeChangeClick = (num , state) => { + if (state===true){ + setDialogSize('xl'); + + } + else { + setDialogSize('sm'); + } + }; + return ( <> { - setOpenConfirmDialog(true) + setOpenConfirmDialog(true); }} > + PaperProps={{sx: {transition: 'all .3s', boxShadow: 'rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px'}}} maxWidth={dialogSize}> {t("TransportationAssistance.confirm")} - + ) diff --git a/src/components/dashboard/transportation-assistance/TableToolbar.jsx b/src/components/dashboard/transportation-assistance/TableToolbar.jsx new file mode 100644 index 0000000..1f7c2d2 --- /dev/null +++ b/src/components/dashboard/transportation-assistance/TableToolbar.jsx @@ -0,0 +1,13 @@ +import {Stack} from "@mui/material"; +import PrintExcel from "@/components/dashboard/transportation-assistance/Buttons/printExcel"; + +const TableToolbar = ({table}) => { + + return ( + + + + ) +} + +export default TableToolbar \ No newline at end of file diff --git a/src/components/dashboard/transportation-assistance/index.jsx b/src/components/dashboard/transportation-assistance/index.jsx index 473837c..a1cdae0 100644 --- a/src/components/dashboard/transportation-assistance/index.jsx +++ b/src/components/dashboard/transportation-assistance/index.jsx @@ -6,6 +6,7 @@ import TableRowActions from "./TableRowActions"; import moment from "jalali-moment"; import DataTable from "@/core/components/DataTable"; import MuiDatePicker from "@/core/components/MuiDatePicker"; +import TableToolbar from "@/components/dashboard/transportation-assistance/TableToolbar"; function DashboardTransportationAssistanceComponent() { const t = useTranslations(); @@ -161,7 +162,8 @@ function DashboardTransportationAssistanceComponent() { tableUrl={GET_TRANSPORTATION_ASSISTANCE} columns={columns} selectableRow={false} - enableCustomToolbar={false} + enableCustomToolbar={true} + CustomToolbar={TableToolbar} enableLastUpdate={true} sorting={[{ id: 'id', desc: false diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx index 81fd444..fffd570 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx @@ -51,6 +51,7 @@ const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => { if (hasSubItems) { dispatch({type: "COLLAPSE_MENU", key: item.key}); } + handleDrawerToggle() }} sx={{ minHeight: 48, diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx index 3ef8731..41bff6d 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx @@ -43,6 +43,9 @@ const SidebarListSubItem = ({item, handleDrawerToggle}) => { to={{ pathname: subitem.route, }} + onClick={() => { + handleDrawerToggle() + }} sx={{ minHeight: 48, }} 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/components/CustomAccordion.jsx b/src/core/components/CustomAccordion.jsx new file mode 100644 index 0000000..eb2efcc --- /dev/null +++ b/src/core/components/CustomAccordion.jsx @@ -0,0 +1,72 @@ +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Typography, + Grid, ButtonGroup, Button, CircularProgress +} from "@mui/material"; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import VerifiedIcon from '@mui/icons-material/Verified'; +import Check from "@mui/icons-material/Check"; +import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; +import VisibilityRoundedIcon from "@mui/icons-material/VisibilityRounded"; +import {useTranslations} from "next-intl"; +import {useState} from "react"; + +const CustomAccordion = ({ title, + hasOpened, + hasAccordionOpened, + handleAccordionChange, + accordionIndex, + handleSizeChange }) => { + const t = useTranslations(); + const [downloadStatus, setDownloadStatus] = useState('initial'); + const handleDownloadClick = (setStatus) => { + + console.log(setStatus) + setStatus('downloading'); + setTimeout(() => { + setStatus('completed'); + setTimeout(() => { + setStatus('initial'); + }, 2500); + }, 2000); + // Perform the actual download logic here + }; + return( + handleAccordionChange(accordionIndex)} sx={{ + mb: 2, + boxShadow: 1, + backgroundColor: "#f7f7f7" + }}> + }> + + + {title} + {hasAccordionOpened && } + + + + + + + + + + + + + +); +} + +export default CustomAccordion; diff --git a/src/core/components/DataTable.jsx b/src/core/components/DataTable.jsx index ea3a117..8274f96 100644 --- a/src/core/components/DataTable.jsx +++ b/src/core/components/DataTable.jsx @@ -114,6 +114,7 @@ function DataTable(props) { renderTopToolbarCustomActions={({table}) => (<> {props.enableCustomToolbar /* send condition */ ? /* send component */ : } )} renderBottomToolbarCustomActions={({table}) => (<> diff --git a/src/core/components/DownloadSection.jsx b/src/core/components/DownloadSection.jsx new file mode 100644 index 0000000..fc20d9c --- /dev/null +++ b/src/core/components/DownloadSection.jsx @@ -0,0 +1,38 @@ +import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; +import Check from '@mui/icons-material/Check'; +import {CircularProgress} from "@mui/material" +const DownloadSection = ({ downloadStatus, onDownloadClick }) => { + return ( +
+ {downloadStatus === 'completed' ? ( + <> + + + ) : downloadStatus === 'downloading' ? ( + + ) : ( + <> + + + )} +
+ ); +}; + +export default DownloadSection; diff --git a/src/core/data/apiRoutes.js b/src/core/data/apiRoutes.js index dba065e..d184ab2 100644 --- a/src/core/data/apiRoutes.js +++ b/src/core/data/apiRoutes.js @@ -21,6 +21,9 @@ export const CONFIRM_PASSENGER_OFFICE = export const REJECT_PASSENGER_OFFICE = BASE_URL + "/dashboard/passenger_office_chief/reject"; + +export const EXPORT_PASSENGER_OFFICE = + BASE_URL + "/dashboard/passenger_office_chief/export" //passenger office //passenger boss @@ -32,17 +35,23 @@ export const CONFIRM_PASSENGER_BOSS = export const REJECT_PASSENGER_BOSS = BASE_URL + "/dashboard/province_working_group/reject"; +export const EXPORT_PASSENGER_BOSS = + BASE_URL + "/dashboard/province_working_group/export"; //passenger boss //transportation assistance export const GET_TRANSPORTATION_ASSISTANCE = BASE_URL + "/dashboard/transportation_assistant/show"; +export const GET_TRANSPORTATION_ASSISTANCE_DETAILS = + BASE_URL + "/dashboard/transportation_assistant"; export const CONFIRM_TRANSPORTATION_ASSISTANCE = BASE_URL + "/dashboard/transportation_assistant/confirm"; export const REJECT_TRANSPORTATION_ASSISTANCE = BASE_URL + "/dashboard/transportation_assistant/reject"; +export const EXPORT_TRANSPORTATION_ASSISTANCE = + BASE_URL + "/dashboard/transportation_assistant/export"; //transportation assistance //machinary office @@ -54,6 +63,8 @@ export const CONFIRM_MACHINARY_OFFICE = export const REJECT_MACHINARY_OFFICE = BASE_URL + "/dashboard/machinery_expert/reject"; +export const EXPORT_MACHINARY_OFFICE = + BASE_URL + "/dashboard/machinery_expert/export"; //machinary office //commercial chief @@ -76,6 +87,8 @@ export const CONFIRM_NAVGAN_PROVINCE_MANAGER = export const REJECT_NAVGAN_PROVINCE_MANAGER = BASE_URL + "/dashboard/province_manager/reject"; +export const EXPORT_NAVGAN_PROVINCE_MANAGER = + BASE_URL + "/dashboard/province_manager/export"; //navgan province manager // refahi province manager @@ -197,3 +210,13 @@ 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 + +//loan history +export const GET_LOAN_HISTORY = BASE_URL + "/dashboard/navgan_loans" +export const GET_HISTORY_DETAIL = BASE_URL + "/dashboard/navgan_loans" diff --git a/src/core/data/sidebarMenu.jsx b/src/core/data/sidebarMenu.jsx index c54bbca..2fd6792 100644 --- a/src/core/data/sidebarMenu.jsx +++ b/src/core/data/sidebarMenu.jsx @@ -13,6 +13,8 @@ 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'; +import SecurityIcon from '@mui/icons-material/Security'; const sidebarMenu = [ [ @@ -24,6 +26,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 +157,7 @@ const sidebarMenu = [ route: "/dashboard/expert-management", icon: , selected: false, - permission: "manage_navgan_loan", + permission: "manage_experts", }, { key: "sidebar.user-management", @@ -166,6 +177,15 @@ const sidebarMenu = [ selected: false, permission: "manage_roles", }, + { + key: "sidebar.loan-history", + name: "loan-history", + type: "page", + route: "/dashboard/loan-history", + icon: , + selected: false, + permission: "manage_navgan_loan", + }, ], ]; 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/loan-history/index.jsx b/src/pages/dashboard/loan-history/index.jsx new file mode 100644 index 0000000..d84d43e --- /dev/null +++ b/src/pages/dashboard/loan-history/index.jsx @@ -0,0 +1,21 @@ +import {parse} from "next-useragent"; +import DashboardLoanHistoryComponent from "@/components/dashboard/loan-history"; + +export default function LoanHistory() { + 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.loan_history", + isBot, + locale, + layout: {name: 'DashboardLayout', props: {permissions: ["manage_navgan_loan"]}} + }, + }; +} 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'} + }, + }; +}