diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..a4cba6c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,81 @@ +image: node:latest + +cache: + paths: + - node_modules + +stages: + - lint + - test + - test:build + - deploy + +merge:test:lint: + stage: lint + script: + - npm install + - cp example.env.local .env.local + - cp example.env.local .env.test + - npm run lint + only: + - merge_requests + +merge:test:jest: + stage: test + script: + - npm install + - cp example.env.local .env.local + - cp example.env.local .env.test + - npm run test + only: + - merge_requests + +#merge:test:build: +# stage: test:build +# script: +# - npm install +# - cp example.env.local .env.local +# - cp example.env.local .env.test +# - npm run build +# only: +# - merge_requests + +test:lint: + stage: lint + script: + - npm install + - cp example.env.local .env.local + - cp example.env.local .env.test + - npm run lint + only: + - develop + - main + +test:jest: + stage: test + script: + - npm install + - cp example.env.local .env.local + - cp example.env.local .env.test + - npm run test + only: + - develop + - main + +#test:build: +# stage: test:build +# script: +# - npm install +# - cp example.env.local .env.local +# - cp example.env.local .env.test +# - npm run build +# only: +# - develop +# - main + +deploy: + stage: deploy + script: + - echo "Run deploy project" + only: + - main \ No newline at end of file diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000..3b67130 --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,16 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + e2e: { + setupNodeEvents(on, config) { + // implement node event listeners here + }, + }, + + component: { + devServer: { + framework: "next", + bundler: "webpack", + }, + }, +}); diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..66ea16e --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,25 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) \ No newline at end of file diff --git a/cypress/support/component-index.html b/cypress/support/component-index.html new file mode 100644 index 0000000..3e16e9b --- /dev/null +++ b/cypress/support/component-index.html @@ -0,0 +1,14 @@ + + + + + + + Components App + +
+ + +
+ + \ No newline at end of file diff --git a/cypress/support/component.js b/cypress/support/component.js new file mode 100644 index 0000000..8f9154b --- /dev/null +++ b/cypress/support/component.js @@ -0,0 +1,27 @@ +// *********************************************************** +// This example support/component.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/react18' + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount() \ No newline at end of file diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 0000000..0e7290a --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/e2e.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') \ No newline at end of file diff --git a/example.env.local b/example.env.local index b9a7e4c..a2d8ab0 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.8.10" +NEXT_PUBLIC_API_VERSION = "1.10.2" NEXT_PUBLIC_DEFAULT_LANGUAGE = "fa" NEXT_PUBLIC_DEFAULT_DIRECTION = "rtl" @@ -7,6 +7,7 @@ NEXT_PUBLIC_PRIMARY_MAIN = "#084070" NEXT_PUBLIC_SECONDARY_MAIN = "#FF4E00" NEXT_PUBLIC_BASE_URL = "https://loan.witel.ir" +NEXT_PUBLIC_POWERED_BY_URL = "https://witel.ir" #["NEXT_PUBLIC_HAS_WIDGET" , "NEXT_PUBLIC_HAS_NOTIFICATION"] NEXT_PUBLIC_HAS_VALUE=["NEXT_PUBLIC_HAS_NOTIFICATION"] diff --git a/jest.config.mjs b/jest.config.mjs new file mode 100644 index 0000000..5bda02f --- /dev/null +++ b/jest.config.mjs @@ -0,0 +1,17 @@ +import nextJest from 'next/jest.js' + +const createJestConfig = nextJest({ + // Provide the path to your Next.js app to load next.config.js and .env files in your test environment + dir: './', +}) + +// Add any custom config to be passed to Jest +/** @type {import('jest').Config} */ +const config = { + // Add more setup options before each test is run + setupFilesAfterEnv: ['/jest.setup.js'], + testEnvironment: 'jest-environment-jsdom', +} + +// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async +export default createJestConfig(config) \ No newline at end of file diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..06299aa --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,22 @@ +import '@testing-library/jest-dom' +import {server} from "./mocks/server"; +import mockRouter from 'next-router-mock' + +jest.mock('next/router', () => jest.requireActual('next-router-mock')) + +beforeAll(() => { + server.listen() +}) +beforeEach(() => { + localStorage.clear(); + mockRouter.query = {} + +}) + +afterEach(() => { + server.resetHandlers() +}) + +afterAll(() => { + server.close() +}) \ No newline at end of file diff --git a/mocks/AppWithProvider.jsx b/mocks/AppWithProvider.jsx new file mode 100644 index 0000000..404c839 --- /dev/null +++ b/mocks/AppWithProvider.jsx @@ -0,0 +1,13 @@ +import App from "@/pages/_app"; +import fa from "&/locales/fa/app.json" + +const translations = {fa}; +const MockAppWithProviders = ({children, locale, title, isBot}) => { + const pageProps = { + title: title || '', isBot: isBot || true, locale: locale || 'fa', messages: translations[locale || 'fa'] + } + + return ( (<>{children})} pageProps={pageProps}/>) +} + +export default MockAppWithProviders \ No newline at end of file diff --git a/mocks/handler.js b/mocks/handler.js new file mode 100644 index 0000000..a4e5e8f --- /dev/null +++ b/mocks/handler.js @@ -0,0 +1,5 @@ +import {userHandler} from "./handlers/user"; + +export const handler = [ + ...userHandler +]; diff --git a/mocks/handlers/user.js b/mocks/handlers/user.js new file mode 100644 index 0000000..10677de --- /dev/null +++ b/mocks/handlers/user.js @@ -0,0 +1,19 @@ +import {rest} from "msw"; +import {GET_USER_ROUTE} from "@/core/data/apiRoutes"; + +export const userHandler = [ + rest.get(GET_USER_ROUTE, (req, res, ctx) => { + return res(ctx.json({ + data: { + id: 10, + full_name: "Witel Company", + position: "Software Engineer", + permissions: [ + "manage_users", + "manage_roles", + "manage_boss" + ], + } + })) + }), +] diff --git a/mocks/server.js b/mocks/server.js new file mode 100644 index 0000000..0bf9e08 --- /dev/null +++ b/mocks/server.js @@ -0,0 +1,4 @@ +import {setupServer} from "msw/node"; +import {handler} from "./handler"; + +export const server = setupServer(...handler) \ No newline at end of file diff --git a/package.json b/package.json index 229374d..4583a4c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,11 @@ "lint": "next lint", "update": "run-script-os", "update:win32": "echo 'ok'", - "update:linux": "rm -rf .next/ && npm i && npm run build && sudo systemctl restart loan_facilities_develop_expert.service" + "update:linux": "rm -rf .next/ && npm i && npm run build && sudo systemctl restart loan_facilities_develop_expert.service", + "test": "jest", + "test:watch": "jest --watchAll", + "cypress:open": "cypress open", + "cypress:run": "cypress run" }, "dependencies": { "@emotion/react": "^11.10.6", @@ -51,7 +55,17 @@ }, "devDependencies": { "@faker-js/faker": "^7.6.0", - "eslint-config-next": "^13.3.0", + "@testing-library/jest-dom": "^6.1.4", + "@testing-library/react": "^14.0.0", + "@testing-library/user-event": "^14.5.1", + "cypress": "^13.4.0", + "eslint-config-next": "^13.5.6", + "eslint-plugin-jest-dom": "^5.1.0", + "eslint-plugin-testing-library": "^6.1.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "msw": "^1.3.1", + "next-router-mock": "^0.9.10", "run-script-os": "^1.1.6" } } diff --git a/public/fontiran.scss b/public/fontiran.scss index 86d585d..5ee0036 100644 --- a/public/fontiran.scss +++ b/public/fontiran.scss @@ -167,3 +167,10 @@ This set of fonts are used in this project under the license: (.....) url('./fonts/woff/Parastoo-Bold.woff') format('woff'), /* FF3.6+, IE9, Chrome6+, Saf5.1+*/ url('./fonts/ttf/Parastoo-Bold.ttf') format('truetype'); } + +@font-face { + font-family: Bnazanin; + font-style: normal; + font-weight: normal; + src: url('./fonts/ttf/BNazanin.ttf') format('truetype'); +} diff --git a/public/fonts/ttf/BNazanin.ttf b/public/fonts/ttf/BNazanin.ttf new file mode 100644 index 0000000..4e59251 Binary files /dev/null and b/public/fonts/ttf/BNazanin.ttf differ diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000..075e3fe Binary files /dev/null and b/public/images/logo.png differ diff --git a/public/images/logo.svg b/public/images/logo.svg deleted file mode 100644 index eebb9fa..0000000 --- a/public/images/logo.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - MARHABA - - - diff --git a/public/locales/fa/app.json b/public/locales/fa/app.json index da7e0ee..b36f081 100644 --- a/public/locales/fa/app.json +++ b/public/locales/fa/app.json @@ -2,6 +2,7 @@ "app_name": "سامانه جامع تسهیلات", "app_short_name": "سامانه تسهیلات", "dashboard": "داشبورد", + "powered_by_witel": "توسعه یافته توسط وایتل", "first_page": "خوش آمدید", "login": "ورود", "login_expert": "ورود کارشناس", @@ -15,6 +16,11 @@ "between": "میان", "online_message": "شما به اینترنت وصل هستید", "offline_message": "اتصال شما به اینترنت قطع شده است", + "routing_to": "در حال انتقال به صفحه", + "data_not_found": "اطلاعاتی یافت نشد!", + "btn_print": "چاپ", + "print_loading": "درحال پردازش اطلاعات ...", + "button_back_to_previous": "بازگشت به صفحه قبل", "header": { "open_profile": "پروفایل", "edit_profile": " پروفایل", @@ -161,7 +167,10 @@ "updated_at": "تاریخ بروزرسانی", "navgan_id": "کد ناوگان", "vehicle_type": "نوع ماشین", - "state_name": "وضعیت درخواست" + "state_name": "وضعیت درخواست", + "confirm": "ارجاع به کارشناس ماشین آلات", + "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf" }, "CommercialChief": { "name": "نام", @@ -183,7 +192,10 @@ "updated_at": "تاریخ بروزرسانی", "navgan_id": "کد ناوگان", "vehicle_type": "نوع ماشین", - "state_name": "وضعیت درخواست" + "state_name": "وضعیت درخواست", + "confirm": "تایید نهایی و ارجاع به بانک", + "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf" }, "PassengerBoss": { "name": "نام", @@ -194,7 +206,14 @@ "updated_at": "تاریخ بروزرسانی", "navgan_id": "کد ناوگان", "vehicle_type": "نوع ماشین", - "state_name": "وضعیت درخواست" + "state_name": "وضعیت درخواست", + "confirm": "ارجاع به معاون حمل و نقل", + "upload_file": "صورت جلسه کارگروه استانی را بارگذاری کنید", + "upload_file_required": "وارد کردن صورت جلسه کارگروه استانی الزامیست", + "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", + "print_final_credit_amount": "چاپ صورت جلسه تعیین مبلغ نهایی", + "max_amount": "حداکثر مبلغ تصویب شده {value} میلیون ریال می باشد" }, "RefahiProvinceManager": { "name": "نام", @@ -262,7 +281,10 @@ "updated_at": "تاریخ بروزرسانی", "navgan_id": "کد ناوگان", "vehicle_type": "نوع ماشین", - "state_name": "وضعیت درخواست" + "state_name": "وضعیت درخواست", + "confirm": "ارجاع به مدیر کل استانی", + "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf" }, "InspectorExpert": { "name": "نام", @@ -284,7 +306,14 @@ "updated_at": "تاریخ بروزرسانی", "navgan_id": "کد ناوگان", "vehicle_type": "نوع ماشین", - "state_name": "وضعیت درخواست" + "state_name": "وضعیت درخواست", + "confirm": "ارجاع به کارگروه استانی", + "upload_file": "گزارش کارشناسی را بارگذاری کنید", + "upload_file_required": "وارد کردن گزارش کارشناسی الزامیست", + "upload_file_unit": "فایل بارگذاری شده باید حداکثر 2Mb باشد", + "upload_file_format": "فرمت قابل قبول : png,jpg,pdf", + "print_report": "چاپ گزارش کارشناسی ماشین آلات", + "max_amount": "حداکثر مبلغ پیشنهادی {value} میلیون ریال می باشد" }, "UserManagement": { "name": "نام", @@ -306,23 +335,24 @@ "updated_at": "تاریخ بروزرسانی" }, "ConfirmDialog": { - "confirm": "تایید", + "confirm": "ارجاع", "context": "آیا از تایید این آیتم اطمینان دارید؟", "button-cancel": "بستن", - "button-confirm": "تایید", + "button-confirm": "ارجاع", "description_error": "وارد کردن توضیحات الزامی است!", "optional": " (اختیاری)", - "unit": " (ریال)", + "unit": " (میلیون ریال)", "description": "توضیحات خود را وارد نمائید", "choose_file": "انتخاب فایل", "approved_amount": "مبلغ تصویب شده", "approved_amount_error": "وارد کردن مبلغ تصویب شده الزامیست", "approved_amount_positive": "مبلغ تصویب شده باید مثبت باشد", "approved_amount_number": "مبلغ تصویب شده باید عدد باشد", - "toman": "تومان", + "toman": "میلیون تومان", "proposed_amount_error": "وارد کردن مقدار پیشنهادی الزامیست", "proposed_amount": "مقدار پیشنهادی", - "proposed_amount_positive": "مقدار پیشنهادی باید مثبت باشد" + "proposed_amount_positive": "مقدار پیشنهادی باید مثبت باشد", + "proposed_amount_number": "مقدار پیشنهادی باید عدد باشد" }, "ReviseDialog": { "revise": " نیاز به ویرایش", @@ -361,7 +391,8 @@ "permission_min_error": "حداقل باید یک دسترسی انتخاب شود", "type_id": "نوع کاربر", "navgan_id": "کد ناوگان", - "button-update": "ویرایش" + "button-update": "ویرایش", + "loading_permissions_list": "درحال دریافت لیست دسترسی ها" }, "UploadSystem": { "upload_file": "فایل خود را بارگذاری کنید", @@ -468,7 +499,8 @@ "type_id": "نوع کاربر", "navgan_id": "کد ناوگان", "button-cancel": "انصراف", - "button-add": "ثبت" + "button-add": "ثبت", + "loading_permissions_list": "درحال دریافت لیست دسترسی ها" }, "reports": { "loan_progress": "درصد پیشرفت تسویه وام", diff --git a/public/print.scss b/public/print.scss new file mode 100644 index 0000000..990a647 --- /dev/null +++ b/public/print.scss @@ -0,0 +1,9 @@ +@media print { + body { + margin: 0; + padding: 0; + } + @page { + size: A4; + } +} \ No newline at end of file diff --git a/src/components/dashboard/commercial-chief/TableTollbar.jsx b/src/components/dashboard/commercial-chief/TableTollbar.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/dashboard/development-assistant/TableTollbar.jsx b/src/components/dashboard/development-assistant/TableTollbar.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/dashboard/inspector-expert/TableTollbar.jsx b/src/components/dashboard/inspector-expert/TableTollbar.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/dashboard/machinary-office/Buttons/printReport.jsx b/src/components/dashboard/machinary-office/Buttons/printReport.jsx new file mode 100644 index 0000000..47aab16 --- /dev/null +++ b/src/components/dashboard/machinary-office/Buttons/printReport.jsx @@ -0,0 +1,48 @@ +import PrintIcon from "@mui/icons-material/Print"; +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useRouter} from "next/router"; +import {useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {GET_MACHINARY_OFFICE} from "@/core/data/apiRoutes"; +import usePrint from "@/lib/app/hooks/usePrint"; + +const PrintReport = () => { + const t = useTranslations(); + const router = useRouter() + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {setPrintPage, setPrintTitle} = usePrint() + const clickHandler = () => { + setLoading(true) + const params = new URLSearchParams(); + params.set("start", '0'); + params.set("filters", '[]'); + params.set("sorting", '[]'); + requestServer(`${GET_MACHINARY_OFFICE}?${params}`, 'get').then((response) => { + const _data = response.data.data + setPrintPage(_data.length) + setPrintTitle(t('MachinaryOffice.print_report')) + sessionStorage.setItem('report-print', JSON.stringify(_data)) + router.push('/dashboard/machinery-expert/prints/report') + }).catch(() => { + setLoading(false) + }) + } + + return ( + + ) +} + +export default PrintReport \ No newline at end of file diff --git a/src/components/dashboard/machinary-office/Form/ConfirmForm/ConfirmContent.jsx b/src/components/dashboard/machinary-office/Form/ConfirmForm/ConfirmContent.jsx index 261c083..16b7ca3 100644 --- a/src/components/dashboard/machinary-office/Form/ConfirmForm/ConfirmContent.jsx +++ b/src/components/dashboard/machinary-office/Form/ConfirmForm/ConfirmContent.jsx @@ -1,16 +1,19 @@ import UploadSystem from "@/core/components/UploadSystem"; import useNotification from "@/lib/app/hooks/useNotification"; import useRequest from "@/lib/app/hooks/useRequest"; -import {Button, DialogActions, DialogContent, Stack, TextField, Typography} from "@mui/material" +import {Button, DialogActions, DialogContent, FormHelperText, Stack, TextField, Typography} from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; import {CONFIRM_MACHINARY_OFFICE} from "@/core/data/apiRoutes"; -import UploadFileNotification from "@/core/components/notifications/UploadFileNotification"; import {useState} from "react"; import PriceField from "@/core/components/PriceField"; import * as Yup from "yup"; -const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { +const vehicle_amount = { + "اتوبوس": 7000, + "مینی بوس": 2000, +} +const ConfirmContent = ({mutate, setOpenConfirmDialog, rowId, vehicle_type}) => { const t = useTranslations(); const [selectedImage, setSelectedImage] = useState(""); const [fileType, setfileType] = useState(null); @@ -18,14 +21,24 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const [showAddIcon, setShowAddIcon] = useState(true); const requestServer = useRequest({auth: true}) const {update_notification} = useNotification() - const validationSchema = Yup.object().shape({ proposed_amount: Yup.mixed().test( "is-number", `${t("ConfirmDialog.proposed_amount_number")}`, (value) => !isNaN(value) - ).test("positive", `${t("ConfirmDialog.proposed_amount_positive")}`, (value) => value >= 0) - .required(t("ConfirmDialog.proposed_amount_error")) + ).test("max-amount", `${t("MachinaryOffice.max_amount", {value: parseInt(vehicle_amount[vehicle_type]).toLocaleString('en')})}`, + (value) => value <= vehicle_amount[vehicle_type]) + .test("positive", `${t("ConfirmDialog.proposed_amount_positive")}`, (value) => value >= 0) + .required(t("ConfirmDialog.proposed_amount_error")), + confirm_img: Yup.mixed() + .required(t("MachinaryOffice.upload_file_required")) + .test('fileSize', `${t("MachinaryOffice.upload_file_unit")}`, (value) => { + return value.size <= 2 * 1024 * 1024; + }) + .test('fileType', `${t("MachinaryOffice.upload_file_format")}`, (value) => { + const allowedTypes = ['image/jpg', 'image/jpeg', 'image/png', 'application/pdf']; + return allowedTypes.includes(value.type); + }) }); const formik = useFormik({ @@ -37,9 +50,8 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); formData.append("proposed_amount", values.proposed_amount); - if (values.description != "") formData.append("description", values.description); + if (values.description != "") formData.append("expert_description", values.description); if (values.confirm_img != null) formData.append("attachment", values.confirm_img); - requestServer(`${CONFIRM_MACHINARY_OFFICE}/${rowId}`, 'post', { data: formData, }).then((response) => { @@ -60,19 +72,14 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const handleUploadChange = (event) => { const uploadedFile = event.target?.files?.[0]; if (uploadedFile) { - const maxFileSize = 2 * 1024 * 1024; - if (uploadedFile.size > maxFileSize) { - UploadFileNotification(t); - event.target.value = ""; - return; - } - const fileType = event.target?.files?.[0].type; const fileName = event.target?.files?.[0].name; setSelectedImage(URL.createObjectURL(uploadedFile)); setfileType(fileType); setfileName(fileName); - formik.setFieldValue("confirm_img", uploadedFile); + formik.setFieldValue("confirm_img", uploadedFile).then(() => { + formik.setFieldTouched("confirm_img", true); + }) setShowAddIcon(false); } }; @@ -129,7 +136,7 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { /> - {t("UploadSystem.upload_file")}{t("UploadSystem.optional")} + {t("MachinaryOffice.upload_file")} { fileName={fileName} imageAlt={t("app_name")} imageSize={[250, 150]} + onBlur={formik.handleBlur("confirm_img")} + error={ + formik.touched.confirm_img && + Boolean(formik.errors.confirm_img) + } setShowAddIcon={setShowAddIcon} showAddIcon={showAddIcon} /> + + {formik.touched.confirm_img && formik.errors.confirm_img} + diff --git a/src/components/dashboard/machinary-office/Form/ConfirmForm/index.jsx b/src/components/dashboard/machinary-office/Form/ConfirmForm/index.jsx index 95a6367..3affc7b 100644 --- a/src/components/dashboard/machinary-office/Form/ConfirmForm/index.jsx +++ b/src/components/dashboard/machinary-office/Form/ConfirmForm/index.jsx @@ -1,10 +1,10 @@ import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; import {useTranslations} from "next-intl"; -import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; import {useState} from "react"; import ConfirmContent from "./ConfirmContent"; +import CallMadeIcon from '@mui/icons-material/CallMade'; -const Confirm = ({rowId, mutate}) => { +const Confirm = ({rowId, mutate, vehicle_type}) => { const t = useTranslations(); const [openConfirmDialog, setOpenConfirmDialog] = useState(false); return ( @@ -16,13 +16,14 @@ const Confirm = ({rowId, mutate}) => { setOpenConfirmDialog(true) }} > - + - {t("ConfirmDialog.confirm")} - + {t("MachinaryOffice.confirm")} + ) diff --git a/src/components/dashboard/machinary-office/Form/RejectForm/RejectContent.jsx b/src/components/dashboard/machinary-office/Form/RejectForm/RejectContent.jsx index 340a757..479b213 100644 --- a/src/components/dashboard/machinary-office/Form/RejectForm/RejectContent.jsx +++ b/src/components/dashboard/machinary-office/Form/RejectForm/RejectContent.jsx @@ -6,7 +6,7 @@ import {useFormik} from "formik"; import {useTranslations} from "next-intl"; import {useState} from "react"; import * as Yup from "yup"; -import {REJECT_INSPECTOR_EXPERT} from "@/core/data/apiRoutes"; +import {REJECT_MACHINARY_OFFICE} from "@/core/data/apiRoutes"; const RejectContent = ({rowId, mutate, setOpenRejectDialog}) => { const [selectedImage, setSelectedImage] = useState(""); @@ -29,10 +29,10 @@ const RejectContent = ({rowId, mutate, setOpenRejectDialog}) => { validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - formData.append("description", values.description); + formData.append("expert_description", values.description); if (values.reject_img != null) formData.append("attachment", values.reject_img); - requestServer(`${REJECT_INSPECTOR_EXPERT}/${rowId}`, 'post', { + requestServer(`${REJECT_MACHINARY_OFFICE}/${rowId}`, 'post', { data: formData, }).then((response) => { setOpenRejectDialog(false) diff --git a/src/components/dashboard/machinary-office/Prints/report/Content.jsx b/src/components/dashboard/machinary-office/Prints/report/Content.jsx new file mode 100644 index 0000000..8098ecb --- /dev/null +++ b/src/components/dashboard/machinary-office/Prints/report/Content.jsx @@ -0,0 +1,96 @@ +import PrintablePage from "@/core/components/PrintablePage"; +import {Box, Stack, Typography} from "@mui/material"; + +const Content = ({data}) => { + return ( + <> + + + {'گزارش کارشناسی ماشین آلات'} + + + {'با توجه به نامه شماره'} + 35482 + {'مورخ'} + 1402/08/20 + {'معاون محترم برنامه ریزی سازمان راهداری و حمل و نقل جاده ای در خصوص تسهیلات بند (الف) تبصره (18) قانون بودجه سال'} + 1402 + {'موضوع بازسازی ناوگان اتوبوس و مینی بوس و نیز تصویب آن درشورای برنامه ریزی و توسعه استان'} + تهران + {'و نامه شماره'} + 35482 + {'مورخ'} + 1402/08/20 + {'اداره کل راهداری و حمل و نقل جاده ای'} + تهران + {'در خصوص استفاده از ظرفیت انجمن های صنفی رانندگان در تاریخ'} + 1402/08/20 + {'از ناوگان اتوبوس/مینی بوس به شماره پلاک انتظامی'} + 25پ4582 + {'و شماره هوشمند ناوگان'} + 5875698725 + {'به مالکیت آقای'} + امیرحسین محمودی + {'با کد ملی'} + 0311318897 + {'و شماره تماس'} + 09120630655 + {'ساکن شهرستان'} + تهران + {'در جلسه مطرح شد که نتیجه آن به شرح ذیل مورد تایید می باشد.'} + + + {'مبلغ پیشنهادی به عدد .............................................. ریال و به حروف ...................................................................... ریال می باشد.'} + {'توضیحات : ................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................'} + + {'نام و نام خانوادگی کارشناس:'} + {'تاریخ و امضاء'} + + + + + ) +} + +export default Content \ No newline at end of file diff --git a/src/components/dashboard/machinary-office/Prints/report/index.jsx b/src/components/dashboard/machinary-office/Prints/report/index.jsx new file mode 100644 index 0000000..54dca24 --- /dev/null +++ b/src/components/dashboard/machinary-office/Prints/report/index.jsx @@ -0,0 +1,60 @@ +import {useEffect, useState} from "react"; +import CenterLayout from "@/layouts/CenterLayout"; +import {useTranslations} from "next-intl"; +import {Stack, Typography} from "@mui/material"; +import Content from "@/components/dashboard/machinary-office/Prints/report/Content"; + +const ReportComponent = () => { + const t = useTranslations() + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [isNotData, setIsNotData] = useState(true) + + useEffect(() => { + if (sessionStorage.getItem('report-print') === null) { + setLoading(false) + setIsNotData(true) + sessionStorage.setItem('report-print', 'seen') + } else if (sessionStorage.getItem('report-print') === 'seen') { + setLoading(false) + setData(null) + setIsNotData(true) + } else if (data) { + setLoading(false) + sessionStorage.setItem('report-print', 'seen') + setIsNotData(false) + } else { + setLoading(true) + setData(JSON.parse(sessionStorage.getItem('report-print'))) + setIsNotData(true) + } + }, [data]); + + return ( + + {loading ? ( + + + + {t('print_loading')} + + + + ) : !data ? ( + + + {t('data_not_found')} + + + ) : !data.length ? ( + + + {t('data_not_found')} + + + ) : data.map(item => )} + + ) +} + +export default ReportComponent \ No newline at end of file diff --git a/src/components/dashboard/machinary-office/TableRowActions.jsx b/src/components/dashboard/machinary-office/TableRowActions.jsx index 69f9d6f..f50938e 100644 --- a/src/components/dashboard/machinary-office/TableRowActions.jsx +++ b/src/components/dashboard/machinary-office/TableRowActions.jsx @@ -3,11 +3,11 @@ import Confirm from "./Form/ConfirmForm"; import Reject from "./Form/RejectForm"; const TableRow = ({row, mutate}) => { - return ( - - - ); -} - -export default TableToolbar; diff --git a/src/components/dashboard/machinary-office/TableToolbar.jsx b/src/components/dashboard/machinary-office/TableToolbar.jsx new file mode 100644 index 0000000..9704f95 --- /dev/null +++ b/src/components/dashboard/machinary-office/TableToolbar.jsx @@ -0,0 +1,12 @@ +import {Stack} from "@mui/material"; +import PrintReport from "@/components/dashboard/machinary-office/Buttons/printReport"; + +const TableToolbar = () => { + return ( + + + + ) +} + +export default TableToolbar \ No newline at end of file diff --git a/src/components/dashboard/machinary-office/index.jsx b/src/components/dashboard/machinary-office/index.jsx index 820519e..853c065 100644 --- a/src/components/dashboard/machinary-office/index.jsx +++ b/src/components/dashboard/machinary-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/machinary-office/TableToolbar"; function DashboardMachinaryOfficeComponent() { const t = useTranslations(); @@ -160,7 +161,8 @@ function DashboardMachinaryOfficeComponent() { tableUrl={GET_MACHINARY_OFFICE} columns={columns} selectableRow={false} - enableCustomToolbar={false} + enableCustomToolbar={true} + CustomToolbar={TableToolbar} enableLastUpdate={true} sorting={[{ id: 'id', desc: false diff --git a/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/ConfirmContent.jsx b/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/ConfirmContent.jsx index 5de9eb3..daa4df9 100644 --- a/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/ConfirmContent.jsx +++ b/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/ConfirmContent.jsx @@ -1,12 +1,12 @@ import UploadSystem from "@/core/components/UploadSystem"; import useNotification from "@/lib/app/hooks/useNotification"; import useRequest from "@/lib/app/hooks/useRequest"; -import {Button, DialogActions, DialogContent, Stack, TextField, Typography} from "@mui/material" +import {Button, DialogActions, DialogContent, FormHelperText, Stack, TextField, Typography} from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; import {CONFIRM_NAVGAN_PROVINCE_MANAGER} from "@/core/data/apiRoutes"; -import UploadFileNotification from "@/core/components/notifications/UploadFileNotification"; import {useState} from "react"; +import * as Yup from "yup"; const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const t = useTranslations(); @@ -17,14 +17,27 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const requestServer = useRequest({auth: true}) const {update_notification} = useNotification() + const validationSchema = Yup.object().shape({ + confirm_img: Yup.mixed().notRequired(t("ConfirmDialog.approved_amount_error")) + .test('fileSize', `${t("NavganProvinceManager.upload_file_unit")}`, (value) => { + if (!value) return true + return value.size <= 2 * 1024 * 1024; + }) + .test('fileType', `${t("NavganProvinceManager.upload_file_format")}`, (value) => { + if (!value) return true + const allowedTypes = ['image/jpg', 'image/jpeg', 'image/png', 'application/pdf']; // Define your allowed file types + return allowedTypes.includes(value.type); + }) + }) + const formik = useFormik({ initialValues: { description: "", confirm_img: null - }, + }, validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - if (values.description != "") formData.append("description", values.description); + if (values.description != "") formData.append("expert_description", values.description); if (values.confirm_img != null) formData.append("attachment", values.confirm_img); requestServer(`${CONFIRM_NAVGAN_PROVINCE_MANAGER}/${rowId}`, 'post', { @@ -47,23 +60,17 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const handleUploadChange = (event) => { const uploadedFile = event.target?.files?.[0]; if (uploadedFile) { - const maxFileSize = 2 * 1024 * 1024; - if (uploadedFile.size > maxFileSize) { - UploadFileNotification(t); - event.target.value = ""; - return; - } - const fileType = event.target?.files?.[0].type; const fileName = event.target?.files?.[0].name; setSelectedImage(URL.createObjectURL(uploadedFile)); setfileType(fileType); setfileName(fileName); - formik.setFieldValue("confirm_img", uploadedFile); + formik.setFieldValue("confirm_img", uploadedFile).then(() => { + formik.setFieldTouched("confirm_img", true); + }) setShowAddIcon(false); } }; - return ( <> @@ -96,7 +103,17 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { imageSize={[250, 150]} setShowAddIcon={setShowAddIcon} showAddIcon={showAddIcon} + onBlur={() => formik.handleBlur("confirm_img")} + error={ + formik.touched.confirm_img && + Boolean(formik.errors.confirm_img) + } /> + + {formik.touched.confirm_img && formik.errors.confirm_img} + @@ -106,7 +123,7 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { {t("ConfirmDialog.button-cancel")} diff --git a/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/index.jsx b/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/index.jsx index 95a6367..2d983db 100644 --- a/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/index.jsx +++ b/src/components/dashboard/navgan-province-manager/Form/ConfirmForm/index.jsx @@ -1,9 +1,8 @@ import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; import {useTranslations} from "next-intl"; -import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; import {useState} 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); @@ -16,12 +15,12 @@ const Confirm = ({rowId, mutate}) => { setOpenConfirmDialog(true) }} > - + - {t("ConfirmDialog.confirm")} + {t("NavganProvinceManager.confirm")} diff --git a/src/components/dashboard/navgan-province-manager/Form/RejectForm/RejectContent.jsx b/src/components/dashboard/navgan-province-manager/Form/RejectForm/RejectContent.jsx index 2502898..0b26c8b 100644 --- a/src/components/dashboard/navgan-province-manager/Form/RejectForm/RejectContent.jsx +++ b/src/components/dashboard/navgan-province-manager/Form/RejectForm/RejectContent.jsx @@ -29,7 +29,7 @@ const RejectContent = ({rowId, mutate, setOpenRejectDialog}) => { validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - formData.append("description", values.description); + formData.append("expert_description", values.description); if (values.reject_img != null) formData.append("attachment", values.reject_img); requestServer(`${REJECT_NAVGAN_PROVINCE_MANAGER}/${rowId}`, 'post', { diff --git a/src/components/dashboard/navgan-province-manager/TableTollbar.jsx b/src/components/dashboard/navgan-province-manager/TableTollbar.jsx deleted file mode 100644 index e267691..0000000 --- a/src/components/dashboard/navgan-province-manager/TableTollbar.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import {Stack, Tooltip} from "@mui/material"; -import {useTranslations} from "next-intl"; - -function TableToolbar() { - const t = useTranslations(); - return ( - - - - ); -} - -export default TableToolbar; diff --git a/src/components/dashboard/passenger-boss/Buttons/printFinalCreditAmount.jsx b/src/components/dashboard/passenger-boss/Buttons/printFinalCreditAmount.jsx new file mode 100644 index 0000000..58a97af --- /dev/null +++ b/src/components/dashboard/passenger-boss/Buttons/printFinalCreditAmount.jsx @@ -0,0 +1,48 @@ +import PrintIcon from "@mui/icons-material/Print"; +import {Button, CircularProgress} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useRouter} from "next/router"; +import {useState} from "react"; +import useRequest from "@/lib/app/hooks/useRequest"; +import {GET_PASSENGER_BOSS} from "@/core/data/apiRoutes"; +import usePrint from "@/lib/app/hooks/usePrint"; + +const PrintFinalCreditAmount = () => { + const t = useTranslations(); + const router = useRouter() + const [loading, setLoading] = useState(false) + const requestServer = useRequest({auth: true, pending: false, success: {notification: {show: false}}}) + const {setPrintPage, setPrintTitle} = usePrint() + const clickHandler = () => { + setLoading(true) + const params = new URLSearchParams(); + params.set("start", '0'); + params.set("filters", '[]'); + params.set("sorting", '[]'); + requestServer(`${GET_PASSENGER_BOSS}?${params}`, 'get').then((response) => { + const _data = response.data.data + setPrintPage(_data.length) + setPrintTitle(t('PassengerBoss.print_final_credit_amount')) + sessionStorage.setItem('final-credit-amount-print', JSON.stringify(_data)) + router.push('/dashboard/passenger-boss/prints/final-credit-amount') + }).catch(() => { + setLoading(false) + }) + } + + return ( + + ) +} + +export default PrintFinalCreditAmount \ No newline at end of file diff --git a/src/components/dashboard/passenger-boss/Form/ConfirmForm/ConfirmContent.jsx b/src/components/dashboard/passenger-boss/Form/ConfirmForm/ConfirmContent.jsx index 66ccd27..f0a8c12 100644 --- a/src/components/dashboard/passenger-boss/Form/ConfirmForm/ConfirmContent.jsx +++ b/src/components/dashboard/passenger-boss/Form/ConfirmForm/ConfirmContent.jsx @@ -1,16 +1,19 @@ import UploadSystem from "@/core/components/UploadSystem"; import useNotification from "@/lib/app/hooks/useNotification"; import useRequest from "@/lib/app/hooks/useRequest"; -import {Button, DialogActions, DialogContent, Stack, TextField, Typography} from "@mui/material" +import {Button, DialogActions, DialogContent, FormHelperText, Stack, TextField, Typography} from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; import {CONFIRM_PASSENGER_BOSS} from "@/core/data/apiRoutes"; -import UploadFileNotification from "@/core/components/notifications/UploadFileNotification"; import {useState} from "react"; import PriceField from "@/core/components/PriceField"; import * as Yup from "yup"; -const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { +const vehicle_amount = { + "اتوبوس": 7000, + "مینی بوس": 2000, +} +const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog, vehicle_type}) => { const t = useTranslations(); const [selectedImage, setSelectedImage] = useState(""); const [fileType, setfileType] = useState(null); @@ -24,8 +27,19 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { "is-number", `${t("ConfirmDialog.approved_amount_number")}`, (value) => !isNaN(value) - ).test("positive", `${t("ConfirmDialog.approved_amount_positive")}`, (value) => value >= 0) - .required(t("ConfirmDialog.approved_amount_error")) + ).test("max-amount", `${t("PassengerBoss.max_amount", {value: parseInt(vehicle_amount[vehicle_type]).toLocaleString('en')})}`, + (value) => value <= vehicle_amount[vehicle_type]) + .test("positive", `${t("ConfirmDialog.approved_amount_positive")}`, (value) => value >= 0) + .required(t("ConfirmDialog.approved_amount_error")), + confirm_img: Yup.mixed() + .required(t("PassengerBoss.upload_file_required")) + .test('fileSize', `${t("PassengerBoss.upload_file_unit")}`, (value) => { + return value.size <= 2 * 1024 * 1024; + }) + .test('fileType', `${t("PassengerBoss.upload_file_format")}`, (value) => { + const allowedTypes = ['image/jpg', 'image/jpeg', 'image/png', 'application/pdf']; // Define your allowed file types + return allowedTypes.includes(value.type); + }) }); const formik = useFormik({ @@ -37,8 +51,8 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); formData.append("approved_amount", values.approved_amount); - if (values.description != "") formData.append("description", values.description); - if (values.confirm_img != null) formData.append("attachment", values.confirm_img); + if (values.description != "") formData.append("expert_description", values.description); + formData.append("attachment", values.confirm_img); requestServer(`${CONFIRM_PASSENGER_BOSS}/${rowId}`, 'post', { data: formData, @@ -60,19 +74,14 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const handleUploadChange = (event) => { const uploadedFile = event.target?.files?.[0]; if (uploadedFile) { - const maxFileSize = 2 * 1024 * 1024; - if (uploadedFile.size > maxFileSize) { - UploadFileNotification(t); - event.target.value = ""; - return; - } - const fileType = event.target?.files?.[0].type; const fileName = event.target?.files?.[0].name; setSelectedImage(URL.createObjectURL(uploadedFile)); setfileType(fileType); setfileName(fileName); - formik.setFieldValue("confirm_img", uploadedFile); + formik.setFieldValue("confirm_img", uploadedFile).then(() => { + formik.setFieldTouched("confirm_img", true); + }) setShowAddIcon(false); } }; @@ -83,7 +92,6 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { formik.setFieldValue("approved_amount", formik.values.approved_amount) } }; - return ( <> @@ -129,7 +137,7 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { /> - {t("UploadSystem.upload_file")}{t("UploadSystem.optional")} + {t("PassengerBoss.upload_file")} { imageSize={[250, 150]} setShowAddIcon={setShowAddIcon} showAddIcon={showAddIcon} + onBlur={() => 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/passenger-boss/Form/ConfirmForm/index.jsx b/src/components/dashboard/passenger-boss/Form/ConfirmForm/index.jsx index 95a6367..004d119 100644 --- a/src/components/dashboard/passenger-boss/Form/ConfirmForm/index.jsx +++ b/src/components/dashboard/passenger-boss/Form/ConfirmForm/index.jsx @@ -1,10 +1,10 @@ import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; import {useTranslations} from "next-intl"; -import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; import {useState} from "react"; import ConfirmContent from "./ConfirmContent"; +import CallMadeIcon from '@mui/icons-material/CallMade'; -const Confirm = ({rowId, mutate}) => { +const Confirm = ({rowId, mutate, vehicle_type}) => { const t = useTranslations(); const [openConfirmDialog, setOpenConfirmDialog] = useState(false); return ( @@ -16,13 +16,14 @@ const Confirm = ({rowId, mutate}) => { setOpenConfirmDialog(true) }} > - + - {t("ConfirmDialog.confirm")} - + {t("PassengerBoss.confirm")} + ) diff --git a/src/components/dashboard/passenger-boss/Form/RejectForm/RejectContent.jsx b/src/components/dashboard/passenger-boss/Form/RejectForm/RejectContent.jsx index 6b6b385..93fe8c3 100644 --- a/src/components/dashboard/passenger-boss/Form/RejectForm/RejectContent.jsx +++ b/src/components/dashboard/passenger-boss/Form/RejectForm/RejectContent.jsx @@ -29,7 +29,7 @@ const RejectContent = ({rowId, mutate, setOpenRejectDialog}) => { validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - formData.append("description", values.description); + formData.append("expert_description", values.description); if (values.reject_img != null) formData.append("attachment", values.reject_img); requestServer(`${REJECT_PASSENGER_BOSS}/${rowId}`, 'post', { diff --git a/src/components/dashboard/passenger-boss/Prints/final-credit-amount/Content.jsx b/src/components/dashboard/passenger-boss/Prints/final-credit-amount/Content.jsx new file mode 100644 index 0000000..e9844ee --- /dev/null +++ b/src/components/dashboard/passenger-boss/Prints/final-credit-amount/Content.jsx @@ -0,0 +1,123 @@ +import PrintablePage from "@/core/components/PrintablePage"; +import {Box, Grid, Stack, Typography} from "@mui/material"; + +const Content = ({data}) => { + return ( + <> + + + {'فرم صورتجلسه تعیین مبلغ نهایی اعتبار'} + + + {'با توجه به نامه شماره'} + 35482 + {'مورخ'} + 1402/08/20 + {'معاون محترم برنامه ریزی سازمان راهداری و حمل و نقل جاده ای در خصوص تسهیلات بند (الف) تبصره (18) قانون بودجه سال'} + 1402 + {'موضوع بازسازی ناوگان اتوبوس و مینی بوس و نیز تصویب آن درشورای برنامه ریزی و توسعه استان'} + تهران + {'و نامه شماره'} + 35482 + {'مورخ'} + 1402/08/20 + {'اداره کل راهداری و حمل و نقل جاده ای'} + تهران + {'در خصوص استفاده از ظرفیت انجمن های صنفی رانندگان در تاریخ'} + 1402/08/20 + {'از ناوگان اتوبوس/مینی بوس به شماره پلاک انتظامی'} + 25پ4582 + {'و شماره هوشمند ناوگان'} + 5875698725 + {'به مالکیت آقای'} + امیرحسین محمودی + {'با کد ملی'} + 0311318897 + {'و شماره تماس'} + 09120630655 + {'ساکن شهرستان'} + تهران + {'در جلسه مطرح شد که نتیجه آن به شرح ذیل مورد تایید می باشد.'} + + + {'با توجه به گزارش واصله از اداره ماشین آلات (نامه در پیوست موجود می باشد) جهت بازسازی ناوگان مذکور مبلغ .............................................. ریال معادل .............................................. تومان مورد تایید می باشد.'} + + + + + رئیس انجمن + + + + + رئیس اداره مسافر + + + + + + + + + + + امضاء + + + + + امضاء + + + + + امضاء + + + + + امضاء + + + + + + ) +} + +export default Content \ No newline at end of file diff --git a/src/components/dashboard/passenger-boss/Prints/final-credit-amount/index.jsx b/src/components/dashboard/passenger-boss/Prints/final-credit-amount/index.jsx new file mode 100644 index 0000000..9fd6a78 --- /dev/null +++ b/src/components/dashboard/passenger-boss/Prints/final-credit-amount/index.jsx @@ -0,0 +1,60 @@ +import {useEffect, useState} from "react"; +import CenterLayout from "@/layouts/CenterLayout"; +import {useTranslations} from "next-intl"; +import {Stack, Typography} from "@mui/material"; +import Content from "@/components/dashboard/passenger-boss/Prints/final-credit-amount/Content"; + +const FinalCreditAmountComponent = () => { + const t = useTranslations() + const [data, setData] = useState(null) + const [loading, setLoading] = useState(true) + const [isNotData, setIsNotData] = useState(true) + + useEffect(() => { + if (sessionStorage.getItem('final-credit-amount-print') === null) { + setLoading(false) + setIsNotData(true) + sessionStorage.setItem('final-credit-amount-print', 'seen') + } else if (sessionStorage.getItem('final-credit-amount-print') === 'seen') { + setLoading(false) + setData(null) + setIsNotData(true) + } else if (data) { + setLoading(false) + sessionStorage.setItem('final-credit-amount-print', 'seen') + setIsNotData(false) + } else { + setLoading(true) + setData(JSON.parse(sessionStorage.getItem('final-credit-amount-print'))) + setIsNotData(true) + } + }, [data]); + + return ( + + {loading ? ( + + + + {t('print_loading')} + + + + ) : !data ? ( + + + {t('data_not_found')} + + + ) : !data.length ? ( + + + {t('data_not_found')} + + + ) : data.map(item => )} + + ) +} + +export default FinalCreditAmountComponent \ No newline at end of file diff --git a/src/components/dashboard/passenger-boss/TableRowActions.jsx b/src/components/dashboard/passenger-boss/TableRowActions.jsx index d10e248..cef474d 100644 --- a/src/components/dashboard/passenger-boss/TableRowActions.jsx +++ b/src/components/dashboard/passenger-boss/TableRowActions.jsx @@ -8,6 +8,7 @@ const TableRow = ({row, mutate}) => { - - - ); -} - -export default TableToolbar; diff --git a/src/components/dashboard/passenger-boss/TableToolbar.jsx b/src/components/dashboard/passenger-boss/TableToolbar.jsx new file mode 100644 index 0000000..ed224f4 --- /dev/null +++ b/src/components/dashboard/passenger-boss/TableToolbar.jsx @@ -0,0 +1,12 @@ +import {Stack} from "@mui/material"; +import PrintFinalCreditAmount from "@/components/dashboard/passenger-boss/Buttons/printFinalCreditAmount"; + +const TableToolbar = () => { + return ( + + + + ) +} + +export default TableToolbar \ No newline at end of file diff --git a/src/components/dashboard/passenger-boss/index.jsx b/src/components/dashboard/passenger-boss/index.jsx index c338f6e..1a13b3b 100644 --- a/src/components/dashboard/passenger-boss/index.jsx +++ b/src/components/dashboard/passenger-boss/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-boss/TableToolbar"; function DashboardPassengerOfficeComponent() { const t = useTranslations(); @@ -161,7 +162,8 @@ function DashboardPassengerOfficeComponent() { tableUrl={GET_PASSENGER_BOSS} columns={columns} selectableRow={false} - enableCustomToolbar={false} + enableCustomToolbar={true} + CustomToolbar={TableToolbar} enableLastUpdate={true} sorting={[{ id: 'id', desc: false diff --git a/src/components/dashboard/passenger-office/Form/ConfirmForm/ConfirmContent.jsx b/src/components/dashboard/passenger-office/Form/ConfirmForm/ConfirmContent.jsx index 46a735f..d86c4f2 100644 --- a/src/components/dashboard/passenger-office/Form/ConfirmForm/ConfirmContent.jsx +++ b/src/components/dashboard/passenger-office/Form/ConfirmForm/ConfirmContent.jsx @@ -1,12 +1,12 @@ import UploadSystem from "@/core/components/UploadSystem"; import useNotification from "@/lib/app/hooks/useNotification"; import useRequest from "@/lib/app/hooks/useRequest"; -import {Button, DialogActions, DialogContent, Stack, TextField, Typography} from "@mui/material" +import {Button, DialogActions, DialogContent, FormHelperText, Stack, TextField, Typography} from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; import {CONFIRM_PASSENGER_OFFICE} from "@/core/data/apiRoutes"; -import UploadFileNotification from "@/core/components/notifications/UploadFileNotification"; import {useState} from "react"; +import * as Yup from "yup"; const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const t = useTranslations(); @@ -17,11 +17,24 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const requestServer = useRequest({auth: true}) const {update_notification} = useNotification() + const validationSchema = Yup.object().shape({ + confirm_img: Yup.mixed().notRequired(t("ConfirmDialog.approved_amount_error")) + .test('fileSize', `${t("PassengerOffice.upload_file_unit")}`, (value) => { + if (!value) return true + return value.size <= 2 * 1024 * 1024; + }) + .test('fileType', `${t("PassengerOffice.upload_file_format")}`, (value) => { + if (!value) return true + const allowedTypes = ['image/jpg', 'image/jpeg', 'image/png', 'application/pdf']; // Define your allowed file types + return allowedTypes.includes(value.type); + }) + }) + const formik = useFormik({ initialValues: { description: "", confirm_img: null - }, + }, validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); if (values.description != "") formData.append("description", values.description); @@ -47,19 +60,14 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const handleUploadChange = (event) => { const uploadedFile = event.target?.files?.[0]; if (uploadedFile) { - const maxFileSize = 2 * 1024 * 1024; - if (uploadedFile.size > maxFileSize) { - UploadFileNotification(t); - event.target.value = ""; - return; - } - const fileType = event.target?.files?.[0].type; const fileName = event.target?.files?.[0].name; setSelectedImage(URL.createObjectURL(uploadedFile)); setfileType(fileType); setfileName(fileName); - formik.setFieldValue("confirm_img", uploadedFile); + formik.setFieldValue("confirm_img", uploadedFile).then(() => { + formik.setFieldTouched("confirm_img", true); + }) setShowAddIcon(false); } }; @@ -96,7 +104,17 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { imageSize={[250, 150]} setShowAddIcon={setShowAddIcon} showAddIcon={showAddIcon} + onBlur={() => formik.handleBlur("confirm_img")} + error={ + formik.touched.confirm_img && + Boolean(formik.errors.confirm_img) + } /> + + {formik.touched.confirm_img && formik.errors.confirm_img} + @@ -106,7 +124,7 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { {t("ConfirmDialog.button-cancel")} diff --git a/src/components/dashboard/passenger-office/Form/ConfirmForm/index.jsx b/src/components/dashboard/passenger-office/Form/ConfirmForm/index.jsx index 95a6367..fa47fcf 100644 --- a/src/components/dashboard/passenger-office/Form/ConfirmForm/index.jsx +++ b/src/components/dashboard/passenger-office/Form/ConfirmForm/index.jsx @@ -1,9 +1,8 @@ import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; import {useTranslations} from "next-intl"; -import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; import {useState} 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); @@ -16,12 +15,12 @@ const Confirm = ({rowId, mutate}) => { setOpenConfirmDialog(true) }} > - + - {t("ConfirmDialog.confirm")} + {t("PassengerOffice.confirm")} diff --git a/src/components/dashboard/passenger-office/Form/RejectForm/RejectContent.jsx b/src/components/dashboard/passenger-office/Form/RejectForm/RejectContent.jsx index fd7ea2c..2a8a82d 100644 --- a/src/components/dashboard/passenger-office/Form/RejectForm/RejectContent.jsx +++ b/src/components/dashboard/passenger-office/Form/RejectForm/RejectContent.jsx @@ -29,7 +29,7 @@ const RejectContent = ({rowId, mutate, setOpenRejectDialog}) => { validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - formData.append("description", values.description); + formData.append("expert_description", values.description); if (values.reject_img != null) formData.append("attachment", values.reject_img); requestServer(`${REJECT_PASSENGER_OFFICE}/${rowId}`, 'post', { diff --git a/src/components/dashboard/passenger-office/TableTollbar.jsx b/src/components/dashboard/passenger-office/TableTollbar.jsx deleted file mode 100644 index e267691..0000000 --- a/src/components/dashboard/passenger-office/TableTollbar.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import {Stack, Tooltip} from "@mui/material"; -import {useTranslations} from "next-intl"; - -function TableToolbar() { - const t = useTranslations(); - return ( - - - - ); -} - -export default TableToolbar; diff --git a/src/components/dashboard/province-head-expert/TableTollbar.jsx b/src/components/dashboard/province-head-expert/TableTollbar.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/dashboard/refahi-province-manager/TableTollbar.jsx b/src/components/dashboard/refahi-province-manager/TableTollbar.jsx deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx b/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx index 45b14ba..e276e5d 100644 --- a/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx +++ b/src/components/dashboard/role-management/Form/CreateForm/CreateContent.jsx @@ -1,6 +1,6 @@ import { Button, - Checkbox, + Checkbox, CircularProgress, DialogActions, DialogContent, FormControl, @@ -9,7 +9,7 @@ import { FormLabel, Grid, Stack, - TextField + TextField, Typography } from "@mui/material"; import {useTranslations} from "next-intl"; import useRequest from "@/lib/app/hooks/useRequest"; @@ -21,7 +21,7 @@ import usePermissions from "@/lib/app/hooks/usePermissions"; const CreateContent = ({mutate, setOpenConfirmDialog}) => { const t = useTranslations(); const requestServer = useRequest({auth: true}) - const {permissions_list} = usePermissions() + const {permissions_list, isLoading} = usePermissions() const validationSchema = Yup.object().shape({ name: Yup.string().required(t("AddDialog.name_error")), name_fa: Yup.string().required(t("AddDialog.name_fa_error")), @@ -93,29 +93,40 @@ const CreateContent = ({mutate, setOpenConfirmDialog}) => { sx={{mt: 2}} > {formik.touched.permissions && formik.errors.permissions} - - {permissions_list ? ( - permissions_list.map((permission) => ( - - { - if (e.target.checked) { - formik.setFieldValue("permissions", [...formik.values.permissions, permission.id]) - } else { - formik.setFieldValue("permissions", formik.values.permissions.filter((id) => id !== permission.id)) - } - }} + {isLoading ? + + + {t("AddDialog.loading_permissions_list")} + + : ( + + <> + {permissions_list.map((permission, index) => ( + + { + if (e.target.checked) { + formik.setFieldValue("permissions", [...formik.values.permissions, permission.id]) + } else { + formik.setFieldValue("permissions", formik.values.permissions.filter((id) => id !== permission.id)) + } + }} + /> + } + label={permission.name_fa} /> - } - label={permission.name_fa} - /> - - )) - ) : null} - + + ))} + + + ) + } diff --git a/src/components/dashboard/role-management/Form/DeleteForm.jsx b/src/components/dashboard/role-management/Form/DeleteForm.jsx index 78319bd..5f2d169 100644 --- a/src/components/dashboard/role-management/Form/DeleteForm.jsx +++ b/src/components/dashboard/role-management/Form/DeleteForm.jsx @@ -24,6 +24,7 @@ const DeleteForm = ({rowId, mutate}) => { const handleSubmit = () => { setIsSubmitting(true) requestServer(`${DELETE_ROLE_MANAGEMENT}/${rowId}`, 'delete').then((response) => { + setOpenConfirmDialog(false) mutate() update_notification() }).catch(() => { diff --git a/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx b/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx index 8e13b12..037ed99 100644 --- a/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx +++ b/src/components/dashboard/role-management/Form/UpdateForm/UpdateContent.jsx @@ -4,7 +4,7 @@ import usePermissions from "@/lib/app/hooks/usePermissions"; import useRequest from "@/lib/app/hooks/useRequest"; import { Button, - Checkbox, + Checkbox, CircularProgress, DialogActions, DialogContent, FormControl, @@ -13,7 +13,7 @@ import { FormLabel, Grid, Stack, - TextField + TextField, Typography } from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; @@ -23,7 +23,7 @@ const UpdateContent = ({row, mutate, setOpenConfirmDialog}) => { const t = useTranslations(); const requestServer = useRequest({auth: true}) const {update_notification} = useNotification() - const {permissions_list} = usePermissions() + const {permissions_list, isLoading} = usePermissions() const validationSchema = Yup.object().shape({ name: Yup.string().required(t("UpdateDialog.name_error")), @@ -93,29 +93,40 @@ const UpdateContent = ({row, mutate, setOpenConfirmDialog}) => { sx={{mt: 2}} > {formik.touched.permissions && formik.errors.permissions} - - {permissions_list ? ( - permissions_list.map((permission) => ( - - { - if (e.target.checked) { - formik.setFieldValue("permissions", [...formik.values.permissions, permission.id]) - } else { - formik.setFieldValue("permissions", formik.values.permissions.filter((id) => id !== permission.id)) - } - }} + {isLoading ? + + + {t("UpdateDialog.loading_permissions_list")} + + : ( + + <> + {permissions_list.map((permission) => ( + + { + if (e.target.checked) { + formik.setFieldValue("permissions", [...formik.values.permissions, permission.id]) + } else { + formik.setFieldValue("permissions", formik.values.permissions.filter((id) => id !== permission.id)) + } + }} + /> + } + label={permission.name_fa} /> - } - label={permission.name_fa} - /> - - )) - ) : null} - + + ))} + + + ) + } diff --git a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx index dc4d093..0dcec2a 100644 --- a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx +++ b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/ConfirmContent.jsx @@ -1,12 +1,12 @@ import UploadSystem from "@/core/components/UploadSystem"; import useNotification from "@/lib/app/hooks/useNotification"; import useRequest from "@/lib/app/hooks/useRequest"; -import {Button, DialogActions, DialogContent, Stack, TextField, Typography} from "@mui/material" +import {Button, DialogActions, DialogContent, FormHelperText, Stack, TextField, Typography} from "@mui/material" import {useFormik} from "formik"; import {useTranslations} from "next-intl"; import {CONFIRM_TRANSPORTATION_ASSISTANCE} from "@/core/data/apiRoutes"; -import UploadFileNotification from "@/core/components/notifications/UploadFileNotification"; import {useState} from "react"; +import * as Yup from "yup"; const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const t = useTranslations(); @@ -17,14 +17,27 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const requestServer = useRequest({auth: true}) const {update_notification} = useNotification() + const validationSchema = Yup.object().shape({ + confirm_img: Yup.mixed().notRequired(t("ConfirmDialog.approved_amount_error")) + .test('fileSize', `${t("TransportationAssistance.upload_file_unit")}`, (value) => { + if (!value) return true + return value.size <= 2 * 1024 * 1024; + }) + .test('fileType', `${t("TransportationAssistance.upload_file_format")}`, (value) => { + if (!value) return true + const allowedTypes = ['image/jpg', 'image/jpeg', 'image/png', 'application/pdf']; // Define your allowed file types + return allowedTypes.includes(value.type); + }) + }) + const formik = useFormik({ initialValues: { description: "", confirm_img: null - }, + }, validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - if (values.description != "") formData.append("description", values.description); + if (values.description != "") formData.append("expert_description", values.description); if (values.confirm_img != null) formData.append("attachment", values.confirm_img); requestServer(`${CONFIRM_TRANSPORTATION_ASSISTANCE}/${rowId}`, 'post', { @@ -47,19 +60,14 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { const handleUploadChange = (event) => { const uploadedFile = event.target?.files?.[0]; if (uploadedFile) { - const maxFileSize = 2 * 1024 * 1024; - if (uploadedFile.size > maxFileSize) { - UploadFileNotification(t); - event.target.value = ""; - return; - } - const fileType = event.target?.files?.[0].type; const fileName = event.target?.files?.[0].name; setSelectedImage(URL.createObjectURL(uploadedFile)); setfileType(fileType); setfileName(fileName); - formik.setFieldValue("confirm_img", uploadedFile); + formik.setFieldValue("confirm_img", uploadedFile).then(() => { + formik.setFieldTouched("confirm_img", true); + }) setShowAddIcon(false); } }; @@ -96,7 +104,17 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { imageSize={[250, 150]} setShowAddIcon={setShowAddIcon} showAddIcon={showAddIcon} + onBlur={() => formik.handleBlur("confirm_img")} + error={ + formik.touched.confirm_img && + Boolean(formik.errors.confirm_img) + } /> + + {formik.touched.confirm_img && formik.errors.confirm_img} + @@ -106,7 +124,7 @@ const ConfirmContent = ({rowId, mutate, setOpenConfirmDialog}) => { {t("ConfirmDialog.button-cancel")} diff --git a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx index 95a6367..2ef7d25 100644 --- a/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx +++ b/src/components/dashboard/transportation-assistance/Form/ConfirmForm/index.jsx @@ -1,9 +1,8 @@ import {Dialog, DialogTitle, IconButton, Tooltip} from "@mui/material"; import {useTranslations} from "next-intl"; -import ThumbUpAltIcon from "@mui/icons-material/ThumbUpAlt"; import {useState} 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); @@ -16,12 +15,12 @@ const Confirm = ({rowId, mutate}) => { setOpenConfirmDialog(true) }} > - + - {t("ConfirmDialog.confirm")} + {t("TransportationAssistance.confirm")} diff --git a/src/components/dashboard/transportation-assistance/Form/RejectForm/RejectContent.jsx b/src/components/dashboard/transportation-assistance/Form/RejectForm/RejectContent.jsx index 3641f71..b692df4 100644 --- a/src/components/dashboard/transportation-assistance/Form/RejectForm/RejectContent.jsx +++ b/src/components/dashboard/transportation-assistance/Form/RejectForm/RejectContent.jsx @@ -29,7 +29,7 @@ const RejectContent = ({rowId, mutate, setOpenRejectDialog}) => { validationSchema, onSubmit: (values, {setSubmitting}) => { const formData = new FormData(); - formData.append("description", values.description); + formData.append("expert_description", values.description); if (values.reject_img != null) formData.append("attachment", values.reject_img); requestServer(`${REJECT_TRANSPORTATION_ASSISTANCE}/${rowId}`, 'post', { diff --git a/src/components/dashboard/transportation-assistance/TableTollbar.jsx b/src/components/dashboard/transportation-assistance/TableTollbar.jsx deleted file mode 100644 index e267691..0000000 --- a/src/components/dashboard/transportation-assistance/TableTollbar.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import {Stack, Tooltip} from "@mui/material"; -import {useTranslations} from "next-intl"; - -function TableToolbar() { - const t = useTranslations(); - return ( - - - - ); -} - -export default TableToolbar; diff --git a/src/components/first/__tests__/index.test.js b/src/components/first/__tests__/index.test.js new file mode 100644 index 0000000..e6aff67 --- /dev/null +++ b/src/components/first/__tests__/index.test.js @@ -0,0 +1,61 @@ +import {render, screen, waitFor} from "@testing-library/react"; +import FirstComponent from "@/components/first"; +import MockAppWithProviders from "../../../../mocks/AppWithProvider"; +import {server} from "../../../../mocks/server"; +import {rest} from "msw"; +import {GET_USER_ROUTE} from "@/core/data/apiRoutes"; + +describe("First Component From First Page", () => { + describe("Rendering", () => { + it("App Name Text Rendered", () => { + render(); + const appNameElement = screen.queryByText(/سامانه جامع تسهیلات/i); + expect(appNameElement).toBeInTheDocument() + }); + it("App version Text Rendered", () => { + render(); + const versionControler = screen.queryByText(process.env.NEXT_PUBLIC_API_VERSION, {exact: false}); + expect(versionControler).toBeInTheDocument() + }); + it("Powered By Rendered With Currect URL", () => { + render(); + const linkElement = screen.queryByText('توسعه یافته توسط وایتل'); + expect(linkElement).toBeInTheDocument() + expect(linkElement).toHaveAttribute('href', process.env.NEXT_PUBLIC_POWERED_BY_URL); + }); + }); + describe("Behavioral", () => { + it("Show Login Button And Do Not Show Dashboard Button When User Is Not Authenticated", async () => { + render(); + await waitFor(() => { + const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i) + const authenticationButtonDashboard = screen.queryByText(/داشبورد/i) + expect(authenticationButtonLogin).toBeInTheDocument() + expect(authenticationButtonDashboard).not.toBeInTheDocument() + }) + }); + it("Show Dashboard Button And Do Not Show Login Button When User Is Authenticated", async () => { + localStorage.setItem("_token", 'token'); + render(); + await waitFor(() => { + const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i) + const authenticationButtonDashboard = screen.queryByText(/داشبورد/i) + expect(authenticationButtonLogin).not.toBeInTheDocument() + expect(authenticationButtonDashboard).toBeInTheDocument() + }) + }); + it("Show Login Button And Do Not Show Dashboard Button When User Authentication Is Expired", async () => { + localStorage.setItem("_token", 'token'); + server.use(rest.get(GET_USER_ROUTE, (req, res, ctx) => { + return res(ctx.status(401)) + })) + render(); + await waitFor(() => { + const authenticationButtonLogin = screen.queryByText(/ورود کارشناس/i) + const authenticationButtonDashboard = screen.queryByText(/داشبورد/i) + expect(authenticationButtonLogin).toBeInTheDocument() + expect(authenticationButtonDashboard).not.toBeInTheDocument() + }) + }); + }); +}); \ No newline at end of file diff --git a/src/components/first/index.jsx b/src/components/first/index.jsx index cd5b5a5..2d144f1 100644 --- a/src/components/first/index.jsx +++ b/src/components/first/index.jsx @@ -1,11 +1,10 @@ -import {NextLinkComposed} from "@/core/components/LinkRouting"; +import LinkRouting, {NextLinkComposed} from "@/core/components/LinkRouting"; import CenterLayout from "@/layouts/CenterLayout"; import FullPageLayout from "@/layouts/FullPageLayout"; import useUser from "@/lib/app/hooks/useUser"; import {Button, Stack, Typography} from "@mui/material"; import {useTranslations} from "next-intl"; import SvgDashboard from "@/core/components/svgs/SvgDashboard"; -import process from "next/dist/build/webpack/loaders/resolve-url-loader/lib/postcss"; const FirstComponent = () => { const t = useTranslations(); @@ -18,28 +17,16 @@ const FirstComponent = () => { {t("app_name")} - {isAuth ? ( - - ) : ( - - )} + { color: 'primary.main', fontFamily: 'Arial', fontWeight: 'bold' - }}>v{process.env.NEXT_PUBLIC_API_VERSION} + }} + > + v{process.env.NEXT_PUBLIC_API_VERSION} + + + + + {t("powered_by_witel")} + ); }; -export default FirstComponent; +export default FirstComponent; \ No newline at end of file diff --git a/src/components/layouts/Dashboard/Content/index.jsx b/src/components/layouts/Dashboard/Content/index.jsx new file mode 100644 index 0000000..e3f39db --- /dev/null +++ b/src/components/layouts/Dashboard/Content/index.jsx @@ -0,0 +1,65 @@ +import LoadingHardPage from "@/core/components/LoadingHardPage"; +import RolePermissionMiddleware from "@/middlewares/RolePermission"; +import BreadCrumbs from "@/components/layouts/Dashboard/Breadcrumbs"; +import FullPageLayout from "@/layouts/FullPageLayout"; +import {useEffect, useMemo, useState} from "react"; +import sidebarMenu from "@/core/data/sidebarMenu"; +import {useRouter} from "next/router"; +import {useTranslations} from "next-intl"; + +const Content = (props) => { + const t = useTranslations() + const router = useRouter() + const [routing, setRouting] = useState(false) + const [routingItem, setRoutingItem] = useState({}) + const pageList = useMemo(() => { + const list = [] + for (const menu of sidebarMenu) { + for (const item of menu) { + if (item.type === 'menu') { + for (const subItem of item.subItem) { + list.push(subItem) + } + } else { + list.push(item) + } + } + } + + return list + }, []) + + useEffect(() => { + const handlerStartRoute = (url) => { + setRoutingItem(pageList.find(page => page.route === url)) + setRouting(true) + } + const handlerCompleteRoute = () => { + setRouting(false) + } + + router.events.on('routeChangeStart', handlerStartRoute) + router.events.on('routeChangeComplete', handlerCompleteRoute) + + return () => { + router.events.off('routeChangeStart', handlerStartRoute) + router.events.off('routeChangeComplete', handlerCompleteRoute) + } + }, [router]); + + return ( + + + + + {props.children} + + + + ) +} + +export default Content \ No newline at end of file diff --git a/src/components/layouts/Dashboard/Header/index.jsx b/src/components/layouts/Dashboard/Header/index.jsx index 8760960..8050806 100644 --- a/src/components/layouts/Dashboard/Header/index.jsx +++ b/src/components/layouts/Dashboard/Header/index.jsx @@ -1,5 +1,5 @@ import MenuIcon from "@mui/icons-material/Menu"; -import {AppBar, Box, Container, CssBaseline, IconButton, Stack, Toolbar, useTheme} from "@mui/material"; +import {AppBar, Box, Container, IconButton, Stack, Toolbar, useTheme} from "@mui/material"; import ProfileMenu from "./ProfileMenu"; function Header({drawerWidth, handleDrawerToggle}) { @@ -7,7 +7,6 @@ function Header({drawerWidth, handleDrawerToggle}) { return ( <> - itemsArr.map((item) => - action.key == item.key - ? {...item, showSubItem: !item.showSubItem} - : item + action.key === item.key ? {...item, showSubItem: !item.showSubItem} : item ) ); case "SELECTED": return state.map((itemsArr) => - itemsArr.map((item) => - action.route == item.route - ? {...item, selected: true} - : {...item, selected: false} - ) + itemsArr.map((item) => { + return item.type === "page" + ? {...item, selected: action.route === item.route, routing: false} + : item.subItem && Array.isArray(item.subItem) + ? { + ...item, + subItem: item.subItem.map((subitem) => ({ + ...subitem, + selected: subitem.route === action.route, + routing: false + })), + showSubItem: item.subItem.some( + (subitem) => subitem.selected + ), + } + : item; + }) + ); + case "SELECTING": + return state.map((itemsArr) => + itemsArr.map((item) => { + return item.type === "page" + ? {...item, selected: action.route === item.route, routing: action.route === item.route} + : item.subItem && Array.isArray(item.subItem) + ? { + ...item, + subItem: item.subItem.map((subitem) => ({ + ...subitem, + selected: subitem.route === action.route, + routing: subitem.route === action.route + })), + showSubItem: item.subItem.some( + (subitem) => subitem.selected + ), + } + : item; + }) ); default: throw new Error(); @@ -32,26 +62,48 @@ export default function SidebarList({handleDrawerToggle}) { const [itemMenu, dispatch] = useReducer(reducer, sidebarMenu); const {user} = useUser(); const router = useRouter(); + const [selectedKey, setSelectedKey] = useState(null); useEffect(() => { dispatch({type: "SELECTED", route: router.pathname}); - }, [router.pathname]); + setSelectedKey(router.pathname); + const handlerStartRoute = (url) => { + dispatch({type: "SELECTING", route: url}); + } - const filteredItemMenu = itemMenu[0].filter( - (item) => user.permissions.includes(item.permission) || item.permission === "all" - ); + router.events.on('routeChangeStart', handlerStartRoute) + + return () => { + router.events.off('routeChangeStart', handlerStartRoute) + } + }, [router]); + + + useEffect(() => { + selectedKey && document.querySelector(`[data-route="${selectedKey}"]`)?.scrollIntoView({ + behavior: "smooth", + block: "center" + }); + }, [selectedKey]); return ( - - {filteredItemMenu.map((item, index) => ( - + + {itemMenu.map((itemArr, index) => ( + + {itemArr.map((item) => + + {(user?.permissions?.includes(item.permission) || item.permission === "all") && + } + + )} + + ))} - {filteredItemMenu.length > 0 && } + ); } diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx index ebeb14c..81fd444 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarListItem.jsx @@ -1,7 +1,16 @@ import {NextLinkComposed} from "@/core/components/LinkRouting"; import ExpandLess from "@mui/icons-material/ExpandLess"; import ExpandMore from "@mui/icons-material/ExpandMore"; -import {Badge, IconButton, ListItem, ListItemButton, ListItemIcon, ListItemText, Typography,} from "@mui/material"; +import { + Badge, + CircularProgress, + IconButton, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Typography, +} from "@mui/material"; import {useTranslations} from "next-intl"; import {Fragment} from "react"; import SidebarListSubItem from "./SidebarListSubItem"; @@ -9,23 +18,27 @@ import useNotification from "@/lib/app/hooks/useNotification"; const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => { const t = useTranslations(); - const {notification_count} = useNotification() + const {notification_count} = useNotification(); + const hasSubItems = item.type === "menu" && item.subItem && item.subItem.length > 0; + const renderBadge = () => { + return !hasSubItems ? ( + + + + ) : null; + }; return ( - - - - - }> + <> + { }, })} onClick={() => { - if (item.type == "menu") { + if (hasSubItems) { dispatch({type: "COLLAPSE_MENU", key: item.key}); } - handleDrawerToggle(); }} sx={{ minHeight: 48, @@ -50,10 +62,13 @@ const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => { minWidth: 0, justifyContent: "center", color: "primary.main", + width: 40, + height: 24, pr: 2, }} > - {item.icon} + {item.routing ? + : item.icon} { } /> - - {item.type == "menu" && - (item.showSubItem ? : )} + {hasSubItems && (item.showSubItem ? : )} {item.subItem && ( @@ -77,7 +90,7 @@ const SidebarListItem = ({item, dispatch, handleDrawerToggle}) => { handleDrawerToggle={handleDrawerToggle} /> )} - + ); }; diff --git a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx index 3407081..3ef8731 100644 --- a/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx +++ b/src/components/layouts/Dashboard/Sidebar/SidebarListSubItem.jsx @@ -1,18 +1,44 @@ import {NextLinkComposed} from "@/core/components/LinkRouting"; -import InboxIcon from "@mui/icons-material/MoveToInbox"; -import {Collapse, List, ListItemButton, ListItemIcon, ListItemText,} from "@mui/material"; +import { + Badge, + CircularProgress, + Collapse, + IconButton, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Typography, +} from "@mui/material"; import {useTranslations} from "next-intl"; -import {Fragment} from "react"; +import useNotification from "@/lib/app/hooks/useNotification"; const SidebarListSubItem = ({item, handleDrawerToggle}) => { const t = useTranslations(); + const {notification_count} = useNotification(); + const renderBadge = (subitem) => ( + + + + ); return ( - - - {item.subItem.map((subitem, index) => ( - + + + + {item.subItem.map((subitem) => ( + { sx={{ minHeight: 48, }} - onClick={(event) => { - if (item.type == "menu") { - dispatch({type: "COLLAPSE_MENU", key: item.key}); - } - handleDrawerToggle(); - }} > - + {subitem.routing ? + : subitem.icon} - + + {t(subitem.secondary)} + + ) : null + }/> - + ))} + ); }; diff --git a/src/components/layouts/Dashboard/Sidebar/index.jsx b/src/components/layouts/Dashboard/Sidebar/index.jsx index 2498dfd..27f2d0d 100644 --- a/src/components/layouts/Dashboard/Sidebar/index.jsx +++ b/src/components/layouts/Dashboard/Sidebar/index.jsx @@ -1,13 +1,31 @@ -import {Box, Drawer} from "@mui/material"; +import {Box, Drawer, useMediaQuery} from "@mui/material"; import SidebarDrawer from "./SidebarDrawer"; +import {useTheme} from "@mui/material/styles"; const Sidebar = (props) => { + const theme = useTheme(); + const isUpSm = useMediaQuery((theme.breakpoints.up('sm'))) return ( + > {isUpSm ? ( + + + + ) : ( { "& .MuiDrawer-paper": { boxSizing: "border-box", width: props.drawerWidth, + overflow: "hidden", }, }} > - - - + )} ); }; diff --git a/src/components/layouts/Dashboard/index.jsx b/src/components/layouts/Dashboard/index.jsx index f54a894..ef3f3bd 100644 --- a/src/components/layouts/Dashboard/index.jsx +++ b/src/components/layouts/Dashboard/index.jsx @@ -1,10 +1,9 @@ import {useState} from "react"; -import Header from "./header"; -import Sidebar from "./sidebar"; import {Toolbar} from "@mui/material"; -import BreadCrumbs from "./breadcrumbs"; import FullPageLayout from "@/layouts/FullPageLayout"; -import RolePermissionMiddleware from "@/middlewares/RolePermission"; +import Header from "@/components/layouts/Dashboard/Header"; +import Sidebar from "@/components/layouts/Dashboard/Sidebar"; +import Content from "@/components/layouts/Dashboard/Content"; const drawerWidth = 240; @@ -17,6 +16,7 @@ const DashboardLayouts = (props) => { const handleDrawerToggle = () => { setMobileOpen(!mobileOpen); }; + return (
{ sx={{flexGrow: 1, width: {sm: `calc(100% - ${drawerWidth}px)`}}} > - - - - {props.children} - - + ); diff --git a/src/components/layouts/Print/Header/ProfileData.jsx b/src/components/layouts/Print/Header/ProfileData.jsx new file mode 100644 index 0000000..548378b --- /dev/null +++ b/src/components/layouts/Print/Header/ProfileData.jsx @@ -0,0 +1,18 @@ +import {Avatar, Stack, Typography} from "@mui/material"; +import useUser from "@/lib/app/hooks/useUser"; + +export default function ProfileData() { + const {user} = useUser(); + return ( + + + + {user.username} + + + ); +} diff --git a/src/components/layouts/Print/Header/ProfileMenu.jsx b/src/components/layouts/Print/Header/ProfileMenu.jsx new file mode 100644 index 0000000..415d501 --- /dev/null +++ b/src/components/layouts/Print/Header/ProfileMenu.jsx @@ -0,0 +1,62 @@ +import {Avatar, IconButton, Menu, Tooltip} from "@mui/material"; +import {useState} from "react"; +import ProfileData from "./ProfileData"; +import ProfileOptions from "./ProfileOptions"; +import {useTranslations} from "next-intl"; +import useUser from "@/lib/app/hooks/useUser"; + +function ProfileMenu() { + const t = useTranslations(); + const [anchorElUser, setAnchorElUser] = useState(null); + const {user} = useUser(); + + const handleOpenUserMenu = (event) => { + setAnchorElUser(event.currentTarget); + }; + const handleCloseUserMenu = () => { + setAnchorElUser(null); + }; + + return ( + <> + + + + + + + + + + + ); +} + +export default ProfileMenu; diff --git a/src/components/layouts/Print/Header/ProfileOptionLogOut.jsx b/src/components/layouts/Print/Header/ProfileOptionLogOut.jsx new file mode 100644 index 0000000..2a6cf85 --- /dev/null +++ b/src/components/layouts/Print/Header/ProfileOptionLogOut.jsx @@ -0,0 +1,49 @@ +import {Box, Button, MenuItem, Typography} from "@mui/material"; +import MeetingRoomIcon from "@mui/icons-material/MeetingRoom"; +import useUser from "@/lib/app/hooks/useUser"; +import {useTranslations} from "next-intl"; + +export default function ProfileOptionLogOut({handleCloseUserMenu}) { + const t = useTranslations(); + const {clearToken} = useUser(); + const handleClickLogOut = () => { + handleCloseUserMenu(); + clearToken(); + }; + return ( + <> + + + + + + + {t("header.logout")} + + + + + ); +} diff --git a/src/components/layouts/Print/Header/ProfileOptions.jsx b/src/components/layouts/Print/Header/ProfileOptions.jsx new file mode 100644 index 0000000..6e7bdba --- /dev/null +++ b/src/components/layouts/Print/Header/ProfileOptions.jsx @@ -0,0 +1,49 @@ +import {Box, MenuItem, Typography} from "@mui/material"; +import {NextLinkComposed} from "@/core/components/LinkRouting"; +import {useTranslations} from "next-intl"; +import headerProfileItems from "@/core/data/headerProfileItems"; +import ProfileOptionLogOut from "./ProfileOptionLogOut"; + +export default function ProfileOptions({handleCloseUserMenu}) { + const t = useTranslations(); + + return ( + <> + {headerProfileItems.map((profile_item) => ( + + + + {profile_item.icon} + + + {t(profile_item.name)} + + + + ))} + + + ); +} diff --git a/src/components/layouts/Print/Header/index.jsx b/src/components/layouts/Print/Header/index.jsx new file mode 100644 index 0000000..7c0d990 --- /dev/null +++ b/src/components/layouts/Print/Header/index.jsx @@ -0,0 +1,72 @@ +import {AppBar, Button, Container, Stack, Toolbar, Typography, useTheme} from "@mui/material"; +import {useTranslations} from "next-intl"; +import {useRouter} from "next/router"; +import {useState} from "react"; +import usePrint from "@/lib/app/hooks/usePrint"; + +function Header({drawerWidth, handleDrawerToggle}) { + const theme = useTheme(); + const t = useTranslations() + const router = useRouter() + const {printDetails} = usePrint() + const [disabled, setDisabled] = useState(false) + + return ( + + + + + + + + {printDetails.title} + {'تعداد صفحه:'} {printDetails.count} + + + + + + + + ); +} + +export default Header; \ No newline at end of file diff --git a/src/components/layouts/Print/index.jsx b/src/components/layouts/Print/index.jsx new file mode 100644 index 0000000..afa6585 --- /dev/null +++ b/src/components/layouts/Print/index.jsx @@ -0,0 +1,14 @@ +import Header from "@/components/layouts/Print/Header"; +import {Toolbar} from "@mui/material"; + +const Print = (props) => { + return ( + <> +
+ + {props.children} + + ) +} + +export default Print \ No newline at end of file diff --git a/src/components/login-expert/index.jsx b/src/components/login-expert/index.jsx index 221c530..23185df 100644 --- a/src/components/login-expert/index.jsx +++ b/src/components/login-expert/index.jsx @@ -1,5 +1,4 @@ import LinkRouting from "@/core/components/LinkRouting"; -import PasswordField from "@/core/components/PasswordField"; import StyledForm from "@/core/components/StyledForm"; import {GET_USER_TOKEN} from "@/core/data/apiRoutes"; import CenterLayout from "@/layouts/CenterLayout"; @@ -14,6 +13,7 @@ import * as Yup from "yup"; import useDirection from "@/lib/app/hooks/useDirection"; import SvgLogin from "@/core/components/svgs/SvgLogin"; import useRequest from "@/lib/app/hooks/useRequest"; +import PasswordField from "@/core/components/PasswordField"; const LoginComponent = () => { const t = useTranslations(); diff --git a/src/core/components/DataTable.jsx b/src/core/components/DataTable.jsx index 70cee09..ea3a117 100644 --- a/src/core/components/DataTable.jsx +++ b/src/core/components/DataTable.jsx @@ -14,7 +14,6 @@ function DataTable(props) { const [columnFilters, setColumnFilters] = useState([]); const [sorting, setSorting] = useState(props.sorting || []); const [pagination, setPagination] = useState({pageIndex: 0, pageSize: 10}); - const [rowCount, setRowCount] = useState(0); const [columnFilterFns, setColumnFilterFns] = useState(() => { let output = {}; const list = props.columns.map((item) => item.enableColumnFilter ? {[item.id]: item.filterFn} : {[item.id]: ""}); @@ -34,8 +33,11 @@ function DataTable(props) { const tableLocalization = useMemo(() => languageList.find((item) => item.key == languageApp).tableLocalization, [languageApp, languageList]); const fetchUrl = useMemo(() => { - const url = new URL(props.tableUrl); - url.searchParams.set("start", `${pagination.pageIndex * pagination.pageSize}`); + const params = new URLSearchParams(); + params.set( + "start", + `${pagination.pageIndex * pagination.pageSize}` + ); const filters = columnFilters.map((filter) => { let datatype; for (const i in props.columns) { @@ -47,10 +49,10 @@ function DataTable(props) { ...filter, fn: columnFilterFns[filter.id], datatype: datatype, }; }); - url.searchParams.set("size", pagination.pageSize); - url.searchParams.set("filters", JSON.stringify(filters ?? [])); - url.searchParams.set("sorting", JSON.stringify(sorting ?? [])); - return url; + params.set("size", pagination.pageSize); + params.set("filters", JSON.stringify(filters ?? [])); + params.set("sorting", JSON.stringify(sorting ?? [])); + return `${props.tableUrl}?${params}`; }, [props.tableUrl, columnFilters, columnFilterFns, pagination, sorting, props.columns,]); const {data, isValidating, mutate} = useSWR(fetchUrl, (...args) => @@ -60,7 +62,7 @@ function DataTable(props) { }).then((response) => response.data).catch(() => { }) , { - revalidateIfStale: false, + revalidateIfStale: true, revalidateOnFocus: false, revalidateOnReconnect: true, keepPreviousData: true diff --git a/src/core/components/LoadingHardPage.jsx b/src/core/components/LoadingHardPage.jsx index c3b24d6..59e4a02 100644 --- a/src/core/components/LoadingHardPage.jsx +++ b/src/core/components/LoadingHardPage.jsx @@ -1,4 +1,4 @@ -import {Backdrop, Box, styled} from "@mui/material"; +import {Backdrop, Box, Stack, styled, Typography} from "@mui/material"; import SvgLoading from "@/core/components/svgs/SvgLoading"; const LoadingImage = styled(Box)({ @@ -9,7 +9,7 @@ const LoadingImage = styled(Box)({ }, "50%": { // opacity: 1, - transform: "scale(2)", + transform: "scale(.5)", }, "100%": { // opacity: 0, @@ -19,20 +19,29 @@ const LoadingImage = styled(Box)({ animation: "load 2s infinite", }); -const LoadingHardPage = ({children, loading}) => { +const LoadingHardPage = ({children, loading, sx = {}, icon = null, width = 200, height = 200, label = ''}) => { return ( <> theme.zIndex.drawer + 1}} + sx={{bgcolor: "#fff", zIndex: (theme) => theme.zIndex.drawer + 1, ...sx}} open={loading} > - - - + + + {icon ? ( + + {icon} + + ) : ( + + )} + + {label} + {children} diff --git a/src/core/components/LtrTextField.jsx b/src/core/components/LtrTextField.jsx new file mode 100644 index 0000000..aa9efcb --- /dev/null +++ b/src/core/components/LtrTextField.jsx @@ -0,0 +1,11 @@ +import {styled, TextField} from "@mui/material"; + +const LtrTextField = styled(TextField)` + .MuiInputBase-input { + /* @noflip */ + direction: ltr; + text-align: left; + } +`; + +export default LtrTextField \ No newline at end of file diff --git a/src/core/components/NetworkComponent.jsx b/src/core/components/NetworkComponent.jsx index 8ebbb68..9610b1c 100644 --- a/src/core/components/NetworkComponent.jsx +++ b/src/core/components/NetworkComponent.jsx @@ -6,13 +6,13 @@ import WifiOffIcon from '@mui/icons-material/WifiOff'; import {useTranslations} from "next-intl"; const NetworkComponent = () => { - const toastId = useRef(null); + const networkToastId = useRef(null); const network = useNetwork() const t = useTranslations() useEffect(() => { if (network.online) { - toast.update(toastId.current, { + toast.update(networkToastId.current, { type: toast.TYPE.SUCCESS, render: t('online_message'), autoClose: 2000, @@ -22,8 +22,9 @@ const NetworkComponent = () => { }); return } - toast.dismiss() - toastId.current = toast.warn(t('offline_message'), { + networkToastId.current = toast.warn(t('offline_message'), { + containerId: 'connection', + draggable: false, autoClose: false, closeButton: false, closeOnClick: false, icon: }) }, [network.online]); diff --git a/src/core/components/PasswordField.jsx b/src/core/components/PasswordField/index.jsx similarity index 100% rename from src/core/components/PasswordField.jsx rename to src/core/components/PasswordField/index.jsx diff --git a/src/core/components/PriceField.jsx b/src/core/components/PriceField.jsx index e62e4e1..1df1ba2 100644 --- a/src/core/components/PriceField.jsx +++ b/src/core/components/PriceField.jsx @@ -1,12 +1,13 @@ -import {Stack, TextField, Typography} from "@mui/material"; +import {Stack, Typography} from "@mui/material"; import {useTranslations} from "next-intl"; +import LtrTextField from "@/core/components/LtrTextField"; const PriceField = (props) => { const t = useTranslations(); return ( - diff --git a/src/core/components/PrintablePage.jsx b/src/core/components/PrintablePage.jsx new file mode 100644 index 0000000..70a264b --- /dev/null +++ b/src/core/components/PrintablePage.jsx @@ -0,0 +1,82 @@ +import {Box, Container, Divider, Grid, Paper, Stack, Typography} from '@mui/material'; + +const PrintablePage = ({children, header, footer}) => ( + <> + + + + + + + + + + باسمه تعالی + جمهوری اسلامی ایران + وزارت راه و + شهرسازی + سازمان + راهداری و حمل و نقل جاده + ای + + + + شماره: + تاریخ: + پیوست: + + + + + {children} + + + + + + آدرس: تهران-بلوار + کشاورز-خیابان فلسطین جنوبی-خیابان + دمشق-پلاک17 + تلفن: 88-88804379 + تلفن گویا: 88804400 + کدپستی: 1416753941 + + + صندوق پستی: 3773-14155 + پست الکترونیک: info@rmto.ir + سایت الکترونیک: www.rmto.ir + + + + + + +); + +export default PrintablePage; \ No newline at end of file diff --git a/src/core/components/UploadSystem.jsx b/src/core/components/UploadSystem.jsx index b40fb46..ea573d7 100644 --- a/src/core/components/UploadSystem.jsx +++ b/src/core/components/UploadSystem.jsx @@ -10,7 +10,6 @@ const UploadSystem = ({ handleUploadChange, fieldname, setFieldValue, - imageAlt, imageSize, fileType, fileName, @@ -19,7 +18,6 @@ const UploadSystem = ({ }) => { const t = useTranslations(); const fileInputRef = useRef(null); - const handleClick = () => { fileInputRef.current.click(); }; @@ -69,13 +67,14 @@ const UploadSystem = ({ variant="subtitle2" sx={{ fontWeight: 600, - fontSize: "1rem", + fontSize: "0.8rem", color: "#a19d9d", mt: 1, }} textAlign="center" > - {t("UploadSystem.upload_file")} + {t("UploadSystem.upload_file_format")}
+ {t("UploadSystem.upload_file_unit")}
diff --git a/src/core/components/notifications/ErrorNotification.jsx b/src/core/components/notifications/ErrorNotification.jsx index 4ff1ab0..776c904 100644 --- a/src/core/components/notifications/ErrorNotification.jsx +++ b/src/core/components/notifications/ErrorNotification.jsx @@ -2,8 +2,8 @@ import DangerousIcon from "@mui/icons-material/Dangerous"; import {Box, Typography} from "@mui/material"; import {toast} from "react-toastify"; -const ErrorNotification = (t, status, message) => { - toast( +const ErrorNotification = (pushToastList, notificationType, t, status, message) => { + const toastId = toast( () => ( <> { ), { + containerId: 'validation', autoClose: false, closeOnClick: false, draggable: false, } ); + pushToastList(notificationType, toastId); }; -export default ErrorNotification; +export default ErrorNotification; \ No newline at end of file diff --git a/src/core/components/notifications/PendingNotification.jsx b/src/core/components/notifications/PendingNotification.jsx index 846b847..cc28a58 100644 --- a/src/core/components/notifications/PendingNotification.jsx +++ b/src/core/components/notifications/PendingNotification.jsx @@ -1,12 +1,14 @@ import {toast} from "react-toastify"; -const PendingNotification = (t) => { - toast(t("notifications.pending"), { +const PendingNotification = (pushToastList, notificationType, t) => { + const toastId = toast(t("notifications.pending"), { + containerId: 'validation', autoClose: false, closeButton: false, closeOnClick: false, draggable: false, }); + pushToastList(notificationType, toastId); }; -export default PendingNotification; +export default PendingNotification; \ No newline at end of file diff --git a/src/core/components/notifications/SuccessNotification.jsx b/src/core/components/notifications/SuccessNotification.jsx index 998deaa..1207000 100644 --- a/src/core/components/notifications/SuccessNotification.jsx +++ b/src/core/components/notifications/SuccessNotification.jsx @@ -2,8 +2,8 @@ import BeenhereIcon from "@mui/icons-material/Beenhere"; import {Box, Typography} from "@mui/material"; import {toast} from "react-toastify"; -const SuccessNotification = (t, status) => { - toast( +const SuccessNotification = (pushToastList, notificationType, t, status) => { + const toastId = toast( () => ( <> { ), { + containerId: 'validation', autoClose: 3000, hideProgressBar: true, pauseOnHover: true, @@ -37,6 +38,7 @@ const SuccessNotification = (t, status) => { draggable: true, } ); + pushToastList(notificationType, toastId); }; -export default SuccessNotification; +export default SuccessNotification; \ No newline at end of file diff --git a/src/core/components/notifications/UploadFileNotification.jsx b/src/core/components/notifications/UploadFileNotification.jsx index 6556944..1c20ba3 100644 --- a/src/core/components/notifications/UploadFileNotification.jsx +++ b/src/core/components/notifications/UploadFileNotification.jsx @@ -26,6 +26,8 @@ const UploadFileNotification = (t) => { ), { + containerId: 'validation', + toastId: 'upload', autoClose: 3000, hideProgressBar: true, pauseOnHover: true, @@ -35,4 +37,4 @@ const UploadFileNotification = (t) => { ); }; -export default UploadFileNotification; +export default UploadFileNotification; \ No newline at end of file diff --git a/src/core/components/notifications/WarningNotification.jsx b/src/core/components/notifications/WarningNotification.jsx index 8493180..bc2a1fb 100644 --- a/src/core/components/notifications/WarningNotification.jsx +++ b/src/core/components/notifications/WarningNotification.jsx @@ -2,8 +2,8 @@ import ReportIcon from "@mui/icons-material/Report"; import {Box, Typography} from "@mui/material"; import {toast} from "react-toastify"; -const WarningNotification = (t, status) => { - toast( +const WarningNotification = (pushToastList, notificationType, t, status) => { + const toastId = toast( () => ( <> { ), { + containerId: 'validation', autoClose: false, closeOnClick: false, draggable: false, } ); + pushToastList(notificationType, toastId); }; -export default WarningNotification; +export default WarningNotification; \ No newline at end of file diff --git a/src/core/components/notifications/index.jsx b/src/core/components/notifications/index.jsx index bc8eb66..34e44f3 100644 --- a/src/core/components/notifications/index.jsx +++ b/src/core/components/notifications/index.jsx @@ -1,54 +1,27 @@ -import {toast} from "react-toastify"; -import ErrorNotification from "./ErrorNotification"; -import WarningNotification from "./WarningNotification"; -import SuccessNotification from "./SuccessNotification"; +import pendingNotification from "@/core/components/notifications/PendingNotification"; +import WarningNotification from "@/core/components/notifications/WarningNotification"; +import ErrorNotification from "@/core/components/notifications/ErrorNotification"; +import SuccessNotification from "@/core/components/notifications/SuccessNotification"; -const Notifications = async (t, response) => { - const {status, data} = response != undefined ? response : "" - toast.dismiss(); - switch (status) { - case 200: - SuccessNotification(t, status); +const Notifications = (pushToastList, notificationType, t, status, message) => { + switch (notificationType) { + case "pending": + pendingNotification(pushToastList, notificationType, t); break; - case 400: - ErrorNotification(t, status); + case "warning": + WarningNotification(pushToastList, notificationType, t, status); break; - case 401: - ErrorNotification(t, status); + case "error": + if (message) { + ErrorNotification(pushToastList, notificationType, t, status, message) + } else { + ErrorNotification(pushToastList, notificationType, t, status) + } break; - case 403: - ErrorNotification(t, status); - break; - case 422: - ErrorNotification(t, status, data.message); - break; - case 500: - WarningNotification(t, status); - break; - case 503: - WarningNotification(t, status); - break; - case 504: - WarningNotification(t, status); - break; - default: - toast(t("notifications.pending"), { - autoClose: false, - closeOnClick: false, - draggable: false, - }); + case "success": + SuccessNotification(pushToastList, notificationType, t, status); break; } }; export default Notifications; - -/* -usage document - -** for pending use ( Notifications( t, undefined) ) this before your request. -** for success use ( Notifications( t, response) ) this inside .then() of your request. -** for Error and Warning use ( Notifications( t, error.response) ) this inside .catche() of your request. - -end usage document -*/ diff --git a/src/core/data/sidebarMenu.jsx b/src/core/data/sidebarMenu.jsx index 5d0386c..09b81da 100644 --- a/src/core/data/sidebarMenu.jsx +++ b/src/core/data/sidebarMenu.jsx @@ -21,7 +21,7 @@ const sidebarMenu = [ key: "sidebar.dashboard", type: "page", route: "/dashboard", - icon: , + icon: , selected: false, permission: "all", }, @@ -40,7 +40,7 @@ const sidebarMenu = [ name: "passenger_office_chief", type: "page", route: "/dashboard/passenger-office-chief", - icon: , + icon: , selected: false, permission: "manage_passenger_office_navgan", }, @@ -49,7 +49,7 @@ const sidebarMenu = [ name: "machinery_expert", type: "page", route: "/dashboard/machinery-expert", - icon: , + icon: , selected: false, permission: "manage_machinery_navgan", }, @@ -59,7 +59,7 @@ const sidebarMenu = [ name: "province_working_group", type: "page", route: "/dashboard/passenger-boss", - icon: , + icon: , selected: false, permission: "manage_province_working_group_navgan", }, @@ -69,7 +69,7 @@ const sidebarMenu = [ name: "transportation_assistant", type: "page", route: "/dashboard/transportation-assistant", - icon: , + icon: , selected: false, permission: "manage_transportation_navgan", }, @@ -79,7 +79,7 @@ const sidebarMenu = [ name: "province_manager_navgan", type: "page", route: "/dashboard/navgan-province-manager", - icon: , + icon: , selected: false, permission: "manage_province_affairs_navgan", }, @@ -88,7 +88,7 @@ const sidebarMenu = [ name: "province_head_expert", type: "page", route: "/dashboard/province-head-expert", - icon: , + icon: , selected: false, permission: "manage_province_headquarter_refahi", }, @@ -97,7 +97,7 @@ const sidebarMenu = [ name: "inspector_expert", type: "page", route: "/dashboard/inspector-expert", - icon: , + icon: , selected: false, permission: "manage_inspector_refahi", }, @@ -106,7 +106,7 @@ const sidebarMenu = [ name: "commercial_chief", type: "page", route: "/dashboard/commercial-chief", - icon: , + icon: , selected: false, permission: "manage_commercial_refahi", }, @@ -115,7 +115,7 @@ const sidebarMenu = [ name: "development_assistant", type: "page", route: "/dashboard/development-assistant", - icon: , + icon: , selected: false, permission: "manage_development_refahi", }, @@ -125,7 +125,7 @@ const sidebarMenu = [ name: "province_manager_refahi", type: "page", route: "/dashboard/refahi-province-manager", - icon: , + icon: , selected: false, permission: "manage_province_affairs_refahi", }, @@ -135,7 +135,7 @@ const sidebarMenu = [ name: "refahi_loan_management", type: "page", route: "/dashboard/refahi-loan-management", - icon: , + icon: , selected: false, permission: "manage_refahi_loan", }, @@ -145,7 +145,7 @@ const sidebarMenu = [ name: "navgan_loan_management", type: "page", route: "/dashboard/navgan-loan-management", - icon: , + icon: , selected: false, permission: "manage_navgan_loan", }, @@ -154,7 +154,7 @@ const sidebarMenu = [ name: "expert_management", type: "page", route: "/dashboard/expert-management", - icon: , + icon: , selected: false, permission: "manage_experts", }, @@ -163,7 +163,7 @@ const sidebarMenu = [ name: "user_management", type: "page", route: "/dashboard/user-management", - icon: , + icon: , selected: false, permission: "manage_users", }, @@ -172,7 +172,7 @@ const sidebarMenu = [ name: "role_management", type: "page", route: "/dashboard/role-management", - icon: , + icon: , selected: false, permission: "manage_roles", }, diff --git a/src/core/utils/errorHandler.js b/src/core/utils/errorHandler.js index 8dbd364..e380e84 100644 --- a/src/core/utils/errorHandler.js +++ b/src/core/utils/errorHandler.js @@ -1,45 +1,48 @@ -import ErrorNotification from "@/core/components/notifications/ErrorNotification"; -import WarningNotification from "@/core/components/notifications/WarningNotification"; -import {toast} from "react-toastify"; +import Notifications from "@/core/components/notifications"; -export const errorSetting = (t, notification) => { - if (notification) toast.dismiss(); +export const errorSetting = (dismissToastList, t, notification) => { + if (notification) { + dismissToastList(["pending", "warning", "error", "success"]) + } } - -export const errorRequest = (t, notification) => { - if (notification) toast.dismiss(); -} - -export const errorResponse = (response, clearToken, t, notification) => { - if (notification) toast.dismiss(); - if (isServerError(response.status)) { - errorServer(response, t, notification) - } else if (isClientError(response.status)) { - errorClient(response, clearToken, t, notification) +export const errorRequest = (dismissToastList, t, notification) => { + if (notification) { + dismissToastList(["pending", "warning", "error", "success"]) } } -const errorServer = (response, t, notification) => { - if (notification) WarningNotification(t, response.status); +export const errorResponse = (pushToastList, dismissToastList, response, clearToken, t, notification) => { + if (notification) { + dismissToastList(["pending", "warning", "error", "success"]) + } + if (isServerError(response.status)) { + errorServer(pushToastList, response, t, notification) + } else if (isClientError(response.status)) { + errorClient(pushToastList, response, clearToken, t, notification) + } } -const errorClient = (response, clearToken, t, notification) => { + +const errorServer = (pushToastList, response, t, notification) => { + if (notification) Notifications(pushToastList, "warning", t, response.status); +} +const errorClient = (pushToastList, response, clearToken, t, notification) => { switch (response.status) { case 401: clearToken() - if (notification) ErrorNotification(t, response.status) + if (notification) Notifications(pushToastList, "error", t, response.status); break; case 422: if ('type' in response.data) { - errorLogic(response, t, notification) + errorLogic(pushToastList, response, t, notification) break; } - errorValidation(response, t, notification) + errorValidation(pushToastList, response, t, notification) break; case 429: - if (notification) ErrorNotification(t, response.status) + if (notification) Notifications(pushToastList, "error", t, response.status); break default: - if (notification) ErrorNotification(t, response.status) + if (notification) Notifications(pushToastList, "error", t, response.status); break } } @@ -47,16 +50,16 @@ const errorClient = (response, clearToken, t, notification) => { const isServerError = status => status >= 500 && status <= 599; const isClientError = status => status >= 400 && status <= 499; -const errorLogic = (response, t, notification) => { - if (notification) ErrorNotification(t, response.status, response.data.message) +const errorLogic = (pushToastList, response, t, notification) => { + if (notification) Notifications(pushToastList, "error", t, response.status, response.data.message); } -const errorValidation = (response, t, notification) => { +const errorValidation = (pushToastList, response, t, notification) => { if (notification) { const errorsMap = Object.keys(response.data.errors) const errorsArray = response.data.errors errorsMap.map((item, index) => { - ErrorNotification(t, response.status, errorsArray[item][0]); + Notifications(pushToastList, "error", t, response.status, errorsArray[item][0]); }) } } \ No newline at end of file diff --git a/src/core/utils/succesHandler.js b/src/core/utils/succesHandler.js index a61ac85..c852f86 100644 --- a/src/core/utils/succesHandler.js +++ b/src/core/utils/succesHandler.js @@ -1,9 +1,8 @@ -import SuccessNotification from "@/core/components/notifications/SuccessNotification"; -import {toast} from "react-toastify"; +import Notifications from "@/core/components/notifications"; -export const successRequest = (response, t, options) => { +export const successRequest = (pushToastList, dismissToastList, response, t, options) => { if (options.notification && options.success.notification.show) { - toast.dismiss(); - SuccessNotification(t, response.status) + dismissToastList(["pending", "warning", "error", "success"]) + Notifications(pushToastList, "success", t, response.status); } } \ No newline at end of file diff --git a/src/layouts/AppLayout.jsx b/src/layouts/AppLayout.jsx index 4fc5e1d..5c01881 100644 --- a/src/layouts/AppLayout.jsx +++ b/src/layouts/AppLayout.jsx @@ -47,7 +47,16 @@ function AppLayout({children, isBot}) { color={theme.palette.secondary.dark} options={{showSpinner: false}} /> - + + {children} ); diff --git a/src/layouts/PrintLayout.jsx b/src/layouts/PrintLayout.jsx new file mode 100644 index 0000000..301a161 --- /dev/null +++ b/src/layouts/PrintLayout.jsx @@ -0,0 +1,12 @@ +import WithAuthMiddleware from "@/middlewares/WithAuth"; +import Print from "@/components/layouts/Print"; + +const PrintLayout = (props) => { + return ( + + + + ) +} + +export default PrintLayout \ No newline at end of file diff --git a/src/layouts/index.jsx b/src/layouts/index.jsx index 1506661..a69d886 100644 --- a/src/layouts/index.jsx +++ b/src/layouts/index.jsx @@ -1,14 +1,16 @@ import DashboardLayout from "@/layouts/DashboardLayout"; import {Fragment} from "react"; +import PrintLayout from "@/layouts/PrintLayout"; const layoutList = { - DashboardLayout + DashboardLayout, + PrintLayout } const Layout = ({layout = {}, children}) => { const Component = layoutList[layout?.name] || Fragment - const props = layout.props || {} + const props = layout?.props || {} return ( diff --git a/src/lib/app/contexts/language.jsx b/src/lib/app/contexts/language.jsx index a31ffd9..243d7e8 100644 --- a/src/lib/app/contexts/language.jsx +++ b/src/lib/app/contexts/language.jsx @@ -1,8 +1,8 @@ import {FA_DATATABLE_LOCALIZATION} from "&/locales/fa/datatable"; -import {FA_CHART_LOCALIZATION} from "&/locales/fa/chart"; import {useRouter} from "next/router"; import {createContext, useEffect, useState} from "react"; import useUser from "../hooks/useUser"; +import {FA_CHART_LOCALIZATION} from "&/locales/fa/chart"; export const LanguageContext = createContext(); @@ -20,7 +20,7 @@ export const LanguageProvider = ({children}) => { ]; const {user, userChangedLanguage, changeLanguageState} = useUser(); const [languageIsReady, setLanguageIsReady] = useState(false); - const [languageApp, setLanguageApp] = useState(); + const [languageApp, setLanguageApp] = useState(process.env.NEXT_PUBLIC_DEFAULT_LANGUAGE); const [directionApp, setDirectionApp] = useState( process.env.NEXT_PUBLIC_DEFAULT_DIRECTION ); @@ -28,9 +28,7 @@ export const LanguageProvider = ({children}) => { useEffect(() => { const lang = localStorage.getItem("_language"); - if (!lang && !languageApp) { - setLanguageApp(process.env.NEXT_PUBLIC_DEFAULT_LANGUAGE); - } else if (lang) { + if (lang) { setLanguageApp(lang); } }, []); diff --git a/src/lib/app/contexts/print.jsx b/src/lib/app/contexts/print.jsx new file mode 100644 index 0000000..20fd1dc --- /dev/null +++ b/src/lib/app/contexts/print.jsx @@ -0,0 +1,24 @@ +import {createContext, useReducer} from "react"; + +export const PrintContext = createContext(); + +const reducer = (state, action) => { + switch (action.type) { + case "SET_PRINT_PAGE": + return { + ...state, count: action.count + }; + case "SET_PRINT_TITLE": + return { + ...state, title: action.title + }; + } +}; +export const PrintProvider = ({children}) => { + const [printDetails, printDispatch] = useReducer(reducer, {title: '', count: 0}); + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/lib/app/contexts/toast.jsx b/src/lib/app/contexts/toast.jsx new file mode 100644 index 0000000..b9d4cbc --- /dev/null +++ b/src/lib/app/contexts/toast.jsx @@ -0,0 +1,37 @@ +import {createContext, useReducer} from "react"; +import {toast} from "react-toastify"; + +export const ToastContext = createContext() + +const reducer = (state, action) => { + switch (action.type) { + case "PUSH": + return { + ...state, + [action.toast_type]: [...state[action.toast_type], action.toast_id] + }; + case "DISMISS": + action.toast_type.map((item) => { + state[item].map((id) => { + toast.dismiss(id); + }) + state[item] = [] + }); + return state; + } +}; + +export const ToastProvider = ({children}) => { + const [state, dispatch] = useReducer(reducer, { + pending: [], + error: [], + warning: [], + success: [] + }); + + return ( + + {children} + + ); +}; diff --git a/src/lib/app/contexts/user.jsx b/src/lib/app/contexts/user.jsx index 10611b8..7f5c97c 100644 --- a/src/lib/app/contexts/user.jsx +++ b/src/lib/app/contexts/user.jsx @@ -1,7 +1,6 @@ import {GET_USER_ROUTE} from "@/core/data/apiRoutes"; import axios from "axios"; import {createContext, useCallback, useEffect, useReducer} from "react"; -import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler"; const initialUser = { isAuth: false, @@ -78,13 +77,7 @@ export const UserProvider = ({children}) => { if (typeof callback === "function") callback(data); }) .catch(error => { - if (error.response) { - errorResponse(error.response, clearToken, null, false) - } else if (error.request) { - errorRequest(null, false) - } else { - errorSetting(null, false) - } + if (error.response.status === 401) clearToken() }) }, [state.token] diff --git a/src/lib/app/hooks/usePermissions.jsx b/src/lib/app/hooks/usePermissions.jsx index 0e6aa07..2e6fff5 100644 --- a/src/lib/app/hooks/usePermissions.jsx +++ b/src/lib/app/hooks/usePermissions.jsx @@ -11,15 +11,16 @@ const usePermissions = () => { }) }; - const {data} = useSWR(GET_PERMISSIONS_LIST, fetcher, { + const {data, isLoading} = useSWR(GET_PERMISSIONS_LIST, fetcher, { revalidateIfStale: false, revalidateOnFocus: false, - revalidateOnReconnect: false + revalidateOnReconnect: false, + keepPreviousData : true }) const permissions_list = data //swr config // render data - return {permissions_list} + return {permissions_list, isLoading} } export default usePermissions; \ No newline at end of file diff --git a/src/lib/app/hooks/usePrint.jsx b/src/lib/app/hooks/usePrint.jsx new file mode 100644 index 0000000..35a1017 --- /dev/null +++ b/src/lib/app/hooks/usePrint.jsx @@ -0,0 +1,18 @@ +import {useContext} from "react"; +import {PrintContext} from "@/lib/app/contexts/print"; + +const usePrint = () => { + const {printDetails, printDispatch} = useContext(PrintContext); + + const setPrintPage = (count) => { + printDispatch({type: 'SET_PRINT_PAGE', count}) + } + + const setPrintTitle = (title) => { + printDispatch({type: 'SET_PRINT_TITLE', title}) + } + + return {printDetails, setPrintPage, setPrintTitle} +}; + +export default usePrint; \ No newline at end of file diff --git a/src/lib/app/hooks/useRequest.jsx b/src/lib/app/hooks/useRequest.jsx index d361e08..4c3be1d 100644 --- a/src/lib/app/hooks/useRequest.jsx +++ b/src/lib/app/hooks/useRequest.jsx @@ -1,10 +1,11 @@ import axios from "axios"; import {successRequest} from "@/core/utils/succesHandler"; -import PendingNotification from "@/core/components/notifications/PendingNotification"; import {useTranslations} from "next-intl"; import useUser from "@/lib/app/hooks/useUser"; import {errorRequest, errorResponse, errorSetting} from "@/core/utils/errorHandler"; import useNetwork from "@/lib/app/hooks/useNetwork"; +import Notifications from "@/core/components/notifications"; +import useToast from "@/lib/app/hooks/useToast"; const defaultOptions = { auth: false, data: {}, requestOptions: { @@ -23,6 +24,7 @@ const useRequest = (initOptions) => { const network = useNetwork() const t = useTranslations() const {token, clearToken} = useUser() + const {pushToastList, dismissToastList} = useToast(); let _options = {...defaultOptions, ...initOptions} function requestServer(url = '', method = 'get', options) { @@ -33,27 +35,30 @@ const useRequest = (initOptions) => { headers: {..._options.requestOptions.headers, authorization: `Bearer ${token}`} } } - return new Promise((resolve, reject) => { if (!network.online) { reject() return } - if (_options.notification && _options.failed.notification.show && _options.pending) PendingNotification(t) + if (_options.notification && _options.failed.notification.show && _options.pending) { + dismissToastList(["pending", "warning", "error", "success"]); + Notifications(pushToastList, "pending", t); + } + axios({ url: url, method: method, data: _options.data, ..._options.requestOptions }) .then(response => { - successRequest(response, t, _options) + successRequest(pushToastList, dismissToastList, response, t, _options) resolve(response) }) .catch(error => { if (error.response) { - errorResponse(error.response, clearToken, t, _options.notification && _options.failed.notification.show) + errorResponse(pushToastList, dismissToastList, error.response, clearToken, t, _options.notification && _options.failed.notification.show) } else if (error.request) { - errorRequest(t, _options.notification && _options.failed.notification.show) + errorRequest(dismissToastList, t, _options.notification && _options.failed.notification.show) } else { - errorSetting(t, _options.notification && _options.failed.notification.show) + errorSetting(dismissToastList, t, _options.notification && _options.failed.notification.show) } reject(error) }) diff --git a/src/lib/app/hooks/useToast.jsx b/src/lib/app/hooks/useToast.jsx new file mode 100644 index 0000000..5e1d5b4 --- /dev/null +++ b/src/lib/app/hooks/useToast.jsx @@ -0,0 +1,21 @@ +import {useContext} from "react"; +import {ToastContext} from "@/lib/app/contexts/toast"; + +const useToast = () => { + const {dispatch} = useContext(ToastContext); + + const pushToastList = (toast_type, toast_id) => { + dispatch({type: "PUSH", toast_type, toast_id}); + }; + + const dismissToastList = (toast_type) => { + dispatch({type: "DISMISS", toast_type}); + }; + + return { + pushToastList, + dismissToastList + } +}; + +export default useToast; \ No newline at end of file diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 342f2a4..9824687 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -8,6 +8,8 @@ import "moment/locale/fa"; import {NextIntlProvider} from "next-intl"; import TitlePage from "@/core/components/TitlePage"; import Layout from "@/layouts"; +import {ToastProvider} from "@/lib/app/contexts/toast"; +import {PrintProvider} from "@/lib/app/contexts/print"; const App = ({Component, pageProps}) => { @@ -15,15 +17,19 @@ const App = ({Component, pageProps}) => { <> - + {pageProps.messages ? : ''} - - - - - + + + + + + + + + diff --git a/src/pages/dashboard/change-password/index.jsx b/src/pages/dashboard/change-password/index.jsx index d23721d..ef75fb7 100644 --- a/src/pages/dashboard/change-password/index.jsx +++ b/src/pages/dashboard/change-password/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.change_password", isBot, + locale, layout: {name: 'DashboardLayout'} }, }; diff --git a/src/pages/dashboard/commercial-chief/index.jsx b/src/pages/dashboard/commercial-chief/index.jsx index e5dc96c..1ec5fbf 100644 --- a/src/pages/dashboard/commercial-chief/index.jsx +++ b/src/pages/dashboard/commercial-chief/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.commercial_chief_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_commercial_refahi"]}} }, }; diff --git a/src/pages/dashboard/development-assistant/index.jsx b/src/pages/dashboard/development-assistant/index.jsx index 6f1a0ff..346a340 100644 --- a/src/pages/dashboard/development-assistant/index.jsx +++ b/src/pages/dashboard/development-assistant/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.development_assistant_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_development_refahi"]}} }, }; diff --git a/src/pages/dashboard/edit-profile/index.jsx b/src/pages/dashboard/edit-profile/index.jsx index dede28e..032a607 100644 --- a/src/pages/dashboard/edit-profile/index.jsx +++ b/src/pages/dashboard/edit-profile/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.edit_profile", isBot, + locale, layout: {name: 'DashboardLayout'} }, }; diff --git a/src/pages/dashboard/expert-management/index.jsx b/src/pages/dashboard/expert-management/index.jsx index 2db2389..5f1c29a 100644 --- a/src/pages/dashboard/expert-management/index.jsx +++ b/src/pages/dashboard/expert-management/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.expert_management", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_experts"]}} }, }; diff --git a/src/pages/dashboard/index.jsx b/src/pages/dashboard/index.jsx index f3cf481..0e309c3 100644 --- a/src/pages/dashboard/index.jsx +++ b/src/pages/dashboard/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.dashboard_page", isBot, + locale, layout: {name: 'DashboardLayout'} }, }; diff --git a/src/pages/dashboard/inspector-expert/index.jsx b/src/pages/dashboard/inspector-expert/index.jsx index f953f30..c2d14f8 100644 --- a/src/pages/dashboard/inspector-expert/index.jsx +++ b/src/pages/dashboard/inspector-expert/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.inspector_expert_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_inspector_refahi"]}} }, }; diff --git a/src/pages/dashboard/machinery-expert/index.jsx b/src/pages/dashboard/machinery-expert/index.jsx index 5b2f945..df23f2e 100644 --- a/src/pages/dashboard/machinery-expert/index.jsx +++ b/src/pages/dashboard/machinery-expert/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.machinary_office_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_machinery_navgan"]}} }, }; diff --git a/src/pages/dashboard/machinery-expert/prints/report.jsx b/src/pages/dashboard/machinery-expert/prints/report.jsx new file mode 100644 index 0000000..526851b --- /dev/null +++ b/src/pages/dashboard/machinery-expert/prints/report.jsx @@ -0,0 +1,21 @@ +import {parse} from "next-useragent"; +import ReportComponent from "@/components/dashboard/machinary-office/Prints/report"; + +export default function Report() { + 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.machinary_office_page", + isBot, + locale, + layout: {name: 'PrintLayout'} + }, + }; +} \ No newline at end of file diff --git a/src/pages/dashboard/navgan-loan-management/index.jsx b/src/pages/dashboard/navgan-loan-management/index.jsx index 61922ad..792ea3e 100644 --- a/src/pages/dashboard/navgan-loan-management/index.jsx +++ b/src/pages/dashboard/navgan-loan-management/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.loan_management_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_navgan_loan"]}} }, }; diff --git a/src/pages/dashboard/navgan-province-manager/index.jsx b/src/pages/dashboard/navgan-province-manager/index.jsx index b0c0be6..39102cb 100644 --- a/src/pages/dashboard/navgan-province-manager/index.jsx +++ b/src/pages/dashboard/navgan-province-manager/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.province_manager_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_province_affairs_navgan"]}} }, }; diff --git a/src/pages/dashboard/passenger-boss/index.jsx b/src/pages/dashboard/passenger-boss/index.jsx index a549f74..c38b95f 100644 --- a/src/pages/dashboard/passenger-boss/index.jsx +++ b/src/pages/dashboard/passenger-boss/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.passenger_boss_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_province_working_group_navgan"]}} }, }; diff --git a/src/pages/dashboard/passenger-boss/prints/final-credit-amount.jsx b/src/pages/dashboard/passenger-boss/prints/final-credit-amount.jsx new file mode 100644 index 0000000..0bd763b --- /dev/null +++ b/src/pages/dashboard/passenger-boss/prints/final-credit-amount.jsx @@ -0,0 +1,21 @@ +import {parse} from "next-useragent"; +import FinalCreditAmountComponent from "@/components/dashboard/passenger-boss/Prints/final-credit-amount"; + +export default function FinalCreditAmount() { + 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.passenger_boss_page", + isBot, + locale, + layout: {name: 'PrintLayout'} + }, + }; +} \ No newline at end of file diff --git a/src/pages/dashboard/passenger-office-chief/index.jsx b/src/pages/dashboard/passenger-office-chief/index.jsx index 03aa612..5ca084e 100644 --- a/src/pages/dashboard/passenger-office-chief/index.jsx +++ b/src/pages/dashboard/passenger-office-chief/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.passenger_office_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_passenger_office_navgan"]}} }, }; diff --git a/src/pages/dashboard/province-head-expert/index.jsx b/src/pages/dashboard/province-head-expert/index.jsx index 64bffb1..b556d20 100644 --- a/src/pages/dashboard/province-head-expert/index.jsx +++ b/src/pages/dashboard/province-head-expert/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.province_head_expert", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_province_headquarter_refahi"]}} }, }; diff --git a/src/pages/dashboard/refahi-loan-management/index.jsx b/src/pages/dashboard/refahi-loan-management/index.jsx index 1ae6954..4aab9a0 100644 --- a/src/pages/dashboard/refahi-loan-management/index.jsx +++ b/src/pages/dashboard/refahi-loan-management/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.loan_management_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_refahi_loan"]}} }, }; diff --git a/src/pages/dashboard/refahi-province-manager/index.jsx b/src/pages/dashboard/refahi-province-manager/index.jsx index 9922b50..cc5c1eb 100644 --- a/src/pages/dashboard/refahi-province-manager/index.jsx +++ b/src/pages/dashboard/refahi-province-manager/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.province_manager_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_province_affairs_refahi"]}} }, }; diff --git a/src/pages/dashboard/role-management/index.jsx b/src/pages/dashboard/role-management/index.jsx index 97238db..7ae6d45 100644 --- a/src/pages/dashboard/role-management/index.jsx +++ b/src/pages/dashboard/role-management/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.role_management_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_roles"]}} }, }; diff --git a/src/pages/dashboard/transportation-assistant/index.jsx b/src/pages/dashboard/transportation-assistant/index.jsx index eccb330..7c0b4b6 100644 --- a/src/pages/dashboard/transportation-assistant/index.jsx +++ b/src/pages/dashboard/transportation-assistant/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.transportation_assistance", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_transportation_navgan"]}} }, }; diff --git a/src/pages/dashboard/user-management/index.jsx b/src/pages/dashboard/user-management/index.jsx index f0f9b24..e92c2b2 100644 --- a/src/pages/dashboard/user-management/index.jsx +++ b/src/pages/dashboard/user-management/index.jsx @@ -14,6 +14,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Dashboard.user_management_page", isBot, + locale, layout: {name: 'DashboardLayout', props: {permissions: ["manage_users"]}} }, }; diff --git a/src/pages/index.jsx b/src/pages/index.jsx index 2e146c6..996ba03 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -16,6 +16,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "first_page", isBot, + locale }, }; } diff --git a/src/pages/login-expert.jsx b/src/pages/login-expert.jsx index 1cc6abf..393f916 100644 --- a/src/pages/login-expert.jsx +++ b/src/pages/login-expert.jsx @@ -17,6 +17,7 @@ export async function getServerSideProps({req, locale}) { messages: (await import(`&/locales/${locale}/app.json`)).default, title: "Titles.title_login_expert_page", isBot, + locale }, }; }