Compare commits
80 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2f91d31816 | |||
| 40437079c9 | |||
| 605114e0e4 | |||
| 0ce0b608a4 | |||
| 457bef64c4 | |||
| f78a2c3cbb | |||
| 2c7679e52b | |||
| 42194d2030 | |||
| 6520dd0365 | |||
| 6cf30a591c | |||
| 1680cb44b7 | |||
| 51afdd47a0 | |||
| f0d9a17e01 | |||
| 3308f14d49 | |||
| 7f77165ec4 | |||
| 0435654762 | |||
| f94374958d | |||
| a26f07d2ad | |||
| d1b74585d7 | |||
| 6115e0ded2 | |||
| 40f80fac3a | |||
| 188c7c45af | |||
| f6c369f866 | |||
| a05fa5d0d7 | |||
| 69677a0a63 | |||
| b5c7501192 | |||
| 42a0f3ad1c | |||
| 0689fb307a | |||
| 09c7385faa | |||
| 3745206604 | |||
| f42a4ff63b | |||
| 18674a5b75 | |||
| 68e7fd8cd1 | |||
| e66af994af | |||
| 1e0d3dcf0e | |||
| 92e3505bd9 | |||
| 241f583572 | |||
| 3ea5c940c0 | |||
| 806854327a | |||
| a80e35a510 | |||
| cb85157cf7 | |||
| caaff22c66 | |||
| a9a996e1e8 | |||
| ca20435807 | |||
| 61ed1980f1 | |||
| 93478c8c80 | |||
| a9a6d90281 | |||
| 376ea3a3e8 | |||
| e7e8f3e5cc | |||
| e7e704aaf7 | |||
| 9c2a22e012 | |||
| acc013ad81 | |||
| 1261c9dcc2 | |||
| a540d79715 | |||
| c6283ae130 | |||
| 1ad74cfed7 | |||
| be0922cfcc | |||
| dec752bc17 | |||
| 22032b885c | |||
| f7d177bb95 | |||
| 0d6269004e | |||
| 05dc5505cd | |||
| a975bd1f88 | |||
| 01bb171ebb | |||
| 5f88728d67 | |||
| 855afc05e5 | |||
| e3ca4a2052 | |||
| 5fb97a5e79 | |||
| 63ec584aca | |||
| 70ea71cbfa | |||
| 6d8e9ddb5b | |||
| 16889752a1 | |||
| 5c05af0828 | |||
| 8753f23146 | |||
| 9ee9132ed8 | |||
| 46bb4e4228 | |||
| efb260b66d | |||
| a798a36fcd | |||
| 5d9a86f2cf | |||
| 6b70eb8e27 |
@@ -1,5 +1,5 @@
|
|||||||
HOST="rms.witel.ir"
|
HOST="rms.witel.ir"
|
||||||
HOST_RMTO="rms.rmto.ir"
|
HOST_RMTO="rms.rmto.ir"
|
||||||
NEXT_PUBLIC_VERSION="1.8.8"
|
NEXT_PUBLIC_VERSION="1.9.2"
|
||||||
NEXT_PUBLIC_API_URL="https://rms.witel.ir"
|
NEXT_PUBLIC_API_URL="https://rms.witel.ir"
|
||||||
NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map"
|
NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map"
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"@emotion/react": "^11.11.3",
|
"@emotion/react": "^11.11.3",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@hookform/resolvers": "^3.9.0",
|
"@hookform/resolvers": "^3.9.0",
|
||||||
|
"@mapbox/polyline": "^1.2.1",
|
||||||
"@mui/icons-material": "^5.16.7",
|
"@mui/icons-material": "^5.16.7",
|
||||||
"@mui/lab": "^5.0.0-alpha.173",
|
"@mui/lab": "^5.0.0-alpha.173",
|
||||||
"@mui/material": "^5.16.7",
|
"@mui/material": "^5.16.7",
|
||||||
|
|||||||
18
public/icons/destination-icon.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="20" height="23" viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<foreignObject x="-63" y="-64" width="146" height="149"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(32.5px);clip-path:url(#bgblur_0_2302_1310_clip_path);height:100%;width:100%"></div></foreignObject><g filter="url(#filter0_d_2302_1310)" data-figma-bg-blur-radius="65">
|
||||||
|
<path d="M9.38305 1.02444C15.543 0.551472 19.9197 7.03396 17.1518 12.6025C15.8547 15.2112 13.6759 15.7958 11.9558 17.881C11.6271 18.2796 10.7507 19.7126 10.4472 19.8829C9.98265 20.1439 9.56634 19.9365 9.25987 19.5758C8.76416 18.992 8.37012 18.2543 7.83732 17.6549C6.56763 16.2249 4.84751 15.4187 3.69508 13.9418C-0.231962 8.90724 3.07767 1.50856 9.38305 1.02444ZM9.55002 5.15175C6.61587 5.45517 5.13915 8.92434 6.86743 11.3212C8.38571 13.4272 11.6293 13.425 13.1468 11.3212C15.1415 8.554 12.925 4.80298 9.55076 5.15175H9.55002Z" fill="#EF4444"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_2302_1310" x="-63" y="-64" width="146" height="149" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2302_1310"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2302_1310" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_2302_1310_clip_path" transform="translate(63 64)"><path d="M9.38305 1.02444C15.543 0.551472 19.9197 7.03396 17.1518 12.6025C15.8547 15.2112 13.6759 15.7958 11.9558 17.881C11.6271 18.2796 10.7507 19.7126 10.4472 19.8829C9.98265 20.1439 9.56634 19.9365 9.25987 19.5758C8.76416 18.992 8.37012 18.2543 7.83732 17.6549C6.56763 16.2249 4.84751 15.4187 3.69508 13.9418C-0.231962 8.90724 3.07767 1.50856 9.38305 1.02444ZM9.55002 5.15175C6.61587 5.45517 5.13915 8.92434 6.86743 11.3212C8.38571 13.4272 11.6293 13.425 13.1468 11.3212C15.1415 8.554 12.925 4.80298 9.55076 5.15175H9.55002Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
18
public/icons/origin-icon.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<foreignObject x="-42.6271" y="-43.6271" width="105.045" height="105.045"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(22.31px);clip-path:url(#bgblur_0_2302_1307_clip_path);height:100%;width:100%"></div></foreignObject><g filter="url(#filter0_d_2302_1307)" data-figma-bg-blur-radius="44.6271">
|
||||||
|
<path d="M10.3018 1.00977C14.4733 1.22136 17.7908 4.67141 17.791 8.89551C17.7908 13.2559 14.2559 16.7908 9.89551 16.791C5.67141 16.7908 2.22136 13.4733 2.00977 9.30176L2 8.89551L2.00977 8.48926C2.22119 4.31755 5.6713 1.00017 9.89551 1L10.3018 1.00977ZM9.89551 5.80566C8.18945 5.80584 6.80584 7.18945 6.80566 8.89551L6.80957 9.05371C6.89208 10.6863 8.2425 11.9842 9.89551 11.9844C11.5485 11.9842 12.898 10.6863 12.9805 9.05371L12.9844 8.89551L12.9805 8.73633C12.9003 7.1565 11.6336 5.88942 10.0537 5.80957L9.89551 5.80566Z" fill="#16A795"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_2302_1307" x="-42.6271" y="-43.6271" width="105.045" height="105.045" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="0.686571"/>
|
||||||
|
<feGaussianBlur stdDeviation="0.686571"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2302_1307"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2302_1307" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_2302_1307_clip_path" transform="translate(42.6271 43.6271)"><path d="M10.3018 1.00977C14.4733 1.22136 17.7908 4.67141 17.791 8.89551C17.7908 13.2559 14.2559 16.7908 9.89551 16.791C5.67141 16.7908 2.22136 13.4733 2.00977 9.30176L2 8.89551L2.00977 8.48926C2.22119 4.31755 5.6713 1.00017 9.89551 1L10.3018 1.00977ZM9.89551 5.80566C8.18945 5.80584 6.80584 7.18945 6.80566 8.89551L6.80957 9.05371C6.89208 10.6863 8.2425 11.9842 9.89551 11.9844C11.5485 11.9842 12.898 10.6863 12.9805 9.05371L12.9844 8.89551L12.9805 8.73633C12.9003 7.1565 11.6336 5.88942 10.0537 5.80957L9.89551 5.80566Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
"use client";
|
||||||
|
import PageLoading from "@/core/components/PageLoading";
|
||||||
|
|
||||||
|
const Loading = () => {
|
||||||
|
return <PageLoading />;
|
||||||
|
};
|
||||||
|
export default Loading;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import WithPermission from "@/core/middlewares/withPermission";
|
||||||
|
import MachinesPage from "@/components/dashboard/machines";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "کارتابل ماشینها",
|
||||||
|
};
|
||||||
|
const Page = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WithPermission permission_name={["all"]}>
|
||||||
|
<MachinesPage />
|
||||||
|
</WithPermission>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import WithPermission from "@/core/middlewares/withPermission";
|
||||||
|
import DriverReportsPage from "@/components/dashboard/roadMissions/reports/diverReports";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "کارتابل گزارش راننده",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<WithPermission
|
||||||
|
permission_name={[
|
||||||
|
"all", // TODO : change permission
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<DriverReportsPage />
|
||||||
|
</WithPermission>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import WithPermission from "@/core/middlewares/withPermission";
|
||||||
|
import MachinesTypeReportsPage from "@/components/dashboard/roadMissions/reports/machinesTypeReports";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "کارتابل گزارش نوع ماشینها",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<WithPermission
|
||||||
|
permission_name={[
|
||||||
|
"all", // TODO : change permission
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MachinesTypeReportsPage />
|
||||||
|
</WithPermission>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import MachinesReportsPage from "@/components/dashboard/roadMissions/reports/machinesReports";
|
||||||
|
import WithPermission from "@/core/middlewares/withPermission";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "کارتابل گزارش ماشینها",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Page() {
|
||||||
|
return (
|
||||||
|
<WithPermission
|
||||||
|
permission_name={[
|
||||||
|
"all", // TODO : change permission
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MachinesReportsPage />
|
||||||
|
</WithPermission>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
src/assets/images/destination-icon.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="20" height="23" viewBox="0 0 20 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<foreignObject x="-63" y="-64" width="146" height="149"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(32.5px);clip-path:url(#bgblur_0_2302_1310_clip_path);height:100%;width:100%"></div></foreignObject><g filter="url(#filter0_d_2302_1310)" data-figma-bg-blur-radius="65">
|
||||||
|
<path d="M9.38305 1.02444C15.543 0.551472 19.9197 7.03396 17.1518 12.6025C15.8547 15.2112 13.6759 15.7958 11.9558 17.881C11.6271 18.2796 10.7507 19.7126 10.4472 19.8829C9.98265 20.1439 9.56634 19.9365 9.25987 19.5758C8.76416 18.992 8.37012 18.2543 7.83732 17.6549C6.56763 16.2249 4.84751 15.4187 3.69508 13.9418C-0.231962 8.90724 3.07767 1.50856 9.38305 1.02444ZM9.55002 5.15175C6.61587 5.45517 5.13915 8.92434 6.86743 11.3212C8.38571 13.4272 11.6293 13.425 13.1468 11.3212C15.1415 8.554 12.925 4.80298 9.55076 5.15175H9.55002Z" fill="#EF4444"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_2302_1310" x="-63" y="-64" width="146" height="149" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2302_1310"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2302_1310" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_2302_1310_clip_path" transform="translate(63 64)"><path d="M9.38305 1.02444C15.543 0.551472 19.9197 7.03396 17.1518 12.6025C15.8547 15.2112 13.6759 15.7958 11.9558 17.881C11.6271 18.2796 10.7507 19.7126 10.4472 19.8829C9.98265 20.1439 9.56634 19.9365 9.25987 19.5758C8.76416 18.992 8.37012 18.2543 7.83732 17.6549C6.56763 16.2249 4.84751 15.4187 3.69508 13.9418C-0.231962 8.90724 3.07767 1.50856 9.38305 1.02444ZM9.55002 5.15175C6.61587 5.45517 5.13915 8.92434 6.86743 11.3212C8.38571 13.4272 11.6293 13.425 13.1468 11.3212C15.1415 8.554 12.925 4.80298 9.55076 5.15175H9.55002Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
21
src/assets/images/prev-destination-icon.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="42" height="66" viewBox="0 0 42 66" fill="#F15152" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.3" d="M21 66C23.7614 66 26 64.8807 26 63.5C26 62.1193 23.7614 61 21 61C18.2386 61 16 62.1193 16 63.5C16 64.8807 18.2386 66 21 66Z" fill="#F15152"/>
|
||||||
|
<path fill-rule="#F15152" clip-rule="evenodd" d="M21 0C32.598 0 42 9.40202 42 21C42 31.9234 33.6599 40.8989 23 41.906V62C23 63.1046 22.1046 64 21 64C19.8954 64 19 63.1046 19 62V41.906C8.34014 40.8989 0 31.9234 0 21C0 9.40202 9.40202 0 21 0Z" fill="#F15152"/>
|
||||||
|
<foreignObject x="-61" y="-61" width="164" height="164"></foreignObject><g filter="url(#filter0_d_173_69)" data-figma-bg-blur-radius="65">
|
||||||
|
<path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z" fill="#F15152"/>
|
||||||
|
</g>
|
||||||
|
<path d="M28 21C28 17.134 24.866 14 21 14C17.134 14 14 17.134 14 21C14 24.866 17.134 28 21 28C24.866 28 28 24.866 28 21Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_173_69" x="-61" y="-61" width="164" height="164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_173_69"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_173_69" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_173_69_clip_path" transform="translate(61 61)"><path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
21
src/assets/images/prev-source-icon.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="42" height="66" viewBox="0 0 42 66" fill="#2A8A7E" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.3" d="M21 66C23.7614 66 26 64.8807 26 63.5C26 62.1193 23.7614 61 21 61C18.2386 61 16 62.1193 16 63.5C16 64.8807 18.2386 66 21 66Z" fill="#2A8A7E"/>
|
||||||
|
<path fill-rule="#2A8A7E" clip-rule="evenodd" d="M21 0C32.598 0 42 9.40202 42 21C42 31.9234 33.6599 40.8989 23 41.906V62C23 63.1046 22.1046 64 21 64C19.8954 64 19 63.1046 19 62V41.906C8.34014 40.8989 0 31.9234 0 21C0 9.40202 9.40202 0 21 0Z" fill="#2A8A7E"/>
|
||||||
|
<foreignObject x="-61" y="-61" width="164" height="164"></foreignObject><g filter="url(#filter0_d_173_69)" data-figma-bg-blur-radius="65">
|
||||||
|
<path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z" fill="#2A8A7E"/>
|
||||||
|
</g>
|
||||||
|
<path d="M28 21C28 17.134 24.866 14 21 14C17.134 14 14 17.134 14 21C14 24.866 17.134 28 21 28C24.866 28 28 24.866 28 21Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_173_69" x="-61" y="-61" width="164" height="164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_173_69"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_173_69" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_173_69_clip_path" transform="translate(61 61)"><path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
20
src/assets/images/prev-waypoint-icon.svg
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<svg width="30" height="35" viewBox="0 0 30 35" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M14.4453 1.5127C24.4711 1.08082 31.4173 11.6767 27.2842 20.9209L27.0752 21.3672C25.9614 23.6466 24.4616 25.0302 23.0117 26.2031C21.6264 27.3238 20.4204 28.1673 19.3184 29.4482L19.0986 29.71C18.9038 29.9504 18.5282 30.5355 18.0273 31.2695C17.8006 31.6018 17.5566 31.95 17.332 32.2393C17.2202 32.3833 17.0974 32.5329 16.9727 32.666C16.8723 32.7731 16.6754 32.9748 16.4141 33.124L16.4131 33.123C15.7088 33.5252 14.941 33.6027 14.2256 33.3691C13.5676 33.1543 13.0766 32.7195 12.7383 32.3145V32.3135C12.3056 31.795 11.9245 31.2168 11.5986 30.7314C11.3403 30.3467 11.107 30.0062 10.8682 29.7012L10.626 29.4072C9.78615 28.4447 8.79047 27.6829 7.65723 26.7939C6.63272 25.9903 5.49745 25.0829 4.54102 23.9053L4.35254 23.666C-2.23923 15.0669 3.27657 2.37649 13.958 1.54199L14.4453 1.5127ZM14.4834 10.8291H14.4795C11.4118 11.1538 9.65384 14.8452 11.3496 17.6201L11.5254 17.8857L11.6885 18.1016C13.3725 20.1841 16.6502 20.1828 18.334 18.1006L18.4961 17.8857C20.762 14.6868 18.2266 10.4356 14.4834 10.8291Z" stroke="white" stroke-width="3"/>
|
||||||
|
<foreignObject x="-62" y="-62" width="154" height="159"></foreignObject><g filter="url(#filter0_d_225_89)">
|
||||||
|
<path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z" fill="#FFC107"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_225_89" x="-62" y="-62" width="154" height="159" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_225_89"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_225_89" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_225_89_clip_path" transform="translate(62 62)"><path d="M14.0746 3.0373C23.3145 2.3154 29.8796 12.2097 25.7277 20.7091C23.782 24.6909 20.5139 25.583 17.9337 28.7657C17.4406 29.3741 16.126 31.5614 15.6708 31.8213C14.974 32.2197 14.3495 31.903 13.8898 31.3525C13.1462 30.4615 12.5552 29.3355 11.756 28.4207C9.85145 26.2379 7.27127 25.0075 5.54262 22.7533C-0.347943 15.0689 4.61651 3.77623 14.0746 3.0373ZM14.325 9.33689C9.9238 9.79999 7.70872 15.095 10.3011 18.7534C12.5786 21.9678 17.444 21.9644 19.7203 18.7534C22.7123 14.5298 19.3874 8.80455 14.3261 9.33689H14.325Z"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
18
src/assets/images/source-icon.svg
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<svg width="20" height="19" viewBox="0 0 20 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<foreignObject x="-42.6271" y="-43.6271" width="105.045" height="105.045"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(22.31px);clip-path:url(#bgblur_0_2302_1307_clip_path);height:100%;width:100%"></div></foreignObject><g filter="url(#filter0_d_2302_1307)" data-figma-bg-blur-radius="44.6271">
|
||||||
|
<path d="M10.3018 1.00977C14.4733 1.22136 17.7908 4.67141 17.791 8.89551C17.7908 13.2559 14.2559 16.7908 9.89551 16.791C5.67141 16.7908 2.22136 13.4733 2.00977 9.30176L2 8.89551L2.00977 8.48926C2.22119 4.31755 5.6713 1.00017 9.89551 1L10.3018 1.00977ZM9.89551 5.80566C8.18945 5.80584 6.80584 7.18945 6.80566 8.89551L6.80957 9.05371C6.89208 10.6863 8.2425 11.9842 9.89551 11.9844C11.5485 11.9842 12.898 10.6863 12.9805 9.05371L12.9844 8.89551L12.9805 8.73633C12.9003 7.1565 11.6336 5.88942 10.0537 5.80957L9.89551 5.80566Z" fill="#16A795"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_2302_1307" x="-42.6271" y="-43.6271" width="105.045" height="105.045" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="0.686571"/>
|
||||||
|
<feGaussianBlur stdDeviation="0.686571"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_2302_1307"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_2302_1307" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_2302_1307_clip_path" transform="translate(42.6271 43.6271)"><path d="M10.3018 1.00977C14.4733 1.22136 17.7908 4.67141 17.791 8.89551C17.7908 13.2559 14.2559 16.7908 9.89551 16.791C5.67141 16.7908 2.22136 13.4733 2.00977 9.30176L2 8.89551L2.00977 8.48926C2.22119 4.31755 5.6713 1.00017 9.89551 1L10.3018 1.00977ZM9.89551 5.80566C8.18945 5.80584 6.80584 7.18945 6.80566 8.89551L6.80957 9.05371C6.89208 10.6863 8.2425 11.9842 9.89551 11.9844C11.5485 11.9842 12.898 10.6863 12.9805 9.05371L12.9844 8.89551L12.9805 8.73633C12.9003 7.1565 11.6336 5.88942 10.0537 5.80957L9.89551 5.80566Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
21
src/assets/images/waypoint-icon.svg
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<svg width="42" height="66" viewBox="0 0 42 66" fill="#FFC107" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path opacity="0.3" d="M21 66C23.7614 66 26 64.8807 26 63.5C26 62.1193 23.7614 61 21 61C18.2386 61 16 62.1193 16 63.5C16 64.8807 18.2386 66 21 66Z" fill="#FFC107"/>
|
||||||
|
<path clip-rule="evenodd" d="M21 0C32.598 0 42 9.40202 42 21C42 31.9234 33.6599 40.8989 23 41.906V62C23 63.1046 22.1046 64 21 64C19.8954 64 19 63.1046 19 62V41.906C8.34014 40.8989 0 31.9234 0 21C0 9.40202 9.40202 0 21 0Z" fill="#FFC107"/>
|
||||||
|
<foreignObject x="-61" y="-61" width="164" height="164"></foreignObject><g filter="url(#filter0_d_173_69)">
|
||||||
|
<path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z" fill="#FFC107"/>
|
||||||
|
</g>
|
||||||
|
<path d="M28 21C28 17.134 24.866 14 21 14C17.134 14 14 17.134 14 21C14 24.866 17.134 28 21 28C24.866 28 28 24.866 28 21Z" fill="white"/>
|
||||||
|
<defs>
|
||||||
|
<filter id="filter0_d_173_69" x="-61" y="-61" width="164" height="164" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||||
|
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||||
|
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||||
|
<feOffset dy="1"/>
|
||||||
|
<feGaussianBlur stdDeviation="1"/>
|
||||||
|
<feComposite in2="hardAlpha" operator="out"/>
|
||||||
|
<feColorMatrix type="matrix" values="0 0 0 0 0.235294 0 0 0 0 0.25098 0 0 0 0 0.262745 0 0 0 0.3 0"/>
|
||||||
|
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_173_69"/>
|
||||||
|
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_173_69" result="shape"/>
|
||||||
|
</filter>
|
||||||
|
<clipPath id="bgblur_0_173_69_clip_path" transform="translate(61 61)"><path d="M38 21C38 11.6112 30.3888 4 21 4C11.6112 4 4 11.6112 4 21C4 30.3888 11.6112 38 21 38C30.3888 38 38 30.3888 38 21Z"/>
|
||||||
|
</clipPath></defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
134
src/components/dashboard/machines/MachinesList.jsx
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
"use client";
|
||||||
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
|
import { GET_MACHINES_LIST } from "@/core/utils/routes";
|
||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import RowActions from "./RowActions";
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: "", label: "همه وضعیت ها" },
|
||||||
|
{ value: 1, label: "آماده" },
|
||||||
|
{ value: 2, label: "رزرو" },
|
||||||
|
{ value: 3, label: "در ماموریت" },
|
||||||
|
{ value: 4, label: "خارج از رده" },
|
||||||
|
{ value: 5, label: "در تعمیر" },
|
||||||
|
{ value: 6, label: "فروخته شده" },
|
||||||
|
{ value: 7, label: "نیاز به تعمیر" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const MachinesList = () => {
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "کد یکتا", // Unique Code
|
||||||
|
id: "id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 50,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
header: "اطلاعات ماشینها",
|
||||||
|
id: "machines_info",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
enableSorting: false,
|
||||||
|
datatype: "text",
|
||||||
|
grow: false,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
accessorKey: "car_name",
|
||||||
|
header: "نام ماشین",
|
||||||
|
id: "car_name",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "car_type",
|
||||||
|
header: "نوع ماشین",
|
||||||
|
id: "car_type",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "machine_code",
|
||||||
|
header: "کد ماشین",
|
||||||
|
id: "machine_code",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "plak_number",
|
||||||
|
header: "پلاک",
|
||||||
|
id: "plak_number",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "status",
|
||||||
|
header: "وضعیت",
|
||||||
|
id: "status",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
columnSelectOption: () => {
|
||||||
|
return statusOptions.map((status) => ({
|
||||||
|
value: status.value,
|
||||||
|
label: status.label,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
Cell: ({ row }) => {
|
||||||
|
const status = statusOptions.find((option) => option.value == row.original.status);
|
||||||
|
return <Typography variant="body2">{status?.label || ""}</Typography>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
|
||||||
|
<DataTableWithAuth
|
||||||
|
need_filter={true}
|
||||||
|
columns={columns}
|
||||||
|
table_url={GET_MACHINES_LIST}
|
||||||
|
page_name={"machines"}
|
||||||
|
table_name={"machinesList"}
|
||||||
|
sorting={[{ id: "id", desc: true }]}
|
||||||
|
enableRowActions
|
||||||
|
positionActionsColumn={"first"}
|
||||||
|
RowActions={RowActions}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MachinesList;
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Button, DialogActions, DialogContent, Stack, Typography } from "@mui/material";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import useRequest from "@/lib/hooks/useRequest";
|
||||||
|
import { DELETE_TOLL_HOUSE_ITEM } from "@/core/utils/routes";
|
||||||
|
|
||||||
|
const DeleteContent = ({ rowId, mutate, setOpenDeleteDialog }) => {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const requestServer = useRequest({ notificationSuccess: true });
|
||||||
|
const handleClick = () => {
|
||||||
|
setSubmitting(true);
|
||||||
|
requestServer(`${DELETE_TOLL_HOUSE_ITEM}/${rowId}`, "delete")
|
||||||
|
.then(() => {
|
||||||
|
mutate();
|
||||||
|
setOpenDeleteDialog(false);
|
||||||
|
setSubmitting(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogContent>
|
||||||
|
<Stack alignItems="center" spacing={2}>
|
||||||
|
<Typography mt={2}>آیا از حذف راهدارخانه اطمینان دارید؟</Typography>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ justifyContent: "center", paddingBottom: 2, width: "100%" }}>
|
||||||
|
<Button onClick={() => setOpenDeleteDialog(false)} variant="outlined" color="secondary">
|
||||||
|
خیر !
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleClick} variant="contained" color="error" disabled={submitting}>
|
||||||
|
{submitting ? "درحال حذف..." : "بله اطمینان دارم"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DeleteContent;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import DeleteContent from "./DeleteContent";
|
||||||
|
const DeleteDialog = ({ rowId, mutate }) => {
|
||||||
|
const [openDeleteDialog, setOpenDeleteDialog] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="حذف">
|
||||||
|
<IconButton color="error" onClick={() => setOpenDeleteDialog(true)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog
|
||||||
|
open={openDeleteDialog}
|
||||||
|
onClose={() => setOpenDeleteDialog(false)}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
dir="rtl"
|
||||||
|
maxWidth={"md"}
|
||||||
|
>
|
||||||
|
<DialogTitle>حذف راهدارخانه</DialogTitle>
|
||||||
|
{openDeleteDialog && (
|
||||||
|
<DeleteContent rowId={rowId} mutate={mutate} setOpenDeleteDialog={setOpenDeleteDialog} />
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DeleteDialog;
|
||||||
281
src/components/dashboard/machines/RowActions/Edit/Form/index.jsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
|
import {
|
||||||
|
GET_CITIES_LIST,
|
||||||
|
GET_PROVINCES_LIST,
|
||||||
|
GET_STATIONS_LIST,
|
||||||
|
UPDATE_MACHINE_STATUS_AND_LOCATION,
|
||||||
|
} from "@/core/utils/routes";
|
||||||
|
import useRequest from "@/lib/hooks/useRequest";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { Beenhere, ExitToApp } from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
CircularProgress,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
MenuItem,
|
||||||
|
Select,
|
||||||
|
Stack,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { DialogHeader } from "next/dist/client/components/react-dev-overlay/internal/components/Dialog";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
|
import { number, object } from "yup";
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ value: 1, label: "آماده" },
|
||||||
|
{ value: 2, label: "رزرو" },
|
||||||
|
{ value: 3, label: "در ماموریت" },
|
||||||
|
{ value: 4, label: "خارج از رده" },
|
||||||
|
{ value: 5, label: "در تعمیر" },
|
||||||
|
{ value: 6, label: "فروخته شده" },
|
||||||
|
{ value: 7, label: "نیاز به تعمیر" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const validationSchema = object({
|
||||||
|
status: number().required("انتخاب وضعیت الزامی است"),
|
||||||
|
province_id: number().required("انتخاب استان الزامی است"),
|
||||||
|
city_id: number().required("انتخاب شهر الزامی است"),
|
||||||
|
station_id: number().required("انتخاب راهداری الزامی است"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const EditFormContent = ({ row, rowId, mutate, setOpen }) => {
|
||||||
|
const requestServer = useRequest();
|
||||||
|
|
||||||
|
const [provinces, setProvinces] = useState([]);
|
||||||
|
const [cities, setCities] = useState([]);
|
||||||
|
const [stations, setStations] = useState([]);
|
||||||
|
|
||||||
|
const [loadingProvinces, setLoadingProvinces] = useState(false);
|
||||||
|
const [loadingCities, setLoadingCities] = useState(false);
|
||||||
|
const [loadingStations, setLoadingStations] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useForm({
|
||||||
|
defaultValues: {
|
||||||
|
status: row.original.status,
|
||||||
|
province_id: row.original.province_id,
|
||||||
|
city_id: row.original.city_id,
|
||||||
|
station_id: row.original.station_id,
|
||||||
|
},
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
const provinceId = useWatch({
|
||||||
|
control,
|
||||||
|
name: "province_id",
|
||||||
|
});
|
||||||
|
|
||||||
|
const cityId = useWatch({
|
||||||
|
control,
|
||||||
|
name: "city_id",
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadProvinces = async () => {
|
||||||
|
try {
|
||||||
|
setLoadingProvinces(true);
|
||||||
|
const response = await requestServer(GET_PROVINCES_LIST, "get");
|
||||||
|
setProvinces(response.data?.data || []);
|
||||||
|
} finally {
|
||||||
|
setLoadingProvinces(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadCities = async (provinceId) => {
|
||||||
|
if (!provinceId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingCities(true);
|
||||||
|
const response = await requestServer(`${GET_CITIES_LIST}?province_id=${provinceId}`, "get");
|
||||||
|
setCities(response.data?.data || []);
|
||||||
|
} finally {
|
||||||
|
setLoadingCities(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadStations = async (cityId) => {
|
||||||
|
if (!cityId) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoadingStations(true);
|
||||||
|
const response = await requestServer(`${GET_STATIONS_LIST}/search_by_city?city_id=${cityId}`, "get");
|
||||||
|
setStations(response.data?.data || []);
|
||||||
|
} finally {
|
||||||
|
setLoadingStations(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProvinces();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initial edit mode loading
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (row.original.province_id) {
|
||||||
|
loadCities(row.original.province_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.original.city_id) {
|
||||||
|
loadStations(row.original.city_id);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!provinceId) return;
|
||||||
|
|
||||||
|
loadCities(provinceId);
|
||||||
|
|
||||||
|
if (provinceId !== row.original.province_id) {
|
||||||
|
setValue("city_id", "");
|
||||||
|
setValue("station_id", "");
|
||||||
|
setStations([]);
|
||||||
|
}
|
||||||
|
}, [provinceId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!cityId) return;
|
||||||
|
|
||||||
|
loadStations(cityId);
|
||||||
|
|
||||||
|
if (cityId !== row.original.city_id) {
|
||||||
|
setValue("station_id", "");
|
||||||
|
}
|
||||||
|
}, [cityId]);
|
||||||
|
|
||||||
|
const onSubmit = async (data) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
formData.append("status", String(data.status));
|
||||||
|
formData.append("province_id", String(data.province_id));
|
||||||
|
formData.append("city_id", String(data.city_id));
|
||||||
|
formData.append("station_id", String(data.station_id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await requestServer(`${UPDATE_MACHINE_STATUS_AND_LOCATION}/${rowId}`, "post", {
|
||||||
|
data: formData,
|
||||||
|
});
|
||||||
|
|
||||||
|
mutate();
|
||||||
|
setOpen(false);
|
||||||
|
} catch (error) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>ویرایش ماشین</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogContent dividers>
|
||||||
|
<StyledForm id="updateMachineForm" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Controller
|
||||||
|
name="province_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl fullWidth size="small" error={!!error}>
|
||||||
|
<InputLabel>استان</InputLabel>
|
||||||
|
|
||||||
|
<Select {...field} label="استان" disabled={loadingProvinces}>
|
||||||
|
{provinces.map((province) => (
|
||||||
|
<MenuItem key={province.id} value={province.id}>
|
||||||
|
{province.name_fa}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="city_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl fullWidth size="small" error={!!error}>
|
||||||
|
<InputLabel>شهر</InputLabel>
|
||||||
|
|
||||||
|
<Select {...field} label="شهر" disabled={!provinceId || loadingCities}>
|
||||||
|
{cities.map((city) => (
|
||||||
|
<MenuItem key={city.id} value={city.id}>
|
||||||
|
{city.name_fa}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="station_id"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl fullWidth size="small" error={!!error}>
|
||||||
|
<InputLabel>راهداری</InputLabel>
|
||||||
|
|
||||||
|
<Select {...field} label="راهداری" disabled={!cityId || loadingStations}>
|
||||||
|
{stations.map((station) => (
|
||||||
|
<MenuItem key={station.id} value={station.id}>
|
||||||
|
{station.name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="status"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl fullWidth size="small" error={!!error}>
|
||||||
|
<InputLabel>وضعیت</InputLabel>
|
||||||
|
|
||||||
|
<Select {...field} label="وضعیت">
|
||||||
|
{statusOptions.map((item) => (
|
||||||
|
<MenuItem key={item.value} value={item.value}>
|
||||||
|
{item.label}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{(loadingCities || loadingStations) && (
|
||||||
|
<Stack direction="row" justifyContent="center">
|
||||||
|
<CircularProgress size={24} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</StyledForm>
|
||||||
|
</DialogContent>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={() => setOpen(false)} variant="outlined" color="secondary" startIcon={<ExitToApp />}>
|
||||||
|
بستن
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
form="updateMachineForm"
|
||||||
|
endIcon={<Beenhere />}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "در حال ویرایش" : "ویرایش ماشین"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditFormContent;
|
||||||
44
src/components/dashboard/machines/RowActions/Edit/index.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { SwapHoriz } from "@mui/icons-material";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import { Dialog, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import EditFormContent from "./Form";
|
||||||
|
|
||||||
|
const Edit = ({ row, mutate, rowId }) => {
|
||||||
|
const [openEditDialog, setOpenEditDialog] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="ویرایش" arrow placement="right">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
sx={{ textTransform: "unset", alignSelf: "center" }}
|
||||||
|
onClick={() => {
|
||||||
|
setOpenEditDialog(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SwapHoriz />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog open={openEditDialog} fullWidth maxWidth="xs">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpenEditDialog(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
{openEditDialog && (
|
||||||
|
<EditFormContent rowId={rowId} row={row} setOpen={setOpenEditDialog} mutate={mutate} />
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Edit;
|
||||||
14
src/components/dashboard/machines/RowActions/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { usePermissions } from "@/lib/hooks/usePermissions";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import Edit from "./Edit";
|
||||||
|
|
||||||
|
const RowActions = ({ row, mutate }) => {
|
||||||
|
const { data: userPermissions } = usePermissions();
|
||||||
|
const hasEditPermission = userPermissions.some((item) => ["update-machine"].includes(item));
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
|
{hasEditPermission && <Edit row={row} mutate={mutate} rowId={row.getValue("id")} />}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RowActions;
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { FeatureGroup, useMap } from "react-leaflet";
|
||||||
|
|
||||||
|
const ShowBound = ({ bound }) => {
|
||||||
|
const map = useMap();
|
||||||
|
const featureRef = useRef(null);
|
||||||
|
|
||||||
|
const safeFitBounds = (layer) => {
|
||||||
|
if (!layer || !map) return;
|
||||||
|
try {
|
||||||
|
const latLngs = layer.getLatLngs();
|
||||||
|
map.fitBounds(latLngs, {
|
||||||
|
paddingTopLeft: [20, 20],
|
||||||
|
paddingBottomRight: [20, 20],
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("fitBounds failed:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
bound?.editing?.disable();
|
||||||
|
featureRef.current.addLayer(bound);
|
||||||
|
safeFitBounds(bound);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <FeatureGroup ref={featureRef} />;
|
||||||
|
};
|
||||||
|
export default ShowBound;
|
||||||
65
src/components/dashboard/machines/ShowProjectArea/index.jsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
const { IconButton, Tooltip, Box, Dialog, DialogTitle, Typography, DialogContent } = require("@mui/material");
|
||||||
|
import MapLayer from "@/core/components/MapLayer";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import LayersIcon from "@mui/icons-material/Layers";
|
||||||
|
import L from "leaflet";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import ShowBound from "./ShowBound";
|
||||||
|
|
||||||
|
const ShowProjectArea = ({ primaryArea }) => {
|
||||||
|
const [openShowAreaDialog, setOpenShowAreaDialog] = useState(false);
|
||||||
|
const bound = useMemo(() => {
|
||||||
|
const coordinates = Object.values(primaryArea.coordinates);
|
||||||
|
|
||||||
|
const latLngCoords = coordinates.map(([lat, lng]) => [parseFloat(lng), parseFloat(lat)]);
|
||||||
|
|
||||||
|
return primaryArea.type === "polygon"
|
||||||
|
? L.polygon(latLngCoords, { color: "#ff5555" })
|
||||||
|
: L.polyline(latLngCoords);
|
||||||
|
}, [primaryArea]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Tooltip title="نمایش پلیگان" placement="right" arrow>
|
||||||
|
<IconButton size="small" color="primary" onClick={() => setOpenShowAreaDialog(true)}>
|
||||||
|
<LayersIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog
|
||||||
|
open={openShowAreaDialog}
|
||||||
|
onClose={() => setOpenShowAreaDialog(false)}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
transition: "all .3s",
|
||||||
|
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||||
|
borderRadius: 2,
|
||||||
|
padding: 1,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
maxWidth="sm"
|
||||||
|
fullWidth
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ display: "flex", justifyContent: "space-between", padding: "16px 24px" }}>
|
||||||
|
<Typography variant="body1" sx={{ fontWeight: "bold", fontSize: "large" }}>
|
||||||
|
محدوده طرح
|
||||||
|
</Typography>
|
||||||
|
<IconButton onClick={() => setOpenShowAreaDialog(false)} size="small">
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Box sx={{ width: "100%", height: "400px" }}>
|
||||||
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
|
<ShowBound bound={bound} />
|
||||||
|
</MapLayer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowProjectArea;
|
||||||
14
src/components/dashboard/machines/index.jsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"use client";
|
||||||
|
import PageTitle from "@/core/components/PageTitle";
|
||||||
|
import { Stack } from "@mui/material";
|
||||||
|
import MachinesList from "./MachinesList";
|
||||||
|
|
||||||
|
const MachinesPage = () => {
|
||||||
|
return (
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<PageTitle title={"لیست ماشین ها"} />
|
||||||
|
<MachinesList />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MachinesPage;
|
||||||
@@ -59,7 +59,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Stack alignItems="center" spacing={2}>
|
<Stack alignItems="center" spacing={2}>
|
||||||
<Typography mt={2}>آیا از ورود خودرو و پایان ماموریت اطمینان دارید؟</Typography>
|
<Typography mt={2}>آیا از ورود خودرو و پایان ماموریت اطمینان دارید؟</Typography>
|
||||||
<Stack>
|
<Stack sx={{ width: "87%" }}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"end_km"}
|
name={"end_km"}
|
||||||
@@ -73,6 +73,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
|
|||||||
field.onChange(e.target.value);
|
field.onChange(e.target.value);
|
||||||
}}
|
}}
|
||||||
size="small"
|
size="small"
|
||||||
|
sx={{ width: "100%" }}
|
||||||
fullWidth
|
fullWidth
|
||||||
label="کیلومتر خودرو / ساعت کار"
|
label="کیلومتر خودرو / ساعت کار"
|
||||||
InputLabelProps={{ shrink: true }}
|
InputLabelProps={{ shrink: true }}
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
|
|||||||
const onSubmit = (values) => {
|
const onSubmit = (values) => {
|
||||||
const formattedDate = values.start_date;
|
const formattedDate = values.start_date;
|
||||||
const formattedTime = format(values.start_time, "HH:mm");
|
const formattedTime = format(values.start_time, "HH:mm");
|
||||||
console.log(formattedDate, formattedTime);
|
|
||||||
|
|
||||||
requestServer(`${START_MISSION}/${rowId}`, "post", {
|
requestServer(`${START_MISSION}/${rowId}`, "post", {
|
||||||
data: {
|
data: {
|
||||||
@@ -60,7 +59,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Stack alignItems="center" spacing={2}>
|
<Stack alignItems="center" spacing={2}>
|
||||||
<Typography mt={2}>آیا از خروج خودرو و شروع ماموریت اطمینان دارید؟</Typography>
|
<Typography mt={2}>آیا از خروج خودرو و شروع ماموریت اطمینان دارید؟</Typography>
|
||||||
<Stack>
|
<Stack sx={{ width: "84%" }}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"code"}
|
name={"code"}
|
||||||
@@ -85,7 +84,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack>
|
<Stack sx={{ width: "84%" }}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"km"}
|
name={"km"}
|
||||||
|
|||||||
@@ -1,116 +1,113 @@
|
|||||||
import { Delete, Route } from "@mui/icons-material";
|
"use client";
|
||||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
|
||||||
import { useReducer } from "react";
|
|
||||||
import DrawBound from "./DrawBound";
|
|
||||||
|
|
||||||
const statusType = [
|
import { useEffect, useState } from "react";
|
||||||
{
|
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
id: 0,
|
import L from "leaflet";
|
||||||
message: "برای آغاز ترسیم مسیر، کلیک کنید!",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
label: "شروع ترسیم مسیر",
|
|
||||||
key: "start",
|
|
||||||
color: "primary",
|
|
||||||
icon: <Route />,
|
|
||||||
onclick: (controlDispach) => {
|
|
||||||
controlDispach({ type: "SET_STATUS", status: 1 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
message: "مسیر موردنظر را با کلیک روی نقشه مشخص کنید. برای اتمام، روی نقطهی پایانی کلیک کنید!",
|
|
||||||
buttons: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
message: "برای اصلاح مسیر، گوشهها را بکشید. برای ترسیم دوباره، آن را حذف کنید",
|
|
||||||
buttons: [
|
|
||||||
{
|
|
||||||
label: "حذف",
|
|
||||||
key: "end",
|
|
||||||
color: "error",
|
|
||||||
icon: <Delete />,
|
|
||||||
onclick: (controlDispach) => {
|
|
||||||
controlDispach({ type: "SET_STATUS", status: 0 });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const createInitialState = (bound) => {
|
// ===== Icons =====
|
||||||
if (bound) {
|
const originIcon = new L.Icon({
|
||||||
return { status: 2 };
|
iconUrl: "/icons/origin-icon.svg",
|
||||||
}
|
iconSize: [32, 32],
|
||||||
return { status: 0 };
|
iconAnchor: [16, 32],
|
||||||
|
});
|
||||||
|
|
||||||
|
const destinationIcon = new L.Icon({
|
||||||
|
iconUrl: "/icons/destination-icon.svg",
|
||||||
|
iconSize: [32, 32],
|
||||||
|
iconAnchor: [16, 32],
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Track Center =====
|
||||||
|
function MapCenterTracker({ setCenter }) {
|
||||||
|
const map = useMap();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateCenter = () => {
|
||||||
|
setCenter(map.getCenter());
|
||||||
};
|
};
|
||||||
|
|
||||||
const reducer = (state, action) => {
|
updateCenter();
|
||||||
switch (action.type) {
|
|
||||||
case "SET_STATUS":
|
map.on("move", updateCenter);
|
||||||
return { ...state, status: action.status };
|
|
||||||
default:
|
return () => {
|
||||||
return state;
|
map.off("move", updateCenter);
|
||||||
|
};
|
||||||
|
}, [map, setCenter]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Fixed Center Marker =====
|
||||||
|
function FixedCenterMarker() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -100%)",
|
||||||
|
zIndex: 1000,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src="/icons/origin-icon.svg" width={40} height={40} alt="center" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Example() {
|
||||||
|
const [center, setCenter] = useState(null);
|
||||||
|
|
||||||
|
const [origin, setOrigin] = useState(null);
|
||||||
|
const [destination, setDestination] = useState(null);
|
||||||
|
|
||||||
|
const [step, setStep] = useState("origin");
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
if (!center) return;
|
||||||
|
|
||||||
|
if (step === "origin") {
|
||||||
|
setOrigin(center);
|
||||||
|
setStep("destination");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step === "destination") {
|
||||||
|
setDestination(center);
|
||||||
|
setStep("done");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const MapControlPolyline = ({ bound, setBound }) => {
|
|
||||||
const [control, controlDispach] = useReducer(reducer, bound, createInitialState);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div>
|
||||||
<DrawBound bound={bound} setBound={setBound} control={control} controlDispach={controlDispach} />
|
<div
|
||||||
<Stack
|
style={{
|
||||||
direction={"row"}
|
position: "relative",
|
||||||
justifyContent={"center"}
|
height: "400px",
|
||||||
sx={{ position: "absolute", left: 0, top: 0, zIndex: 2000, width: "100%" }}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
background: (theme) => theme.palette.info.main,
|
|
||||||
borderBottomLeftRadius: 8,
|
|
||||||
borderBottomRightRadius: 8,
|
|
||||||
py: 0.5,
|
|
||||||
px: 2,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography align="center" component={"div"} variant="caption" color={"#fff"}>
|
<MapCenterTracker setCenter={setCenter} />
|
||||||
{statusType.find((st) => st.id == control.status).message}
|
|
||||||
</Typography>
|
{/* saved markers */}
|
||||||
</Box>
|
{origin && <Marker position={origin} icon={originIcon} />}
|
||||||
</Stack>
|
|
||||||
<Box
|
{destination && <Marker position={destination} icon={destinationIcon} />}
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
{/* route line */}
|
||||||
left: 0,
|
{origin && destination && <Polyline positions={[origin, destination]} />}
|
||||||
bottom: 0,
|
|
||||||
zIndex: 2000,
|
{/* fixed center pin */}
|
||||||
width: "100%",
|
{step !== "done" && <FixedCenterMarker />}
|
||||||
py: 1,
|
</div>
|
||||||
background: "linear-gradient(to top, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0))",
|
|
||||||
}}
|
<div style={{ marginTop: 16, position: "absolute", z: 50, top: 5 }}>
|
||||||
>
|
<button onClick={handleOk}>
|
||||||
<Stack direction={"row"} justifyContent={"center"} spacing={2}>
|
{step === "origin" && "Set Origin"}
|
||||||
{statusType
|
{step === "destination" && "Set Destination"}
|
||||||
.find((st) => st.id == control.status)
|
{step === "done" && "Completed"}
|
||||||
.buttons.map((button) => (
|
</button>
|
||||||
<Button
|
</div>
|
||||||
size="small"
|
</div>
|
||||||
key={button.key}
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => button.onclick(controlDispach)}
|
|
||||||
color={button.color}
|
|
||||||
startIcon={button.icon}
|
|
||||||
>
|
|
||||||
{button.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
export default MapControlPolyline;
|
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
const AlarmText = ({ isSource }) => {
|
||||||
|
if (isSource) return null;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 5,
|
||||||
|
left: 5,
|
||||||
|
zIndex: 1000,
|
||||||
|
border: "1px solid #ed0f02",
|
||||||
|
backgroundColor: "#f19898",
|
||||||
|
py: 0.5,
|
||||||
|
px: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
opacity: 0.8,
|
||||||
|
boxShadow: "rgba(0, 0, 0, 0.35) 0px 5px 15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: "bold" }} color="#ed0f02">
|
||||||
|
با کلیک بر روی مارکر موقعیت را انتخاب کنید
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmText;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
|
||||||
|
const ROW_COLORS = {
|
||||||
|
source: "#0EA37A", // سبز
|
||||||
|
waypoint: "#F5A623", // زرد/نارنجی
|
||||||
|
dest: "#E53935", // قرمز
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
|
||||||
|
|
||||||
|
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: color,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 12,
|
||||||
|
color: value ? "#1f1f1f" : "#bdbdbd",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || placeholder}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{onEdit && (
|
||||||
|
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
|
||||||
|
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{onRemove && (
|
||||||
|
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
|
||||||
|
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DataBox = ({
|
||||||
|
isSource,
|
||||||
|
isDest,
|
||||||
|
sourcePosition,
|
||||||
|
destPosition,
|
||||||
|
editingTarget,
|
||||||
|
onEditSource,
|
||||||
|
onEditDest,
|
||||||
|
waypoints = [],
|
||||||
|
onEditWaypoint,
|
||||||
|
onAddWaypoint,
|
||||||
|
onRemoveWaypoint,
|
||||||
|
canAddWaypoint,
|
||||||
|
}) => {
|
||||||
|
if (!isSource) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 12,
|
||||||
|
left: 12,
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.95)",
|
||||||
|
backdropFilter: "blur(6px)",
|
||||||
|
border: "1px solid #ececec",
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.75,
|
||||||
|
width: 230,
|
||||||
|
maxWidth: "70vw",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
color={ROW_COLORS.source}
|
||||||
|
label="مبدا"
|
||||||
|
value={formatCoord(sourcePosition)}
|
||||||
|
onEdit={isSource && isDest ? onEditSource : null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{waypoints.map((wp, index) => (
|
||||||
|
<Row
|
||||||
|
key={`waypoint-${index}`}
|
||||||
|
color={ROW_COLORS.waypoint}
|
||||||
|
label={`نقطه میانی ${index + 1}`}
|
||||||
|
value={formatCoord(wp.position)}
|
||||||
|
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
|
||||||
|
onRemove={() => onRemoveWaypoint(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isDest && (
|
||||||
|
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canAddWaypoint && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={0.5}
|
||||||
|
onClick={onAddWaypoint}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color: ROW_COLORS.waypoint,
|
||||||
|
py: 0.25,
|
||||||
|
"&:hover": { opacity: 0.75 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||||
|
افزودن نقطه میانی
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataBox;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
|
||||||
|
const ShowRoute = ({ area }) => {
|
||||||
|
const map = useMap();
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
const [showRoute, setShowRoute] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!coords.length) return;
|
||||||
|
|
||||||
|
setShowRoute(false);
|
||||||
|
|
||||||
|
map.flyToBounds(coords, {
|
||||||
|
padding: [50, 50],
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.once("moveend", () => {
|
||||||
|
setShowRoute(true);
|
||||||
|
});
|
||||||
|
}, [area]);
|
||||||
|
if (!area) return null;
|
||||||
|
const coords = polyline.decode(area, 6);
|
||||||
|
|
||||||
|
if (coords.length === 0) return null;
|
||||||
|
|
||||||
|
const sourcePosition = coords[0];
|
||||||
|
const destPosition = coords[coords.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||||
|
|
||||||
|
<Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
<Marker position={destPosition} icon={destIcon} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowRoute;
|
||||||
|
|
||||||
|
// import { useEffect, useState } from "react";
|
||||||
|
// import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
// import L from "leaflet";
|
||||||
|
// import polyline from "@mapbox/polyline";
|
||||||
|
// import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
// import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||||
|
//
|
||||||
|
// const defaultIconSize = [35, 35];
|
||||||
|
//
|
||||||
|
// const createCustomIcon = (size, iconUrl) => {
|
||||||
|
// return L.icon({
|
||||||
|
// iconUrl: iconUrl,
|
||||||
|
// iconSize: size,
|
||||||
|
// iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
// popupAnchor: [0, -size[1]],
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
|
||||||
|
//
|
||||||
|
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
|
||||||
|
// const ShowRoute = ({ area, locations = [] }) => {
|
||||||
|
// const map = useMap();
|
||||||
|
// const [showRoute, setShowRoute] = useState(false);
|
||||||
|
//
|
||||||
|
// const coords = area ? polyline.decode(area, 6) : [];
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!coords.length) return;
|
||||||
|
//
|
||||||
|
// setShowRoute(false);
|
||||||
|
//
|
||||||
|
// map.flyToBounds(coords, {
|
||||||
|
// padding: [50, 50],
|
||||||
|
// duration: 1.5,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// map.once("moveend", () => {
|
||||||
|
// setShowRoute(true);
|
||||||
|
// });
|
||||||
|
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
// }, [area]);
|
||||||
|
//
|
||||||
|
// if (!area || coords.length === 0) return null;
|
||||||
|
//
|
||||||
|
// const sourcePosition = coords[0];
|
||||||
|
// const destPosition = coords[coords.length - 1];
|
||||||
|
//
|
||||||
|
// // نقاط میانی فقط از روی locations قابل استخراجان (بین اولی و آخری)
|
||||||
|
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||||
|
//
|
||||||
|
// <Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
//
|
||||||
|
// {middleWaypoints.map((wp, index) => (
|
||||||
|
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
|
||||||
|
// ))}
|
||||||
|
//
|
||||||
|
// <Marker position={destPosition} icon={destIcon} />
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export default ShowRoute;
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import { Marker, Polyline, useMapEvents } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
|
||||||
|
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
|
||||||
|
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import DataBox from "./DataBox";
|
||||||
|
import AlarmText from "./AlarmText";
|
||||||
|
|
||||||
|
const MAX_WAYPOINTS = 2;
|
||||||
|
|
||||||
|
const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||||
|
const mapPrevSourceMarker = useRef();
|
||||||
|
const mapPrevDestMarker = useRef();
|
||||||
|
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی بهازای هر waypoint
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const [isSource, setIsSource] = useState(false);
|
||||||
|
const [sourcePosition, setSourcePosition] = useState(null);
|
||||||
|
|
||||||
|
const [isDest, setIsDest] = useState(false);
|
||||||
|
const [destPosition, setDestPosition] = useState(null);
|
||||||
|
|
||||||
|
const [waypoints, setWaypoints] = useState([]);
|
||||||
|
|
||||||
|
const [routeCoords, setRouteCoords] = useState([]);
|
||||||
|
const [alternateCoords, setAlternateCoords] = useState([]);
|
||||||
|
const [editingTarget, setEditingTarget] = useState(null);
|
||||||
|
const [selectedRoute, setSelectedRoute] = useState(null);
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIcon = createCustomIcon(
|
||||||
|
defaultIconSize,
|
||||||
|
isSource && editingTarget !== "source" ? SourceIcon.src : PrevSourceIcon.src
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevDestIcon = createCustomIcon(
|
||||||
|
defaultIconSize,
|
||||||
|
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
|
||||||
|
);
|
||||||
|
|
||||||
|
const getWaypointIcon = (index) => {
|
||||||
|
const wp = waypoints[index];
|
||||||
|
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
|
||||||
|
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveTarget = () => {
|
||||||
|
if (editingTarget) return editingTarget;
|
||||||
|
if (!isSource) return "source";
|
||||||
|
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
|
||||||
|
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
|
||||||
|
if (!isDest) return "dest";
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = useMapEvents({
|
||||||
|
move(e) {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
const center = e.target.getCenter();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setLatLng(center);
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setLatLng(center);
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
movestart() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveend() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mapRef.current = map;
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (locations.length < 2) return;
|
||||||
|
const first = { lat: locations[0].lat, lng: locations[0].lon };
|
||||||
|
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
|
||||||
|
const middle = locations.slice(1, -1).map((loc) => ({
|
||||||
|
position: { lat: loc.lat, lng: loc.lon },
|
||||||
|
confirmed: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSourcePosition(first);
|
||||||
|
setDestPosition(last);
|
||||||
|
setWaypoints(middle);
|
||||||
|
setIsSource(true);
|
||||||
|
setIsDest(true);
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
const decoded = polyline.decode(area, 6);
|
||||||
|
setRouteCoords(decoded);
|
||||||
|
setSelectedRoute({ index: "main", coords: decoded });
|
||||||
|
setTimeout(() => {
|
||||||
|
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
fetchRoute(first, last, middle);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
|
||||||
|
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
|
||||||
|
const locs = [
|
||||||
|
{ lat: source.lat, lon: source.lng },
|
||||||
|
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
|
||||||
|
{ lat: dest.lat, lon: dest.lng },
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://jouya.141.ir/api/router/drive", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ locations: locs }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
const shape = data.trip.legs[0].shape;
|
||||||
|
setLocations(locs);
|
||||||
|
setArea(shape);
|
||||||
|
const decoded = polyline.decode(shape, 6);
|
||||||
|
setRouteCoords(decoded);
|
||||||
|
setSelectedRoute({ index: "main", coords: decoded });
|
||||||
|
|
||||||
|
if (data.alternates?.length > 0) {
|
||||||
|
const decodedAlternates = data.alternates.map((alt) => polyline.decode(alt.trip.legs[0].shape, 6));
|
||||||
|
setAlternateCoords(decodedAlternates);
|
||||||
|
} else {
|
||||||
|
setAlternateCoords([]);
|
||||||
|
}
|
||||||
|
map.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Routing error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetRoute = () => {
|
||||||
|
setRouteCoords([]);
|
||||||
|
setAlternateCoords([]);
|
||||||
|
setSelectedRoute(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkerClick = async () => {
|
||||||
|
if (!isSource || editingTarget === "source") {
|
||||||
|
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
|
||||||
|
setSourcePosition(clickedPosition);
|
||||||
|
setIsSource(true);
|
||||||
|
const wasEditing = editingTarget === "source";
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (wasEditing && destPosition) {
|
||||||
|
await fetchRoute(clickedPosition, destPosition, waypoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDestMarkerClick = async () => {
|
||||||
|
if (!isDest || editingTarget === "dest") {
|
||||||
|
const clickedPosition = mapPrevDestMarker.current.getLatLng();
|
||||||
|
setDestPosition(clickedPosition);
|
||||||
|
setIsDest(true);
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
await fetchRoute(sourcePosition, clickedPosition, waypoints);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWaypointMarkerClick = async (index) => {
|
||||||
|
const wp = waypoints[index];
|
||||||
|
if (!wp || wp.confirmed) return; // فقط روی نقطهی تأییدنشده کلیک معتبره (یا حالت ادیت)
|
||||||
|
|
||||||
|
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
|
||||||
|
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
|
||||||
|
setWaypoints(updated);
|
||||||
|
const wasEditing = editingTarget === `waypoint-${index}`;
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (sourcePosition && destPosition) {
|
||||||
|
await fetchRoute(sourcePosition, destPosition, updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSource = () => {
|
||||||
|
setEditingTarget("source");
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditDest = () => {
|
||||||
|
setEditingTarget("dest");
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditWaypoint = (index) => {
|
||||||
|
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed میکنیم تا با حرکت نقشه جابهجا بشه
|
||||||
|
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
|
||||||
|
setEditingTarget(`waypoint-${index}`);
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
|
||||||
|
const handleAddWaypoint = () => {
|
||||||
|
if (waypoints.length >= MAX_WAYPOINTS) return;
|
||||||
|
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
|
||||||
|
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
|
||||||
|
setEditingTarget(`waypoint-${waypoints.length}`);
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// حذف یک نقطه میانی
|
||||||
|
const handleRemoveWaypoint = async (index) => {
|
||||||
|
const updated = waypoints.filter((_, i) => i !== index);
|
||||||
|
setWaypoints(updated);
|
||||||
|
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
|
||||||
|
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (sourcePosition && destPosition) {
|
||||||
|
await fetchRoute(sourcePosition, destPosition, updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectRoute = (index, coords) => {
|
||||||
|
setSelectedRoute({ index, coords });
|
||||||
|
const encoded = polyline.encode(coords, 6);
|
||||||
|
setArea(encoded);
|
||||||
|
map.flyToBounds(coords, { padding: [50, 50], duration: 0.3 });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataBox
|
||||||
|
isSource={isSource}
|
||||||
|
isDest={isDest}
|
||||||
|
sourcePosition={sourcePosition}
|
||||||
|
destPosition={destPosition}
|
||||||
|
editingTarget={editingTarget}
|
||||||
|
onEditSource={handleEditSource}
|
||||||
|
onEditDest={handleEditDest}
|
||||||
|
waypoints={waypoints}
|
||||||
|
onEditWaypoint={handleEditWaypoint}
|
||||||
|
onAddWaypoint={handleAddWaypoint}
|
||||||
|
onRemoveWaypoint={handleRemoveWaypoint}
|
||||||
|
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
|
||||||
|
/>
|
||||||
|
<AlarmText isSource={isSource} />
|
||||||
|
|
||||||
|
<Marker
|
||||||
|
position={sourcePosition || map.getCenter()}
|
||||||
|
ref={mapPrevSourceMarker}
|
||||||
|
eventHandlers={{ click: handleMarkerClick }}
|
||||||
|
icon={defaultIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{waypoints.map((wp, index) => (
|
||||||
|
<Marker
|
||||||
|
key={`waypoint-${index}`}
|
||||||
|
position={wp.position || map.getCenter()}
|
||||||
|
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
|
||||||
|
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
|
||||||
|
icon={getWaypointIcon(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isSource && (
|
||||||
|
<Marker
|
||||||
|
position={destPosition || map.getCenter()}
|
||||||
|
ref={mapPrevDestMarker}
|
||||||
|
eventHandlers={{ click: handleDestMarkerClick }}
|
||||||
|
icon={prevDestIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alternateCoords.map((coords, index) => (
|
||||||
|
<Polyline
|
||||||
|
key={`alternate-${index}-${selectedRoute?.index === index}`}
|
||||||
|
positions={coords}
|
||||||
|
color={selectedRoute?.index === index ? "#015688" : "#91B2C6"}
|
||||||
|
weight={selectedRoute?.index === index ? 7 : 6}
|
||||||
|
opacity={selectedRoute?.index === index ? 1 : 0.7}
|
||||||
|
eventHandlers={{ click: () => handleSelectRoute(index, coords) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{routeCoords.length > 0 && (
|
||||||
|
<Polyline
|
||||||
|
key={`main-${selectedRoute?.index === "main"}`}
|
||||||
|
positions={routeCoords}
|
||||||
|
color={selectedRoute?.index === "main" ? "#015688" : "#91B2C6"}
|
||||||
|
weight={selectedRoute?.index === "main" ? 7 : 6}
|
||||||
|
opacity={selectedRoute?.index === "main" ? 1 : 0.7}
|
||||||
|
eventHandlers={{ click: () => handleSelectRoute("main", routeCoords) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Routing;
|
||||||
@@ -2,19 +2,21 @@ import MapLoading from "@/core/components/MapLayer/Loading";
|
|||||||
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
|
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import MapControlPolyline from "./MapControlPolyline";
|
import Routing from "./Routing";
|
||||||
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
||||||
loading: () => <MapLoading />,
|
loading: () => <MapLoading />,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
|
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
const [bound, setBound] = useState(allData.bound);
|
const [area, setArea] = useState(allData.area);
|
||||||
|
const [locations, setLocations] = useState(allData.locations);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
setAllData({ bound: bound });
|
setAllData({ area: area });
|
||||||
|
setAllData({ locations: locations });
|
||||||
setTabState((s) => s + 1);
|
setTabState((s) => s + 1);
|
||||||
}, [bound]);
|
}, [area]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -22,7 +24,7 @@ const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ width: "100%", height: "400px" }}>
|
<Box sx={{ width: "100%", height: "400px" }}>
|
||||||
<MapLayer style={{ borderRadius: "4px" }}>
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
<MapControlPolyline bound={bound} setBound={setBound} boundType={"polyline"} />
|
<Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
|
||||||
</MapLayer>
|
</MapLayer>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -31,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
||||||
{"مرحله قبلی"}
|
{"مرحله قبلی"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}>
|
<Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
|
||||||
{!bound ? "در انتظار ترسیم" : "مرحله بعد"}
|
{!area ? "در انتظار ترسیم" : "مرحله بعد"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -6,107 +6,107 @@ import { Controller, useWatch } from "react-hook-form";
|
|||||||
|
|
||||||
const MissionDates = ({ control }) => {
|
const MissionDates = ({ control }) => {
|
||||||
const type = useWatch({ control, name: "type" });
|
const type = useWatch({ control, name: "type" });
|
||||||
|
|
||||||
const start_date = useWatch({ control, name: "start_date" });
|
const start_date = useWatch({ control, name: "start_date" });
|
||||||
const end_date = useWatch({ control, name: "end_date" });
|
const end_date = useWatch({ control, name: "end_date" });
|
||||||
|
|
||||||
const start_time = useWatch({ control, name: "start_time" });
|
const start_time = useWatch({ control, name: "start_time" });
|
||||||
const end_time = useWatch({ control, name: "end_time" });
|
const end_time = useWatch({ control, name: "end_time" });
|
||||||
|
|
||||||
const startDateTime = makeDateTime(start_date, start_time);
|
|
||||||
const endDateTime = makeDateTime(end_date, end_time);
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
|
const startDateTime = start_date && start_time ? makeDateTime(start_date, start_time) : null;
|
||||||
|
|
||||||
|
const endDateTime = end_date && end_time ? makeDateTime(end_date, end_time) : null;
|
||||||
|
|
||||||
|
const isSameDay =
|
||||||
|
start_date && end_date && new Date(start_date).toDateString() === new Date(end_date).toDateString();
|
||||||
|
|
||||||
|
const isToday = start_date && new Date(start_date).toDateString() === now.toDateString();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
type && (
|
type && (
|
||||||
<>
|
<>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"start_date"}
|
name="start_date"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
|
||||||
<MuiDatePicker
|
<MuiDatePicker
|
||||||
name="start_date"
|
name="start_date"
|
||||||
error={error}
|
|
||||||
value={field.value}
|
value={field.value}
|
||||||
|
error={error}
|
||||||
disableFuture={false}
|
disableFuture={false}
|
||||||
minDate={now}
|
minDate={now}
|
||||||
maxDate={end_date ?? null}
|
maxDate={end_date ?? null}
|
||||||
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
|
label="تاریخ شروع ماموریت"
|
||||||
label={"تاریخ شروع ماموریت"}
|
placeholder="تاریخ شروع ماموریت را وارد کنید"
|
||||||
setFieldValue={(name, value) => field.onChange(value || [])}
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
helperText={error ? error.message : null}
|
helperText={error?.message}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{type == 1 && (
|
{type == 1 && (
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"start_time"}
|
name="start_time"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
|
||||||
<MuiTimePicker
|
<MuiTimePicker
|
||||||
name="start_time"
|
name="start_time"
|
||||||
error={error}
|
|
||||||
value={field.value}
|
value={field.value}
|
||||||
minTime={
|
error={error}
|
||||||
start_date && new Date(start_date).toDateString() === now.toDateString()
|
minTime={isToday ? now : null}
|
||||||
? now
|
maxTime={isSameDay && endDateTime ? endDateTime : null}
|
||||||
: null
|
label="زمان شروع ماموریت"
|
||||||
}
|
placeholder="زمان شروع ماموریت را وارد کنید"
|
||||||
maxTime={endDateTime ?? null}
|
|
||||||
placeholder={"زمان شروع ماموریت را وارد کنید"}
|
|
||||||
label={"زمان شروع ماموریت"}
|
|
||||||
setFieldValue={(name, value) => field.onChange(value || null)}
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
helperText={error ? error.message : null}
|
helperText={error?.message}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"end_date"}
|
name="end_date"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
|
||||||
<MuiDatePicker
|
<MuiDatePicker
|
||||||
name="end_date"
|
name="end_date"
|
||||||
error={error}
|
|
||||||
value={field.value}
|
value={field.value}
|
||||||
|
error={error}
|
||||||
disableFuture={false}
|
disableFuture={false}
|
||||||
minDate={start_date ? start_date : now}
|
minDate={start_date || now}
|
||||||
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
|
label="تاریخ پایان ماموریت"
|
||||||
label={"تاریخ پایان ماموریت"}
|
placeholder="تاریخ پایان ماموریت را وارد کنید"
|
||||||
setFieldValue={(name, value) => field.onChange(value || [])}
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
helperText={error ? error.message : null}
|
helperText={error?.message}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{type == 1 && (
|
{type == 1 && (
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name={"end_time"}
|
name="end_time"
|
||||||
render={({ field, fieldState: { error } }) => {
|
render={({ field, fieldState: { error } }) => (
|
||||||
return (
|
|
||||||
<MuiTimePicker
|
<MuiTimePicker
|
||||||
name="end_time"
|
name="end_time"
|
||||||
error={error}
|
|
||||||
value={field.value}
|
value={field.value}
|
||||||
minTime={startDateTime ?? null}
|
error={error}
|
||||||
placeholder={"زمان پایان ماموریت را وارد کنید"}
|
minTime={isSameDay && startDateTime ? startDateTime : null}
|
||||||
label={"زمان پایان ماموریت"}
|
label="زمان پایان ماموریت"
|
||||||
|
placeholder="زمان پایان ماموریت را وارد کنید"
|
||||||
setFieldValue={(name, value) => field.onChange(value || null)}
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
helperText={error ? error.message : null}
|
helperText={error?.message}
|
||||||
/>
|
/>
|
||||||
);
|
)}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
@@ -114,4 +114,5 @@ const MissionDates = ({ control }) => {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MissionDates;
|
export default MissionDates;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
|
||||||
|
const ItemSelectField = ({ control, setValue }) => {
|
||||||
|
const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems();
|
||||||
|
function getItemName(val) {
|
||||||
|
return itemsList.find((item) => item.id === val)?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"item_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="موضوع"
|
||||||
|
selectors={itemsList}
|
||||||
|
isLoading={loadingItemsList}
|
||||||
|
schema={{ name: "name", value: "id" }}
|
||||||
|
error={errorItemsList}
|
||||||
|
helperText={error?.message}
|
||||||
|
onChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
setValue("explanation", getItemName(value));
|
||||||
|
setValue("sub_item_id", "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ItemSelectField;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
|
||||||
|
const SubItemSelectField = ({ control, watch, setValue, getValues }) => {
|
||||||
|
const itemId = watch("item_id");
|
||||||
|
const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId);
|
||||||
|
|
||||||
|
function createExplanation(val) {
|
||||||
|
const currentExplanation = getValues("explanation");
|
||||||
|
const selectedItem = subItemsList.find((item) => item.id === val)?.name;
|
||||||
|
return `${currentExplanation}-${selectedItem}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"sub_item_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="اقدام انجام شده"
|
||||||
|
selectors={subItemsList}
|
||||||
|
isLoading={loadingSubItemsList}
|
||||||
|
schema={{ name: "name", value: "id" }}
|
||||||
|
error={errorSubItemsList}
|
||||||
|
helperText={error?.message}
|
||||||
|
onChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
setValue("explanation", createExplanation(val));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SubItemSelectField;
|
||||||
@@ -9,12 +9,15 @@ import { ExitToApp } from "@mui/icons-material";
|
|||||||
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { array, object, string } from "yup";
|
import { array, object, string } from "yup";
|
||||||
|
import ItemSelectField from "./ItemSelectField";
|
||||||
|
import SubItemSelectField from "./SubItemSelectField";
|
||||||
|
|
||||||
const validationSchema = object({
|
const validationSchema = object({
|
||||||
explanation: string().required("موضوع را مشخص کنید!"),
|
|
||||||
end_point: string().required("مقصد را مشخص کنید!"),
|
end_point: string().required("مقصد را مشخص کنید!"),
|
||||||
region: string().required("محور ماموریت را مشخص کنید!"),
|
region: string().required("محور ماموریت را مشخص کنید!"),
|
||||||
category_id: string().required("نوع ماموریت را مشخص کنید!"),
|
category_id: string().required("نوع ماموریت را مشخص کنید!"),
|
||||||
|
item_id: string().required("موضوع را مشخص کنید!"),
|
||||||
|
sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"),
|
||||||
requested_machines: array().min(1, "نوع خودرو را مشخص کنید!").required("نوع خودرو را مشخص کنید!"),
|
requested_machines: array().min(1, "نوع خودرو را مشخص کنید!").required("نوع خودرو را مشخص کنید!"),
|
||||||
road_observed_id: string().when("category_id", {
|
road_observed_id: string().when("category_id", {
|
||||||
is: "3",
|
is: "3",
|
||||||
@@ -25,6 +28,8 @@ const validationSchema = object({
|
|||||||
|
|
||||||
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
|
item_id: allData.item_id,
|
||||||
|
sub_item_id: allData.sub_item_id,
|
||||||
explanation: allData.explanation,
|
explanation: allData.explanation,
|
||||||
end_point: allData.end_point,
|
end_point: allData.end_point,
|
||||||
region: allData.region,
|
region: allData.region,
|
||||||
@@ -33,7 +38,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
road_observed_id: allData.road_observed_id,
|
road_observed_id: allData.road_observed_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { control, handleSubmit } = useForm({
|
const { control, handleSubmit, watch, setValue, getValues } = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(validationSchema),
|
||||||
mode: "all",
|
mode: "all",
|
||||||
@@ -51,24 +56,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller
|
<ItemSelectField control={control} watch={watch} setValue={setValue} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<SubItemSelectField
|
||||||
control={control}
|
control={control}
|
||||||
name={"explanation"}
|
watch={watch}
|
||||||
render={({ field, fieldState: { error } }) => (
|
setValue={setValue}
|
||||||
<PersianTextField
|
getValues={getValues}
|
||||||
value={field.value}
|
|
||||||
variant="outlined"
|
|
||||||
name="explanation"
|
|
||||||
onChange={field.onChange}
|
|
||||||
label="موضوع"
|
|
||||||
placeholder={"موضوع را وارد کنید"}
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
error={error}
|
|
||||||
helperText={error?.message}
|
|
||||||
InputLabelProps={{ shrink: true }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typogr
|
|||||||
import moment from "jalali-moment";
|
import moment from "jalali-moment";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import ShowBound from "../../../showBound";
|
import ShowRoute from "../Area/Routing/ShowRoute";
|
||||||
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
||||||
loading: () => <MapLoading />,
|
loading: () => <MapLoading />,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -23,11 +23,16 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
|
|||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Box sx={{ flex: 1 }}>
|
<Box sx={{ flex: 1 }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ width: "100%", height: "200px" }}>
|
<Stack spacing={1}>
|
||||||
|
<Divider>
|
||||||
|
<Chip color="primary" variant="outlined" label="مسیر ماموریت" />
|
||||||
|
</Divider>
|
||||||
|
<Box sx={{ width: "100%", height: "400px" }}>
|
||||||
<MapLayer style={{ borderRadius: "4px" }}>
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
<ShowBound bound={allData.bound} />
|
<ShowRoute area={allData.area} />
|
||||||
</MapLayer>
|
</MapLayer>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Stack>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Divider>
|
<Divider>
|
||||||
<Chip color="primary" variant="outlined" label="مشخصات ماموریت" />
|
<Chip color="primary" variant="outlined" label="مشخصات ماموریت" />
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ const Create = ({ mutate }) => {
|
|||||||
|
|
||||||
const submitForm = async (result) => {
|
const submitForm = async (result) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const bound = result.bound.getLatLngs();
|
|
||||||
let area = bound.map((latlng) => [latlng.lng, latlng.lat]);
|
|
||||||
|
|
||||||
await requestServer(REQUEST_MISSION, "post", {
|
await requestServer(REQUEST_MISSION, "post", {
|
||||||
data: {
|
data: {
|
||||||
@@ -31,10 +29,7 @@ const Create = ({ mutate }) => {
|
|||||||
road_observed_id: result.road_observed_id,
|
road_observed_id: result.road_observed_id,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
area: {
|
encoded_route: result.area,
|
||||||
type: result.bound_type,
|
|
||||||
coordinates: area,
|
|
||||||
},
|
|
||||||
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
|
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
|
||||||
requested_machines: result.requested_machines,
|
requested_machines: result.requested_machines,
|
||||||
type: result.type,
|
type: result.type,
|
||||||
@@ -49,6 +44,8 @@ const Create = ({ mutate }) => {
|
|||||||
start_date: result.start_date,
|
start_date: result.start_date,
|
||||||
end_date: result.end_date,
|
end_date: result.end_date,
|
||||||
}),
|
}),
|
||||||
|
item_id: result.item_id,
|
||||||
|
sub_item_id: result.sub_item_id,
|
||||||
},
|
},
|
||||||
hasSidebarUpdate: true,
|
hasSidebarUpdate: true,
|
||||||
})
|
})
|
||||||
@@ -98,11 +95,13 @@ const Create = ({ mutate }) => {
|
|||||||
defaultValues={{
|
defaultValues={{
|
||||||
explanation: "",
|
explanation: "",
|
||||||
category_id: "",
|
category_id: "",
|
||||||
|
item_id: null,
|
||||||
|
sub_item_id: null,
|
||||||
road_observed_id: "",
|
road_observed_id: "",
|
||||||
rahdaran: [],
|
rahdaran: [],
|
||||||
requested_machines: [],
|
requested_machines: [],
|
||||||
bound: null,
|
area: null,
|
||||||
bound_type: "polyline",
|
locations: [],
|
||||||
type: "",
|
type: "",
|
||||||
start_date: "",
|
start_date: "",
|
||||||
start_time: null,
|
start_time: null,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Close } from "@mui/icons-material";
|
||||||
|
import { Dialog, DialogTitle, IconButton } from "@mui/material";
|
||||||
|
import ViolationsList from "./ViolationsList";
|
||||||
|
|
||||||
|
const ViolationsDialog = ({ open, setOpen, mutate }) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} fullWidth maxWidth="sm">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
<DialogTitle>تخلفات</DialogTitle>
|
||||||
|
{open && <ViolationsList setOpen={setOpen} mutate={mutate} />}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ViolationsDialog;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
const AlarmText = ({ isSource }) => {
|
||||||
|
if (isSource) return null;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 5,
|
||||||
|
left: 5,
|
||||||
|
zIndex: 1000,
|
||||||
|
border: "1px solid #ed0f02",
|
||||||
|
backgroundColor: "#f19898",
|
||||||
|
py: 0.5,
|
||||||
|
px: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
opacity: 0.8,
|
||||||
|
boxShadow: "rgba(0, 0, 0, 0.35) 0px 5px 15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: "bold" }} color="#ed0f02">
|
||||||
|
با کلیک بر روی مارکر موقعیت را انتخاب کنید
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmText;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
|
||||||
|
const ROW_COLORS = {
|
||||||
|
source: "#0EA37A", // سبز
|
||||||
|
waypoint: "#F5A623", // زرد/نارنجی
|
||||||
|
dest: "#E53935", // قرمز
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
|
||||||
|
|
||||||
|
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: color,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 12,
|
||||||
|
color: value ? "#1f1f1f" : "#bdbdbd",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || placeholder}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{onEdit && (
|
||||||
|
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
|
||||||
|
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{onRemove && (
|
||||||
|
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
|
||||||
|
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DataBox = ({
|
||||||
|
isSource,
|
||||||
|
isDest,
|
||||||
|
sourcePosition,
|
||||||
|
destPosition,
|
||||||
|
editingTarget,
|
||||||
|
onEditSource,
|
||||||
|
onEditDest,
|
||||||
|
waypoints = [],
|
||||||
|
onEditWaypoint,
|
||||||
|
onAddWaypoint,
|
||||||
|
onRemoveWaypoint,
|
||||||
|
canAddWaypoint,
|
||||||
|
}) => {
|
||||||
|
if (!isSource) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 12,
|
||||||
|
left: 12,
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.95)",
|
||||||
|
backdropFilter: "blur(6px)",
|
||||||
|
border: "1px solid #ececec",
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.75,
|
||||||
|
width: 230,
|
||||||
|
maxWidth: "70vw",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
color={ROW_COLORS.source}
|
||||||
|
label="مبدا"
|
||||||
|
value={formatCoord(sourcePosition)}
|
||||||
|
onEdit={isSource && isDest ? onEditSource : null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{waypoints.map((wp, index) => (
|
||||||
|
<Row
|
||||||
|
key={`waypoint-${index}`}
|
||||||
|
color={ROW_COLORS.waypoint}
|
||||||
|
label={`نقطه میانی ${index + 1}`}
|
||||||
|
value={formatCoord(wp.position)}
|
||||||
|
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
|
||||||
|
onRemove={() => onRemoveWaypoint(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isDest && (
|
||||||
|
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canAddWaypoint && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={0.5}
|
||||||
|
onClick={onAddWaypoint}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color: ROW_COLORS.waypoint,
|
||||||
|
py: 0.25,
|
||||||
|
"&:hover": { opacity: 0.75 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||||
|
افزودن نقطه میانی
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataBox;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
|
||||||
|
const ShowRoute = ({ area }) => {
|
||||||
|
const map = useMap();
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
const [showRoute, setShowRoute] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!coords.length) return;
|
||||||
|
|
||||||
|
setShowRoute(false);
|
||||||
|
|
||||||
|
map.flyToBounds(coords, {
|
||||||
|
padding: [50, 50],
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.once("moveend", () => {
|
||||||
|
setShowRoute(true);
|
||||||
|
});
|
||||||
|
}, [area]);
|
||||||
|
if (!area) return null;
|
||||||
|
const coords = polyline.decode(area, 6);
|
||||||
|
|
||||||
|
if (coords.length === 0) return null;
|
||||||
|
|
||||||
|
const sourcePosition = coords[0];
|
||||||
|
const destPosition = coords[coords.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||||
|
|
||||||
|
<Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
<Marker position={destPosition} icon={destIcon} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowRoute;
|
||||||
|
|
||||||
|
// import { useEffect, useState } from "react";
|
||||||
|
// import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
// import L from "leaflet";
|
||||||
|
// import polyline from "@mapbox/polyline";
|
||||||
|
// import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
// import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||||
|
//
|
||||||
|
// const defaultIconSize = [35, 35];
|
||||||
|
//
|
||||||
|
// const createCustomIcon = (size, iconUrl) => {
|
||||||
|
// return L.icon({
|
||||||
|
// iconUrl: iconUrl,
|
||||||
|
// iconSize: size,
|
||||||
|
// iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
// popupAnchor: [0, -size[1]],
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
|
||||||
|
//
|
||||||
|
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
|
||||||
|
// const ShowRoute = ({ area, locations = [] }) => {
|
||||||
|
// const map = useMap();
|
||||||
|
// const [showRoute, setShowRoute] = useState(false);
|
||||||
|
//
|
||||||
|
// const coords = area ? polyline.decode(area, 6) : [];
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!coords.length) return;
|
||||||
|
//
|
||||||
|
// setShowRoute(false);
|
||||||
|
//
|
||||||
|
// map.flyToBounds(coords, {
|
||||||
|
// padding: [50, 50],
|
||||||
|
// duration: 1.5,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// map.once("moveend", () => {
|
||||||
|
// setShowRoute(true);
|
||||||
|
// });
|
||||||
|
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
// }, [area]);
|
||||||
|
//
|
||||||
|
// if (!area || coords.length === 0) return null;
|
||||||
|
//
|
||||||
|
// const sourcePosition = coords[0];
|
||||||
|
// const destPosition = coords[coords.length - 1];
|
||||||
|
//
|
||||||
|
// // نقاط میانی فقط از روی locations قابل استخراجان (بین اولی و آخری)
|
||||||
|
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||||
|
//
|
||||||
|
// <Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
//
|
||||||
|
// {middleWaypoints.map((wp, index) => (
|
||||||
|
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
|
||||||
|
// ))}
|
||||||
|
//
|
||||||
|
// <Marker position={destPosition} icon={destIcon} />
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export default ShowRoute;
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import { Marker, Polyline, useMapEvents } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
|
||||||
|
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
|
||||||
|
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import DataBox from "./DataBox";
|
||||||
|
import AlarmText from "./AlarmText";
|
||||||
|
|
||||||
|
const MAX_WAYPOINTS = 2;
|
||||||
|
|
||||||
|
const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||||
|
const mapPrevSourceMarker = useRef();
|
||||||
|
const mapPrevDestMarker = useRef();
|
||||||
|
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی بهازای هر waypoint
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const [isSource, setIsSource] = useState(false);
|
||||||
|
const [sourcePosition, setSourcePosition] = useState(null);
|
||||||
|
|
||||||
|
const [isDest, setIsDest] = useState(false);
|
||||||
|
const [destPosition, setDestPosition] = useState(null);
|
||||||
|
|
||||||
|
const [waypoints, setWaypoints] = useState([]);
|
||||||
|
|
||||||
|
const [routeCoords, setRouteCoords] = useState([]);
|
||||||
|
const [alternateCoords, setAlternateCoords] = useState([]);
|
||||||
|
const [editingTarget, setEditingTarget] = useState(null);
|
||||||
|
const [selectedRoute, setSelectedRoute] = useState(null);
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIcon = createCustomIcon(
|
||||||
|
defaultIconSize,
|
||||||
|
isSource && editingTarget !== "source" ? SourceIcon.src : PrevSourceIcon.src
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevDestIcon = createCustomIcon(
|
||||||
|
defaultIconSize,
|
||||||
|
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
|
||||||
|
);
|
||||||
|
|
||||||
|
const getWaypointIcon = (index) => {
|
||||||
|
const wp = waypoints[index];
|
||||||
|
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
|
||||||
|
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveTarget = () => {
|
||||||
|
if (editingTarget) return editingTarget;
|
||||||
|
if (!isSource) return "source";
|
||||||
|
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
|
||||||
|
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
|
||||||
|
if (!isDest) return "dest";
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = useMapEvents({
|
||||||
|
move(e) {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
const center = e.target.getCenter();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setLatLng(center);
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setLatLng(center);
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
movestart() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveend() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mapRef.current = map;
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (locations.length < 2) return;
|
||||||
|
const first = { lat: locations[0].lat, lng: locations[0].lon };
|
||||||
|
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
|
||||||
|
const middle = locations.slice(1, -1).map((loc) => ({
|
||||||
|
position: { lat: loc.lat, lng: loc.lon },
|
||||||
|
confirmed: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSourcePosition(first);
|
||||||
|
setDestPosition(last);
|
||||||
|
setWaypoints(middle);
|
||||||
|
setIsSource(true);
|
||||||
|
setIsDest(true);
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
const decoded = polyline.decode(area, 6);
|
||||||
|
setRouteCoords(decoded);
|
||||||
|
setSelectedRoute({ index: "main", coords: decoded });
|
||||||
|
setTimeout(() => {
|
||||||
|
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
fetchRoute(first, last, middle);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
|
||||||
|
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
|
||||||
|
const locs = [
|
||||||
|
{ lat: source.lat, lon: source.lng },
|
||||||
|
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
|
||||||
|
{ lat: dest.lat, lon: dest.lng },
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://jouya.141.ir/api/router/drive", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ locations: locs }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
const shape = data.trip.legs[0].shape;
|
||||||
|
setLocations(locs);
|
||||||
|
setArea(shape);
|
||||||
|
const decoded = polyline.decode(shape, 6);
|
||||||
|
setRouteCoords(decoded);
|
||||||
|
setSelectedRoute({ index: "main", coords: decoded });
|
||||||
|
|
||||||
|
if (data.alternates?.length > 0) {
|
||||||
|
const decodedAlternates = data.alternates.map((alt) => polyline.decode(alt.trip.legs[0].shape, 6));
|
||||||
|
setAlternateCoords(decodedAlternates);
|
||||||
|
} else {
|
||||||
|
setAlternateCoords([]);
|
||||||
|
}
|
||||||
|
map.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Routing error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetRoute = () => {
|
||||||
|
setRouteCoords([]);
|
||||||
|
setAlternateCoords([]);
|
||||||
|
setSelectedRoute(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkerClick = async () => {
|
||||||
|
if (!isSource || editingTarget === "source") {
|
||||||
|
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
|
||||||
|
setSourcePosition(clickedPosition);
|
||||||
|
setIsSource(true);
|
||||||
|
const wasEditing = editingTarget === "source";
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (wasEditing && destPosition) {
|
||||||
|
await fetchRoute(clickedPosition, destPosition, waypoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDestMarkerClick = async () => {
|
||||||
|
if (!isDest || editingTarget === "dest") {
|
||||||
|
const clickedPosition = mapPrevDestMarker.current.getLatLng();
|
||||||
|
setDestPosition(clickedPosition);
|
||||||
|
setIsDest(true);
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
await fetchRoute(sourcePosition, clickedPosition, waypoints);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWaypointMarkerClick = async (index) => {
|
||||||
|
const wp = waypoints[index];
|
||||||
|
if (!wp || wp.confirmed) return; // فقط روی نقطهی تأییدنشده کلیک معتبره (یا حالت ادیت)
|
||||||
|
|
||||||
|
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
|
||||||
|
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
|
||||||
|
setWaypoints(updated);
|
||||||
|
const wasEditing = editingTarget === `waypoint-${index}`;
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (sourcePosition && destPosition) {
|
||||||
|
await fetchRoute(sourcePosition, destPosition, updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSource = () => {
|
||||||
|
setEditingTarget("source");
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditDest = () => {
|
||||||
|
setEditingTarget("dest");
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditWaypoint = (index) => {
|
||||||
|
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed میکنیم تا با حرکت نقشه جابهجا بشه
|
||||||
|
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
|
||||||
|
setEditingTarget(`waypoint-${index}`);
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
|
||||||
|
const handleAddWaypoint = () => {
|
||||||
|
if (waypoints.length >= MAX_WAYPOINTS) return;
|
||||||
|
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
|
||||||
|
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
|
||||||
|
setEditingTarget(`waypoint-${waypoints.length}`);
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// حذف یک نقطه میانی
|
||||||
|
const handleRemoveWaypoint = async (index) => {
|
||||||
|
const updated = waypoints.filter((_, i) => i !== index);
|
||||||
|
setWaypoints(updated);
|
||||||
|
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
|
||||||
|
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (sourcePosition && destPosition) {
|
||||||
|
await fetchRoute(sourcePosition, destPosition, updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectRoute = (index, coords) => {
|
||||||
|
setSelectedRoute({ index, coords });
|
||||||
|
const encoded = polyline.encode(coords, 6);
|
||||||
|
setArea(encoded);
|
||||||
|
map.flyToBounds(coords, { padding: [50, 50], duration: 0.3 });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataBox
|
||||||
|
isSource={isSource}
|
||||||
|
isDest={isDest}
|
||||||
|
sourcePosition={sourcePosition}
|
||||||
|
destPosition={destPosition}
|
||||||
|
editingTarget={editingTarget}
|
||||||
|
onEditSource={handleEditSource}
|
||||||
|
onEditDest={handleEditDest}
|
||||||
|
waypoints={waypoints}
|
||||||
|
onEditWaypoint={handleEditWaypoint}
|
||||||
|
onAddWaypoint={handleAddWaypoint}
|
||||||
|
onRemoveWaypoint={handleRemoveWaypoint}
|
||||||
|
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
|
||||||
|
/>
|
||||||
|
<AlarmText isSource={isSource} />
|
||||||
|
|
||||||
|
<Marker
|
||||||
|
position={sourcePosition || map.getCenter()}
|
||||||
|
ref={mapPrevSourceMarker}
|
||||||
|
eventHandlers={{ click: handleMarkerClick }}
|
||||||
|
icon={defaultIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{waypoints.map((wp, index) => (
|
||||||
|
<Marker
|
||||||
|
key={`waypoint-${index}`}
|
||||||
|
position={wp.position || map.getCenter()}
|
||||||
|
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
|
||||||
|
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
|
||||||
|
icon={getWaypointIcon(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isSource && (
|
||||||
|
<Marker
|
||||||
|
position={destPosition || map.getCenter()}
|
||||||
|
ref={mapPrevDestMarker}
|
||||||
|
eventHandlers={{ click: handleDestMarkerClick }}
|
||||||
|
icon={prevDestIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alternateCoords.map((coords, index) => (
|
||||||
|
<Polyline
|
||||||
|
key={`alternate-${index}-${selectedRoute?.index === index}`}
|
||||||
|
positions={coords}
|
||||||
|
color={selectedRoute?.index === index ? "#015688" : "#91B2C6"}
|
||||||
|
weight={selectedRoute?.index === index ? 7 : 6}
|
||||||
|
opacity={selectedRoute?.index === index ? 1 : 0.7}
|
||||||
|
eventHandlers={{ click: () => handleSelectRoute(index, coords) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{routeCoords.length > 0 && (
|
||||||
|
<Polyline
|
||||||
|
key={`main-${selectedRoute?.index === "main"}`}
|
||||||
|
positions={routeCoords}
|
||||||
|
color={selectedRoute?.index === "main" ? "#015688" : "#91B2C6"}
|
||||||
|
weight={selectedRoute?.index === "main" ? 7 : 6}
|
||||||
|
opacity={selectedRoute?.index === "main" ? 1 : 0.7}
|
||||||
|
eventHandlers={{ click: () => handleSelectRoute("main", routeCoords) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Routing;
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import MapLoading from "@/core/components/MapLayer/Loading";
|
||||||
|
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import Routing from "./Routing";
|
||||||
|
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
||||||
|
loading: () => <MapLoading />,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
|
const [area, setArea] = useState(allData.area);
|
||||||
|
const [locations, setLocations] = useState(allData.locations);
|
||||||
|
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
setAllData({ area: area });
|
||||||
|
setAllData({ locations: locations });
|
||||||
|
setTabState((s) => s + 1);
|
||||||
|
}, [area]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Box sx={{ width: "100%", height: "400px" }}>
|
||||||
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
|
<Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
|
||||||
|
</MapLayer>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
||||||
|
{"مرحله قبلی"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
|
||||||
|
{!area ? "در انتظار ترسیم" : "مرحله بعد"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Area;
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import MuiDatePicker from "@/core/components/MuiDatePicker";
|
||||||
|
import MuiTimePicker from "@/core/components/MuiTimePicker";
|
||||||
|
import { makeDateTime } from "@/core/utils/makeDateTime";
|
||||||
|
import { Grid } from "@mui/material";
|
||||||
|
import { Controller, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
|
const MissionDates = ({ control }) => {
|
||||||
|
const start_date = useWatch({ control, name: "start_date" });
|
||||||
|
const end_date = useWatch({ control, name: "end_date" });
|
||||||
|
const start_time = useWatch({ control, name: "start_time" });
|
||||||
|
const end_time = useWatch({ control, name: "end_time" });
|
||||||
|
|
||||||
|
const startDateTime = makeDateTime(start_date, start_time);
|
||||||
|
const endDateTime = makeDateTime(end_date, end_time);
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"start_date"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<MuiDatePicker
|
||||||
|
name="start_date"
|
||||||
|
error={error}
|
||||||
|
value={field.value}
|
||||||
|
maxDate={end_date ?? null}
|
||||||
|
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
|
||||||
|
label={"تاریخ شروع ماموریت"}
|
||||||
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
|
helperText={error ? error.message : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"start_time"}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<MuiTimePicker
|
||||||
|
name="start_time"
|
||||||
|
error={error}
|
||||||
|
value={field.value}
|
||||||
|
views={["hours", "minutes"]}
|
||||||
|
maxTime={
|
||||||
|
endDateTime &&
|
||||||
|
start_date &&
|
||||||
|
end_date &&
|
||||||
|
new Date(start_date).toDateString() === new Date(end_date).toDateString()
|
||||||
|
? endDateTime
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
placeholder={"زمان شروع ماموریت را وارد کنید"}
|
||||||
|
label={"زمان شروع ماموریت"}
|
||||||
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
|
helperText={error ? error.message : null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"end_date"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<MuiDatePicker
|
||||||
|
name="end_date"
|
||||||
|
error={error}
|
||||||
|
value={field.value}
|
||||||
|
minDate={start_date ?? null}
|
||||||
|
maxDate={now}
|
||||||
|
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
|
||||||
|
label={"تاریخ پایان ماموریت"}
|
||||||
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
|
helperText={error ? error.message : null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"end_time"}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<MuiTimePicker
|
||||||
|
name="end_time"
|
||||||
|
error={error}
|
||||||
|
value={field.value}
|
||||||
|
views={["hours", "minutes"]}
|
||||||
|
minTime={
|
||||||
|
startDateTime &&
|
||||||
|
end_date &&
|
||||||
|
start_date &&
|
||||||
|
new Date(start_date).toDateString() === new Date(end_date).toDateString()
|
||||||
|
? startDateTime
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
maxTime={
|
||||||
|
end_date && new Date(end_date).toDateString() === now.toDateString() ? now : null
|
||||||
|
}
|
||||||
|
placeholder={"زمان پایان ماموریت را وارد کنید"}
|
||||||
|
label={"زمان پایان ماموریت"}
|
||||||
|
setFieldValue={(name, value) => field.onChange(value || null)}
|
||||||
|
helperText={error ? error.message : null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MissionDates;
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
|
import { missionTypes } from "@/core/utils/missionTypes";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
||||||
|
import moment from "jalali-moment";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { object, string } from "yup";
|
||||||
|
import MissionDates from "./MissionDates";
|
||||||
|
|
||||||
|
const validationSchema = object({
|
||||||
|
start_date: string().required("تاریخ شروع ماموریت را مشخص کنید!"),
|
||||||
|
start_time: string().required("زمان شروع ماموریت را مشخص کنید!"),
|
||||||
|
end_date: string().required("تاریخ پایان ماموریت را مشخص کنید!"),
|
||||||
|
end_time: string().required("زمان شروع ماموریت را مشخص کنید!"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const GetDateTime = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
|
const defaultValues = {
|
||||||
|
start_date: allData.start_date,
|
||||||
|
start_time: allData.start_time ? moment(allData.start_time).toDate() : null,
|
||||||
|
end_date: allData.end_date,
|
||||||
|
end_time: allData.end_time ? moment(allData.end_time).toDate() : null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { control, handleSubmit } = useForm({
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNext = (data) => {
|
||||||
|
setAllData(data);
|
||||||
|
setTabState((s) => s + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm onSubmit={handleSubmit(handleNext)}>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Stack>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<MissionDates control={control} />
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
||||||
|
{"مرحله قبلی"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="large" type="submit">
|
||||||
|
مرحله بعد
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default GetDateTime;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Done } from "@mui/icons-material";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
|
||||||
|
const SelectId = ({ row, onChange, setOpenFastReactDialog }) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
onChange(row.original.id);
|
||||||
|
setOpenFastReactDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="انتخاب" arrow placement="right">
|
||||||
|
<IconButton color="primary" sx={{ textTransform: "unset", alignSelf: "center" }} onClick={handleClick}>
|
||||||
|
<Done />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SelectId;
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import SelectId from "./SelectId";
|
||||||
|
|
||||||
|
const RowActions = ({ row, onChange, setOpenFastReactDialog }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectId row={row} onChange={onChange} setOpenFastReactDialog={setOpenFastReactDialog} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RowActions;
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
|
import { GET_FAST_REACT_COMPLAINTS } from "@/core/utils/routes";
|
||||||
|
import { Box, Stack, Typography } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import RowActions from "./RowActions";
|
||||||
|
import { usePermissions } from "@/lib/hooks/usePermissions";
|
||||||
|
import { useAuth } from "@/lib/contexts/auth";
|
||||||
|
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
|
||||||
|
import DescriptionForm from "@/components/dashboard/fastReact/complaintList/RowActions/DescriptionDialog";
|
||||||
|
import LocationForm from "@/components/dashboard/fastReact/complaintList/RowActions/LocationDialog";
|
||||||
|
import moment from "jalali-moment";
|
||||||
|
import useProvinces from "@/lib/hooks/useProvince";
|
||||||
|
import useEdaratLists from "@/lib/hooks/useEdaratLists";
|
||||||
|
|
||||||
|
const FastReactList = ({ onChange, setOpenFastReactDialog }) => {
|
||||||
|
const { data: userPermissions } = usePermissions();
|
||||||
|
const hasCountryPermission = userPermissions?.includes("show-fast-react");
|
||||||
|
const { user } = useAuth();
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const dynamicColumns = {
|
||||||
|
header: "استان",
|
||||||
|
id: "road_observeds__province_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
grow: false,
|
||||||
|
size: 130,
|
||||||
|
ColumnSelectComponent: (props) => {
|
||||||
|
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||||
|
const getColumnSelectOptions = useMemo(() => {
|
||||||
|
if (loadingProvinces) {
|
||||||
|
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||||
|
}
|
||||||
|
if (errorProvinces) {
|
||||||
|
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: "", label: "کل کشور" },
|
||||||
|
...provinces.map((province) => ({
|
||||||
|
value: province.id,
|
||||||
|
label: province.name_fa,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [provinces, errorProvinces, loadingProvinces]);
|
||||||
|
return (
|
||||||
|
<CustomSelectByDependency
|
||||||
|
{...props}
|
||||||
|
value={loadingProvinces ? "loading" : props.filterParameters.value}
|
||||||
|
columnSelectOption={getColumnSelectOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Cell: ({ row }) => <>{row.original.province_fa}</>,
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "کد یکتا",
|
||||||
|
id: "road_observeds__id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
...(hasCountryPermission ? [dynamicColumns] : []),
|
||||||
|
{
|
||||||
|
header: "اداره",
|
||||||
|
id: "road_observeds__edarate_shahri_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
dependencyId: hasCountryPermission ? "road_observeds__province_id" : null,
|
||||||
|
grow: false,
|
||||||
|
size: 120,
|
||||||
|
ColumnSelectComponent: (props) => {
|
||||||
|
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
|
||||||
|
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
|
||||||
|
);
|
||||||
|
const [prevDependency, setPrevDependency] = useState(
|
||||||
|
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
|
||||||
|
);
|
||||||
|
|
||||||
|
const getColumnSelectOptions = useMemo(() => {
|
||||||
|
if (hasCountryPermission && props.dependencyFieldValue.value === "") {
|
||||||
|
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
|
||||||
|
}
|
||||||
|
if (loadingEdaratList) {
|
||||||
|
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||||
|
}
|
||||||
|
if (errorEdaratList) {
|
||||||
|
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: "", label: "کل ادارات" },
|
||||||
|
...edaratList.map((edare) => ({
|
||||||
|
value: edare.id,
|
||||||
|
label: edare.name_fa,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [edaratList, loadingEdaratList, errorEdaratList]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasCountryPermission) return;
|
||||||
|
if (prevDependency === props.dependencyFieldValue?.value) return;
|
||||||
|
props.handleChange({ ...props.filterParameters, value: "" });
|
||||||
|
setPrevDependency(props.dependencyFieldValue?.value);
|
||||||
|
}, [props.dependencyFieldValue?.value, hasCountryPermission]);
|
||||||
|
return (
|
||||||
|
<CustomSelectByDependency
|
||||||
|
{...props}
|
||||||
|
value={
|
||||||
|
props.dependencyFieldValue?.value === ""
|
||||||
|
? "empty"
|
||||||
|
: loadingEdaratList
|
||||||
|
? "loading"
|
||||||
|
: props.filterParameters.value
|
||||||
|
}
|
||||||
|
columnSelectOption={getColumnSelectOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Cell: ({ row }) => <>{row.original.edarate_shahri_name_fa}</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "اطلاعات ثبت شده در سامانه سوانح",
|
||||||
|
id: "fkInfo",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
enableSorting: false,
|
||||||
|
grow: false,
|
||||||
|
size: 50,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
accessorKey: "fk_RegisteredEventMessage",
|
||||||
|
header: "کد سوانح",
|
||||||
|
id: "fk_RegisteredEventMessage",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Title",
|
||||||
|
header: "موضوع گزارش",
|
||||||
|
id: "Title",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "FeatureTypeTitle",
|
||||||
|
header: "نوع گزارش",
|
||||||
|
id: "FeatureTypeTitle",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "Description",
|
||||||
|
header: "توضیح گزارش",
|
||||||
|
id: "Description",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
muiTableBodyCellProps: {
|
||||||
|
sx: {
|
||||||
|
borderLeft: "1px solid #e1e1e1",
|
||||||
|
py: 0,
|
||||||
|
"&:first-of-type": {
|
||||||
|
borderLeft: "unset",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cell: ({ renderedCellValue }) => {
|
||||||
|
if (renderedCellValue) {
|
||||||
|
return (
|
||||||
|
<Stack alignItems={"center"} justifyContent={"center"}>
|
||||||
|
<DescriptionForm description={renderedCellValue} title={"توضیح گزارش"} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Typography variant="body2">-</Typography>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "موقعیت",
|
||||||
|
id: "location",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
enableSorting: false,
|
||||||
|
datatype: "array",
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
muiTableBodyCellProps: {
|
||||||
|
sx: {
|
||||||
|
borderLeft: "1px solid #e1e1e1",
|
||||||
|
py: 0,
|
||||||
|
"&:first-of-type": {
|
||||||
|
borderLeft: "unset",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Stack alignItems={"center"} justifyContent={"center"}>
|
||||||
|
<LocationForm start_lat={row.original.lat} start_lng={row.original.lng} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "MobileForSendEventSms",
|
||||||
|
header: "شماره تماس گیرنده",
|
||||||
|
id: "MobileForSendEventSms",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
sortDescFirst: false,
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorFn: (row) => moment(row.StartTime_DateTime).locale("fa").format("HH:mm | yyyy/MM/DD"),
|
||||||
|
header: "تاریخ ثبت",
|
||||||
|
id: "StartTime_DateTime",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "date",
|
||||||
|
filterMode: "between",
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ mt: 3, p: 1 }}>
|
||||||
|
<DataTableWithAuth
|
||||||
|
need_filter={true}
|
||||||
|
table_title="لیست شکایات"
|
||||||
|
columns={columns}
|
||||||
|
sorting={[{ id: "StartTime_DateTime", desc: true }]}
|
||||||
|
table_url={GET_FAST_REACT_COMPLAINTS}
|
||||||
|
page_name={"roadMissionsOperator"}
|
||||||
|
table_name={"roadMissionsOperatorFastReactList"}
|
||||||
|
enableRowActions
|
||||||
|
positionActionsColumn={"first"}
|
||||||
|
RowActions={(props) => (
|
||||||
|
<RowActions {...props} onChange={onChange} setOpenFastReactDialog={setOpenFastReactDialog} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default FastReactList;
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { Close, Edit } from "@mui/icons-material";
|
||||||
|
import { Button, Chip, Dialog, Divider, IconButton } from "@mui/material";
|
||||||
|
import FastReactList from "./List";
|
||||||
|
import { useState, forwardRef } from "react";
|
||||||
|
|
||||||
|
const FastReactDialog = forwardRef(({ onChange, value }, ref) => {
|
||||||
|
const [openFastReactDialog, setOpenFastReactDialog] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Divider ref={ref}>
|
||||||
|
{value === "" ? (
|
||||||
|
<Button onClick={() => setOpenFastReactDialog(true)} variant="outlined">
|
||||||
|
انتخاب شکایت
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Chip
|
||||||
|
label={`کد شکایت: ${value}`}
|
||||||
|
variant="outlined"
|
||||||
|
deleteIcon={<Edit />}
|
||||||
|
onDelete={() => setOpenFastReactDialog(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Divider>
|
||||||
|
<Dialog open={openFastReactDialog} fullWidth maxWidth="md">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpenFastReactDialog(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
{openFastReactDialog && (
|
||||||
|
<FastReactList onChange={onChange} setOpenFastReactDialog={setOpenFastReactDialog} />
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
FastReactDialog.displayName = "FastReactDialog";
|
||||||
|
|
||||||
|
export default FastReactDialog;
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Divider, Stack } from "@mui/material";
|
||||||
|
import { Controller, useWatch } from "react-hook-form";
|
||||||
|
import FastReactDialog from "./Dialog";
|
||||||
|
|
||||||
|
const FastReactCode = ({ control }) => {
|
||||||
|
const category_id = useWatch({ control, name: "category_id" });
|
||||||
|
|
||||||
|
if (category_id != 3) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"road_observed_id"}
|
||||||
|
render={({ field }) => (
|
||||||
|
<Stack justifyContent={"center"} sx={{ height: "100%" }}>
|
||||||
|
<FastReactDialog {...field} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default FastReactCode;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
|
||||||
|
const ItemSelectField = ({ control, setValue }) => {
|
||||||
|
const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems();
|
||||||
|
function getItemName(val) {
|
||||||
|
return itemsList.find((item) => item.id === val)?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"item_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="موضوع"
|
||||||
|
selectors={itemsList}
|
||||||
|
isLoading={loadingItemsList}
|
||||||
|
schema={{ name: "name", value: "id" }}
|
||||||
|
error={errorItemsList}
|
||||||
|
helperText={error?.message}
|
||||||
|
onChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
setValue("explanation", getItemName(value));
|
||||||
|
setValue("sub_item_id", "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ItemSelectField;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
|
||||||
|
const SubItemSelectField = ({ control, watch, setValue, getValues }) => {
|
||||||
|
const itemId = watch("item_id");
|
||||||
|
const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId);
|
||||||
|
|
||||||
|
function createExplanation(val) {
|
||||||
|
const currentExplanation = getValues("explanation");
|
||||||
|
const selectedItem = subItemsList.find((item) => item.id === val)?.name;
|
||||||
|
return `${currentExplanation}-${selectedItem}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"sub_item_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="اقدام انجام شده"
|
||||||
|
selectors={subItemsList}
|
||||||
|
isLoading={loadingSubItemsList}
|
||||||
|
schema={{ name: "name", value: "id" }}
|
||||||
|
error={errorSubItemsList}
|
||||||
|
helperText={error?.message}
|
||||||
|
onChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
setValue("explanation", createExplanation(val));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SubItemSelectField;
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import PersianTextField from "@/core/components/PersianTextField";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
|
import { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
|
||||||
|
import { missionRegions } from "@/core/utils/missionRegions";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { ExitToApp } from "@mui/icons-material";
|
||||||
|
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { object, string } from "yup";
|
||||||
|
import ItemSelectField from "./ItemSelectField";
|
||||||
|
import SubItemSelectField from "./SubItemSelectField";
|
||||||
|
|
||||||
|
const validationSchema = object({
|
||||||
|
item_id: string().required("موضوع را مشخص کنید!"),
|
||||||
|
sub_item_id: string().required("اقدام انجام شده را مشخص کنید!"),
|
||||||
|
end_point: string().required("مقصد را مشخص کنید!"),
|
||||||
|
region: string().required("محور ماموریت را مشخص کنید!"),
|
||||||
|
category_id: string().required("نوع ماموریت را مشخص کنید!"),
|
||||||
|
road_observed_id: string().when("category_id", {
|
||||||
|
is: "3",
|
||||||
|
then: (schema) => schema.required("کد یکتا شکایت را مشخص کنید!"),
|
||||||
|
otherwise: (schema) => schema.notRequired(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
|
const defaultValues = {
|
||||||
|
item_id: allData.item_id,
|
||||||
|
sub_item_id: allData.sub_item_id,
|
||||||
|
explanation: allData.explanation,
|
||||||
|
end_point: allData.end_point,
|
||||||
|
region: allData.region,
|
||||||
|
category_id: allData.category_id,
|
||||||
|
road_observed_id: allData.road_observed_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { control, handleSubmit, watch, setValue, getValues } = useForm({
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleNext = (data) => {
|
||||||
|
setAllData(data);
|
||||||
|
setTabState((s) => s + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm onSubmit={handleSubmit(handleNext)}>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Stack>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<ItemSelectField control={control} watch={watch} setValue={setValue} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<SubItemSelectField
|
||||||
|
control={control}
|
||||||
|
watch={watch}
|
||||||
|
setValue={setValue}
|
||||||
|
getValues={getValues}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"region"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="محدوده"
|
||||||
|
selectors={missionRegions}
|
||||||
|
schema={{ name: "name_fa", value: "id" }}
|
||||||
|
error={error}
|
||||||
|
onChange={field.onChange}
|
||||||
|
helperText={error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"category_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="نوع ماموریت"
|
||||||
|
selectors={missionCategoryTypes}
|
||||||
|
schema={{ name: "name_fa", value: "id" }}
|
||||||
|
error={error}
|
||||||
|
onChange={field.onChange}
|
||||||
|
helperText={error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"end_point"}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<PersianTextField
|
||||||
|
value={field.value}
|
||||||
|
variant="outlined"
|
||||||
|
name="end_point"
|
||||||
|
onChange={field.onChange}
|
||||||
|
label="مقصد"
|
||||||
|
placeholder={"مقصد را وارد کنید"}
|
||||||
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
error={error}
|
||||||
|
helperText={error?.message}
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button
|
||||||
|
onClick={handlePrev}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
startIcon={<ExitToApp />}
|
||||||
|
>
|
||||||
|
{"بستن"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="large" type="submit">
|
||||||
|
مرحله بعد
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default GetItemInfo;
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import RahdarNameOrCode from "@/core/components/RahdarNameOrCode";
|
||||||
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { Done } from "@mui/icons-material";
|
||||||
|
import { Button, Stack } from "@mui/material";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { object } from "yup";
|
||||||
|
|
||||||
|
const schema = object().shape({
|
||||||
|
rahdar: object().required("همراه الزامی است."),
|
||||||
|
});
|
||||||
|
const DriverForm = ({ setDriver, setOpenDriversDialog }) => {
|
||||||
|
const defaultValues = {
|
||||||
|
rahdar: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting, isValid },
|
||||||
|
} = useForm({ defaultValues, resolver: yupResolver(schema), mode: "all" });
|
||||||
|
|
||||||
|
const submit = (data) => {
|
||||||
|
setDriver(data.rahdar);
|
||||||
|
setOpenDriversDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm onSubmit={handleSubmit(submit)}>
|
||||||
|
<Stack direction={{ xs: "column", sm: "row" }} justifyContent={"center"} spacing={2}>
|
||||||
|
<Stack sx={{ minWidth: "250px" }}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<RahdarNameOrCode
|
||||||
|
title="راننده"
|
||||||
|
rahdarsName={field.value}
|
||||||
|
setRahdarsName={(value) => field.onChange(value)}
|
||||||
|
error={error}
|
||||||
|
multiple={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
name={"rahdar"}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
sx={{ textWrap: "nowrap", px: 3 }}
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
startIcon={<Done />}
|
||||||
|
>
|
||||||
|
انتخاب
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DriverForm;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { DialogContent } from "@mui/material";
|
||||||
|
import DriverForm from "./Form";
|
||||||
|
|
||||||
|
const DriversSearch = ({ setDriver, setOpenDriversDialog }) => {
|
||||||
|
return (
|
||||||
|
<DialogContent dividers>
|
||||||
|
<DriverForm setDriver={setDriver} setOpenDriversDialog={setOpenDriversDialog} />
|
||||||
|
</DialogContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DriversSearch;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Close, Edit } from "@mui/icons-material";
|
||||||
|
import { Button, Dialog, DialogTitle, IconButton } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import DriversSearch from "./DriversSearch";
|
||||||
|
|
||||||
|
const DriversDialog = ({ setDriver, mode = "" }) => {
|
||||||
|
const [openDriversDialog, setOpenDriversDialog] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mode == "edit" ? (
|
||||||
|
<IconButton onClick={() => setOpenDriversDialog(true)} size="small">
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => setOpenDriversDialog(true)} variant="contained" fullWidth size="large">
|
||||||
|
انتخاب راننده
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Dialog open={openDriversDialog} fullWidth maxWidth="xs">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpenDriversDialog(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
<DialogTitle>انتخاب راننده</DialogTitle>
|
||||||
|
{openDriversDialog && (
|
||||||
|
<DriversSearch setDriver={setDriver} setOpenDriversDialog={setOpenDriversDialog} />
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DriversDialog;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Done } from "@mui/icons-material";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
|
||||||
|
const Allocate = ({ row, setMachine, setOpenMachinesDialog }) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
setMachine(row.original);
|
||||||
|
setOpenMachinesDialog(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="تخصیص" arrow placement="right">
|
||||||
|
<IconButton color="primary" sx={{ textTransform: "unset", alignSelf: "center" }} onClick={handleClick}>
|
||||||
|
<Done />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Allocate;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import Allocate from "./Allocate";
|
||||||
|
|
||||||
|
const RowActions = ({ row, setMachine, setOpenMachinesDialog }) => {
|
||||||
|
return <Allocate row={row} setMachine={setMachine} setOpenMachinesDialog={setOpenMachinesDialog} />;
|
||||||
|
};
|
||||||
|
export default RowActions;
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import RowActions from "./RowActions";
|
||||||
|
import { GET_MACHINES_TABLE_LIST } from "@/core/utils/routes";
|
||||||
|
|
||||||
|
const MachinesList = ({ machineType, setMachine, setOpenMachinesDialog }) => {
|
||||||
|
const columns = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
accessorKey: "machine_code",
|
||||||
|
header: "کد خودرو",
|
||||||
|
id: "machine_code",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "car_name",
|
||||||
|
header: "نام خودرو",
|
||||||
|
id: "car_name",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "car_type",
|
||||||
|
header: "نوع خودرو",
|
||||||
|
id: "car_type",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ mt: 3, p: 1 }}>
|
||||||
|
<DataTableWithAuth
|
||||||
|
need_filter={true}
|
||||||
|
table_title="لیست خودرو ها"
|
||||||
|
columns={columns}
|
||||||
|
sorting={[{ id: "id", desc: true }]}
|
||||||
|
table_url={GET_MACHINES_TABLE_LIST}
|
||||||
|
page_name={"roadMissionsOperator"}
|
||||||
|
table_name={"roadMissionsOperatorMachinesList"}
|
||||||
|
enableRowActions
|
||||||
|
positionActionsColumn={"first"}
|
||||||
|
RowActions={(props) => (
|
||||||
|
<RowActions {...props} setMachine={setMachine} setOpenMachinesDialog={setOpenMachinesDialog} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MachinesList;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { Close, Edit } from "@mui/icons-material";
|
||||||
|
import { Button, Dialog, IconButton } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import MachinesList from "./MachineList";
|
||||||
|
|
||||||
|
const MachinesDialog = ({ setMachine, mode }) => {
|
||||||
|
const [openMachinesDialog, setOpenMachinesDialog] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{mode == "edit" ? (
|
||||||
|
<IconButton onClick={() => setOpenMachinesDialog(true)} size="small">
|
||||||
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<Button onClick={() => setOpenMachinesDialog(true)} variant="contained" fullWidth size="large">
|
||||||
|
انتخاب خودرو
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Dialog open={openMachinesDialog} fullWidth maxWidth="sm">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpenMachinesDialog(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
{openMachinesDialog && (
|
||||||
|
<MachinesList setMachine={setMachine} setOpenMachinesDialog={setOpenMachinesDialog} />
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MachinesDialog;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typography } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import DriversDialog from "./DriversDialog";
|
||||||
|
|
||||||
|
const MachineAndDriver = ({ setTabState, allData, setAllData, handlePrev }) => {
|
||||||
|
const [driver, setDriver] = useState(allData.driver);
|
||||||
|
|
||||||
|
const handleNext = (data) => {
|
||||||
|
setAllData(data);
|
||||||
|
setTabState((s) => s + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Stack sx={{ my: 1 }} spacing={2}>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
{driver ? (
|
||||||
|
<>
|
||||||
|
<Typography variant="body2">راننده</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip label={driver ? `${driver.name}` : "راننده ای انتخاب نشده"} size="small" />
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<DriversDialog setDriver={setDriver} mode={"edit"} />
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<DriversDialog setDriver={setDriver} />
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
||||||
|
{"مرحله قبلی"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" disabled={!driver} size="large" onClick={() => handleNext({ driver })}>
|
||||||
|
مرحله بعد
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MachineAndDriver;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import RahdarNameOrCode from "@/core/components/RahdarNameOrCode";
|
||||||
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { Add } from "@mui/icons-material";
|
||||||
|
import { Button, Stack } from "@mui/material";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { object } from "yup";
|
||||||
|
|
||||||
|
const schema = object().shape({
|
||||||
|
rahdar: object().required("همراه الزامی است."),
|
||||||
|
});
|
||||||
|
const RahdaranForm = ({ setRahdaran }) => {
|
||||||
|
const defaultValues = {
|
||||||
|
rahdar: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting, isValid },
|
||||||
|
} = useForm({ defaultValues, resolver: yupResolver(schema), mode: "all" });
|
||||||
|
|
||||||
|
const submit = (data) => {
|
||||||
|
setRahdaran((prev) => {
|
||||||
|
const alreadyExists = prev.some((r) => r.id === data.rahdar.id);
|
||||||
|
if (alreadyExists) return prev;
|
||||||
|
return [...prev, data.rahdar];
|
||||||
|
});
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm onSubmit={handleSubmit(submit)}>
|
||||||
|
<Stack direction={{ xs: "column", sm: "row" }} justifyContent={"center"} spacing={2}>
|
||||||
|
<Stack sx={{ minWidth: "250px" }}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<RahdarNameOrCode
|
||||||
|
title="همراه"
|
||||||
|
rahdarsName={field.value}
|
||||||
|
setRahdarsName={(value) => field.onChange(value)}
|
||||||
|
error={error}
|
||||||
|
multiple={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
name={"rahdar"}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
type="submit"
|
||||||
|
sx={{ textWrap: "nowrap", px: 3 }}
|
||||||
|
disabled={isSubmitting || !isValid}
|
||||||
|
startIcon={<Add />}
|
||||||
|
>
|
||||||
|
افزودن همراه
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RahdaranForm;
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { AccountCircle, Delete } from "@mui/icons-material";
|
||||||
|
import { Card, Collapse, IconButton, Stack, Typography } from "@mui/material";
|
||||||
|
import { TransitionGroup } from "react-transition-group";
|
||||||
|
|
||||||
|
const RahdaranList = ({ rahdaran, setRahdaran }) => {
|
||||||
|
const handleRemove = (index) => {
|
||||||
|
setRahdaran((prev) => prev.filter((_, i) => i !== index));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<TransitionGroup>
|
||||||
|
{rahdaran.map((rahdar, index) => (
|
||||||
|
<Collapse key={rahdar.code}>
|
||||||
|
<Card
|
||||||
|
elevation={0}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
border: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
px: 1,
|
||||||
|
py: 1,
|
||||||
|
my: 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack direction={"row"} sx={{ display: "flex", alignItems: "center" }} spacing={1}>
|
||||||
|
<AccountCircle color="primary" sx={{ width: "32px", height: "32px" }} />
|
||||||
|
<Stack>
|
||||||
|
<Stack direction={"row"} spacing={0.5}>
|
||||||
|
<Typography variant="caption">نام و نام خانوادگی:</Typography>
|
||||||
|
<Typography variant="body2">{rahdar.name}</Typography>
|
||||||
|
</Stack>
|
||||||
|
{/*<Stack direction={"row"} spacing={0.5}>*/}
|
||||||
|
{/* <Typography variant="caption">کدملی:</Typography>*/}
|
||||||
|
{/* <Typography variant="body2">-</Typography> /!*{rahdar.code}*!/*/}
|
||||||
|
{/*</Stack>*/}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<IconButton color="error" onClick={() => handleRemove(index)}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
|
</Card>
|
||||||
|
</Collapse>
|
||||||
|
))}
|
||||||
|
{rahdaran.length == 0 && (
|
||||||
|
<Collapse>
|
||||||
|
<Typography align="center">همراهی ثبت نشده است</Typography>
|
||||||
|
</Collapse>
|
||||||
|
)}
|
||||||
|
</TransitionGroup>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RahdaranList;
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Box, Button, Chip, DialogActions, DialogContent, Divider } from "@mui/material";
|
||||||
|
import { useCallback, useState } from "react";
|
||||||
|
import RahdaranForm from "./Form";
|
||||||
|
import RahdaranList from "./List";
|
||||||
|
|
||||||
|
const Rahdaran = ({ allData, setAllData, setTabState, handlePrev }) => {
|
||||||
|
const [rahdaran, setRahdaran] = useState(allData.rahdaran);
|
||||||
|
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
setAllData({ rahdaran: rahdaran });
|
||||||
|
setTabState((s) => s + 1);
|
||||||
|
}, [rahdaran]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<RahdaranForm setRahdaran={setRahdaran} />
|
||||||
|
<Divider sx={{ my: 2 }}>
|
||||||
|
<Chip label="لیست همراهان" />
|
||||||
|
</Divider>
|
||||||
|
<RahdaranList rahdaran={rahdaran} setRahdaran={setRahdaran} />
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
||||||
|
{"مرحله قبلی"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="large" onClick={handleNext}>
|
||||||
|
{"مرحله بعد"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Rahdaran;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import MapLoading from "@/core/components/MapLayer/Loading";
|
||||||
|
import { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
|
||||||
|
import { missionRegions } from "@/core/utils/missionRegions";
|
||||||
|
import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typography } from "@mui/material";
|
||||||
|
import moment from "jalali-moment";
|
||||||
|
import dynamic from "next/dynamic";
|
||||||
|
import { useCallback } from "react";
|
||||||
|
import ShowRoute from "../Area/Routing/ShowRoute";
|
||||||
|
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
||||||
|
loading: () => <MapLoading />,
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
submitForm(allData);
|
||||||
|
}, [allData]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Divider>
|
||||||
|
<Chip color="primary" variant="outlined" label="مسیر ماموریت" />
|
||||||
|
</Divider>
|
||||||
|
<Box sx={{ width: "100%", height: "400px" }}>
|
||||||
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
|
<ShowRoute area={allData.area} />
|
||||||
|
</MapLayer>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Divider>
|
||||||
|
<Chip color="primary" variant="outlined" label="مشخصات ماموریت" />
|
||||||
|
</Divider>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">موضوع ماموریت</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={allData.explanation} />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">محدوده</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={missionRegions.find((r) => r.id == allData.region).name_fa} />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">نوع ماموریت</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={missionCategoryTypes.find((t) => t.id == allData.category_id).name_fa}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
{allData.category_id == 3 && (
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">کد شکایت</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={allData.road_observed_id} />
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">مقصد</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={allData.end_point} />
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">تاریخ شروع ماموریت</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`${moment(allData.start_time).format("HH:mm")} | ${moment(allData.start_date).locale("fa").format("YYYY/MM/DD")}`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">تاریخ پایان ماموریت</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip
|
||||||
|
size="small"
|
||||||
|
label={`${moment(allData.end_time).format("HH:mm")} | ${moment(allData.end_date).locale("fa").format("YYYY/MM/DD")}`}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Divider>
|
||||||
|
<Chip color="primary" variant="outlined" label=" راننده" />
|
||||||
|
</Divider>
|
||||||
|
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">راننده</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={`${allData.driver.code} | ${allData.driver.name}`} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<Divider>
|
||||||
|
<Chip color="primary" variant="outlined" label="همراهان" />
|
||||||
|
</Divider>
|
||||||
|
{allData.rahdaran.length != 0 ? (
|
||||||
|
allData.rahdaran.map((rahdar) => (
|
||||||
|
<Stack key={rahdar.id} direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">{rahdar.name}</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={rahdar.code} />
|
||||||
|
</Stack>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Stack direction={"row"} alignItems={"center"} justifyContent={"center"}>
|
||||||
|
<Typography variant="body2">همراهی ثبت نشده است</Typography>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button onClick={handlePrev} variant="outlined" color="secondary" disabled={submitting} size="large">
|
||||||
|
{"مرحله قبلی"}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="large" disabled={submitting} onClick={handleNext}>
|
||||||
|
تایید و ثبت ماموریت
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Verify;
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
import { Engineering, InsertDriveFile, Map, Person, Verified } from "@mui/icons-material";
|
||||||
|
import { Box, Tab, Tabs } from "@mui/material";
|
||||||
|
import { useReducer, useState } from "react";
|
||||||
|
import Area from "./Area";
|
||||||
|
import GetItemInfo from "./GetItemInfo";
|
||||||
|
import MachineAndDriver from "./MachineAndDriver";
|
||||||
|
import Rahdaran from "./Rahdaran";
|
||||||
|
import Verify from "./Verify";
|
||||||
|
|
||||||
|
function TabPanel(props) {
|
||||||
|
const { children, value, index } = props;
|
||||||
|
return (
|
||||||
|
<div role="tabpanel" hidden={value !== index}>
|
||||||
|
{value === index && <Box>{children}</Box>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reducer = (state, action) => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "changeData":
|
||||||
|
return { ...state, ...action.data };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateForm = ({ defaultValues, submitForm, setOpen, submitting }) => {
|
||||||
|
const [allData, dispatch] = useReducer(reducer, defaultValues);
|
||||||
|
const [tabState, setTabState] = useState(0);
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
const handleChangeTab = (event, newValue) => {
|
||||||
|
setTabState(newValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrev = () => {
|
||||||
|
if (tabState === 0) {
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
setTabState((t) => t - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tabs
|
||||||
|
allowScrollButtonsMobile
|
||||||
|
value={tabState}
|
||||||
|
onChange={handleChangeTab}
|
||||||
|
variant={"fullWidth"}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#efefef",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tab icon={<InsertDriveFile />} label="مشخصات" />
|
||||||
|
<Tab disabled={tabState < 2} icon={<Person />} label="راننده" />
|
||||||
|
<Tab disabled={tabState < 3} icon={<Map />} label="مختصات" />
|
||||||
|
<Tab disabled={tabState < 4} icon={<Engineering />} label="همراهان" />
|
||||||
|
<Tab disabled={tabState < 5} icon={<Verified />} label="بررسی نهایی" />
|
||||||
|
</Tabs>
|
||||||
|
<TabPanel value={tabState} index={0}>
|
||||||
|
<GetItemInfo
|
||||||
|
allData={allData}
|
||||||
|
setAllData={(data) => {
|
||||||
|
dispatch({ type: "changeData", data });
|
||||||
|
}}
|
||||||
|
handlePrev={handlePrev}
|
||||||
|
setTabState={setTabState}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={tabState} index={1}>
|
||||||
|
<MachineAndDriver
|
||||||
|
allData={allData}
|
||||||
|
setAllData={(data) => {
|
||||||
|
dispatch({ type: "changeData", data });
|
||||||
|
}}
|
||||||
|
handlePrev={handlePrev}
|
||||||
|
setTabState={setTabState}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={tabState} index={2}>
|
||||||
|
<Area
|
||||||
|
allData={allData}
|
||||||
|
setAllData={(data) => {
|
||||||
|
dispatch({ type: "changeData", data });
|
||||||
|
}}
|
||||||
|
handlePrev={handlePrev}
|
||||||
|
setTabState={setTabState}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={tabState} index={3}>
|
||||||
|
<Rahdaran
|
||||||
|
allData={allData}
|
||||||
|
setAllData={(data) => {
|
||||||
|
dispatch({ type: "changeData", data });
|
||||||
|
}}
|
||||||
|
handlePrev={handlePrev}
|
||||||
|
setTabState={setTabState}
|
||||||
|
/>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={tabState} index={4}>
|
||||||
|
<Verify allData={allData} handlePrev={handlePrev} submitForm={submitForm} submitting={submitting} />
|
||||||
|
</TabPanel>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CreateForm;
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { MISSOINS_CORRECTION_VIOLATION } from "@/core/utils/routes";
|
||||||
|
import useRequest from "@/lib/hooks/useRequest";
|
||||||
|
import { AddCircleOutline, Close } from "@mui/icons-material";
|
||||||
|
import { Dialog, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import CreateForm from "./Form";
|
||||||
|
|
||||||
|
const MissionCorrection = ({ row, mutate, baseMutate }) => {
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const requestServer = useRequest({ notificationSuccess: true });
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const handleOpen = () => {
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitForm = async (result) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
await requestServer(`${MISSOINS_CORRECTION_VIOLATION}/${row.original.id}`, "post", {
|
||||||
|
data: {
|
||||||
|
explanation: result.explanation,
|
||||||
|
category_id: result.category_id,
|
||||||
|
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
|
||||||
|
zone: result.region,
|
||||||
|
end_point: result.end_point,
|
||||||
|
driver: result.driver.id,
|
||||||
|
encoded_route: result.area,
|
||||||
|
item_id: result.item_id,
|
||||||
|
sub_item_id: result.sub_item_id,
|
||||||
|
},
|
||||||
|
hasSidebarUpdate: true,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
mutate();
|
||||||
|
baseMutate();
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {})
|
||||||
|
.finally(() => {
|
||||||
|
setSubmitting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="تصحیح ماموریت" arrow placement="left">
|
||||||
|
<IconButton aria-label="تصحیح ماموریت" color="primary" onClick={handleOpen}>
|
||||||
|
<AddCircleOutline sx={{ fontSize: "25px" }} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Dialog open={open} fullWidth maxWidth="md">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
{open && (
|
||||||
|
<CreateForm
|
||||||
|
defaultValues={{
|
||||||
|
explanation: "",
|
||||||
|
category_id: "",
|
||||||
|
road_observed_id: "",
|
||||||
|
rahdaran: [],
|
||||||
|
end_point: "",
|
||||||
|
region: "",
|
||||||
|
driver: null,
|
||||||
|
area: null,
|
||||||
|
locations: [],
|
||||||
|
}}
|
||||||
|
submitForm={submitForm}
|
||||||
|
submitting={submitting}
|
||||||
|
setOpen={setOpen}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default MissionCorrection;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import MissionCorrection from "./MissionCorrection";
|
||||||
|
|
||||||
|
export default function RowActions({ row, mutate, baseMutate }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MissionCorrection row={row} mutate={mutate} baseMutate={baseMutate} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import RowActions from "./RowActions";
|
||||||
|
import { GET_OPERATOR_VIOLATIONS_LIST } from "@/core/utils/routes";
|
||||||
|
import useCities from "@/lib/hooks/useCities";
|
||||||
|
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
|
||||||
|
import useProvinces from "@/lib/hooks/useProvince";
|
||||||
|
|
||||||
|
const ViolationsList = ({ setOpen, mutate }) => {
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const dynamicColumns = {
|
||||||
|
header: "استان",
|
||||||
|
id: "province_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
grow: false,
|
||||||
|
size: 130,
|
||||||
|
ColumnSelectComponent: (props) => {
|
||||||
|
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||||
|
const getColumnSelectOptions = useMemo(() => {
|
||||||
|
if (loadingProvinces) {
|
||||||
|
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||||
|
}
|
||||||
|
if (errorProvinces) {
|
||||||
|
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: "", label: "کل کشور" },
|
||||||
|
...provinces.map((province) => ({
|
||||||
|
value: province.id,
|
||||||
|
label: province.name_fa,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [provinces, errorProvinces, loadingProvinces]);
|
||||||
|
return (
|
||||||
|
<CustomSelectByDependency
|
||||||
|
{...props}
|
||||||
|
value={loadingProvinces ? "loading" : props.filterParameters.value}
|
||||||
|
columnSelectOption={getColumnSelectOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Cell: ({ renderedCellValue, row }) => {
|
||||||
|
return <>{row.original.province_name}</>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "کد یکتا",
|
||||||
|
id: "id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "machine_code",
|
||||||
|
header: "کد خودرو",
|
||||||
|
id: "machine_code",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
...[dynamicColumns],
|
||||||
|
{
|
||||||
|
header: "شهر",
|
||||||
|
id: "city_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
dependencyId: "province_id",
|
||||||
|
grow: false,
|
||||||
|
size: 120,
|
||||||
|
ColumnSelectComponent: (props) => {
|
||||||
|
const { cityList, loadingCityList, errorCityList } = useCities(props.dependencyFieldValue.value);
|
||||||
|
|
||||||
|
const getColumnSelectOptions = useMemo(() => {
|
||||||
|
if (props.dependencyFieldValue.value === "") {
|
||||||
|
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
|
||||||
|
}
|
||||||
|
if (loadingCityList) {
|
||||||
|
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||||
|
}
|
||||||
|
if (errorCityList) {
|
||||||
|
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: "", label: "کل ادارات" },
|
||||||
|
...cityList.map((edare) => ({
|
||||||
|
value: edare.id,
|
||||||
|
label: edare.name_fa,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [cityList, loadingCityList, errorCityList]);
|
||||||
|
return (
|
||||||
|
<CustomSelectByDependency
|
||||||
|
{...props}
|
||||||
|
value={
|
||||||
|
props.dependencyFieldValue?.value === ""
|
||||||
|
? "empty"
|
||||||
|
: loadingCityList
|
||||||
|
? "loading"
|
||||||
|
: props.filterParameters.value
|
||||||
|
}
|
||||||
|
columnSelectOption={getColumnSelectOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Cell: ({ row }) => <>{row.original.city_name}</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "enter_time",
|
||||||
|
header: "تاریخ و ساعت ورود",
|
||||||
|
id: "enter_time",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 140,
|
||||||
|
Cell: ({ cell }) => {
|
||||||
|
const value = formatDateTime(cell.getValue());
|
||||||
|
|
||||||
|
if (!value) return "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="font-medium">{value.date}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span className="text-xs text-muted-foreground">{value.time}</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "exit_time",
|
||||||
|
header: "تاریخ و ساعت خروج",
|
||||||
|
id: "exit_time",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 140,
|
||||||
|
Cell: ({ cell }) => {
|
||||||
|
const value = formatDateTime(cell.getValue());
|
||||||
|
|
||||||
|
if (!value) return "-";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col leading-tight">
|
||||||
|
<span className="font-medium">{value.date}</span>
|
||||||
|
<span> - </span>
|
||||||
|
<span className="text-xs text-muted-foreground">{value.time}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ mt: 3, p: 1 }}>
|
||||||
|
<DataTableWithAuth
|
||||||
|
need_filter={true}
|
||||||
|
// table_title="لیست تخلفات"
|
||||||
|
columns={columns}
|
||||||
|
sorting={[{ id: "id", desc: true }]}
|
||||||
|
table_url={GET_OPERATOR_VIOLATIONS_LIST}
|
||||||
|
page_name={"operatorViolations"}
|
||||||
|
table_name={"operatorViolationsList"}
|
||||||
|
enableRowActions
|
||||||
|
positionActionsColumn={"first"}
|
||||||
|
RowActions={(props) => <RowActions {...props} setOpen={setOpen} baseMutate={mutate} />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ViolationsList;
|
||||||
|
|
||||||
|
const formatDateTime = (value) => {
|
||||||
|
if (!value) return null;
|
||||||
|
|
||||||
|
const date = new Date(value.replace(" ", "T"));
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: new Intl.DateTimeFormat("fa-IR", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
}).format(date),
|
||||||
|
|
||||||
|
time: new Intl.DateTimeFormat("fa-IR", {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
}).format(date),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Button, IconButton, Tooltip, useMediaQuery } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import ViolationsDialog from "./Dialog";
|
||||||
|
import theme from "@/core/utils/theme";
|
||||||
|
import RemoveRoadIcon from "@mui/icons-material/RemoveRoad";
|
||||||
|
|
||||||
|
const OperatorViolations = ({ mutate }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="تخلفات">
|
||||||
|
{isMobile ? (
|
||||||
|
<IconButton aria-label="تخلفات" color="primary" onClick={() => setOpen(true)}>
|
||||||
|
<RemoveRoadIcon sx={{ fontSize: "25px" }} />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size={"small"}
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
startIcon={<RemoveRoadIcon />}
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
>
|
||||||
|
تخلفات
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
<ViolationsDialog open={open} setOpen={setOpen} mutate={mutate} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default OperatorViolations;
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import DescriptionDialog from "@/components/dashboard/roadMissions/operator/RowActions/DescriptionDialog";
|
||||||
|
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
|
||||||
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
import { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
|
import { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
|
||||||
import { missionRegions } from "@/core/utils/missionRegions";
|
import { missionRegions } from "@/core/utils/missionRegions";
|
||||||
import { missionTypes } from "@/core/utils/missionTypes";
|
import { missionTypes } from "@/core/utils/missionTypes";
|
||||||
import { GET_ROAD_MISSIONS_OPERATOR_LIST } from "@/core/utils/routes";
|
import { GET_ROAD_MISSIONS_OPERATOR_LIST } from "@/core/utils/routes";
|
||||||
|
import useCities from "@/lib/hooks/useCities";
|
||||||
|
import useProvinces from "@/lib/hooks/useProvince";
|
||||||
import { Box, Stack } from "@mui/material";
|
import { Box, Stack } from "@mui/material";
|
||||||
import moment from "jalali-moment";
|
import moment from "jalali-moment";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import ShowArea from "./Actions/showArea";
|
|
||||||
import RowActions from "./RowActions";
|
import RowActions from "./RowActions";
|
||||||
import MachinesDialog from "./RowActions/Machines";
|
import MachinesDialog from "./RowActions/Machines";
|
||||||
import Toolbar from "./Toolbar";
|
|
||||||
import RahdaranDialog from "./RowActions/Rahdaran";
|
import RahdaranDialog from "./RowActions/Rahdaran";
|
||||||
import DescriptionDialog from "@/components/dashboard/roadMissions/operator/RowActions/DescriptionDialog";
|
import RouteDetail from "./RowActions/RouteDetail";
|
||||||
import useProvinces from "@/lib/hooks/useProvince";
|
import Toolbar from "./Toolbar";
|
||||||
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
|
|
||||||
import useCities from "@/lib/hooks/useCities";
|
|
||||||
|
|
||||||
const OperatorList = () => {
|
const OperatorList = () => {
|
||||||
const columns = useMemo(() => {
|
const columns = useMemo(() => {
|
||||||
@@ -247,7 +247,7 @@ const OperatorList = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "منطقه عملیاتی",
|
header: "منطقه عملیاتی",
|
||||||
id: "area",
|
id: "encoded_route",
|
||||||
enableColumnFilter: false,
|
enableColumnFilter: false,
|
||||||
datatype: "numeric",
|
datatype: "numeric",
|
||||||
filterMode: "equals",
|
filterMode: "equals",
|
||||||
@@ -263,9 +263,9 @@ const OperatorList = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Cell: ({ row }) =>
|
Cell: ({ row }) =>
|
||||||
row.original.area ? (
|
row.original.encoded_route ? (
|
||||||
<Stack alignItems={"center"} justifyContent={"center"}>
|
<Stack alignItems={"center"} justifyContent={"center"}>
|
||||||
<ShowArea area={row.original.area} />
|
<RouteDetail encoded_route={row.original.encoded_route} />
|
||||||
</Stack>
|
</Stack>
|
||||||
) : (
|
) : (
|
||||||
"-"
|
"-"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Box, Typography } from "@mui/material";
|
||||||
|
|
||||||
|
const AlarmText = ({ isSource }) => {
|
||||||
|
if (isSource) return null;
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
top: 5,
|
||||||
|
left: 5,
|
||||||
|
zIndex: 1000,
|
||||||
|
border: "1px solid #ed0f02",
|
||||||
|
backgroundColor: "#f19898",
|
||||||
|
py: 0.5,
|
||||||
|
px: 1,
|
||||||
|
borderRadius: 2,
|
||||||
|
opacity: 0.8,
|
||||||
|
boxShadow: "rgba(0, 0, 0, 0.35) 0px 5px 15px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: "bold" }} color="#ed0f02">
|
||||||
|
با کلیک بر روی مارکر موقعیت را انتخاب کنید
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmText;
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { Box, Stack, IconButton, Typography, Divider } from "@mui/material";
|
||||||
|
import EditIcon from "@mui/icons-material/Edit";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
|
||||||
|
const ROW_COLORS = {
|
||||||
|
source: "#0EA37A", // سبز
|
||||||
|
waypoint: "#F5A623", // زرد/نارنجی
|
||||||
|
dest: "#E53935", // قرمز
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCoord = (pos) => (pos ? `${pos.lat.toFixed(2)}, ${pos.lng.toFixed(2)}` : null);
|
||||||
|
|
||||||
|
const Row = ({ color, label, value, onEdit, onRemove, placeholder = "در حال انتخاب..." }) => (
|
||||||
|
<Stack direction="row" alignItems="center" spacing={1} sx={{ py: 0.5 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: color,
|
||||||
|
flexShrink: 0,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||||
|
<Typography variant="caption" sx={{ color: "#8A8A8A", lineHeight: 1, display: "block" }}>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
fontFamily: "monospace",
|
||||||
|
fontSize: 12,
|
||||||
|
color: value ? "#1f1f1f" : "#bdbdbd",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || placeholder}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{onEdit && (
|
||||||
|
<IconButton size="small" onClick={onEdit} sx={{ p: 0.5 }}>
|
||||||
|
<EditIcon sx={{ fontSize: 16, color: "#757575" }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
{onRemove && (
|
||||||
|
<IconButton size="small" onClick={onRemove} sx={{ p: 0.5 }}>
|
||||||
|
<CloseIcon sx={{ fontSize: 16, color: "#E53935" }} />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
|
||||||
|
const DataBox = ({
|
||||||
|
isSource,
|
||||||
|
isDest,
|
||||||
|
sourcePosition,
|
||||||
|
destPosition,
|
||||||
|
editingTarget,
|
||||||
|
onEditSource,
|
||||||
|
onEditDest,
|
||||||
|
waypoints = [],
|
||||||
|
onEditWaypoint,
|
||||||
|
onAddWaypoint,
|
||||||
|
onRemoveWaypoint,
|
||||||
|
canAddWaypoint,
|
||||||
|
}) => {
|
||||||
|
if (!isSource) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 12,
|
||||||
|
left: 12,
|
||||||
|
zIndex: 1000,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.95)",
|
||||||
|
backdropFilter: "blur(6px)",
|
||||||
|
border: "1px solid #ececec",
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
|
||||||
|
px: 1.25,
|
||||||
|
py: 0.75,
|
||||||
|
width: 230,
|
||||||
|
maxWidth: "70vw",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Row
|
||||||
|
color={ROW_COLORS.source}
|
||||||
|
label="مبدا"
|
||||||
|
value={formatCoord(sourcePosition)}
|
||||||
|
onEdit={isSource && isDest ? onEditSource : null}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{waypoints.map((wp, index) => (
|
||||||
|
<Row
|
||||||
|
key={`waypoint-${index}`}
|
||||||
|
color={ROW_COLORS.waypoint}
|
||||||
|
label={`نقطه میانی ${index + 1}`}
|
||||||
|
value={formatCoord(wp.position)}
|
||||||
|
onEdit={wp.confirmed && editingTarget !== `waypoint-${index}` ? () => onEditWaypoint(index) : null}
|
||||||
|
onRemove={() => onRemoveWaypoint(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isDest && (
|
||||||
|
<Row color={ROW_COLORS.dest} label="مقصد" value={formatCoord(destPosition)} onEdit={onEditDest} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canAddWaypoint && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ my: 0.5 }} />
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
spacing={0.5}
|
||||||
|
onClick={onAddWaypoint}
|
||||||
|
sx={{
|
||||||
|
cursor: "pointer",
|
||||||
|
color: ROW_COLORS.waypoint,
|
||||||
|
py: 0.25,
|
||||||
|
"&:hover": { opacity: 0.75 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AddIcon sx={{ fontSize: 16 }} />
|
||||||
|
<Typography variant="caption" sx={{ fontWeight: 600 }}>
|
||||||
|
افزودن نقطه میانی
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataBox;
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
|
||||||
|
const ShowRoute = ({ area }) => {
|
||||||
|
const map = useMap();
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
const [showRoute, setShowRoute] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!coords.length) return;
|
||||||
|
|
||||||
|
setShowRoute(false);
|
||||||
|
|
||||||
|
map.flyToBounds(coords, {
|
||||||
|
padding: [50, 50],
|
||||||
|
duration: 1.5,
|
||||||
|
});
|
||||||
|
|
||||||
|
map.once("moveend", () => {
|
||||||
|
setShowRoute(true);
|
||||||
|
});
|
||||||
|
}, [area]);
|
||||||
|
if (!area) return null;
|
||||||
|
const coords = polyline.decode(area, 6);
|
||||||
|
|
||||||
|
if (coords.length === 0) return null;
|
||||||
|
|
||||||
|
const sourcePosition = coords[0];
|
||||||
|
const destPosition = coords[coords.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||||
|
|
||||||
|
<Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
<Marker position={destPosition} icon={destIcon} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowRoute;
|
||||||
|
|
||||||
|
// import { useEffect, useState } from "react";
|
||||||
|
// import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
// import L from "leaflet";
|
||||||
|
// import polyline from "@mapbox/polyline";
|
||||||
|
// import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
// import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
// import WaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||||
|
//
|
||||||
|
// const defaultIconSize = [35, 35];
|
||||||
|
//
|
||||||
|
// const createCustomIcon = (size, iconUrl) => {
|
||||||
|
// return L.icon({
|
||||||
|
// iconUrl: iconUrl,
|
||||||
|
// iconSize: size,
|
||||||
|
// iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
// popupAnchor: [0, -size[1]],
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
// const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
// const waypointIcon = createCustomIcon(defaultIconSize, WaypointIcon.src);
|
||||||
|
//
|
||||||
|
// // locations: آرایه اختیاری [{lat, lon}, ...] شامل مبدا، نقاط میانی و مقصد به همون ترتیبی که برای روتینگ فرستاده شده
|
||||||
|
// const ShowRoute = ({ area, locations = [] }) => {
|
||||||
|
// const map = useMap();
|
||||||
|
// const [showRoute, setShowRoute] = useState(false);
|
||||||
|
//
|
||||||
|
// const coords = area ? polyline.decode(area, 6) : [];
|
||||||
|
//
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!coords.length) return;
|
||||||
|
//
|
||||||
|
// setShowRoute(false);
|
||||||
|
//
|
||||||
|
// map.flyToBounds(coords, {
|
||||||
|
// padding: [50, 50],
|
||||||
|
// duration: 1.5,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// map.once("moveend", () => {
|
||||||
|
// setShowRoute(true);
|
||||||
|
// });
|
||||||
|
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
// }, [area]);
|
||||||
|
//
|
||||||
|
// if (!area || coords.length === 0) return null;
|
||||||
|
//
|
||||||
|
// const sourcePosition = coords[0];
|
||||||
|
// const destPosition = coords[coords.length - 1];
|
||||||
|
//
|
||||||
|
// // نقاط میانی فقط از روی locations قابل استخراجان (بین اولی و آخری)
|
||||||
|
// const middleWaypoints = locations.length > 2 ? locations.slice(1, -1) : [];
|
||||||
|
//
|
||||||
|
// return (
|
||||||
|
// <>
|
||||||
|
// {showRoute && <Polyline positions={coords} color="#015688" weight={7} />}
|
||||||
|
//
|
||||||
|
// <Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
//
|
||||||
|
// {middleWaypoints.map((wp, index) => (
|
||||||
|
// <Marker key={`waypoint-${index}`} position={[wp.lat, wp.lon]} icon={waypointIcon} />
|
||||||
|
// ))}
|
||||||
|
//
|
||||||
|
// <Marker position={destPosition} icon={destIcon} />
|
||||||
|
// </>
|
||||||
|
// );
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// export default ShowRoute;
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
import { Marker, Polyline, useMapEvents } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import PrevSourceIcon from "@/assets/images/prev-source-icon.svg";
|
||||||
|
import PrevDestIcon from "@/assets/images/prev-destination-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
import WaypointIcon from "@/assets/images/prev-waypoint-icon.svg";
|
||||||
|
import PrevWaypointIcon from "@/assets/images/waypoint-icon.svg";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import DataBox from "./DataBox";
|
||||||
|
import AlarmText from "./AlarmText";
|
||||||
|
|
||||||
|
const MAX_WAYPOINTS = 2;
|
||||||
|
|
||||||
|
const Routing = ({ setArea, setLocations, locations, area }) => {
|
||||||
|
const mapPrevSourceMarker = useRef();
|
||||||
|
const mapPrevDestMarker = useRef();
|
||||||
|
const mapPrevWaypointMarkers = useRef([]); // ref array, یکی بهازای هر waypoint
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const [isSource, setIsSource] = useState(false);
|
||||||
|
const [sourcePosition, setSourcePosition] = useState(null);
|
||||||
|
|
||||||
|
const [isDest, setIsDest] = useState(false);
|
||||||
|
const [destPosition, setDestPosition] = useState(null);
|
||||||
|
|
||||||
|
const [waypoints, setWaypoints] = useState([]);
|
||||||
|
|
||||||
|
const [routeCoords, setRouteCoords] = useState([]);
|
||||||
|
const [alternateCoords, setAlternateCoords] = useState([]);
|
||||||
|
const [editingTarget, setEditingTarget] = useState(null);
|
||||||
|
const [selectedRoute, setSelectedRoute] = useState(null);
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultIcon = createCustomIcon(
|
||||||
|
defaultIconSize,
|
||||||
|
isSource && editingTarget !== "source" ? SourceIcon.src : PrevSourceIcon.src
|
||||||
|
);
|
||||||
|
|
||||||
|
const prevDestIcon = createCustomIcon(
|
||||||
|
defaultIconSize,
|
||||||
|
isDest && editingTarget !== "dest" ? DestIcon.src : PrevDestIcon.src
|
||||||
|
);
|
||||||
|
|
||||||
|
const getWaypointIcon = (index) => {
|
||||||
|
const wp = waypoints[index];
|
||||||
|
const isConfirmed = wp?.confirmed && editingTarget !== `waypoint-${index}`;
|
||||||
|
return createCustomIcon(defaultIconSize, isConfirmed ? WaypointIcon.src : PrevWaypointIcon.src);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActiveTarget = () => {
|
||||||
|
if (editingTarget) return editingTarget;
|
||||||
|
if (!isSource) return "source";
|
||||||
|
const pendingWaypointIndex = waypoints.findIndex((wp) => !wp.confirmed);
|
||||||
|
if (pendingWaypointIndex !== -1) return `waypoint-${pendingWaypointIndex}`;
|
||||||
|
if (!isDest) return "dest";
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = useMapEvents({
|
||||||
|
move(e) {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
const center = e.target.getCenter();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setLatLng(center);
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setLatLng(center);
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setLatLng(center);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
movestart() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon([45, 45], PrevWaypointIcon.src));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveend() {
|
||||||
|
const target = getActiveTarget();
|
||||||
|
if (target === "source") {
|
||||||
|
mapPrevSourceMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
|
||||||
|
} else if (target === "dest") {
|
||||||
|
mapPrevDestMarker.current?.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
|
||||||
|
} else if (target?.startsWith("waypoint-")) {
|
||||||
|
const idx = Number(target.split("-")[1]);
|
||||||
|
mapPrevWaypointMarkers.current[idx]?.setIcon(createCustomIcon(defaultIconSize, PrevWaypointIcon.src));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
mapRef.current = map;
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (locations.length < 2) return;
|
||||||
|
const first = { lat: locations[0].lat, lng: locations[0].lon };
|
||||||
|
const last = { lat: locations[locations.length - 1].lat, lng: locations[locations.length - 1].lon };
|
||||||
|
const middle = locations.slice(1, -1).map((loc) => ({
|
||||||
|
position: { lat: loc.lat, lng: loc.lon },
|
||||||
|
confirmed: true,
|
||||||
|
}));
|
||||||
|
|
||||||
|
setSourcePosition(first);
|
||||||
|
setDestPosition(last);
|
||||||
|
setWaypoints(middle);
|
||||||
|
setIsSource(true);
|
||||||
|
setIsDest(true);
|
||||||
|
|
||||||
|
if (area) {
|
||||||
|
const decoded = polyline.decode(area, 6);
|
||||||
|
setRouteCoords(decoded);
|
||||||
|
setSelectedRoute({ index: "main", coords: decoded });
|
||||||
|
setTimeout(() => {
|
||||||
|
mapRef.current?.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||||
|
}, 100);
|
||||||
|
} else {
|
||||||
|
fetchRoute(first, last, middle);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchRoute = async (source, dest, waypointsList = waypoints) => {
|
||||||
|
const confirmedWaypoints = waypointsList.filter((wp) => wp.confirmed && wp.position);
|
||||||
|
const locs = [
|
||||||
|
{ lat: source.lat, lon: source.lng },
|
||||||
|
...confirmedWaypoints.map((wp) => ({ lat: wp.position.lat, lon: wp.position.lng })),
|
||||||
|
{ lat: dest.lat, lon: dest.lng },
|
||||||
|
];
|
||||||
|
try {
|
||||||
|
const response = await fetch("https://jouya.141.ir/api/router/drive", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ locations: locs }),
|
||||||
|
});
|
||||||
|
const data = await response.json();
|
||||||
|
const shape = data.trip.legs[0].shape;
|
||||||
|
setLocations(locs);
|
||||||
|
setArea(shape);
|
||||||
|
const decoded = polyline.decode(shape, 6);
|
||||||
|
setRouteCoords(decoded);
|
||||||
|
setSelectedRoute({ index: "main", coords: decoded });
|
||||||
|
|
||||||
|
if (data.alternates?.length > 0) {
|
||||||
|
const decodedAlternates = data.alternates.map((alt) => polyline.decode(alt.trip.legs[0].shape, 6));
|
||||||
|
setAlternateCoords(decodedAlternates);
|
||||||
|
} else {
|
||||||
|
setAlternateCoords([]);
|
||||||
|
}
|
||||||
|
map.flyToBounds(decoded, { padding: [50, 50], duration: 1.5 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Routing error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetRoute = () => {
|
||||||
|
setRouteCoords([]);
|
||||||
|
setAlternateCoords([]);
|
||||||
|
setSelectedRoute(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMarkerClick = async () => {
|
||||||
|
if (!isSource || editingTarget === "source") {
|
||||||
|
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
|
||||||
|
setSourcePosition(clickedPosition);
|
||||||
|
setIsSource(true);
|
||||||
|
const wasEditing = editingTarget === "source";
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (wasEditing && destPosition) {
|
||||||
|
await fetchRoute(clickedPosition, destPosition, waypoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDestMarkerClick = async () => {
|
||||||
|
if (!isDest || editingTarget === "dest") {
|
||||||
|
const clickedPosition = mapPrevDestMarker.current.getLatLng();
|
||||||
|
setDestPosition(clickedPosition);
|
||||||
|
setIsDest(true);
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
await fetchRoute(sourcePosition, clickedPosition, waypoints);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWaypointMarkerClick = async (index) => {
|
||||||
|
const wp = waypoints[index];
|
||||||
|
if (!wp || wp.confirmed) return; // فقط روی نقطهی تأییدنشده کلیک معتبره (یا حالت ادیت)
|
||||||
|
|
||||||
|
const clickedPosition = mapPrevWaypointMarkers.current[index]?.getLatLng();
|
||||||
|
const updated = waypoints.map((w, i) => (i === index ? { position: clickedPosition, confirmed: true } : w));
|
||||||
|
setWaypoints(updated);
|
||||||
|
const wasEditing = editingTarget === `waypoint-${index}`;
|
||||||
|
setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (sourcePosition && destPosition) {
|
||||||
|
await fetchRoute(sourcePosition, destPosition, updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSource = () => {
|
||||||
|
setEditingTarget("source");
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditDest = () => {
|
||||||
|
setEditingTarget("dest");
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditWaypoint = (index) => {
|
||||||
|
// برای ادیت یک waypoint تأییدشده، موقتاً unconfirmed میکنیم تا با حرکت نقشه جابهجا بشه
|
||||||
|
setWaypoints((prev) => prev.map((w, i) => (i === index ? { ...w, confirmed: false } : w)));
|
||||||
|
setEditingTarget(`waypoint-${index}`);
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// اضافه کردن یک نقطه میانی جدید (حداکثر MAX_WAYPOINTS)
|
||||||
|
const handleAddWaypoint = () => {
|
||||||
|
if (waypoints.length >= MAX_WAYPOINTS) return;
|
||||||
|
if (!isSource) return; // قبل از انتخاب مبدا معنی نداره
|
||||||
|
setWaypoints((prev) => [...prev, { position: null, confirmed: false }]);
|
||||||
|
setEditingTarget(`waypoint-${waypoints.length}`);
|
||||||
|
resetRoute();
|
||||||
|
};
|
||||||
|
|
||||||
|
// حذف یک نقطه میانی
|
||||||
|
const handleRemoveWaypoint = async (index) => {
|
||||||
|
const updated = waypoints.filter((_, i) => i !== index);
|
||||||
|
setWaypoints(updated);
|
||||||
|
mapPrevWaypointMarkers.current = mapPrevWaypointMarkers.current.filter((_, i) => i !== index);
|
||||||
|
if (editingTarget === `waypoint-${index}`) setEditingTarget(null);
|
||||||
|
resetRoute();
|
||||||
|
|
||||||
|
if (sourcePosition && destPosition) {
|
||||||
|
await fetchRoute(sourcePosition, destPosition, updated);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectRoute = (index, coords) => {
|
||||||
|
setSelectedRoute({ index, coords });
|
||||||
|
const encoded = polyline.encode(coords, 6);
|
||||||
|
setArea(encoded);
|
||||||
|
map.flyToBounds(coords, { padding: [50, 50], duration: 0.3 });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DataBox
|
||||||
|
isSource={isSource}
|
||||||
|
isDest={isDest}
|
||||||
|
sourcePosition={sourcePosition}
|
||||||
|
destPosition={destPosition}
|
||||||
|
editingTarget={editingTarget}
|
||||||
|
onEditSource={handleEditSource}
|
||||||
|
onEditDest={handleEditDest}
|
||||||
|
waypoints={waypoints}
|
||||||
|
onEditWaypoint={handleEditWaypoint}
|
||||||
|
onAddWaypoint={handleAddWaypoint}
|
||||||
|
onRemoveWaypoint={handleRemoveWaypoint}
|
||||||
|
canAddWaypoint={isSource && waypoints.length < MAX_WAYPOINTS}
|
||||||
|
/>
|
||||||
|
<AlarmText isSource={isSource} />
|
||||||
|
|
||||||
|
<Marker
|
||||||
|
position={sourcePosition || map.getCenter()}
|
||||||
|
ref={mapPrevSourceMarker}
|
||||||
|
eventHandlers={{ click: handleMarkerClick }}
|
||||||
|
icon={defaultIcon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{waypoints.map((wp, index) => (
|
||||||
|
<Marker
|
||||||
|
key={`waypoint-${index}`}
|
||||||
|
position={wp.position || map.getCenter()}
|
||||||
|
ref={(el) => (mapPrevWaypointMarkers.current[index] = el)}
|
||||||
|
eventHandlers={{ click: () => handleWaypointMarkerClick(index) }}
|
||||||
|
icon={getWaypointIcon(index)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isSource && (
|
||||||
|
<Marker
|
||||||
|
position={destPosition || map.getCenter()}
|
||||||
|
ref={mapPrevDestMarker}
|
||||||
|
eventHandlers={{ click: handleDestMarkerClick }}
|
||||||
|
icon={prevDestIcon}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{alternateCoords.map((coords, index) => (
|
||||||
|
<Polyline
|
||||||
|
key={`alternate-${index}-${selectedRoute?.index === index}`}
|
||||||
|
positions={coords}
|
||||||
|
color={selectedRoute?.index === index ? "#015688" : "#91B2C6"}
|
||||||
|
weight={selectedRoute?.index === index ? 7 : 6}
|
||||||
|
opacity={selectedRoute?.index === index ? 1 : 0.7}
|
||||||
|
eventHandlers={{ click: () => handleSelectRoute(index, coords) }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{routeCoords.length > 0 && (
|
||||||
|
<Polyline
|
||||||
|
key={`main-${selectedRoute?.index === "main"}`}
|
||||||
|
positions={routeCoords}
|
||||||
|
color={selectedRoute?.index === "main" ? "#015688" : "#91B2C6"}
|
||||||
|
weight={selectedRoute?.index === "main" ? 7 : 6}
|
||||||
|
opacity={selectedRoute?.index === "main" ? 1 : 0.7}
|
||||||
|
eventHandlers={{ click: () => handleSelectRoute("main", routeCoords) }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Routing;
|
||||||
@@ -2,41 +2,29 @@ import MapLoading from "@/core/components/MapLayer/Loading";
|
|||||||
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
|
import { Box, Button, DialogActions, DialogContent, Stack } from "@mui/material";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import MapControlPolygon from "./MapControlPolygon";
|
import Routing from "./Routing";
|
||||||
import MapControlPolyline from "./MapControlPolyline";
|
|
||||||
import SelectBoundType from "./SelectBoundType";
|
|
||||||
import OldBoundLayer from "./OldBoundLayer";
|
|
||||||
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
||||||
loading: () => <MapLoading />,
|
loading: () => <MapLoading />,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => {
|
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
const [bound, setBound] = useState(allData.bound);
|
const [area, setArea] = useState(allData.area);
|
||||||
const [boundType, setBoundType] = useState(allData.bound_type);
|
const [locations, setLocations] = useState(allData.locations);
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
setAllData({ bound: bound });
|
setAllData({ area: area });
|
||||||
|
setAllData({ locations: locations });
|
||||||
setTabState((s) => s + 1);
|
setTabState((s) => s + 1);
|
||||||
}, [bound]);
|
}, [area]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogContent dividers>
|
<DialogContent dividers>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<SelectBoundType
|
|
||||||
boundType={boundType}
|
|
||||||
setBoundType={setBoundType}
|
|
||||||
setBound={setBound}
|
|
||||||
setAllData={setAllData}
|
|
||||||
/>
|
|
||||||
<Box sx={{ width: "100%", height: "400px" }}>
|
<Box sx={{ width: "100%", height: "400px" }}>
|
||||||
<MapLayer style={{ borderRadius: "4px" }}>
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
{boundType == "polygon" ? (
|
<Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
|
||||||
<MapControlPolygon bound={bound} setBound={setBound} boundType={boundType} />
|
|
||||||
) : (
|
|
||||||
<MapControlPolyline bound={bound} setBound={setBound} boundType={boundType} />
|
|
||||||
)}
|
|
||||||
<OldBoundLayer bound={oldBound} />
|
|
||||||
</MapLayer>
|
</MapLayer>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
@@ -45,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => {
|
|||||||
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
|
||||||
{"مرحله قبلی"}
|
{"مرحله قبلی"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}>
|
<Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
|
||||||
{!bound ? "در انتظار ترسیم" : "مرحله بعد"}
|
{!area ? "در انتظار ترسیم" : "مرحله بعد"}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import useRoadItemGetItems from "@/lib/hooks/useRoadItemGetItems";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
|
||||||
|
const ItemSelectField = ({ control, setValue }) => {
|
||||||
|
const { itemsList, loadingItemsList, errorItemsList } = useRoadItemGetItems();
|
||||||
|
function getItemName(val) {
|
||||||
|
return itemsList.find((item) => item.id === val)?.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"item_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="موضوع"
|
||||||
|
selectors={itemsList}
|
||||||
|
isLoading={loadingItemsList}
|
||||||
|
schema={{ name: "name", value: "id" }}
|
||||||
|
error={errorItemsList}
|
||||||
|
helperText={error?.message}
|
||||||
|
onChange={(value) => {
|
||||||
|
field.onChange(value);
|
||||||
|
setValue("explanation", getItemName(value));
|
||||||
|
setValue("sub_item_id", "");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ItemSelectField;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import useRoadItemGetSubItems from "@/lib/hooks/useRoadItemGetISubtems";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
|
||||||
|
const SubItemSelectField = ({ control, watch, setValue, getValues }) => {
|
||||||
|
const itemId = watch("item_id");
|
||||||
|
const { subItemsList, loadingSubItemsList, errorSubItemsList } = useRoadItemGetSubItems(itemId);
|
||||||
|
|
||||||
|
function createExplanation(val) {
|
||||||
|
const currentExplanation = getValues("explanation");
|
||||||
|
const selectedItem = subItemsList.find((item) => item.id === val)?.name;
|
||||||
|
return `${currentExplanation}-${selectedItem}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"sub_item_id"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="اقدام انجام شده"
|
||||||
|
selectors={subItemsList}
|
||||||
|
isLoading={loadingSubItemsList}
|
||||||
|
schema={{ name: "name", value: "id" }}
|
||||||
|
error={errorSubItemsList}
|
||||||
|
helperText={error?.message}
|
||||||
|
onChange={(val) => {
|
||||||
|
field.onChange(val);
|
||||||
|
setValue("explanation", createExplanation(val));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default SubItemSelectField;
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import LtrTextField from "@/core/components/LtrTextField";
|
||||||
import PersianTextField from "@/core/components/PersianTextField";
|
import PersianTextField from "@/core/components/PersianTextField";
|
||||||
import SelectBox from "@/core/components/SelectBox";
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
import StyledForm from "@/core/components/StyledForm";
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
@@ -8,6 +9,8 @@ import { ExitToApp } from "@mui/icons-material";
|
|||||||
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
import { Box, Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { object, string } from "yup";
|
import { object, string } from "yup";
|
||||||
|
import ItemSelectField from "./ItemSelectField";
|
||||||
|
import SubItemSelectField from "./SubItemSelectField";
|
||||||
|
|
||||||
const validationSchema = object({
|
const validationSchema = object({
|
||||||
explanation: string().required("موضوع را مشخص کنید!"),
|
explanation: string().required("موضوع را مشخص کنید!"),
|
||||||
@@ -23,6 +26,8 @@ const validationSchema = object({
|
|||||||
|
|
||||||
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
||||||
const defaultValues = {
|
const defaultValues = {
|
||||||
|
item_id: allData.item_id,
|
||||||
|
sub_item_id: allData.sub_item_id,
|
||||||
explanation: allData.explanation,
|
explanation: allData.explanation,
|
||||||
end_point: allData.end_point,
|
end_point: allData.end_point,
|
||||||
region: allData.region,
|
region: allData.region,
|
||||||
@@ -30,7 +35,7 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
road_observed_id: allData.road_observed_id,
|
road_observed_id: allData.road_observed_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { control, handleSubmit } = useForm({
|
const { control, handleSubmit, watch, setValue, getValues } = useForm({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
resolver: yupResolver(validationSchema),
|
resolver: yupResolver(validationSchema),
|
||||||
mode: "all",
|
mode: "all",
|
||||||
@@ -48,24 +53,14 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
<Stack>
|
<Stack>
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
<Controller
|
<ItemSelectField control={control} watch={watch} setValue={setValue} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} sm={6}>
|
||||||
|
<SubItemSelectField
|
||||||
control={control}
|
control={control}
|
||||||
name={"explanation"}
|
watch={watch}
|
||||||
render={({ field, fieldState: { error } }) => (
|
setValue={setValue}
|
||||||
<PersianTextField
|
getValues={getValues}
|
||||||
value={field.value}
|
|
||||||
variant="outlined"
|
|
||||||
name="explanation"
|
|
||||||
onChange={field.onChange}
|
|
||||||
label="موضوع"
|
|
||||||
placeholder={"موضوع را وارد کنید"}
|
|
||||||
fullWidth
|
|
||||||
size="small"
|
|
||||||
error={error}
|
|
||||||
helperText={error?.message}
|
|
||||||
InputLabelProps={{ shrink: true }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} sm={6}>
|
<Grid item xs={12} sm={6}>
|
||||||
@@ -125,6 +120,29 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Grid item xs={6} sm={6}>
|
||||||
|
<Stack>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"end_km"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<LtrTextField
|
||||||
|
autoComplete="off"
|
||||||
|
type="tel"
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (isNaN(Number(e.target.value))) return;
|
||||||
|
field.onChange(e.target.value);
|
||||||
|
}}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
label="کیلومتر خودرو / ساعت کار"
|
||||||
|
InputLabelProps={{ shrink: true }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typogr
|
|||||||
import moment from "jalali-moment";
|
import moment from "jalali-moment";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import ShowBound from "../../../../../../Actions/showBound";
|
import ShowRoute from "../Area/Routing/ShowRoute";
|
||||||
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
|
||||||
loading: () => <MapLoading />,
|
loading: () => <MapLoading />,
|
||||||
ssr: false,
|
ssr: false,
|
||||||
@@ -23,7 +23,7 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
|
|||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Box sx={{ width: "100%", height: "200px" }}>
|
<Box sx={{ width: "100%", height: "200px" }}>
|
||||||
<MapLayer style={{ borderRadius: "4px" }}>
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
<ShowBound bound={allData.bound} />
|
<ShowRoute area={allData.area} />
|
||||||
</MapLayer>
|
</MapLayer>
|
||||||
</Box>
|
</Box>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
@@ -60,6 +60,11 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
|
|||||||
<Divider sx={{ flex: 1 }} />
|
<Divider sx={{ flex: 1 }} />
|
||||||
<Chip size="small" label={allData.end_point} />
|
<Chip size="small" label={allData.end_point} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
|
<Typography variant="body2">کیلومتر خودرو / ساعت کار</Typography>
|
||||||
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
<Chip size="small" label={allData.end_km} />
|
||||||
|
</Stack>
|
||||||
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
<Typography variant="body2">تاریخ شروع ماموریت</Typography>
|
<Typography variant="body2">تاریخ شروع ماموریت</Typography>
|
||||||
<Divider sx={{ flex: 1 }} />
|
<Divider sx={{ flex: 1 }} />
|
||||||
@@ -88,16 +93,18 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
|
|||||||
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
<Typography variant="body2">خودرو</Typography>
|
<Typography variant="body2">خودرو</Typography>
|
||||||
<Divider sx={{ flex: 1 }} />
|
<Divider sx={{ flex: 1 }} />
|
||||||
|
{allData.machine && (
|
||||||
<Chip
|
<Chip
|
||||||
size="small"
|
size="small"
|
||||||
label={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
|
label={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack direction={"row"} alignItems={"center"} spacing={1}>
|
{/* <Stack direction={"row"} alignItems={"center"} spacing={1}>
|
||||||
<Typography variant="body2">راننده</Typography>
|
<Typography variant="body2">راننده</Typography>
|
||||||
<Divider sx={{ flex: 1 }} />
|
<Divider sx={{ flex: 1 }} />
|
||||||
<Chip size="small" label={`${allData.driver.code} | ${allData.driver.name}`} />
|
<Chip size="small" label={`${allData.driver.code} | ${allData.driver.name}`} />
|
||||||
</Stack>
|
</Stack> */}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<Divider>
|
<Divider>
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting, oldBound }
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value={tabState} index={2}>
|
<TabPanel value={tabState} index={2}>
|
||||||
<Area
|
<Area
|
||||||
oldBound={oldBound}
|
|
||||||
allData={allData}
|
allData={allData}
|
||||||
setAllData={(data) => {
|
setAllData={(data) => {
|
||||||
dispatch({ type: "changeData", data });
|
dispatch({ type: "changeData", data });
|
||||||
|
|||||||
@@ -9,25 +9,18 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
|
|||||||
const requestServer = useRequest({ notificationSuccess: true });
|
const requestServer = useRequest({ notificationSuccess: true });
|
||||||
|
|
||||||
const submitForm = async (result) => {
|
const submitForm = async (result) => {
|
||||||
setSubmitting(true);
|
console.log("result: ", result);
|
||||||
const bound = result.bound.getLatLngs();
|
|
||||||
let area =
|
|
||||||
result.bound_type === "polygon"
|
|
||||||
? bound.map((ring) => ring.map((latlng) => [latlng.lng, latlng.lat]))[0]
|
|
||||||
: bound.map((latlng) => [latlng.lng, latlng.lat]);
|
|
||||||
|
|
||||||
// بستن polygon
|
setSubmitting(true);
|
||||||
if (result.bound_type === "polygon" && area.length > 0) {
|
|
||||||
const firstPoint = area[0];
|
|
||||||
area.push({ ...firstPoint });
|
|
||||||
}
|
|
||||||
await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", {
|
await requestServer(`${REQUEST_MISSION_CONTINUE_MISSION}/${row.original.id}`, "post", {
|
||||||
data: {
|
data: {
|
||||||
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
|
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
|
||||||
machine_id: result.machine.id,
|
machine_id: result.machine.id,
|
||||||
driver: result.driver.id,
|
encoded_route: result.area,
|
||||||
|
driver: row.original.driver_id,
|
||||||
zone: result.region,
|
zone: result.region,
|
||||||
type: result.type,
|
type: result.type,
|
||||||
|
end_km: result.end_km,
|
||||||
...(result.type == 1
|
...(result.type == 1
|
||||||
? {
|
? {
|
||||||
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
|
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
|
||||||
@@ -36,12 +29,10 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
|
|||||||
end_date: result.end_date,
|
end_date: result.end_date,
|
||||||
}),
|
}),
|
||||||
end_point: result.end_point,
|
end_point: result.end_point,
|
||||||
area: {
|
|
||||||
type: result.bound_type,
|
|
||||||
coordinates: area,
|
|
||||||
},
|
|
||||||
category_id: result.category_id,
|
category_id: result.category_id,
|
||||||
explanation: result.explanation,
|
explanation: result.explanation,
|
||||||
|
item_id: result.item_id,
|
||||||
|
sub_item_id: result.sub_item_id,
|
||||||
...(result.category_id == 3
|
...(result.category_id == 3
|
||||||
? {
|
? {
|
||||||
road_observed_id: result.road_observed_id,
|
road_observed_id: result.road_observed_id,
|
||||||
@@ -64,17 +55,21 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
|
|||||||
<CreateForm
|
<CreateForm
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
explanation: "",
|
explanation: "",
|
||||||
|
item_id: "",
|
||||||
|
sub_item_id: "",
|
||||||
category_id: "",
|
category_id: "",
|
||||||
bound: null,
|
|
||||||
rahdaran: rahdaran.filter((r) => !r.is_driver),
|
rahdaran: rahdaran.filter((r) => !r.is_driver),
|
||||||
bound_type: "polyline",
|
bound_type: "polyline",
|
||||||
type: "",
|
type: "",
|
||||||
|
end_km: "",
|
||||||
end_date: "",
|
end_date: "",
|
||||||
end_time: null,
|
end_time: null,
|
||||||
end_point: "",
|
end_point: "",
|
||||||
region: "",
|
region: "",
|
||||||
machine: machine,
|
machine: machine,
|
||||||
driver: rahdaran.find((r) => r.is_driver),
|
driver: row.original.driver_id,
|
||||||
|
area: null,
|
||||||
|
locations: [],
|
||||||
}}
|
}}
|
||||||
oldBound={oldBound}
|
oldBound={oldBound}
|
||||||
submitForm={submitForm}
|
submitForm={submitForm}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Close } from "@mui/icons-material";
|
||||||
|
import { Dialog, DialogTitle, IconButton } from "@mui/material";
|
||||||
|
import ChangeStatusForm from "./Form";
|
||||||
|
|
||||||
|
const ChangeStatusDialog = ({ open, setOpen, mutate, row }) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} fullWidth maxWidth="xs">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
<DialogTitle>تغییر وضعیت</DialogTitle>
|
||||||
|
{open && <ChangeStatusForm setOpen={setOpen} mutate={mutate} row={row} />}
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ChangeStatusDialog;
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import SelectBox from "@/core/components/SelectBox";
|
||||||
|
import StyledForm from "@/core/components/StyledForm";
|
||||||
|
import { CHANGE_STATUS_OPERATOR, CHANGE_STATUS_RECEIPT } from "@/core/utils/routes";
|
||||||
|
import useRequest from "@/lib/hooks/useRequest";
|
||||||
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { Beenhere, ExitToApp } from "@mui/icons-material";
|
||||||
|
import { Button, DialogActions, DialogContent, Grid, Stack } from "@mui/material";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { object, string } from "yup";
|
||||||
|
|
||||||
|
const validationSchema = object({
|
||||||
|
status: string().required("وضعیت را مشخص کنید!"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ChangeStatusForm = ({ setOpen, mutate, row }) => {
|
||||||
|
const requestServer = useRequest({ notificationSuccess: true });
|
||||||
|
const defaultValues = {
|
||||||
|
status: row.original?.status || "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
} = useForm({
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(validationSchema),
|
||||||
|
mode: "all",
|
||||||
|
});
|
||||||
|
const onSubmitBase = async (data) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("state_id", data.status);
|
||||||
|
await requestServer(`${CHANGE_STATUS_OPERATOR}/${row.original.id}`, "post", {
|
||||||
|
data: formData,
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
mutate();
|
||||||
|
setOpen(false);
|
||||||
|
})
|
||||||
|
.catch((error) => {});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledForm onSubmit={handleSubmit(onSubmitBase)}>
|
||||||
|
<DialogContent dividers sx={{ overflowY: "auto", maxHeight: "70vh" }}>
|
||||||
|
<Stack>
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
<Grid item xs={12} sm={12} sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={"status"}
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<SelectBox
|
||||||
|
value={field.value}
|
||||||
|
label="وضعیت"
|
||||||
|
selectors={[
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name_fa: "درخواست ایجاد شد و در حال بررسی توسط واحد تایید کننده است",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name_fa:
|
||||||
|
"خودرو اختصاص داده شد و درخواست توسط واحد کنترل در حال بررسی است",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
name_fa: "درخواست توسط واحد کنترل تایید شد و ماموریت آغاز می شود",
|
||||||
|
},
|
||||||
|
{ id: 4, name_fa: "اتمام ماموریت" },
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name_fa:
|
||||||
|
"عدم تخصیص خودرو توسط واحد تایید کننده و بازگشت درخواست به کارتابل کاربر",
|
||||||
|
},
|
||||||
|
{ id: 6, name_fa: "ماموریت لغو شد" },
|
||||||
|
{ id: 7, name_fa: "ماموریت بدون فرایند ثبت شد" },
|
||||||
|
]}
|
||||||
|
schema={{ name: "name_fa", value: "id" }}
|
||||||
|
error={error}
|
||||||
|
onChange={field.onChange}
|
||||||
|
helperText={error?.message}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Stack>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<Button
|
||||||
|
onClick={handleClose}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
size="large"
|
||||||
|
startIcon={<ExitToApp />}
|
||||||
|
>
|
||||||
|
بستن
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="large" disabled={isSubmitting} type={"submit"} endIcon={<Beenhere />}>
|
||||||
|
{isSubmitting ? "در حال ثبت" : "ثبت"}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</StyledForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ChangeStatusForm;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Reply } from "@mui/icons-material";
|
||||||
|
import { IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import ChangeStatusDialog from "./Dialog";
|
||||||
|
|
||||||
|
const ChangeStatus = ({ row, mutate }) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="تغییر وضعیت">
|
||||||
|
<IconButton color="primary" onClick={() => setOpen(true)}>
|
||||||
|
<Reply />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<ChangeStatusDialog open={open} setOpen={setOpen} mutate={mutate} row={row} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default ChangeStatus;
|
||||||
@@ -3,61 +3,77 @@ import useRequest from "@/lib/hooks/useRequest";
|
|||||||
import DialogLoading from "@/core/components/DialogLoading";
|
import DialogLoading from "@/core/components/DialogLoading";
|
||||||
import CreateForm from "../../../Actions/Create/Form";
|
import CreateForm from "../../../Actions/Create/Form";
|
||||||
import moment from "jalali-moment";
|
import moment from "jalali-moment";
|
||||||
import L from "leaflet";
|
import polyline from "@mapbox/polyline";
|
||||||
import { GET_RAHDARAN_BY_ID, UPDATE_REQUEST_MISSION } from "@/core/utils/routes";
|
import { GET_RAHDARAN_BY_ID, UPDATE_REQUEST_MISSION } from "@/core/utils/routes";
|
||||||
|
|
||||||
const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
|
const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
|
||||||
const latLngs = useMemo(() => {
|
|
||||||
return row.original.area.coordinates.map(([lng, lat]) => [lat, lng]);
|
|
||||||
}, [row.original.area.coordinates]);
|
|
||||||
|
|
||||||
const bound = useMemo(
|
|
||||||
() => (row.original.area.type === "polygon" ? L.polygon(latLngs) : L.polyline(latLngs)),
|
|
||||||
[row.original.area.type, latLngs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [rahdaran, setRahdaran] = useState(null);
|
const [rahdaran, setRahdaran] = useState(null);
|
||||||
const [rahdaranLoading, setRahdaranLoading] = useState(true);
|
const [rahdaranLoading, setRahdaranLoading] = useState(true);
|
||||||
|
|
||||||
const requestServer = useRequest();
|
const requestServer = useRequest();
|
||||||
|
|
||||||
|
const locations = useMemo(() => {
|
||||||
|
if (!row.original.encoded_route) return [];
|
||||||
|
|
||||||
|
const decoded = polyline.decode(row.original.encoded_route, 6);
|
||||||
|
|
||||||
|
if (decoded.length === 0) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
lat: decoded[0][0],
|
||||||
|
lon: decoded[0][1],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: decoded[decoded.length - 1][0],
|
||||||
|
lon: decoded[decoded.length - 1][1],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [row.original.encoded_route]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setRahdaranLoading(true);
|
setRahdaranLoading(true);
|
||||||
|
|
||||||
requestServer(`${GET_RAHDARAN_BY_ID}/${row.original.id}`, "get")
|
requestServer(`${GET_RAHDARAN_BY_ID}/${row.original.id}`, "get")
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setRahdaran(response.data.data);
|
setRahdaran(response.data.data);
|
||||||
setRahdaranLoading(false);
|
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
setRahdaranLoading(false);
|
setRahdaranLoading(false);
|
||||||
});
|
});
|
||||||
}, [row.original.id]);
|
}, [row.original.id]);
|
||||||
|
|
||||||
const submitForm = async (result) => {
|
const submitForm = async (result) => {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const latlngs = result.bound.getLatLngs();
|
|
||||||
|
|
||||||
const normalized = result.bound_type === "polygon" ? latlngs : [latlngs];
|
|
||||||
|
|
||||||
const area = normalized.map((ring) => ring.map(({ lat, lng }) => [lat, lng]));
|
|
||||||
await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", {
|
await requestServer(`${UPDATE_REQUEST_MISSION}/${row.original.id}`, "post", {
|
||||||
data: {
|
data: {
|
||||||
explanation: result.explanation,
|
explanation: result.explanation,
|
||||||
|
item_id: result.item_id,
|
||||||
|
sub_item_id: result.sub_item_id,
|
||||||
category_id: result.category_id,
|
category_id: result.category_id,
|
||||||
|
|
||||||
...(result.category_id == 3
|
...(result.category_id == 3
|
||||||
? {
|
? {
|
||||||
road_observed_id: result.road_observed_id,
|
road_observed_id: result.road_observed_id,
|
||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
area: {
|
|
||||||
type: result.bound_type,
|
encoded_route: result.area,
|
||||||
coordinates: area[0],
|
|
||||||
},
|
...(result.rahdaran.length !== 0
|
||||||
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
|
? {
|
||||||
|
rahdaran: result.rahdaran.map((r) => r.id),
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
|
||||||
requested_machines: result.requested_machines,
|
requested_machines: result.requested_machines,
|
||||||
type: result.type,
|
type: result.type,
|
||||||
zone: result.region,
|
zone: result.region,
|
||||||
end_point: result.end_point,
|
end_point: result.end_point,
|
||||||
|
|
||||||
...(result.type == 1
|
...(result.type == 1
|
||||||
? {
|
? {
|
||||||
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
|
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
|
||||||
@@ -71,35 +87,43 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
|
|||||||
hasSidebarUpdate: true,
|
hasSidebarUpdate: true,
|
||||||
notificationSuccess: true,
|
notificationSuccess: true,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then(() => {
|
||||||
mutate();
|
mutate();
|
||||||
setOpenEditDialog(false);
|
setOpenEditDialog(false);
|
||||||
})
|
})
|
||||||
.catch((error) => {})
|
.catch(() => {})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return rahdaranLoading ? (
|
||||||
<>
|
|
||||||
{rahdaranLoading ? (
|
|
||||||
<DialogLoading />
|
<DialogLoading />
|
||||||
) : (
|
) : (
|
||||||
<CreateForm
|
<CreateForm
|
||||||
defaultValues={{
|
defaultValues={{
|
||||||
explanation: row.original.explanation,
|
explanation: row.original.explanation,
|
||||||
|
item_id: row.original.item_id,
|
||||||
|
sub_item_id: row.original.sub_item_id,
|
||||||
category_id: row.original.category_id,
|
category_id: row.original.category_id,
|
||||||
road_observed_id: row.original.category_id == 3 ? row.original.road_observed[0].id : "",
|
|
||||||
|
road_observed_id: row.original.category_id == 3 ? (row.original.road_observed?.[0]?.id ?? "") : "",
|
||||||
|
|
||||||
rahdaran: rahdaran.filter((r) => !r.isDriver),
|
rahdaran: rahdaran.filter((r) => !r.isDriver),
|
||||||
|
|
||||||
requested_machines: row.original.requested_machines,
|
requested_machines: row.original.requested_machines,
|
||||||
bound: bound,
|
|
||||||
bound_type: row.original.area.type,
|
area: row.original.encoded_route,
|
||||||
|
locations,
|
||||||
|
|
||||||
type: row.original.type,
|
type: row.original.type,
|
||||||
|
|
||||||
start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
|
start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
|
||||||
start_time: moment(row.original.start_date).toDate(),
|
start_time: moment(row.original.start_date).toDate(),
|
||||||
|
|
||||||
end_date: moment(row.original.end_date).format("YYYY-MM-DD"),
|
end_date: moment(row.original.end_date).format("YYYY-MM-DD"),
|
||||||
end_time: moment(row.original.end_date).toDate(),
|
end_time: moment(row.original.end_date).toDate(),
|
||||||
|
|
||||||
end_point: row.original.end_point,
|
end_point: row.original.end_point,
|
||||||
region: row.original.zone,
|
region: row.original.zone,
|
||||||
}}
|
}}
|
||||||
@@ -109,8 +133,7 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
|
|||||||
setOpen={setOpenEditDialog}
|
setOpen={setOpenEditDialog}
|
||||||
mutate={mutate}
|
mutate={mutate}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EditController;
|
export default EditController;
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import MapIcon from "@mui/icons-material/Map";
|
||||||
|
import MapLayer from "@/core/components/MapLayer";
|
||||||
|
import ShowRoute from "../../Actions/Create/Form/Area/Routing/ShowRoute";
|
||||||
|
|
||||||
|
const RouteDetail = ({ encoded_route }) => {
|
||||||
|
const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="مسیر انتخابی">
|
||||||
|
<IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}>
|
||||||
|
<MapIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
open={openRouteDetailDialog}
|
||||||
|
onClose={() => setOpenRouteDetailDialog(false)}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
dir="rtl"
|
||||||
|
maxWidth={"sm"}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle>
|
||||||
|
<Box sx={{ mx: 1, borderRadius: "5px" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "300px",
|
||||||
|
border: "1px solid #e1e1e1",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
|
<ShowRoute area={encoded_route} />
|
||||||
|
</MapLayer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpenRouteDetailDialog(false)}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
بستن
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RouteDetail;
|
||||||
@@ -3,9 +3,13 @@ import Edit from "./Edit";
|
|||||||
import Delete from "./Delete";
|
import Delete from "./Delete";
|
||||||
import { useAuth } from "@/lib/contexts/auth";
|
import { useAuth } from "@/lib/contexts/auth";
|
||||||
import Add from "./Add";
|
import Add from "./Add";
|
||||||
|
import ChangeStatus from "@/components/dashboard/roadMissions/operator/RowActions/ChangeStatus";
|
||||||
|
import { usePermissions } from "@/lib/hooks/usePermissions";
|
||||||
|
|
||||||
const RowActions = ({ row, mutate }) => {
|
const RowActions = ({ row, mutate }) => {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { data: userPermissions } = usePermissions();
|
||||||
|
const hasChangeStatusPermission = userPermissions.some((item) => ["mission-change-state"].includes(item));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ display: "flex", gap: 1 }}>
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
@@ -20,6 +24,7 @@ const RowActions = ({ row, mutate }) => {
|
|||||||
<Add mutate={mutate} row={row} />
|
<Add mutate={mutate} row={row} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{hasChangeStatusPermission && <ChangeStatus mutate={mutate} row={row} />}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { Stack } from "@mui/material";
|
import { Stack } from "@mui/material";
|
||||||
import Create from "./Actions/Create";
|
import Create from "./Actions/Create";
|
||||||
import CreateWithoutProcess from "./Actions/CreateWithoutProcess";
|
import OperatorViolations from "./Actions/Violations";
|
||||||
|
// import CreateWithoutProcess from "./Actions/CreateWithoutProcess";
|
||||||
|
|
||||||
const Toolbar = ({ table, filterData, mutate }) => {
|
const Toolbar = ({ table, filterData, mutate }) => {
|
||||||
return (
|
return (
|
||||||
<Stack direction={"row"} spacing={1}>
|
<Stack direction={"row"} spacing={1}>
|
||||||
<Create mutate={mutate} />
|
<Create mutate={mutate} />
|
||||||
<CreateWithoutProcess mutate={mutate} />
|
<OperatorViolations mutate={mutate} />
|
||||||
|
{/*<CreateWithoutProcess mutate={mutate} />*/}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
"use client";
|
||||||
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import Toolbar from "./Toolbar";
|
||||||
|
import RowActions from "./RowActions";
|
||||||
|
import { GET_DRIVERS_ACTIVITY_REPORT } from "@/core/utils/routes";
|
||||||
|
|
||||||
|
const DriverReportsList = ({ specialFilter }) => {
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "driver_id",
|
||||||
|
header: "کد یکتا",
|
||||||
|
id: "driver_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
enableSorting: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
grow: false,
|
||||||
|
size: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "driver_name",
|
||||||
|
header: "نام راننده",
|
||||||
|
id: "driver_name",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
enableSorting: true,
|
||||||
|
datatype: "text",
|
||||||
|
grow: false,
|
||||||
|
size: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "func",
|
||||||
|
header: "کارکرد",
|
||||||
|
id: "func",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
enableSorting: false,
|
||||||
|
datatype: "number",
|
||||||
|
grow: false,
|
||||||
|
size: 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "missions",
|
||||||
|
header: "تعداد ماموریت",
|
||||||
|
id: "missions",
|
||||||
|
enableColumnFilter: false,
|
||||||
|
enableSorting: true,
|
||||||
|
datatype: "number",
|
||||||
|
grow: false,
|
||||||
|
size: 50,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
const cleanedData = specialFilter.filter((item) => !(item.id === "province_id" && item.value === "-1"));
|
||||||
|
const RowActionsWithRowData = (props) => <RowActions specialFilter={cleanedData} {...props} />;
|
||||||
|
return (
|
||||||
|
<Box sx={{ p: 1, border: 1, borderColor: "divider", borderRadius: 1 }}>
|
||||||
|
<DataTableWithAuth
|
||||||
|
TableToolbar={Toolbar}
|
||||||
|
need_filter={true}
|
||||||
|
columns={columns}
|
||||||
|
table_url={GET_DRIVERS_ACTIVITY_REPORT}
|
||||||
|
page_name={"driverReport"}
|
||||||
|
table_name={"driverReportList"}
|
||||||
|
enablePagination={false}
|
||||||
|
specialFilter={specialFilter}
|
||||||
|
enableRowActions
|
||||||
|
RowActions={RowActionsWithRowData}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DriverReportsList;
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"use client";
|
||||||
|
import { EXPORT_GET_DRIVER_ACTIVITY_REPORT } from "@/core/utils/routes";
|
||||||
|
import useRequest from "@/lib/hooks/useRequest";
|
||||||
|
import { useTheme } from "@emotion/react";
|
||||||
|
import DescriptionIcon from "@mui/icons-material/Description";
|
||||||
|
import { Button, CircularProgress, IconButton, useMediaQuery } from "@mui/material";
|
||||||
|
import FileSaver from "file-saver";
|
||||||
|
import moment from "jalali-moment";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const PrintExcel = ({ table, filterData }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const requestServer = useRequest({ notificationSuccess: true });
|
||||||
|
const activeFilters = Object.entries(filterData).reduce((acc, [key, filter]) => {
|
||||||
|
if (filter.value) {
|
||||||
|
acc.push({ id: filter.id, value: filter.value });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filterParams = useMemo(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
activeFilters.length > 0 &&
|
||||||
|
activeFilters.map((filter, index) => {
|
||||||
|
params.set(`${filter.id}`, filter.value);
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}, [activeFilters]);
|
||||||
|
|
||||||
|
const clickHandler = () => {
|
||||||
|
setLoading(true);
|
||||||
|
const requestUrl = EXPORT_GET_DRIVER_ACTIVITY_REPORT;
|
||||||
|
requestServer(`${requestUrl}?${filterParams}`, "get", {
|
||||||
|
requestOptions: { responseType: "blob" },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const filename = `خروجی کارتابل گزارشات راننده تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
|
||||||
|
FileSaver.saveAs(response.data, filename);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isMobile ? (
|
||||||
|
<IconButton aria-label="خروجی اکسل" color="success" onClick={clickHandler}>
|
||||||
|
<DescriptionIcon sx={{ fontSize: "25px" }} />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
color="success"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
disabled={loading}
|
||||||
|
sx={{ textTransform: "unset", alignSelf: "center" }}
|
||||||
|
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
|
||||||
|
// onClick={clickHandler}
|
||||||
|
>
|
||||||
|
خروجی اکسل
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default PrintExcel;
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"use client";
|
||||||
|
import { EXPORT_GET_DRIVER_DETAILS_ACTIVITY_REPORT } from "@/core/utils/routes";
|
||||||
|
import useRequest from "@/lib/hooks/useRequest";
|
||||||
|
import { useTheme } from "@emotion/react";
|
||||||
|
import DescriptionIcon from "@mui/icons-material/Description";
|
||||||
|
import { Button, CircularProgress, IconButton, useMediaQuery } from "@mui/material";
|
||||||
|
import FileSaver from "file-saver";
|
||||||
|
import moment from "jalali-moment";
|
||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
const PrintExcel = ({ table, filterData, specialFilterData }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const requestServer = useRequest({ notificationSuccess: true });
|
||||||
|
const activeFilters = Object.entries(filterData).reduce((acc, [key, filter]) => {
|
||||||
|
if (filter.value) {
|
||||||
|
acc.push({ id: filter.id, value: filter.value });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
const activeSpecialFilters = Object.entries(specialFilterData).reduce((acc, [key, filter]) => {
|
||||||
|
if (filter.value) {
|
||||||
|
acc.push({ id: filter.id, value: filter.value });
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
const filterParams = useMemo(() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
activeFilters.length > 0 &&
|
||||||
|
activeFilters.map((filter, index) => {
|
||||||
|
params.set(`${filter.id}`, filter.value);
|
||||||
|
});
|
||||||
|
activeSpecialFilters.length > 0 &&
|
||||||
|
activeSpecialFilters.map((filter, index) => {
|
||||||
|
params.set(`${filter.id}`, filter.value);
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}, [activeFilters, activeSpecialFilters]);
|
||||||
|
|
||||||
|
const clickHandler = () => {
|
||||||
|
setLoading(true);
|
||||||
|
const requestUrl = EXPORT_GET_DRIVER_DETAILS_ACTIVITY_REPORT;
|
||||||
|
requestServer(`${requestUrl}?${filterParams}`, "get", {
|
||||||
|
requestOptions: { responseType: "blob" },
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
|
const filename = `خروجی کارتابل ریزگزارشات راننده تاریخ_${moment().format("jYYYY_jMM_jDD")}.xlsx`;
|
||||||
|
FileSaver.saveAs(response.data, filename);
|
||||||
|
})
|
||||||
|
.catch(() => {})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isMobile ? (
|
||||||
|
<IconButton aria-label="خروجی اکسل" color="success" onClick={clickHandler}>
|
||||||
|
<DescriptionIcon sx={{ fontSize: "25px" }} />
|
||||||
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
color="success"
|
||||||
|
variant="contained"
|
||||||
|
size="small"
|
||||||
|
disabled={loading}
|
||||||
|
sx={{ textTransform: "unset", alignSelf: "center" }}
|
||||||
|
startIcon={loading ? <CircularProgress size={18} color="inherit" /> : <DescriptionIcon />}
|
||||||
|
// onClick={clickHandler}
|
||||||
|
>
|
||||||
|
خروجی اکسل
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default PrintExcel;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
import PrintExcel from "./ExcelPrint";
|
||||||
|
|
||||||
|
const Toolbar = ({ table, filterData, specialFilterData, mutate }) => {
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", gap: 1 }}>
|
||||||
|
<PrintExcel table={table} filterData={filterData} specialFilterData={specialFilterData} />
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Toolbar;
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
|
||||||
|
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
|
||||||
|
import { GET_DRIVER_DETAILS } from "@/core/utils/routes";
|
||||||
|
import useCities from "@/lib/hooks/useCities";
|
||||||
|
import useProvinces from "@/lib/hooks/useProvince";
|
||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import Toolbar from "./Toolbar";
|
||||||
|
|
||||||
|
const DriverList = ({ row, specialFilter }) => {
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
const dynamicColumns = {
|
||||||
|
header: "استان",
|
||||||
|
id: "province_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
grow: false,
|
||||||
|
size: 130,
|
||||||
|
ColumnSelectComponent: (props) => {
|
||||||
|
const { provinces, errorProvinces, loadingProvinces } = useProvinces();
|
||||||
|
const getColumnSelectOptions = useMemo(() => {
|
||||||
|
if (loadingProvinces) {
|
||||||
|
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||||
|
}
|
||||||
|
if (errorProvinces) {
|
||||||
|
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: "", label: "کل کشور" },
|
||||||
|
...provinces.map((province) => ({
|
||||||
|
value: province.id,
|
||||||
|
label: province.name_fa,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [provinces, errorProvinces, loadingProvinces]);
|
||||||
|
return (
|
||||||
|
<CustomSelectByDependency
|
||||||
|
{...props}
|
||||||
|
value={loadingProvinces ? "loading" : props.filterParameters.value}
|
||||||
|
columnSelectOption={getColumnSelectOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Cell: ({ renderedCellValue, row }) => {
|
||||||
|
return <>{row.original.province_name}</>;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "کد یکتا",
|
||||||
|
id: "id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 80,
|
||||||
|
},
|
||||||
|
...[dynamicColumns],
|
||||||
|
{
|
||||||
|
header: "شهر",
|
||||||
|
id: "city_id",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "numeric",
|
||||||
|
filterMode: "equals",
|
||||||
|
dependencyId: "province_id",
|
||||||
|
grow: false,
|
||||||
|
size: 120,
|
||||||
|
ColumnSelectComponent: (props) => {
|
||||||
|
const { cityList, loadingCityList, errorCityList } = useCities(props.dependencyFieldValue.value);
|
||||||
|
|
||||||
|
const getColumnSelectOptions = useMemo(() => {
|
||||||
|
if (props.dependencyFieldValue.value === "") {
|
||||||
|
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
|
||||||
|
}
|
||||||
|
if (loadingCityList) {
|
||||||
|
return [{ value: "loading", label: "در حال بارگذاری..." }];
|
||||||
|
}
|
||||||
|
if (errorCityList) {
|
||||||
|
return [{ value: "error", label: "خطا در بارگذاری" }];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ value: "", label: "کل ادارات" },
|
||||||
|
...cityList.map((edare) => ({
|
||||||
|
value: edare.id,
|
||||||
|
label: edare.name_fa,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
}, [cityList, loadingCityList, errorCityList]);
|
||||||
|
return (
|
||||||
|
<CustomSelectByDependency
|
||||||
|
{...props}
|
||||||
|
value={
|
||||||
|
props.dependencyFieldValue?.value === ""
|
||||||
|
? "empty"
|
||||||
|
: loadingCityList
|
||||||
|
? "loading"
|
||||||
|
: props.filterParameters.value
|
||||||
|
}
|
||||||
|
columnSelectOption={getColumnSelectOptions}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Cell: ({ row }) => <>{row.original.city_name}</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "station_name",
|
||||||
|
header: "راهدارخانه",
|
||||||
|
id: "station_name",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "driver_name",
|
||||||
|
header: "راننده",
|
||||||
|
id: "driver_name",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "km",
|
||||||
|
header: "کیلومتر شروع",
|
||||||
|
id: "km",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "end_km",
|
||||||
|
header: "کیلومتر پایان",
|
||||||
|
id: "end_km",
|
||||||
|
enableColumnFilter: true,
|
||||||
|
datatype: "text",
|
||||||
|
filterMode: "equals",
|
||||||
|
columnFilterModeOptions: ["equals", "contains"],
|
||||||
|
grow: false,
|
||||||
|
size: 100,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box sx={{ mt: 3 }}>
|
||||||
|
<DataTableWithAuth
|
||||||
|
TableToolbar={Toolbar}
|
||||||
|
need_filter={true}
|
||||||
|
columns={columns}
|
||||||
|
specialFilter={specialFilter}
|
||||||
|
specialQueries={{
|
||||||
|
driver_id: row.driver_id,
|
||||||
|
}}
|
||||||
|
sorting={[{ id: "id", desc: true }]}
|
||||||
|
table_url={GET_DRIVER_DETAILS}
|
||||||
|
page_name={"driverListReport"}
|
||||||
|
table_name={"driverList"}
|
||||||
|
positionActionsColumn={"first"}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DriverList;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Box, Button, DialogActions, DialogContent } from "@mui/material";
|
||||||
|
import { Stack } from "@mui/system";
|
||||||
|
import DriverList from "./DriverDetailsDialog/DriverList";
|
||||||
|
|
||||||
|
const DriverDetailsTable = ({ row, setOpenDriverDetails, specialFilter }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DialogContent dividers>
|
||||||
|
<Box sx={{ flex: 1 }}>
|
||||||
|
<Stack spacing={3}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<DriverList specialFilter={specialFilter} row={row} />
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions sx={{ display: "flex", justifyContent: "space-between" }}>
|
||||||
|
<Stack direction={"row"} spacing={1}>
|
||||||
|
<Button onClick={() => setOpenDriverDetails(false)} variant="outlined" color="primary">
|
||||||
|
بستن
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</DialogActions>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DriverDetailsTable;
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Close, RemoveRedEye } from "@mui/icons-material";
|
||||||
|
import { Dialog, DialogTitle, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import DriverDetailsTable from "./Form";
|
||||||
|
|
||||||
|
const DriverDetails = ({ row, specialFilter }) => {
|
||||||
|
const [openDriverDetails, setOpenDriverDetails] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="ریزگزارشات" arrow placement="right">
|
||||||
|
<IconButton
|
||||||
|
color="primary"
|
||||||
|
sx={{ textTransform: "unset", alignSelf: "center" }}
|
||||||
|
onClick={() => {
|
||||||
|
setOpenDriverDetails(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RemoveRedEye />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog open={openDriverDetails} fullWidth maxWidth="md">
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={() => setOpenDriverDetails(false)}
|
||||||
|
sx={(theme) => ({
|
||||||
|
position: "absolute",
|
||||||
|
right: 8,
|
||||||
|
top: 8,
|
||||||
|
zIndex: 50,
|
||||||
|
color: theme.palette.grey[500],
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
<DialogTitle>ریز گزارشات راننده</DialogTitle>
|
||||||
|
{openDriverDetails && (
|
||||||
|
<DriverDetailsTable
|
||||||
|
specialFilter={specialFilter}
|
||||||
|
row={row.original}
|
||||||
|
setOpenDriverDetails={setOpenDriverDetails}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default DriverDetails;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { Marker, Polyline, useMap } from "react-leaflet";
|
||||||
|
import L from "leaflet";
|
||||||
|
import polyline from "@mapbox/polyline";
|
||||||
|
import SourceIcon from "@/assets/images/source-icon.svg";
|
||||||
|
import DestIcon from "@/assets/images/destination-icon.svg";
|
||||||
|
|
||||||
|
const ShowRoute = ({ area }) => {
|
||||||
|
const map = useMap();
|
||||||
|
const defaultIconSize = [35, 35];
|
||||||
|
|
||||||
|
const createCustomIcon = (size, iconUrl) => {
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: iconUrl,
|
||||||
|
iconSize: size,
|
||||||
|
iconAnchor: [size[0] / 2, size[1]],
|
||||||
|
popupAnchor: [0, -size[1]],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (coords.length > 0) {
|
||||||
|
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
|
||||||
|
}
|
||||||
|
}, [area]);
|
||||||
|
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
|
||||||
|
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
|
||||||
|
|
||||||
|
if (!area) return null;
|
||||||
|
const coords = polyline.decode(area, 6);
|
||||||
|
|
||||||
|
if (coords.length === 0) return null;
|
||||||
|
|
||||||
|
const sourcePosition = coords[0];
|
||||||
|
const destPosition = coords[coords.length - 1];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Polyline positions={coords} color="#015688" weight={7} opacity={1} />
|
||||||
|
<Marker position={sourcePosition} icon={sourceIcon} />
|
||||||
|
<Marker position={destPosition} icon={destIcon} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ShowRoute;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Box, Button, Dialog, DialogActions, DialogTitle, IconButton, Tooltip } from "@mui/material";
|
||||||
|
import { useState } from "react";
|
||||||
|
import MapIcon from "@mui/icons-material/Map";
|
||||||
|
import MapLayer from "@/core/components/MapLayer";
|
||||||
|
import ShowRoute from "./ShowRoute";
|
||||||
|
|
||||||
|
const RouteDetail = ({ encoded_route }) => {
|
||||||
|
const [openRouteDetailDialog, setOpenRouteDetailDialog] = useState(false);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Tooltip title="مسیر انتخابی">
|
||||||
|
<IconButton color="primary" onClick={() => setOpenRouteDetailDialog(true)}>
|
||||||
|
<MapIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Dialog
|
||||||
|
fullWidth
|
||||||
|
open={openRouteDetailDialog}
|
||||||
|
onClose={() => setOpenRouteDetailDialog(false)}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
boxShadow: "rgba(60, 64, 67, 0.3) 0px 1px 16px 0px, rgba(60, 64, 67, 0.15) 0px 1px 10px 0px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
dir="rtl"
|
||||||
|
maxWidth={"sm"}
|
||||||
|
>
|
||||||
|
<DialogTitle sx={{ fontSize: "large" }}>مسیر انتخابی</DialogTitle>
|
||||||
|
<Box sx={{ mx: 1, borderRadius: "5px" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "300px",
|
||||||
|
border: "1px solid #e1e1e1",
|
||||||
|
borderRadius: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MapLayer style={{ borderRadius: "4px" }}>
|
||||||
|
<ShowRoute area={encoded_route} />
|
||||||
|
</MapLayer>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={() => setOpenRouteDetailDialog(false)}
|
||||||
|
variant="outlined"
|
||||||
|
color="secondary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
بستن
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RouteDetail;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import DriverDetails from "./DriverDetails";
|
||||||
|
|
||||||
|
const RowActions = ({ row, specialFilter }) => {
|
||||||
|
return <DriverDetails specialFilter={specialFilter} row={row} />;
|
||||||
|
};
|
||||||
|
export default RowActions;
|
||||||