diff --git a/src/app/(withAuth)/(dashboardLayout)/dashboard/machines/loading.jsx b/src/app/(withAuth)/(dashboardLayout)/dashboard/machines/loading.jsx
new file mode 100644
index 0000000..4535a8d
--- /dev/null
+++ b/src/app/(withAuth)/(dashboardLayout)/dashboard/machines/loading.jsx
@@ -0,0 +1,7 @@
+"use client";
+import PageLoading from "@/core/components/PageLoading";
+
+const Loading = () => {
+ return ;
+};
+export default Loading;
diff --git a/src/app/(withAuth)/(dashboardLayout)/dashboard/machines/page.jsx b/src/app/(withAuth)/(dashboardLayout)/dashboard/machines/page.jsx
new file mode 100644
index 0000000..35c27ad
--- /dev/null
+++ b/src/app/(withAuth)/(dashboardLayout)/dashboard/machines/page.jsx
@@ -0,0 +1,17 @@
+import WithPermission from "@/core/middlewares/withPermission";
+import MachinesPage from "@/components/dashboard/machines";
+
+export const metadata = {
+ title: "کارتابل ماشینها",
+};
+const Page = () => {
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+export default Page;
diff --git a/src/components/dashboard/machines/MachinesList.jsx b/src/components/dashboard/machines/MachinesList.jsx
new file mode 100644
index 0000000..880f0cb
--- /dev/null
+++ b/src/components/dashboard/machines/MachinesList.jsx
@@ -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 {status?.label || ""};
+ },
+ },
+ ],
+ },
+ ];
+ }, []);
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+export default MachinesList;
diff --git a/src/components/dashboard/machines/RowActions/DeleteDialog/DeleteContent.jsx b/src/components/dashboard/machines/RowActions/DeleteDialog/DeleteContent.jsx
new file mode 100644
index 0000000..3f3095a
--- /dev/null
+++ b/src/components/dashboard/machines/RowActions/DeleteDialog/DeleteContent.jsx
@@ -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 (
+ <>
+
+
+ آیا از حذف راهدارخانه اطمینان دارید؟
+
+
+
+
+
+
+ >
+ );
+};
+export default DeleteContent;
diff --git a/src/components/dashboard/machines/RowActions/DeleteDialog/index.jsx b/src/components/dashboard/machines/RowActions/DeleteDialog/index.jsx
new file mode 100644
index 0000000..54278bd
--- /dev/null
+++ b/src/components/dashboard/machines/RowActions/DeleteDialog/index.jsx
@@ -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 (
+ <>
+
+ setOpenDeleteDialog(true)}>
+
+
+
+
+ >
+ );
+};
+export default DeleteDialog;
diff --git a/src/components/dashboard/machines/RowActions/Edit/Form/index.jsx b/src/components/dashboard/machines/RowActions/Edit/Form/index.jsx
new file mode 100644
index 0000000..6cfcd9d
--- /dev/null
+++ b/src/components/dashboard/machines/RowActions/Edit/Form/index.jsx
@@ -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 (
+ <>
+
+ ویرایش ماشین
+
+
+
+
+
+ (
+
+ استان
+
+
+
+ )}
+ />
+
+ (
+
+ شهر
+
+
+
+ )}
+ />
+
+ (
+
+ راهداری
+
+
+
+ )}
+ />
+ (
+
+ وضعیت
+
+
+
+ )}
+ />
+
+ {(loadingCities || loadingStations) && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ }
+ >
+ {isSubmitting ? "در حال ویرایش" : "ویرایش ماشین"}
+
+
+ >
+ );
+};
+
+export default EditFormContent;
diff --git a/src/components/dashboard/machines/RowActions/Edit/index.jsx b/src/components/dashboard/machines/RowActions/Edit/index.jsx
new file mode 100644
index 0000000..f3cec95
--- /dev/null
+++ b/src/components/dashboard/machines/RowActions/Edit/index.jsx
@@ -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 (
+ <>
+
+ {
+ setOpenEditDialog(true);
+ }}
+ >
+
+
+
+
+ >
+ );
+};
+export default Edit;
diff --git a/src/components/dashboard/machines/RowActions/index.jsx b/src/components/dashboard/machines/RowActions/index.jsx
new file mode 100644
index 0000000..526fe10
--- /dev/null
+++ b/src/components/dashboard/machines/RowActions/index.jsx
@@ -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 (
+
+ {hasEditPermission && }
+
+ );
+};
+export default RowActions;
diff --git a/src/components/dashboard/machines/ShowProjectArea/ShowBound.jsx b/src/components/dashboard/machines/ShowProjectArea/ShowBound.jsx
new file mode 100644
index 0000000..24db476
--- /dev/null
+++ b/src/components/dashboard/machines/ShowProjectArea/ShowBound.jsx
@@ -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 ;
+};
+export default ShowBound;
diff --git a/src/components/dashboard/machines/ShowProjectArea/index.jsx b/src/components/dashboard/machines/ShowProjectArea/index.jsx
new file mode 100644
index 0000000..250cd52
--- /dev/null
+++ b/src/components/dashboard/machines/ShowProjectArea/index.jsx
@@ -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 (
+
+
+ setOpenShowAreaDialog(true)}>
+
+
+
+
+
+ );
+};
+
+export default ShowProjectArea;
diff --git a/src/components/dashboard/machines/index.jsx b/src/components/dashboard/machines/index.jsx
new file mode 100644
index 0000000..ed3db7c
--- /dev/null
+++ b/src/components/dashboard/machines/index.jsx
@@ -0,0 +1,14 @@
+"use client";
+import PageTitle from "@/core/components/PageTitle";
+import { Stack } from "@mui/material";
+import MachinesList from "./MachinesList";
+
+const MachinesPage = () => {
+ return (
+
+
+
+
+ );
+};
+export default MachinesPage;
diff --git a/src/core/utils/pageMenu.js b/src/core/utils/pageMenu.js
index a770353..99366c4 100644
--- a/src/core/utils/pageMenu.js
+++ b/src/core/utils/pageMenu.js
@@ -1,3 +1,4 @@
+import { ReportProblem, RvHookup } from "@mui/icons-material";
import AccountTreeIcon from "@mui/icons-material/AccountTree";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
import AssessmentIcon from "@mui/icons-material/Assessment";
@@ -5,7 +6,9 @@ import AssignmentLateIcon from "@mui/icons-material/AssignmentLate";
import AssistantIcon from "@mui/icons-material/Assistant";
import BallotIcon from "@mui/icons-material/Ballot";
import BiotechIcon from "@mui/icons-material/Biotech";
+import ContactPageIcon from "@mui/icons-material/ContactPage";
import CottageIcon from "@mui/icons-material/Cottage";
+import DirectionsBusIcon from "@mui/icons-material/DirectionsBus";
import DoorbellIcon from "@mui/icons-material/Doorbell";
import DriveEtaIcon from "@mui/icons-material/DriveEta";
import ElectricBoltIcon from "@mui/icons-material/ElectricBolt";
@@ -23,17 +26,14 @@ import MapIcon from "@mui/icons-material/Map";
import MapsHomeWorkIcon from "@mui/icons-material/MapsHomeWork";
import Mediation from "@mui/icons-material/Mediation";
import PsychologyAltIcon from "@mui/icons-material/PsychologyAlt";
+import RemoveRoadIcon from "@mui/icons-material/RemoveRoad";
import ReportIcon from "@mui/icons-material/Report";
import RouteIcon from "@mui/icons-material/Route";
import ScienceIcon from "@mui/icons-material/Science";
import SecurityIcon from "@mui/icons-material/Security";
import SpaceDashboardIcon from "@mui/icons-material/SpaceDashboard";
import SpeedIcon from "@mui/icons-material/Speed";
-import ContactPageIcon from "@mui/icons-material/ContactPage";
-import DirectionsBusIcon from "@mui/icons-material/DirectionsBus";
import SupervisedUserCircleIcon from "@mui/icons-material/SupervisedUserCircle";
-import RemoveRoadIcon from "@mui/icons-material/RemoveRoad";
-import { ReportProblem, RvHookup } from "@mui/icons-material";
export const pageMenu = [
{
@@ -685,12 +685,12 @@ export const pageMenu = [
permissions: ["manage-damage"],
},
{
- id: "CarDetails",
- label: "اطلاعات خودرو",
+ id: "machines",
+ label: "ماشینها",
type: "page",
- route: "/dashboard/car-details",
+ route: "/dashboard/machines",
icon: ,
- permissions: [""],
+ permissions: ["update-machine"],
},
],
},
diff --git a/src/core/utils/pageMenuDev.js b/src/core/utils/pageMenuDev.js
index e0f742d..4bc0665 100644
--- a/src/core/utils/pageMenuDev.js
+++ b/src/core/utils/pageMenuDev.js
@@ -683,12 +683,12 @@ export const pageMenuDev = [
permissions: ["manage-damage"],
},
{
- id: "CarDetails",
- label: "اطلاعات خودرو",
+ id: "machines",
+ label: "ماشینها",
type: "page",
- route: "/dashboard/car-details",
+ route: "/dashboard/machines",
icon: ,
- permissions: ["show-rahdaran"],
+ permissions: ["update-machine"],
},
],
},
diff --git a/src/core/utils/routes.js b/src/core/utils/routes.js
index a2ea39f..c970699 100644
--- a/src/core/utils/routes.js
+++ b/src/core/utils/routes.js
@@ -277,3 +277,11 @@ export const CONFIRM_VERIFY_LICENSE_FORM = api + "/api/v3/harim/technical_deputy
// History
export const GET_HISTORY_LIST = api + "/api/v3/harim/detail/histories";
export const GET_REQUEST_INQUIRY_PRIVACY = api + "/api/v3/harim/technical_deputy";
+
+// Machines
+export const GET_MACHINES_LIST = api + "/api/v3/cmms_machines";
+export const UPDATE_CMMS_MACHINES = api + "/api/v3/cmms_machines";
+export const GET_PROVINCES_LIST = api + "/api/v3/provinces/list";
+export const GET_CITIES_LIST = api + "/api/v3/cities/list";
+export const GET_STATIONS_LIST = api + "/api/v3/road_maintenance_station";
+export const UPDATE_MACHINE_STATUS_AND_LOCATION = api + "/api/v3/cmms_machines";