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 EditLocationAltIcon from "@mui/icons-material/EditLocationAlt";
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 { mixed, number, object, string } from "yup";
import { array, mixed, number, object, string } from "yup";
import TollHouseInfo from "./TollHouseInfo";
import KeyboardDoubleArrowLeftIcon from "@mui/icons-material/KeyboardDoubleArrowLeft";
import MachineInfo from "@/components/infrastructure/tollHouse/Form/CreateTollHouse/MachineInfo";
import TollHouseLocation from "@/components/infrastructure/tollHouse/Form/CreateTollHouse/TollHouseLocation";
import { Route } from "@mui/icons-material";
import TollHouseArea from "./TollHouseArea";
function TabPanel(props) {
const { children, value, index } = props;
@@ -54,6 +56,23 @@ const validationSchema = object({
return !!value;
})
.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 [tabState, setTabState] = useState(0);
@@ -137,8 +156,9 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
}}
>
<Tab icon={<OtherHousesIcon />} label="اطلاعات راهدارخانه" />
<Tab disabled={tabState === 0} icon={<LocalShippingIcon />} label="ماشین آلات" />
<Tab disabled={tabState === 0 || tabState === 1} icon={<EditLocationAltIcon />} label="موقعیت" />
<Tab disabled={tabState < 1} icon={<LocalShippingIcon />} label="ماشین آلات" />
<Tab disabled={tabState < 2} icon={<EditLocationAltIcon />} label="موقعیت" />
<Tab disabled={tabState < 3} icon={<Route />} label="محدوده" />
</Tabs>
<DialogContent dividers sx={{ overflowY: "auto", maxHeight: "70vh" }}>
<Box sx={{ flex: 1 }}>
@@ -151,6 +171,15 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
<TabPanel value={tabState} index={2}>
<TollHouseLocation control={control} setValue={setValue} errors={errors} />
</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>
</DialogContent>
<DialogActions sx={{ alignItems: "center", justifyContent: "center" }}>
@@ -163,7 +192,7 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
>
{tabState === 0 ? "بستن" : "مرحله قبل"}
</Button>
{tabState !== 2 && (
{tabState !== 3 && (
<Button
onClick={handleNext}
key={"handleNext"}
@@ -174,7 +203,7 @@ const CreateTollHouseContent = ({ setOpen, SubmitCreateTollHouse, defaultValues
مرحله بعد
</Button>
)}
{tabState === 2 && (
{tabState === 3 && (
<Button
variant="contained"
size="large"

View File

@@ -16,9 +16,9 @@ const MachineInfo = ({ control }) => {
<TextField
fullWidth
size="small"
label="ماشین آلات سبک"
label="تعداد ماشین آلات سبک"
value={value}
placeholder="ماشین آلات سبک را وارد کنید"
placeholder="تعداد ماشین آلات سبک را وارد کنید"
error={!!error}
helperText={error ? error.message : null}
variant="outlined"
@@ -48,9 +48,9 @@ const MachineInfo = ({ control }) => {
<TextField
fullWidth
size="small"
label="ماشین آلات نیمه سنگین"
label="تعداد ماشین آلات نیمه سنگین"
value={value}
placeholder="ماشین آلات نیمه سنگین را وارد کنید"
placeholder="تعداد ماشین آلات نیمه سنگین را وارد کنید"
error={!!error}
helperText={error ? error.message : null}
variant="outlined"
@@ -80,9 +80,9 @@ const MachineInfo = ({ control }) => {
<TextField
fullWidth
size="small"
label="ماشین آلات سنگین"
label="تعداد ماشین آلات سنگین"
value={value}
placeholder="ماشین آلات سنگین را وارد کنید"
placeholder="تعداد ماشین آلات سنگین را وارد کنید"
error={!!error}
helperText={error ? error.message : null}
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}
label="نوع"
selectors={[
{ id: 0, name: "داعمی" },
{ id: 1, name: "موقت" },
{ id: 2, name: "فصلی" },
{ id: 1, name: "دائمی" },
{ id: 2, name: "موقت" },
{ id: 3, name: "فصلی" },
]}
schema={{ name: "name", value: "id" }}
error={error}

View File

@@ -51,6 +51,12 @@ const CreateTollHouse = ({ mutate }) => {
});
formData.append("lat", result.start_point.lat);
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", {
data: formData,
})
@@ -83,6 +89,12 @@ const CreateTollHouse = ({ mutate }) => {
overview_files_4: null,
overview_files_5: null,
start_point: "",
area: [
{ lat: "", lon: "" },
{ lat: "", lon: "" },
{ lat: "", lon: "" },
{ lat: "", lon: "" },
],
};
return (
<>

View File

@@ -47,6 +47,12 @@ const EditController = ({ rowId, mutate, setOpenEditDialog }) => {
overview_files_3: tollHouseItemDetails?.files[2]?.path || null,
overview_files_4: tollHouseItemDetails?.files[3]?.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 fields = [
@@ -79,6 +85,11 @@ const EditController = ({ rowId, mutate, setOpenEditDialog }) => {
}
formData.append("lat", result.start_point.lat);
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", {
data: formData,

View File

@@ -12,6 +12,7 @@ import useEdaratLists from "@/lib/hooks/useEdaratLists";
import { usePermissions } from "@/lib/hooks/usePermissions";
import LocationForm from "./RowActions/LocationForm";
import ImageDialog from "./RowActions/ImageForm";
import { useAuth } from "@/lib/contexts/auth";
const statusOptions = [
{ value: "", label: "همه وضعیت ها" },
@@ -21,14 +22,51 @@ const statusOptions = [
const typeOptions = [
{ value: "", label: "همه نوع ها" },
{ value: 0, label: "فصلی" },
{ value: 1, label: "داعمی" },
{ value: 1, label: "دائمی" },
{ value: 2, label: "موقت" },
];
const TollHouseList = () => {
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 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 [
{
accessorKey: "id",
@@ -64,42 +102,7 @@ const TollHouseList = () => {
grow: false,
size: 100,
},
{
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}</>,
},
...(hasCountryPermission ? [provinceColumn] : []),
{
accessorKey: "city_id",
header: "شهرستان", // Office
@@ -107,17 +110,19 @@ const TollHouseList = () => {
enableColumnFilter: true,
datatype: "numeric",
filterMode: "equals",
dependencyId: "province_id",
dependencyId: hasCountryPermission ? "province_id" : null,
grow: false,
size: 120,
ColumnSelectComponent: (props) => {
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(() => {
if (props.dependencyFieldValue.value === "") {
if (hasCountryPermission && props.dependencyFieldValue.value === "") {
return [{ value: "empty", label: "ابتدا استان را انتخاب کنید" }];
}
if (loadingEdaratList) {
@@ -135,10 +140,11 @@ const TollHouseList = () => {
];
}, [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]);
}, [props.dependencyFieldValue?.value, hasCountryPermission]);
return (
<CustomSelectByDependency
{...props}
@@ -194,6 +200,31 @@ const TollHouseList = () => {
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",
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"),
header: "تاریخ ثبت",
@@ -402,7 +408,7 @@ const TollHouseList = () => {
sorting={[{ id: "id", desc: true }]}
TableToolbar={Toolbar}
enableRowActions
positionActionsColumn={"last"}
positionActionsColumn={"first"}
RowActions={RowActions}
/>
</Box>