57 Commits

Author SHA1 Message Date
0689fb307a release v1.9.0 2026-06-15 15:06:42 +03:30
09c7385faa Merge pull request 'bugfix/show_map_delay' (#10) from bugfix/show_map_delay into develop
Reviewed-on: #10
2026-06-15 11:31:17 +00:00
3745206604 build and format 2026-06-15 15:01:34 +03:30
f42a4ff63b send filter data for reports 2026-06-15 15:00:28 +03:30
18674a5b75 add delay to show routes 2026-06-15 11:48:23 +03:30
68e7fd8cd1 Merge pull request 'feature/end_km' (#9) from feature/end_km into develop
Reviewed-on: #9
2026-06-15 07:49:46 +00:00
e66af994af build and format 2026-06-15 11:19:39 +03:30
1e0d3dcf0e add end_km to violations 2026-06-15 11:16:34 +03:30
92e3505bd9 Merge pull request 'feature/machine_type_report' (#8) from feature/machine_type_report into develop
Reviewed-on: #8
2026-06-14 08:20:48 +00:00
241f583572 merge develop 2026-06-14 11:50:40 +03:30
3ea5c940c0 Merge branch 'develop' of http://git.witel.ir/RMS/frontend into feature/machine_type_report 2026-06-14 11:48:59 +03:30
806854327a build and format 2026-06-14 11:48:34 +03:30
a80e35a510 driver reports completed 2026-06-14 11:47:24 +03:30
cb85157cf7 Merge pull request 'add map to new mission on rowAction of operators' (#7) from feature/routing into develop
Reviewed-on: #7
2026-06-14 06:35:03 +00:00
caaff22c66 machine type report completed 2026-06-14 09:56:03 +03:30
a9a996e1e8 add map to new mission on rowAction of operators 2026-06-14 09:49:13 +03:30
ca20435807 Merge pull request 'feature/machines_report' (#6) from feature/machines_report into develop
Reviewed-on: #6
2026-06-14 05:41:54 +00:00
61ed1980f1 cleaned up logs 2026-06-13 16:20:22 +03:30
93478c8c80 Merge branch develop into feature/machines_report 2026-06-13 16:08:33 +03:30
a9a6d90281 implemented machines reports 2026-06-13 16:07:55 +03:30
376ea3a3e8 Merge pull request 'bugfix/control_start_mission' (#5) from bugfix/control_start_mission into develop
Reviewed-on: #5
2026-06-13 12:07:16 +00:00
e7e8f3e5cc build and format 2026-06-13 15:37:34 +03:30
e7e704aaf7 restyle the width of input 2026-06-13 15:36:18 +03:30
9c2a22e012 Merge pull request 'bugfix/bugfix_cartables' (#4) from bugfix/bugfix_cartables into develop
Reviewed-on: #4
2026-06-13 11:49:37 +00:00
acc013ad81 build and format 2026-06-13 15:12:04 +03:30
1261c9dcc2 operator violations page 2026-06-13 15:09:12 +03:30
a540d79715 Merge branch develop into feature/machines_report 2026-06-13 14:04:22 +03:30
c6283ae130 [wip] implementing machines reports 2026-06-13 14:03:47 +03:30
1ad74cfed7 Merge branch 'develop' 2026-06-13 11:54:54 +03:30
be0922cfcc Merge pull request 'feature/routing' (#2) from feature/routing into develop
Reviewed-on: #2
2026-06-13 08:21:42 +00:00
dec752bc17 build and format 2026-06-13 11:48:47 +03:30
22032b885c Merge branch 'feature/routing' of http://git.witel.ir/RMS/frontend into feature/routing 2026-06-13 11:25:23 +03:30
f7d177bb95 Merge branch 'develop' of http://git.witel.ir/RMS/frontend into feature/routing 2026-06-13 11:22:31 +03:30
0d6269004e Merge branch 'develop' of http://git.witel.ir/RMS/frontend into bugfix/bugfix_cartables 2026-06-13 11:19:39 +03:30
05dc5505cd exit station time 2026-06-13 11:16:48 +03:30
a975bd1f88 Merge pull request 'feature/machines' (#3) from feature/machines into develop
Reviewed-on: #3
2026-06-13 07:33:26 +00:00
01bb171ebb build & format 2026-06-13 11:01:50 +03:30
5f88728d67 implemented machines cartable 2026-06-13 10:57:36 +03:30
855afc05e5 complete adding route selection and view of it for violation page 2026-06-13 10:22:49 +03:30
e3ca4a2052 complete transportation page 2026-06-13 06:13:17 +03:30
5fb97a5e79 Merge branch 'develop' of https://git.witel.ir/RMS/frontend into feature/routing 2026-06-13 05:41:22 +03:30
63ec584aca [wip] implementing machines cartable 2026-06-10 16:30:08 +03:30
70ea71cbfa add showroute to transportation component 2026-06-10 16:11:24 +03:30
6d8e9ddb5b complete routing section and view of route on table actions 2026-06-10 16:05:25 +03:30
16889752a1 build and format 2026-06-10 14:48:31 +03:30
5c05af0828 show time on edit dialog and remove time in tashih mamoriat 2026-06-10 14:40:13 +03:30
8753f23146 remove without proccess and add minute to operator 2026-06-10 13:49:22 +03:30
9ee9132ed8 debug filter select option in violation 2026-06-10 13:36:28 +03:30
46bb4e4228 working on map in rms mission 2026-06-06 10:18:30 +03:30
efb260b66d complete most of the routing task just request to server 2026-05-24 15:37:12 +03:30
a798a36fcd Merge tag 'v1.8.8' into develop
v1.8.8
2026-05-23 11:01:29 +03:30
cab6677bca Merge branch 'release/v1.8.8' 2026-05-23 11:01:18 +03:30
b0a0facea8 release v1.8.8 2026-05-23 11:00:40 +03:30
58fc9135ac Merge pull request 'added end_km, fixed time picker & typos' (#1) from feature/road_missions_changes into develop
Reviewed-on: #1
2026-05-23 07:25:47 +00:00
3a28da9b39 build & format 2026-05-23 10:55:02 +03:30
80da9a1e40 added end_km, fixed time picker & typos
- road missions table
2026-05-23 10:46:17 +03:30
5d9a86f2cf work on source marker 2026-05-20 16:30:10 +03:30
178 changed files with 8161 additions and 1931 deletions

View File

@@ -1,5 +1,5 @@
HOST="rms.witel.ir"
HOST_RMTO="rms.rmto.ir"
NEXT_PUBLIC_VERSION="1.8.7"
NEXT_PUBLIC_VERSION="1.9.0"
NEXT_PUBLIC_API_URL="https://rms.witel.ir"
NEXT_PUBLIC_MAPTILE_ENDPOINT="https://rmsmap.rmto.ir/141map"

View File

@@ -15,6 +15,7 @@
"@emotion/react": "^11.11.3",
"@emotion/styled": "^11.11.0",
"@hookform/resolvers": "^3.9.0",
"@mapbox/polyline": "^1.2.1",
"@mui/icons-material": "^5.16.7",
"@mui/lab": "^5.0.0-alpha.173",
"@mui/material": "^5.16.7",

View File

@@ -0,0 +1,7 @@
"use client";
import PageLoading from "@/core/components/PageLoading";
const Loading = () => {
return <PageLoading />;
};
export default Loading;

View File

@@ -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;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -1,18 +1,18 @@
import WithPermission from "@/core/middlewares/withPermission";
import MissionsReportPage from "@/components/dashboard/roadMissions/reports/taradodReports";
export const metadata = {
title: "کارتابل گزارش ترددها",
};
const Page = () => {
return (
<WithPermission
permission_name={[
"all", // TODO : change permission
]}
>
<MissionsReportPage />
</WithPermission>
);
};
export default Page;
import WithPermission from "@/core/middlewares/withPermission";
import MissionsReportPage from "@/components/dashboard/roadMissions/reports/taradodReports";
export const metadata = {
title: "کارتابل گزارش ترددها",
};
const Page = () => {
return (
<WithPermission
permission_name={[
"all", // TODO : change permission
]}
>
<MissionsReportPage />
</WithPermission>
);
};
export default Page;

View File

@@ -1,18 +1,18 @@
import ViolationsReportsPage from "@/components/dashboard/roadMissions/reports/violationsReports";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "کارتابل گزارش تخلفات",
};
export default function Page() {
return (
<WithPermission
permission_name={[
"all", // TODO : change permission
]}
>
<ViolationsReportsPage />
</WithPermission>
);
}
import ViolationsReportsPage from "@/components/dashboard/roadMissions/reports/violationsReports";
import WithPermission from "@/core/middlewares/withPermission";
export const metadata = {
title: "کارتابل گزارش تخلفات",
};
export default function Page() {
return (
<WithPermission
permission_name={[
"all", // TODO : change permission
]}
>
<ViolationsReportsPage />
</WithPermission>
);
}

View 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

View 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

View 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

View 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

View 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;

View File

@@ -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;

View File

@@ -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;

View 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;

View 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;

View 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;

View File

@@ -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;

View 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;

View 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;

View File

@@ -14,6 +14,7 @@ import useCities from "@/lib/hooks/useCities";
const ControlList = () => {
const columns = useMemo(() => {
const dynamicColumns = {
accessorKey: "province_id",
header: "استان",
id: "province_id",
enableColumnFilter: true,

View File

@@ -1,3 +1,4 @@
import LtrTextField from "@/core/components/LtrTextField";
import MuiDatePicker from "@/core/components/MuiDatePicker";
import MuiTimePicker from "@/core/components/MuiTimePicker";
import { FINISH_MISSION } from "@/core/utils/routes";
@@ -14,6 +15,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
const requestServer = useRequest({ notificationSuccess: true });
const validationSchema = object().shape({
end_km: string().required("لطفاً کیلومتر را وارد کنید."),
start_date: mixed().required("تاریخ خروج خودرو را مشخص کنید."),
start_time: mixed().required("ساعت خروج خودرو را مشخص کنید."),
});
@@ -24,6 +26,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
formState: { isSubmitting },
} = useForm({
defaultValues: {
end_km: "",
start_date: null,
start_time: null,
},
@@ -36,6 +39,7 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
const formattedTime = format(values.start_time, "HH:mm");
requestServer(`${FINISH_MISSION}/${rowId}`, "post", {
data: {
end_km: values.end_km,
time: `${formattedDate} ${formattedTime}`,
},
hasSidebarUpdate: true,
@@ -55,6 +59,28 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
<DialogContent>
<Stack alignItems="center" spacing={2}>
<Typography mt={2}>آیا از ورود خودرو و پایان ماموریت اطمینان دارید؟</Typography>
<Stack sx={{ width: "87%" }}>
<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"
sx={{ width: "100%" }}
fullWidth
label="کیلومتر خودرو / ساعت کار"
InputLabelProps={{ shrink: true }}
/>
)}
/>
</Stack>
<Stack>
<Controller
control={control}
@@ -65,8 +91,8 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
error={error}
value={field.value || null}
disableFuture={false}
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
label={"تاریخ شروع ماموریت"}
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
label={"تاریخ پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
@@ -83,8 +109,8 @@ const FinishMissionContent = ({ rowId, mutate, setOpenFinishMissionDialog }) =>
error={error}
value={field.value || null}
views={["hours", "minutes"]}
placeholder={"زمان شروع ماموریت را وارد کنید"}
label={"زمان شروع ماموریت"}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>

View File

@@ -38,7 +38,6 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
const onSubmit = (values) => {
const formattedDate = values.start_date;
const formattedTime = format(values.start_time, "HH:mm");
console.log(formattedDate, formattedTime);
requestServer(`${START_MISSION}/${rowId}`, "post", {
data: {
@@ -60,7 +59,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
<DialogContent dividers>
<Stack alignItems="center" spacing={2}>
<Typography mt={2}>آیا از خروج خودرو و شروع ماموریت اطمینان دارید؟</Typography>
<Stack>
<Stack sx={{ width: "84%" }}>
<Controller
control={control}
name={"code"}
@@ -85,7 +84,7 @@ const StartMissionContent = ({ rowId, mutate, setOpenStartMissionDialog }) => {
)}
/>
</Stack>
<Stack>
<Stack sx={{ width: "84%" }}>
<Controller
control={control}
name={"km"}

View File

@@ -6,13 +6,13 @@ import L from "leaflet";
// ===== Icons =====
const originIcon = new L.Icon({
iconUrl: "/icons/origin-marker.png",
iconUrl: "/icons/origin-icon.svg",
iconSize: [32, 32],
iconAnchor: [16, 32],
});
const destinationIcon = new L.Icon({
iconUrl: "/icons/destination-marker.png",
iconUrl: "/icons/destination-icon.svg",
iconSize: [32, 32],
iconAnchor: [16, 32],
});
@@ -51,7 +51,7 @@ function FixedCenterMarker() {
pointerEvents: "none",
}}
>
<img src="/icons/pin-center.png" width={40} height={40} alt="center" />
<img src="/icons/origin-icon.svg" width={40} height={40} alt="center" />
</div>
);
}
@@ -87,21 +87,21 @@ export default function Example() {
height: "400px",
}}
>
<MapCenterTracker setCenter={setCenter} />
<MapCenterTracker setCenter={setCenter} />
{/* saved markers */}
{origin && <Marker position={origin} icon={originIcon} />}
{/* saved markers */}
{origin && <Marker position={origin} icon={originIcon} />}
{destination && <Marker position={destination} icon={destinationIcon} />}
{destination && <Marker position={destination} icon={destinationIcon} />}
{/* route line */}
{origin && destination && <Polyline positions={[origin, destination]} />}
{/* route line */}
{origin && destination && <Polyline positions={[origin, destination]} />}
{/* fixed center pin */}
{step !== "done" && <FixedCenterMarker />}
</div>
<div style={{ marginTop: 16, position: "absolute", z : 50, top : 5 }}>
<div style={{ marginTop: 16, position: "absolute", z: 50, top: 5 }}>
<button onClick={handleOk}>
{step === "origin" && "Set Origin"}
{step === "destination" && "Set Destination"}

View File

@@ -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;

View File

@@ -0,0 +1,53 @@
import { Box, Button, TextField } from "@mui/material";
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => {
if (!isSource) return null;
return (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
)}
</Box>
);
};
export default DataBox;

View File

@@ -0,0 +1,57 @@
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;

View File

@@ -0,0 +1,219 @@
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 { useEffect, useRef, useState } from "react";
import DataBox from "./DataBox";
import AlarmText from "./AlarmText";
const Routing = ({ setArea, setLocations, locations, area }) => {
const mapPrevSourceMarker = useRef();
const mapPrevDestMarker = useRef();
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 [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 isMovingSource = !isSource || editingTarget === "source";
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
const map = useMapEvents({
move(e) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
},
movestart() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
},
moveend() {
if (isMovingSource)
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
},
});
useEffect(() => {
mapRef.current = map;
}, [map]);
useEffect(() => {
if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
setSourcePosition(restoredSource);
setDestPosition(restoredDest);
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(restoredSource, restoredDest);
}
}, []);
const fetchRoute = async (source, dest) => {
const locs = [
{ lat: source.lat, lon: source.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 handleMarkerClick = async () => {
if (!isSource || editingTarget === "source") {
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
setSourcePosition(clickedPosition);
setIsSource(true);
setEditingTarget(null);
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
if (editingTarget === "source" && destPosition) {
await fetchRoute(clickedPosition, destPosition);
}
}
};
const handleDestMarkerClick = async () => {
if (!isDest || editingTarget === "dest") {
const clickedPosition = mapPrevDestMarker.current.getLatLng();
setDestPosition(clickedPosition);
setIsDest(true);
setEditingTarget(null);
await fetchRoute(sourcePosition, clickedPosition);
}
};
const handleEditSource = () => {
setEditingTarget("source");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
const handleEditDest = () => {
setEditingTarget("dest");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
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}
/>
<AlarmText isSource={isSource} />
<Marker
position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon}
/>
{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;

View File

@@ -2,20 +2,21 @@ 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 MapControlPolyline from "./MapControlPolyline";
import Example from "./MapControlPolyline";
import Routing from "./Routing";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
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(() => {
setAllData({ bound: bound });
setAllData({ area: area });
setAllData({ locations: locations });
setTabState((s) => s + 1);
}, [bound]);
}, [area]);
return (
<>
@@ -23,7 +24,7 @@ const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack spacing={2}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<Example />
<Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
</MapLayer>
</Box>
</Stack>
@@ -32,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}>
{!bound ? "در انتظار ترسیم" : "مرحله بعد"}
<Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
{!area ? "در انتظار ترسیم" : "مرحله بعد"}
</Button>
</DialogActions>
</>

View File

@@ -6,109 +6,107 @@ import { Controller, useWatch } from "react-hook-form";
const MissionDates = ({ control }) => {
const type = useWatch({ control, name: "type" });
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();
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 (
type && (
<>
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"start_date"}
render={({ field, fieldState: { error } }) => {
return (
<MuiDatePicker
name="start_date"
error={error}
value={field.value}
disableFuture={false}
minDate={now}
maxDate={end_date ?? null}
placeholder={"تاریخ شروع ماموریت را وارد کنید"}
label={"تاریخ شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
name="start_date"
render={({ field, fieldState: { error } }) => (
<MuiDatePicker
name="start_date"
value={field.value}
error={error}
disableFuture={false}
minDate={now}
maxDate={end_date ?? null}
label="تاریخ شروع ماموریت"
placeholder="تاریخ شروع ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
{type == 1 && (
<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"]}
minTime={
start_date && new Date(start_date).toDateString() === now.toDateString()
? now
: null
}
maxTime={endDateTime ?? null}
placeholder={"زمان شروع ماموریت را وارد کنید"}
label={"زمان شروع ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
name="start_time"
render={({ field, fieldState: { error } }) => (
<MuiTimePicker
name="start_time"
value={field.value}
error={error}
minTime={isToday ? now : null}
maxTime={isSameDay && endDateTime ? endDateTime : null}
label="زمان شروع ماموریت"
placeholder="زمان شروع ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
)}
<Grid item xs={12} sm={6}>
<Controller
control={control}
name={"end_date"}
render={({ field, fieldState: { error } }) => {
return (
<MuiDatePicker
name="end_date"
error={error}
value={field.value}
disableFuture={false}
minDate={start_date ? start_date : now}
placeholder={"تاریخ پایان ماموریت را وارد کنید"}
label={"تاریخ پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || [])}
helperText={error ? error.message : null}
/>
);
}}
name="end_date"
render={({ field, fieldState: { error } }) => (
<MuiDatePicker
name="end_date"
value={field.value}
error={error}
disableFuture={false}
minDate={start_date || now}
label="تاریخ پایان ماموریت"
placeholder="تاریخ پایان ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
{type == 1 && (
<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"]}
minTime={startDateTime ?? null}
placeholder={"زمان پایان ماموریت را وارد کنید"}
label={"زمان پایان ماموریت"}
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error ? error.message : null}
/>
);
}}
name="end_time"
render={({ field, fieldState: { error } }) => (
<MuiTimePicker
name="end_time"
value={field.value}
error={error}
minTime={isSameDay && startDateTime ? startDateTime : null}
label="زمان پایان ماموریت"
placeholder="زمان پایان ماموریت را وارد کنید"
setFieldValue={(name, value) => field.onChange(value || null)}
helperText={error?.message}
/>
)}
/>
</Grid>
)}
@@ -116,4 +114,5 @@ const MissionDates = ({ control }) => {
)
);
};
export default MissionDates;

View File

@@ -7,7 +7,7 @@ import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typogr
import moment from "jalali-moment";
import dynamic from "next/dynamic";
import { useCallback } from "react";
import ShowBound from "../../../showBound";
import ShowRoute from "../Area/Routing/ShowRoute";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
@@ -23,11 +23,16 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<DialogContent dividers>
<Box sx={{ flex: 1 }}>
<Stack spacing={2}>
<Box sx={{ width: "100%", height: "200px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={allData.bound} />
</MapLayer>
</Box>
<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>
<Stack spacing={1}>
<Divider>
<Chip color="primary" variant="outlined" label="مشخصات ماموریت" />

View File

@@ -19,8 +19,6 @@ const Create = ({ mutate }) => {
const submitForm = async (result) => {
setSubmitting(true);
const bound = result.bound.getLatLngs();
let area = bound.map((latlng) => [latlng.lng, latlng.lat]);
await requestServer(REQUEST_MISSION, "post", {
data: {
@@ -31,10 +29,7 @@ const Create = ({ mutate }) => {
road_observed_id: result.road_observed_id,
}
: {}),
area: {
type: result.bound_type,
coordinates: area,
},
encoded_route: result.area,
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
requested_machines: result.requested_machines,
type: result.type,
@@ -101,8 +96,8 @@ const Create = ({ mutate }) => {
road_observed_id: "",
rahdaran: [],
requested_machines: [],
bound: null,
bound_type: "polyline",
area: null,
locations: [],
type: "",
start_date: "",
start_time: null,

View File

@@ -24,7 +24,7 @@ const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
<Stack spacing={2}>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<MapControlPolygon bound={bound} setBound={setBound} boundType={"polyline"} />
<MapControlPolygon bound={bound} setBound={setBound} boundType={"polyline"} />
</MapLayer>
</Box>
</Stack>

View File

@@ -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="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 && <ViolationsList setOpen={setOpen} mutate={mutate} />}
</Dialog>
);
};
export default ViolationsDialog;

View File

@@ -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;

View File

@@ -0,0 +1,53 @@
import { Box, Button, TextField } from "@mui/material";
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => {
if (!isSource) return null;
return (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
)}
</Box>
);
};
export default DataBox;

View File

@@ -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]],
});
};
const sourceIcon = createCustomIcon(defaultIconSize, SourceIcon.src);
const destIcon = createCustomIcon(defaultIconSize, DestIcon.src);
useEffect(() => {
if (coords.length > 0) {
map.flyToBounds(coords, { padding: [50, 50], duration: 1.5 });
}
}, [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 (
<>
<Polyline positions={coords} color="#015688" weight={7} opacity={1} />
<Marker position={sourcePosition} icon={sourceIcon} />
<Marker position={destPosition} icon={destIcon} />
</>
);
};
export default ShowRoute;

View File

@@ -0,0 +1,219 @@
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 { useEffect, useRef, useState } from "react";
import DataBox from "./DataBox";
import AlarmText from "./AlarmText";
const Routing = ({ setArea, setLocations, locations, area }) => {
const mapPrevSourceMarker = useRef();
const mapPrevDestMarker = useRef();
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 [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 isMovingSource = !isSource || editingTarget === "source";
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
const map = useMapEvents({
move(e) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
},
movestart() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
},
moveend() {
if (isMovingSource)
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
},
});
useEffect(() => {
mapRef.current = map;
}, [map]);
useEffect(() => {
if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
setSourcePosition(restoredSource);
setDestPosition(restoredDest);
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(restoredSource, restoredDest);
}
}, []);
const fetchRoute = async (source, dest) => {
const locs = [
{ lat: source.lat, lon: source.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 handleMarkerClick = async () => {
if (!isSource || editingTarget === "source") {
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
setSourcePosition(clickedPosition);
setIsSource(true);
setEditingTarget(null);
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
if (editingTarget === "source" && destPosition) {
await fetchRoute(clickedPosition, destPosition);
}
}
};
const handleDestMarkerClick = async () => {
if (!isDest || editingTarget === "dest") {
const clickedPosition = mapPrevDestMarker.current.getLatLng();
setDestPosition(clickedPosition);
setIsDest(true);
setEditingTarget(null);
await fetchRoute(sourcePosition, clickedPosition);
}
};
const handleEditSource = () => {
setEditingTarget("source");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
const handleEditDest = () => {
setEditingTarget("dest");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
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}
/>
<AlarmText isSource={isSource} />
<Marker
position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon}
/>
{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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,10 @@
import SelectId from "./SelectId";
const RowActions = ({ row, onChange, setOpenFastReactDialog }) => {
return (
<>
<SelectId row={row} onChange={onChange} setOpenFastReactDialog={setOpenFastReactDialog} />
</>
);
};
export default RowActions;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,150 @@
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 FastReactCode from "./FastReactCode";
const validationSchema = object({
explanation: 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 = {
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 } = 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}>
<Controller
control={control}
name={"explanation"}
render={({ field, fieldState: { error } }) => (
<PersianTextField
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 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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
import Allocate from "./Allocate";
const RowActions = ({ row, setMachine, setOpenMachinesDialog }) => {
return <Allocate row={row} setMachine={setMachine} setOpenMachinesDialog={setOpenMachinesDialog} />;
};
export default RowActions;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,114 @@
import { AccessTime, Engineering, InsertDriveFile, Person, Verified, Map } from "@mui/icons-material";
import { Box, Tab, Tabs } from "@mui/material";
import { useReducer, useState } from "react";
import GetDateTime from "./GetDateTime";
import GetItemInfo from "./GetItemInfo";
import MachineAndDriver from "./MachineAndDriver";
import Rahdaran from "./Rahdaran";
import Verify from "./Verify";
import Area from "./Area";
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;

View File

@@ -0,0 +1,87 @@
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,
},
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;

View File

@@ -0,0 +1,9 @@
import MissionCorrection from "./MissionCorrection";
export default function RowActions({ row, mutate, baseMutate }) {
return (
<>
<MissionCorrection row={row} mutate={mutate} baseMutate={baseMutate} />
</>
);
}

View File

@@ -0,0 +1,141 @@
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}</>,
},
];
}, []);
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;

View File

@@ -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;

View File

@@ -1,21 +1,21 @@
"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 { missionCategoryTypes } from "@/core/utils/missionCategoryTypes";
import { missionRegions } from "@/core/utils/missionRegions";
import { missionTypes } from "@/core/utils/missionTypes";
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 moment from "jalali-moment";
import { useMemo } from "react";
import ShowArea from "./Actions/showArea";
import RowActions from "./RowActions";
import MachinesDialog from "./RowActions/Machines";
import Toolbar from "./Toolbar";
import RahdaranDialog from "./RowActions/Rahdaran";
import DescriptionDialog from "@/components/dashboard/roadMissions/operator/RowActions/DescriptionDialog";
import useProvinces from "@/lib/hooks/useProvince";
import CustomSelectByDependency from "@/core/components/DataTable/filter/fieldsType/CustomSelectByDependency";
import useCities from "@/lib/hooks/useCities";
import RouteDetail from "./RowActions/RouteDetail";
import Toolbar from "./Toolbar";
const OperatorList = () => {
const columns = useMemo(() => {
@@ -247,7 +247,7 @@ const OperatorList = () => {
},
{
header: "منطقه عملیاتی",
id: "area",
id: "encoded_route",
enableColumnFilter: false,
datatype: "numeric",
filterMode: "equals",
@@ -263,9 +263,9 @@ const OperatorList = () => {
},
},
Cell: ({ row }) =>
row.original.area ? (
row.original.encoded_route ? (
<Stack alignItems={"center"} justifyContent={"center"}>
<ShowArea area={row.original.area} />
<RouteDetail encoded_route={row.original.encoded_route} />
</Stack>
) : (
"-"

View File

@@ -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;

View File

@@ -0,0 +1,53 @@
import { Box, Button, TextField } from "@mui/material";
const DataBox = ({ isSource, isDest, sourcePosition, destPosition, editingTarget, onEditSource, onEditDest }) => {
if (!isSource) return null;
return (
<Box
sx={{
position: "absolute",
bottom: 5,
left: 5,
zIndex: 1000,
backgroundColor: "white",
border: "1px solid #e1e1e1",
padding: 1,
borderRadius: 2,
opacity: 0.8,
gap: 1,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
variant="outlined"
size="small"
readOnly
value={sourcePosition ? `${sourcePosition.lat.toFixed(6)}, ${sourcePosition.lng.toFixed(6)}` : ""}
/>
{isSource && isDest && (
<Button variant="contained" color="success" size="small" onClick={onEditSource}>
ویرایش مبدا
</Button>
)}
</div>
{isDest && (
<div style={{ display: "flex", alignItems: "end", gap: 4 }}>
<TextField
size="small"
variant="outlined"
readOnly
value={destPosition ? `${destPosition.lat.toFixed(6)}, ${destPosition.lng.toFixed(6)}` : ""}
/>
<Button variant="contained" color="error" size="small" onClick={onEditDest}>
ویرایش مقصد
</Button>
</div>
)}
</Box>
);
};
export default DataBox;

View File

@@ -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;

View File

@@ -0,0 +1,219 @@
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 { useEffect, useRef, useState } from "react";
import DataBox from "./DataBox";
import AlarmText from "./AlarmText";
const Routing = ({ setArea, setLocations, locations, area }) => {
const mapPrevSourceMarker = useRef();
const mapPrevDestMarker = useRef();
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 [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 isMovingSource = !isSource || editingTarget === "source";
const isMovingDest = isSource && (!isDest || editingTarget === "dest");
const map = useMapEvents({
move(e) {
if (isMovingSource) mapPrevSourceMarker.current.setLatLng(e.target.getCenter());
if (isMovingDest && mapPrevDestMarker.current) mapPrevDestMarker.current.setLatLng(e.target.getCenter());
},
movestart() {
if (isMovingSource) mapPrevSourceMarker.current.setIcon(createCustomIcon([45, 45], PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon([45, 45], PrevDestIcon.src));
},
moveend() {
if (isMovingSource)
mapPrevSourceMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevSourceIcon.src));
if (isMovingDest && mapPrevDestMarker.current)
mapPrevDestMarker.current.setIcon(createCustomIcon(defaultIconSize, PrevDestIcon.src));
},
});
useEffect(() => {
mapRef.current = map;
}, [map]);
useEffect(() => {
if (locations.length < 2) return;
const restoredSource = { lat: locations[0].lat, lng: locations[0].lon };
const restoredDest = { lat: locations[1].lat, lng: locations[1].lon };
setSourcePosition(restoredSource);
setDestPosition(restoredDest);
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(restoredSource, restoredDest);
}
}, []);
const fetchRoute = async (source, dest) => {
const locs = [
{ lat: source.lat, lon: source.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 handleMarkerClick = async () => {
if (!isSource || editingTarget === "source") {
const clickedPosition = mapPrevSourceMarker.current.getLatLng();
setSourcePosition(clickedPosition);
setIsSource(true);
setEditingTarget(null);
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
if (editingTarget === "source" && destPosition) {
await fetchRoute(clickedPosition, destPosition);
}
}
};
const handleDestMarkerClick = async () => {
if (!isDest || editingTarget === "dest") {
const clickedPosition = mapPrevDestMarker.current.getLatLng();
setDestPosition(clickedPosition);
setIsDest(true);
setEditingTarget(null);
await fetchRoute(sourcePosition, clickedPosition);
}
};
const handleEditSource = () => {
setEditingTarget("source");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
const handleEditDest = () => {
setEditingTarget("dest");
setRouteCoords([]);
setAlternateCoords([]);
setSelectedRoute(null);
};
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}
/>
<AlarmText isSource={isSource} />
<Marker
position={sourcePosition || map.getCenter()}
ref={mapPrevSourceMarker}
eventHandlers={{ click: handleMarkerClick }}
icon={defaultIcon}
/>
{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;

View File

@@ -2,41 +2,29 @@ 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 MapControlPolygon from "./MapControlPolygon";
import MapControlPolyline from "./MapControlPolyline";
import SelectBoundType from "./SelectBoundType";
import OldBoundLayer from "./OldBoundLayer";
import Routing from "./Routing";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
});
const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => {
const [bound, setBound] = useState(allData.bound);
const [boundType, setBoundType] = useState(allData.bound_type);
const Area = ({ allData, setAllData, handlePrev, setTabState }) => {
const [area, setArea] = useState(allData.area);
const [locations, setLocations] = useState(allData.locations);
const handleNext = useCallback(() => {
setAllData({ bound: bound });
setAllData({ area: area });
setAllData({ locations: locations });
setTabState((s) => s + 1);
}, [bound]);
}, [area]);
return (
<>
<DialogContent dividers>
<Stack spacing={2}>
<SelectBoundType
boundType={boundType}
setBoundType={setBoundType}
setBound={setBound}
setAllData={setAllData}
/>
<Box sx={{ width: "100%", height: "400px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
{boundType == "polygon" ? (
<MapControlPolygon bound={bound} setBound={setBound} boundType={boundType} />
) : (
<MapControlPolyline bound={bound} setBound={setBound} boundType={boundType} />
)}
<OldBoundLayer bound={oldBound} />
<Routing area={area} setArea={setArea} locations={locations} setLocations={setLocations} />
</MapLayer>
</Box>
</Stack>
@@ -45,8 +33,8 @@ const Area = ({ allData, setAllData, handlePrev, setTabState, oldBound }) => {
<Button onClick={handlePrev} variant="outlined" color="secondary" size="large">
{"مرحله قبلی"}
</Button>
<Button variant="contained" size="large" disabled={!bound} onClick={handleNext}>
{!bound ? "در انتظار ترسیم" : "مرحله بعد"}
<Button variant="contained" size="large" disabled={!area} onClick={handleNext}>
{!area ? "در انتظار ترسیم" : "مرحله بعد"}
</Button>
</DialogActions>
</>

View File

@@ -8,6 +8,7 @@ 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 LtrTextField from "@/core/components/LtrTextField";
const validationSchema = object({
explanation: string().required("موضوع را مشخص کنید!"),
@@ -125,6 +126,29 @@ const GetItemInfo = ({ allData, setAllData, handlePrev, setTabState }) => {
}}
/>
</Grid>
<Grid item xs={12} sm={12}>
<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>
</Stack>
</Box>

View File

@@ -5,7 +5,7 @@ import { Box, Button, Chip, DialogActions, DialogContent, Divider, Stack, Typogr
import moment from "jalali-moment";
import dynamic from "next/dynamic";
import { useCallback } from "react";
import ShowBound from "../../../../../../Actions/showBound";
import ShowRoute from "../Area/Routing/ShowRoute";
const MapLayer = dynamic(() => import("@/core/components/MapLayer"), {
loading: () => <MapLoading />,
ssr: false,
@@ -23,7 +23,7 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<Stack spacing={2}>
<Box sx={{ width: "100%", height: "200px" }}>
<MapLayer style={{ borderRadius: "4px" }}>
<ShowBound bound={allData.bound} />
<ShowRoute area={allData.area} />
</MapLayer>
</Box>
<Stack spacing={1}>
@@ -60,6 +60,11 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
<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={allData.end_km} />
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
<Typography variant="body2">تاریخ شروع ماموریت</Typography>
<Divider sx={{ flex: 1 }} />
@@ -93,11 +98,11 @@ const Verify = ({ allData, handlePrev, submitForm, submitting }) => {
label={`${allData.machine.machine_code} | ${allData.machine.car_name}`}
/>
</Stack>
<Stack direction={"row"} alignItems={"center"} spacing={1}>
{/* <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>
<Stack spacing={1}>
<Divider>

View File

@@ -84,7 +84,6 @@ const CreateForm = ({ defaultValues, submitForm, setOpen, submitting, oldBound }
</TabPanel>
<TabPanel value={tabState} index={2}>
<Area
oldBound={oldBound}
allData={allData}
setAllData={(data) => {
dispatch({ type: "changeData", data });

View File

@@ -10,24 +10,15 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
const submitForm = async (result) => {
setSubmitting(true);
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
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", {
data: {
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
machine_id: result.machine.id,
driver: result.driver.id,
encoded_route: result.area,
driver: row.original.driver_id,
zone: result.region,
type: result.type,
end_km: result.end_km,
...(result.type == 1
? {
end_date: `${result.end_date} ${moment(result.end_time).format("HH:mm")}`,
@@ -36,10 +27,6 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
end_date: result.end_date,
}),
end_point: result.end_point,
area: {
type: result.bound_type,
coordinates: area,
},
category_id: result.category_id,
explanation: result.explanation,
...(result.category_id == 3
@@ -65,16 +52,18 @@ const DialogAdd = ({ mutate, setOpen, oldBound, rahdaran, machine, row }) => {
defaultValues={{
explanation: "",
category_id: "",
bound: null,
rahdaran: rahdaran.filter((r) => !r.is_driver),
bound_type: "polyline",
type: "",
end_km: "",
end_date: "",
end_time: null,
end_point: "",
region: "",
machine: machine,
driver: rahdaran.find((r) => r.is_driver),
driver: row.original.driver_id,
area: null,
locations: [],
}}
oldBound={oldBound}
submitForm={submitForm}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -3,61 +3,75 @@ import useRequest from "@/lib/hooks/useRequest";
import DialogLoading from "@/core/components/DialogLoading";
import CreateForm from "../../../Actions/Create/Form";
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";
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 [rahdaran, setRahdaran] = useState(null);
const [rahdaranLoading, setRahdaranLoading] = useState(true);
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(() => {
setRahdaranLoading(true);
requestServer(`${GET_RAHDARAN_BY_ID}/${row.original.id}`, "get")
.then((response) => {
setRahdaran(response.data.data);
setRahdaranLoading(false);
})
.catch((e) => {
.catch(() => {})
.finally(() => {
setRahdaranLoading(false);
});
}, [row.original.id]);
const submitForm = async (result) => {
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", {
data: {
explanation: result.explanation,
category_id: result.category_id,
...(result.category_id == 3
? {
road_observed_id: result.road_observed_id,
}
: {}),
area: {
type: result.bound_type,
coordinates: area[0],
},
...(result.rahdaran.length != 0 ? { rahdaran: result.rahdaran.map((r) => r.id) } : {}),
encoded_route: result.area,
...(result.rahdaran.length !== 0
? {
rahdaran: result.rahdaran.map((r) => r.id),
}
: {}),
requested_machines: result.requested_machines,
type: result.type,
zone: result.region,
end_point: result.end_point,
...(result.type == 1
? {
start_date: `${result.start_date} ${moment(result.start_time).format("HH:mm")}`,
@@ -71,46 +85,51 @@ const EditController = ({ row, mutate, openEditDialog, setOpenEditDialog }) => {
hasSidebarUpdate: true,
notificationSuccess: true,
})
.then((response) => {
.then(() => {
mutate();
setOpenEditDialog(false);
})
.catch((error) => {})
.catch(() => {})
.finally(() => {
setSubmitting(false);
});
};
return (
<>
{rahdaranLoading ? (
<DialogLoading />
) : (
<CreateForm
defaultValues={{
explanation: row.original.explanation,
category_id: row.original.category_id,
road_observed_id: row.original.category_id == 3 ? row.original.road_observed[0].id : "",
rahdaran: rahdaran.filter((r) => !r.isDriver),
requested_machines: row.original.requested_machines,
bound: bound,
bound_type: row.original.area.type,
type: row.original.type,
start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
start_time: moment(row.original.start_date).toDate(),
end_date: moment(row.original.end_date).format("YYYY-MM-DD"),
end_time: moment(row.original.end_date).toDate(),
end_point: row.original.end_point,
region: row.original.zone,
}}
submitForm={submitForm}
submitting={submitting}
open={openEditDialog}
setOpen={setOpenEditDialog}
mutate={mutate}
/>
)}
</>
return rahdaranLoading ? (
<DialogLoading />
) : (
<CreateForm
defaultValues={{
explanation: row.original.explanation,
category_id: row.original.category_id,
road_observed_id: row.original.category_id == 3 ? (row.original.road_observed?.[0]?.id ?? "") : "",
rahdaran: rahdaran.filter((r) => !r.isDriver),
requested_machines: row.original.requested_machines,
area: row.original.encoded_route,
locations,
type: row.original.type,
start_date: moment(row.original.start_date).format("YYYY-MM-DD"),
start_time: moment(row.original.start_date).toDate(),
end_date: moment(row.original.end_date).format("YYYY-MM-DD"),
end_time: moment(row.original.end_date).toDate(),
end_point: row.original.end_point,
region: row.original.zone,
}}
submitForm={submitForm}
submitting={submitting}
open={openEditDialog}
setOpen={setOpenEditDialog}
mutate={mutate}
/>
);
};
export default EditController;

View File

@@ -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;

View File

@@ -3,9 +3,13 @@ import Edit from "./Edit";
import Delete from "./Delete";
import { useAuth } from "@/lib/contexts/auth";
import Add from "./Add";
import ChangeStatus from "@/components/dashboard/roadMissions/operator/RowActions/ChangeStatus";
import { usePermissions } from "@/lib/hooks/usePermissions";
const RowActions = ({ row, mutate }) => {
const { user } = useAuth();
const { data: userPermissions } = usePermissions();
const hasChangeStatusPermission = userPermissions.some((item) => ["mission-change-state"].includes(item));
return (
<Box sx={{ display: "flex", gap: 1 }}>
@@ -20,6 +24,7 @@ const RowActions = ({ row, mutate }) => {
<Add mutate={mutate} row={row} />
</>
)}
{hasChangeStatusPermission && <ChangeStatus mutate={mutate} row={row} />}
</Box>
);
};

View File

@@ -1,12 +1,14 @@
import { Stack } from "@mui/material";
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 }) => {
return (
<Stack direction={"row"} spacing={1}>
<Create mutate={mutate} />
<CreateWithoutProcess mutate={mutate} />
<OperatorViolations mutate={mutate} />
{/*<CreateWithoutProcess mutate={mutate} />*/}
</Stack>
);
};

View File

@@ -0,0 +1,72 @@
"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";
const DriverReportsList = ({ data, specialFilter }) => {
const columns = useMemo(() => {
return [
{
accessorKey: "driver_id",
header: "کد یکتا",
id: "driver_id",
enableColumnFilter: false,
enableSorting: false,
datatype: "numeric",
grow: false,
size: 50,
},
{
accessorKey: "driver_name",
header: "نام راننده",
id: "driver_name",
enableColumnFilter: false,
enableSorting: false,
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: false,
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}
columns={columns}
data={data.data}
page_name={"driverReport"}
table_name={"driverReportList"}
enablePagination={false}
specialFilter={specialFilter}
enableRowActions
RowActions={RowActionsWithRowData}
/>
</Box>
);
};
export default DriverReportsList;

View File

@@ -0,0 +1,77 @@
"use client";
import {
EXPORT_GET_DRIVER_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_DRIVER_PROVINCE_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 CountryOrProvince = activeFilters.find((item) => item.id === "province_id");
const requestUrl =
CountryOrProvince.value === "-1"
? EXPORT_GET_DRIVER_COUNTRY_ACTIVITY_REPORT
: EXPORT_GET_DRIVER_PROVINCE_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;

View File

@@ -0,0 +1,107 @@
import DataTableWithAuth from "@/core/components/DataTableWithAuth";
import { GET_DRIVER_DETAILS } from "@/core/utils/routes";
import { Box } from "@mui/material";
import { useMemo } from "react";
const DriverList = ({ row, specialFilter }) => {
const columns = useMemo(
() => [
{
accessorKey: "id",
header: "کد یکتا",
id: "id",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 80,
},
{
accessorKey: "province_name",
header: "استان",
id: "province_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
accessorKey: "city_name",
header: "شهر",
id: "city_name",
enableColumnFilter: true,
datatype: "text",
filterMode: "equals",
columnFilterModeOptions: ["equals", "contains"],
grow: false,
size: 100,
},
{
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
need_filter={true}
columns={columns}
specialFilter={specialFilter}
sorting={[{ id: "id", desc: true }]}
table_url={`${GET_DRIVER_DETAILS}/${row.driver_id}`}
page_name={"driverListReport"}
table_name={"driverList"}
positionActionsColumn={"first"}
/>
</Box>
</>
);
};
export default DriverList;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
import DriverDetails from "./DriverDetails";
const RowActions = ({ row, specialFilter }) => {
return <DriverDetails specialFilter={specialFilter} row={row} />;
};
export default RowActions;

View File

@@ -0,0 +1,27 @@
import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment";
import { Controller } from "react-hook-form";
const FromDateController = ({ control }) => {
return (
<Controller
name="from_date"
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker
name="from_date"
error={!!error}
value={value}
label={"تاریخ شروع (از تاریخ)"}
placeholder={"از تاریخ"}
setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form
}}
helperText={error ? error.message : null}
/>
)}
/>
);
};
export default FromDateController;

View File

@@ -0,0 +1,44 @@
import SearchIcon from "@mui/icons-material/Search";
import { Button, Grid } from "@mui/material";
import { Controller } from "react-hook-form";
import FromDateController from "./FromDateController";
import SelectProvince from "./SelectProvince";
import ToDateController from "./ToDateController";
const SearchReportField = ({ control, hasProvincesPermission }) => {
return (
<Grid container spacing={2} sx={{ py: 2 }}>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<FromDateController control={control} />
</Grid>
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<ToDateController control={control} />
</Grid>
{hasProvincesPermission && (
<Grid item xs={12} md={3}>
<SelectProvince control={control} />
</Grid>
)}
<Grid item xs={12} md={hasProvincesPermission ? 3 : 4}>
<Controller
name="submitButton"
control={control}
render={({ field: { value, onChange }, formState: { isSubmitting } }) => (
<Button
variant="contained"
size="medium"
type="submit"
disabled={isSubmitting}
endIcon={<SearchIcon />}
fullWidth
>
{isSubmitting ? "درحال دریافت اطلاعات" : "جستجو"}
</Button>
)}
/>
</Grid>
</Grid>
);
};
export default SearchReportField;

View File

@@ -0,0 +1,45 @@
import useProvinces from "@/lib/hooks/useProvince";
import { FormControl, FormHelperText, InputLabel, MenuItem, Select } from "@mui/material";
import { Controller } from "react-hook-form";
const SelectProvince = ({ control }) => {
const { provinces, loadingProvinces, errorProvinces } = useProvinces();
return (
<Controller
name="province_id"
control={control}
render={({ field, fieldState: { error } }) => (
<FormControl error={!!error} size="small" fullWidth>
<InputLabel id="demo-error-label">استان</InputLabel>
<Select
{...field}
labelId="demo-error-label"
id="province_id"
value={loadingProvinces ? "loading" : field.value || ""}
label="استان"
onChange={field.onChange}
>
{loadingProvinces ? (
<MenuItem value={"loading"}>در حال بارگذاری...</MenuItem>
) : errorProvinces ? (
<MenuItem value={"error"}>خطا در بارگذاری</MenuItem>
) : (
[
<MenuItem key="-1" value={"-1"}>
کل کشور
</MenuItem>,
...provinces.map((province) => (
<MenuItem key={province.id} value={province.id}>
{province.name_fa}
</MenuItem>
)),
]
)}
</Select>
{error && <FormHelperText>{error.message}</FormHelperText>}
</FormControl>
)}
/>
);
};
export default SelectProvince;

View File

@@ -0,0 +1,30 @@
import MuiDatePicker from "@/core/components/MuiDatePicker";
import moment from "jalali-moment";
import { Controller, useWatch } from "react-hook-form";
const ToDateController = ({ control }) => {
const minDate = useWatch({ control, name: "from_date" });
return (
<Controller
name="date_to"
control={control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<MuiDatePicker
name="date_to"
minDate={minDate}
maxDate={new Date()}
error={!!error}
value={value}
label={"تاریخ پایان (تا تاریخ)"}
placeholder={"تا تاریخ"}
setFieldValue={(name, newValue) => {
const formattedDate = moment(new Date(newValue)).locale("en").format("YYYY-MM-DD");
onChange(formattedDate); // Update the field value in react-hook-form
}}
helperText={error ? error.message : null}
/>
)}
/>
);
};
export default ToDateController;

View File

@@ -0,0 +1,13 @@
import StyledForm from "@/core/components/StyledForm";
import SearchReportField from "./SearchReportField";
const SearchReportList = ({ handleSubmit, onSearchSubmit, control, hasProvincesPermission }) => {
return (
<>
<StyledForm onSubmit={handleSubmit(onSearchSubmit)}>
<SearchReportField control={control} hasProvincesPermission={hasProvincesPermission} />
</StyledForm>
</>
);
};
export default SearchReportList;

View File

@@ -0,0 +1,11 @@
import { Box } from "@mui/material";
import PrintExcel from "./ExcelPrint";
const Toolbar = ({ table, filterData, mutate }) => {
return (
<Box sx={{ display: "flex", gap: 1 }}>
<PrintExcel table={table} filterData={filterData} />
</Box>
);
};
export default Toolbar;

View File

@@ -0,0 +1,107 @@
"use client";
import PageTitle from "@/core/components/PageTitle";
import { GET_DRIVER_COUNTRY_ACTIVITY_REPORT, GET_DRIVER_PROVINCE_ACTIVITY_REPORT } from "@/core/utils/routes";
import { useAuth } from "@/lib/contexts/auth";
import { usePermissions } from "@/lib/hooks/usePermissions";
import useRequest from "@/lib/hooks/useRequest";
import { yupResolver } from "@hookform/resolvers/yup";
import { LinearProgress, Stack } from "@mui/material";
import moment from "jalali-moment";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import * as yup from "yup";
import DriverReportsList from "./DriverReportsList";
import SearchReportList from "./Search";
const DriverReportsPage = () => {
const [data, setData] = useState(null);
const { data: userPermissions } = usePermissions();
const { user } = useAuth();
const requestServer = useRequest();
const hasProvincesPermission = userPermissions?.includes("mission-report-country");
const defaultValues = {
from_date: moment(new Date()).format("YYYY-MM-DD"),
date_to: moment(new Date()).format("YYYY-MM-DD"),
province_id: user.province_id || "-1",
};
const defaultFilter = [
{ value: `${defaultValues.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${defaultValues.from_date} 00:00:00`, datatype: "date", id: "start_time", fn: "greaterThan" },
{ value: `${defaultValues.date_to} 23:59:59`, datatype: "date", id: "finish_time", fn: "lessThan" },
];
const [specialFilter, setSpecialFilter] = useState(defaultFilter);
const onSearchSubmit = async (data) => {
const params = new URLSearchParams();
params.set("from_date", `${data.from_date} 00:00:00`);
params.set("date_to", `${data.date_to} 23:59:59`);
setSpecialFilter([
{ value: `${data.province_id}`, datatype: "numeric", id: "province_id", fn: "equals" },
{ value: `${data.from_date} 00:00:00`, datatype: "date", id: "start_time", fn: "greaterThan" },
{ value: `${data.date_to} 23:59:59`, datatype: "date", id: "finish_time", fn: "lessThan" },
]);
if (data.province_id === "-1") {
try {
const response = await requestServer(`${GET_DRIVER_COUNTRY_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
} else {
try {
params.set("province_id", `${data.province_id}`);
const response = await requestServer(`${GET_DRIVER_PROVINCE_ACTIVITY_REPORT}?${params}`);
const result = response.data.data;
setData({
data: result.activities,
filters: data,
});
} catch (e) {}
}
};
useEffect(() => {
if (!userPermissions) return;
onSearchSubmit(defaultValues);
}, [userPermissions]);
const validationSchema = yup.object().shape({
from_date: yup
.string()
.required("تاریخ شروع الزامی است!")
.matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ شروع الزامی است!")
.test("is-valid-date", "تاریخ شروع معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()),
date_to: yup
.string()
.required("تاریخ پایان الزامی است!")
.matches(/^\d{4}-\d{2}-\d{2}$/, "تاریخ پایان الزامی است!")
.test("is-valid-date", "تاریخ پایان معتبر نیست!", (value) => moment(value, "YYYY-MM-DD", true).isValid()),
province_id: yup.string().nullable(),
});
const { control, handleSubmit } = useForm({
defaultValues,
resolver: yupResolver(validationSchema),
mode: "all",
});
return (
<Stack spacing={1}>
<PageTitle title={"کارتابل گزارش راننده"} />
<SearchReportList
control={control}
hasProvincesPermission={hasProvincesPermission}
handleSubmit={handleSubmit}
onSearchSubmit={onSearchSubmit}
/>
{!data ? <LinearProgress /> : <DriverReportsList specialFilter={specialFilter} data={data} />}
</Stack>
);
};
export default DriverReportsPage;

View File

@@ -0,0 +1,77 @@
"use client";
import {
EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT,
EXPORT_GET_MACHINES_PROVINCE_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 CountryOrProvince = activeFilters.find((item) => item.id === "province_id");
const requestUrl =
CountryOrProvince.value === "-1"
? EXPORT_GET_MACHINES_COUNTRY_ACTIVITY_REPORT
: EXPORT_GET_MACHINES_PROVINCE_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;

View File

@@ -0,0 +1,82 @@
"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";
const MachinesReportsList = ({ data, specialFilter }) => {
const columns = useMemo(() => {
return [
{
accessorKey: "machine_id",
header: "کد یکتا",
id: "machine_id",
enableColumnFilter: false,
enableSorting: false,
datatype: "numeric",
grow: false,
size: 50,
},
{
accessorKey: "car_name",
header: "مدل ماشین",
id: "car_name",
enableColumnFilter: false,
enableSorting: false,
datatype: "text",
grow: false,
size: 50,
},
{
accessorKey: "machine_code",
header: "کد ماشین",
id: "machine_code",
enableColumnFilter: false,
enableSorting: false,
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: false,
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}
columns={columns}
data={data.data}
page_name={"roadMissionReport"}
table_name={"roadMissionReportList"}
enablePagination={false}
specialFilter={specialFilter}
enableRowActions
RowActions={RowActionsWithRowData}
/>
</Box>
);
};
export default MachinesReportsList;

Some files were not shown because too many files have changed in this diff Show More