Feature/amirris add area tollhouse (#2)

* add area

* formatting

* change text

* add province permission
This commit is contained in:
Amirhossein Mahmoodi
2025-09-07 10:11:43 +03:30
committed by GitHub
parent 5ac43b6c3b
commit bc74c65622
7 changed files with 222 additions and 84 deletions

View File

@@ -7,13 +7,15 @@ import OtherHousesIcon from "@mui/icons-material/OtherHouses";
import LocalShippingIcon from "@mui/icons-material/LocalShipping"; import LocalShippingIcon from "@mui/icons-material/LocalShipping";
import EditLocationAltIcon from "@mui/icons-material/EditLocationAlt"; import EditLocationAltIcon from "@mui/icons-material/EditLocationAlt";
import React, { useState } from "react"; import React, { useState } from "react";
import { useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { mixed, number, object, string } from "yup"; import { array, mixed, number, object, string } from "yup";
import TollHouseInfo from "./TollHouseInfo"; import TollHouseInfo from "./TollHouseInfo";
import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft"; import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft";
import MachineInfo from "@/components/infrastructure/tollHouse/Form/CreateTollHouse/MachineInfo"; import MachineInfo from "@/components/infrastructure/tollHouse/Form/CreateTollHouse/MachineInfo";
import TollHouseLocation from "@/components/infrastructure/tollHouse/Form/CreateTollHouse/TollHouseLocation"; import TollHouseLocation from "@/components/infrastructure/tollHouse/Form/CreateTollHouse/TollHouseLocation";
import { Route } from "@mui/icons-material";
import TollHouseArea from "./TollHouseArea";
function TabPanel(props) { function TabPanel(props) {
const { children, value, index } = props; const { children, value, index } = props;
@@ -54,6 +56,23 @@ const validationSchema = object({
return !!value; return !!value;
}) })
.required("لطفاً محل پروژه را مشخص کنید!"), .required("لطفاً محل پروژه را مشخص کنید!"),
area: array()
.of(
object().shape({
lat: number()
.typeError("عرض جغرافیایی باید یک عدد معتبر باشد")
.required("عرض جغرافیایی الزامی است")
.min(25, "عرض جغرافیایی نمی‌تواند کمتر از 25 باشد")
.max(40, "عرض جغرافیایی نمی‌تواند بیشتر از 40 باشد"),
lon: number("وارد کردن طول جغرافیایی الزامیست")
.typeError("طول جغرافیایی باید یک عدد معتبر باشد")
.required("طول جغرافیایی الزامی است")
.min(44, "طول جغرافیایی نمی‌تواند کمتر از 44 باشد")
.max(64, "طول جغرافیایی نمی‌تواند بیشتر از 64 باشد"),
})
)
.min(4, "حداقل باید ۴ نقطه وجود داشته باشد")
.required("محدوده الزامی است"),
}); });
const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues }) => { const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues }) => {
const [tabState, setTabState] = useState(0); const [tabState, setTabState] = useState(0);
@@ -137,8 +156,9 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
}} }}
> >
<Tab icon={<OtherHousesIcon />} label="اطلاعات راهدارخانه" /> <Tab icon={<OtherHousesIcon />} label="اطلاعات راهدارخانه" />
<Tab disabled={tabState === 0} icon={<LocalShippingIcon />} label="ماشین آلات" /> <Tab disabled={tabState < 1} icon={<LocalShippingIcon />} label="ماشین آلات" />
<Tab disabled={tabState === 0 || tabState === 1} icon={<EditLocationAltIcon />} label="موقعیت" /> <Tab disabled={tabState < 2} icon={<EditLocationAltIcon />} label="موقعیت" />
<Tab disabled={tabState < 3} icon={<Route />} label="محدوده" />
</Tabs> </Tabs>
<DialogContent dividers sx={{ overflowY: "auto", maxHeight: "70vh" }}> <DialogContent dividers sx={{ overflowY: "auto", maxHeight: "70vh" }}>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1 }}>
@@ -151,6 +171,15 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
<TabPanel value={tabState} index={2}> <TabPanel value={tabState} index={2}>
<TollHouseLocation control={control} setValue={setValue} errors={errors} /> <TollHouseLocation control={control} setValue={setValue} errors={errors} />
</TabPanel> </TabPanel>
<TabPanel value={tabState} index={3}>
<Controller
control={control}
name="area"
render={({ field, fieldState: { error } }) => (
<TollHouseArea value={field.value} onChange={field.onChange} error={error} />
)}
/>
</TabPanel>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}> <DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
@@ -163,7 +192,7 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
> >
{tabState === 0 ? "بستن" : "مرحله قبل"} {tabState === 0 ? "بستن" : "مرحله قبل"}
</Button> </Button>
{tabState !== 2 && ( {tabState !== 3 && (
<Button <Button
onClick={handleNext} onClick={handleNext}
key={"handleNext"} key={"handleNext"}
@@ -174,7 +203,7 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
مرحله بعد مرحله بعد
</Button> </Button>
)} )}
{tabState === 2 && ( {tabState === 3 && (
<Button <Button
variant="contained" variant="contained"
size="large" size="large"

View File

@@ -16,9 +16,9 @@ const MachineInfo = ({ control }) => {
<TextField <TextField
fullWidth fullWidth
size="small" size="small"
label="ماشین آلات سبک" label="تعداد ماشین آلات سبک"
value={value} value={value}
placeholder="ماشین آلات سبک را وارد کنید" placeholder="تعداد ماشین آلات سبک را وارد کنید"
error={!!error} error={!!error}
helperText={error ? error.message : null} helperText={error ? error.message : null}
variant="outlined" variant="outlined"
@@ -48,9 +48,9 @@ const MachineInfo = ({ control }) => {
<TextField <TextField
fullWidth fullWidth
size="small" size="small"
label="ماشین آلات نیمه سنگین" label="تعداد ماشین آلات نیمه سنگین"
value={value} value={value}
placeholder="ماشین آلات نیمه سنگین را وارد کنید" placeholder="تعداد ماشین آلات نیمه سنگین را وارد کنید"
error={!!error} error={!!error}
helperText={error ? error.message : null} helperText={error ? error.message : null}
variant="outlined" variant="outlined"
@@ -80,9 +80,9 @@ const MachineInfo = ({ control }) => {
<TextField <TextField
fullWidth fullWidth
size="small" size="small"
label="ماشین آلات سنگین" label="تعداد ماشین آلات سنگین"
value={value} value={value}
placeholder="ماشین آلات سنگین را وارد کنید" placeholder="تعداد ماشین آلات سنگین را وارد کنید"
error={!!error} error={!!error}
helperText={error ? error.message : null} helperText={error ? error.message : null}
variant="outlined" variant="outlined"

View File

@@ -0,0 +1,80 @@
import LtrTextField from "@/core/components/LtrTextField";
import { Add, Close } from "@mui/icons-material";
import { Button, Collapse, Divider, IconButton, Stack, Typography } from "@mui/material";
import { TransitionGroup } from "react-transition-group";
const TollHouseArea = ({ value, onChange, error = [] }) => {
const handleChange = (index, field, newValue) => {
const newPoints = [...value];
newPoints[index] = { ...newPoints[index], [field]: newValue };
onChange(newPoints);
};
const handleAdd = () => {
onChange([...value, { lat: "", lon: "" }]);
};
const handleRemove = (index) => {
if (value.length > 4) {
const newPoints = value.filter((_, i) => i !== index);
onChange(newPoints);
}
};
return (
<TransitionGroup component={Stack} sx={{ my: 2 }}>
{value.map((point, index) => (
<Collapse key={index}>
<Stack direction={"row"} sx={{ my: 2 }} alignItems={"center"} spacing={1}>
<Typography>#{index + 1}</Typography>
{index >= 4 && (
<IconButton color="error" onClick={() => handleRemove(index)}>
<Close />
</IconButton>
)}
<Divider sx={{ flex: 1 }} />
<Stack direction={{ xs: "column", sm: "row" }} spacing={2}>
<LtrTextField
autoComplete="off"
size="small"
label="عرض جغرافیایی (Latitude)"
sx={{ width: 200 }}
InputLabelProps={{ shrink: true }}
value={point.lat}
onChange={(e) => handleChange(index, "lat", e.target.value)}
error={Boolean(error[index]?.lat)}
helperText={error[index]?.lat?.message}
/>
<LtrTextField
autoComplete="off"
sx={{ width: 200 }}
size="small"
label="طول جغرافیایی (Longitude)"
InputLabelProps={{ shrink: true }}
value={point.lon}
onChange={(e) => handleChange(index, "lon", e.target.value)}
error={Boolean(error[index]?.lon)}
helperText={error[index]?.lon?.message}
/>
</Stack>
</Stack>
</Collapse>
))}
<Collapse>
<Stack direction={"row"} alignItems={"center"} sx={{ mt: 2 }}>
<Divider sx={{ flex: 1 }}>
<Button
variant="outlined"
startIcon={<Add />}
onClick={handleAdd}
sx={{ alignSelf: "flex-start" }}
>
افزودن نقطه
</Button>
</Divider>
</Stack>
</Collapse>
</TransitionGroup>
);
};
export default TollHouseArea;

View File

@@ -106,9 +106,9 @@ const TollHouseInfo = ({ control }) => {
value={field.value} value={field.value}
label="نوع" label="نوع"
selectors={[ selectors={[
{ id: 0, name: "داعمی" }, { id: 1, name: "دائمی" },
{ id: 1, name: "موقت" }, { id: 2, name: "موقت" },
{ id: 2, name: "فصلی" }, { id: 3, name: "فصلی" },
]} ]}
schema={{ name: "name", value: "id" }} schema={{ name: "name", value: "id" }}
error={error} error={error}

View File

@@ -51,6 +51,12 @@ const CreateTollHouse = ({ mutate }) => {
}); });
formData.append("lat", result.start_point.lat); formData.append("lat", result.start_point.lat);
formData.append("lng", result.start_point.lng); formData.append("lng", result.start_point.lng);
formData.append("area[type]", "polygon");
result.area.map((point, pointIndex) => {
formData.append(`area[coordinates][${pointIndex}][0]`, point.lat);
formData.append(`area[coordinates][${pointIndex}][1]`, point.lon);
});
await requestServer(`${CREATE_TOLL_HOUSE}`, "post", { await requestServer(`${CREATE_TOLL_HOUSE}`, "post", {
data: formData, data: formData,
}) })
@@ -83,6 +89,12 @@ const CreateTollHouse = ({ mutate }) => {
overview_files_4: null, overview_files_4: null,
overview_files_5: null, overview_files_5: null,
start_point: "", start_point: "",
area: [
{ lat: "", lon: "" },
{ lat: "", lon: "" },
{ lat: "", lon: "" },
{ lat: "", lon: "" },
],
}; };
return ( return (
<> <>

View File

@@ -47,6 +47,12 @@ const EditController = ({ rowId, mutate, setOpenEditDialog }) => {
overview_files_3: tollHouseItemDetails?.files[2]?.path || null, overview_files_3: tollHouseItemDetails?.files[2]?.path || null,
overview_files_4: tollHouseItemDetails?.files[3]?.path || null, overview_files_4: tollHouseItemDetails?.files[3]?.path || null,
overview_files_5: tollHouseItemDetails?.files[4]?.path || null, overview_files_5: tollHouseItemDetails?.files[4]?.path || null,
area: tollHouseItemDetails?.area?.coordinates.map(([lat, lon]) => ({ lat, lon })) || [
{ lat: "", lon: "" },
{ lat: "", lon: "" },
{ lat: "", lon: "" },
{ lat: "", lon: "" },
],
}; };
const HandleSubmit = async (result) => { const HandleSubmit = async (result) => {
const fields = [ const fields = [
@@ -79,6 +85,11 @@ const EditController = ({ rowId, mutate, setOpenEditDialog }) => {
} }
formData.append("lat", result.start_point.lat); formData.append("lat", result.start_point.lat);
formData.append("lng", result.start_point.lng); formData.append("lng", result.start_point.lng);
formData.append("area[type]", "polygon");
result.area.map((point, pointIndex) => {
formData.append(`area[coordinates][${pointIndex}][0]`, point.lat);
formData.append(`area[coordinates][${pointIndex}][1]`, point.lon);
});
await requestServer(`${UPDATE_TOLL_HOUSE_ITEM}/${rowId}`, "post", { await requestServer(`${UPDATE_TOLL_HOUSE_ITEM}/${rowId}`, "post", {
data: formData, data: formData,

View File

@@ -12,6 +12,7 @@ import useEdaratLists from "@/lib/hooks/useEdaratLists";
import { usePermissions } from "@/lib/hooks/usePermissions"; import { usePermissions } from "@/lib/hooks/usePermissions";
import LocationForm from "./RowActions/LocationForm"; import LocationForm from "./RowActions/LocationForm";
import ImageDialog from "./RowActions/ImageForm"; import ImageDialog from "./RowActions/ImageForm";
import { useAuth } from "@/lib/contexts/auth";
const statusOptions = [ const statusOptions = [
{ value: "", label: "همه وضعیت ها" }, { value: "", label: "همه وضعیت ها" },
@@ -21,14 +22,51 @@ const statusOptions = [
const typeOptions = [ const typeOptions = [
{ value: "", label: "همه نوع ها" }, { value: "", label: "همه نوع ها" },
{ value: 0, label: "فصلی" }, { value: 0, label: "فصلی" },
{ value: 1, label: "داعمی" }, { value: 1, label: "دائمی" },
{ value: 2, label: "موقت" }, { value: 2, label: "موقت" },
]; ];
const TollHouseList = () => { const TollHouseList = () => {
const { data: userPermissions } = usePermissions(); const { data: userPermissions } = usePermissions();
const hasProvincePermission = userPermissions.some((item) => ["add-tollhouse"].includes(item)); const hasCountryPermission = userPermissions?.includes("show-tollhouse");
const { user } = useAuth();
const columns = useMemo(() => { const columns = useMemo(() => {
const provinceColumn = {
accessorKey: "province_id",
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: ({ row }) => <>{row.original.province_name}</>,
};
return [ return [
{ {
accessorKey: "id", accessorKey: "id",
@@ -64,42 +102,7 @@ const TollHouseList = () => {
grow: false, grow: false,
size: 100, size: 100,
}, },
{ ...(hasCountryPermission ? [provinceColumn] : []),
accessorKey: "province_id",
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: ({ row }) => <>{row.original.province_name}</>,
},
{ {
accessorKey: "city_id", accessorKey: "city_id",
header: "شهرستان", // Office header: "شهرستان", // Office
@@ -107,17 +110,19 @@ const TollHouseList = () => {
enableColumnFilter: true, enableColumnFilter: true,
datatype: "numeric", datatype: "numeric",
filterMode: "equals", filterMode: "equals",
dependencyId: "province_id", dependencyId: hasCountryPermission ? "province_id" : null,
grow: false, grow: false,
size: 120, size: 120,
ColumnSelectComponent: (props) => { ColumnSelectComponent: (props) => {
const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists( const { edaratList, loadingEdaratList, errorEdaratList } = useEdaratLists(
props.dependencyFieldValue.value hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
);
const [prevDependency, setPrevDependency] = useState(
hasCountryPermission ? props.dependencyFieldValue.value : user.province_id
); );
const [prevDependency, setPrevDependency] = useState(props.dependencyFieldValue.value);
const getColumnSelectOptions = useMemo(() => { const getColumnSelectOptions = useMemo(() => {
if (props.dependencyFieldValue.value === "") { if (hasCountryPermission && props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }]; return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
} }
if (loadingEdaratList) { if (loadingEdaratList) {
@@ -135,10 +140,11 @@ const TollHouseList = () => {
]; ];
}, [edaratList, loadingEdaratList, errorEdaratList]); }, [edaratList, loadingEdaratList, errorEdaratList]);
useEffect(() => { useEffect(() => {
if (hasCountryPermission) return;
if (prevDependency === props.dependencyFieldValue?.value) return; if (prevDependency === props.dependencyFieldValue?.value) return;
props.handleChange({ ...props.filterParameters, value: "" }); props.handleChange({ ...props.filterParameters, value: "" });
setPrevDependency(props.dependencyFieldValue?.value); setPrevDependency(props.dependencyFieldValue?.value);
}, [props.dependencyFieldValue?.value]); }, [props.dependencyFieldValue?.value, hasCountryPermission]);
return ( return (
<CustomSelectByDependency <CustomSelectByDependency
{...props} {...props}
@@ -194,6 +200,31 @@ const TollHouseList = () => {
return <Typography variant={"body2"}>{row.original.type_fa}</Typography>; return <Typography variant={"body2"}>{row.original.type_fa}</Typography>;
}, },
}, },
{
header: "موقعیت", // Location
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: ({ renderedCellValue, row }) => {
return (
<Stack alignItems={"center"} justifyContent={"center"}>
<LocationForm start_lat={row.original.lat} start_lng={row.original.lng} />
</Stack>
);
},
},
{ {
accessorKey: "phone", accessorKey: "phone",
header: "تلفن راهدارخانه", header: "تلفن راهدارخانه",
@@ -352,31 +383,6 @@ const TollHouseList = () => {
}, },
], ],
}, },
{
header: "موقعیت", // Location
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: ({ renderedCellValue, row }) => {
return (
<Stack alignItems={"center"} justifyContent={"center"}>
<LocationForm start_lat={row.original.lat} start_lng={row.original.lng} />
</Stack>
);
},
},
{ {
accessorFn: (row) => moment(row.created_at).locale("fa").format("HH:mm | yyyy/MM/DD"), accessorFn: (row) => moment(row.created_at).locale("fa").format("HH:mm | yyyy/MM/DD"),
header: "تاریخ ثبت", header: "تاریخ ثبت",
@@ -402,7 +408,7 @@ const TollHouseList = () => {
sorting={[{ id: "id", desc: true }]} sorting={[{ id: "id", desc: true }]}
TableToolbar={Toolbar} TableToolbar={Toolbar}
enableRowActions enableRowActions
positionActionsColumn={"last"} positionActionsColumn={"first"}
RowActions={RowActions} RowActions={RowActions}
/> />
</Box> </Box>