diff --git a/src/App.tsx b/src/App.tsx index 57f432b..05cb4ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -82,7 +82,7 @@ const queryClient = new QueryClient({ // Clear tokens and redirect to login await removeToken(); await removeRefreshToken(); - window.location.href = '/auth/login'; + // window.location.href = '/auth/login'; } } } diff --git a/src/config/axios.ts b/src/config/axios.ts index 7751219..24f2c04 100644 --- a/src/config/axios.ts +++ b/src/config/axios.ts @@ -64,7 +64,7 @@ axiosInstance.interceptors.response.use( } else { await removeRefreshToken(); await removeToken(); - window.location.href = `/`; + // window.location.href = `/`; return; } } catch (refreshError) { diff --git a/src/pages/settings/GeneralSettings.tsx b/src/pages/settings/GeneralSettings.tsx index 28674e1..2beb2e5 100644 --- a/src/pages/settings/GeneralSettings.tsx +++ b/src/pages/settings/GeneralSettings.tsx @@ -16,6 +16,7 @@ import { useFormik } from 'formik' import * as yup from 'yup' import { toast } from 'react-toastify' import { extractErrorMessage } from '@/config/func' +import ServiceAreaPicker from './components/ServiceAreaPicker' const validationSchema = yup.object({ name: yup.string().required('نام رستوران الزامی است'), @@ -46,6 +47,7 @@ const GeneralSettings: FC = () => { longitude: 0, description: '', logo: '', + serviceArea: undefined, }, validationSchema, onSubmit: (values) => { @@ -91,13 +93,14 @@ const GeneralSettings: FC = () => { formik.setValues({ name: restaurantData.name || '', menuColor: restaurantData.menuColor || '#000000', - phone: restaurantData.phoneNumber || '', + phone: restaurantData.phone || '', instagram: restaurantData.instagram || '', address: restaurantData.address || '', latitude: restaurantData.latitude || 0, longitude: restaurantData.longitude || 0, description: restaurantData.description || '', logo: restaurantData.logo || '', + serviceArea: restaurantData.serviceArea || undefined, }) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -207,6 +210,14 @@ const GeneralSettings: FC = () => { /> +
+ formik.setFieldValue('serviceArea', polygon)} + /> +
+ {/* */} +
- - + + + + void + error_text?: string +} + +const ServiceAreaInput: FC = ({ + label, + polygon, + onClick, + error_text, +}) => { + const getDisplayText = () => { + if (polygon?.coordinates?.[0] && polygon.coordinates[0].length > 0) { + // در GeoJSON Polygon، اولین نقطه در انتها تکرار می‌شود تا بسته شود + // پس باید 1 را کم کنیم تا تعداد واقعی نقاط را نشان دهیم + const pointCount = polygon.coordinates[0].length + const actualPointCount = pointCount > 0 ? pointCount - 1 : 0 + return `ناحیه انتخاب شده (${actualPointCount} نقطه)` + } + return 'روی نقشه کلیک کنید تا ناحیه را انتخاب کنید' + } + + return ( +
+ {label &&
{label}
} +
+ + {getDisplayText()} +
+ {error_text && ( + + )} +
+ ) +} + +export default ServiceAreaInput + diff --git a/src/pages/settings/components/ServiceAreaPicker.tsx b/src/pages/settings/components/ServiceAreaPicker.tsx new file mode 100644 index 0000000..ec3248a --- /dev/null +++ b/src/pages/settings/components/ServiceAreaPicker.tsx @@ -0,0 +1,235 @@ +import { type FC, useState, useEffect } from 'react' +import { MapContainer, TileLayer, Polygon, Polyline, useMapEvents, useMap } from 'react-leaflet' +import type { LatLngTuple } from 'leaflet' +import type { LatLng } from 'leaflet' +import type { GeoJSONPolygon, GeoJSONPoint } from '../types/Types' +import { Trash } from 'iconsax-react' + +interface ServiceAreaPickerProps { + initialPolygon?: GeoJSONPolygon; + onPolygonSelect: (polygon: GeoJSONPolygon | null) => void +} + +const ChangeView = ({ center, zoom }: { center: LatLngTuple; zoom: number }) => { + const map = useMap() + useEffect(() => { + map.setView(center, zoom) + }, [map, center, zoom]) + return null +} + +const ServiceAreaPicker: FC = ({ initialPolygon, onPolygonSelect }) => { + const [points, setPoints] = useState([]) + const [isDrawing, setIsDrawing] = useState(false) + const [isPolygonClosed, setIsPolygonClosed] = useState(false) + const mapCenter: LatLngTuple = [35.6892, 51.3890] // تهران به عنوان پیش‌فرض + + const handleClear = () => { + setPoints([]) + setIsDrawing(false) + setIsPolygonClosed(false) + onPolygonSelect(null) + } + + // تبدیل GeoJSON Polygon به LatLngTuple[] + const convertGeoJSONToPoints = (polygon: GeoJSONPolygon): LatLngTuple[] => { + if (!polygon.coordinates || polygon.coordinates.length === 0) { + return [] + } + // اولین ring از coordinates را بگیر (outer ring) + const ring = polygon.coordinates[0] + // تبدیل [lng, lat] به [lat, lng] و حذف آخرین نقطه اگر تکراری باشد + const convertedPoints: LatLngTuple[] = ring.map((coord) => [coord[1], coord[0]]) + // اگر اولین و آخرین نقطه یکسان هستند، آخرین را حذف کن + if (convertedPoints.length > 0 && + convertedPoints[0][0] === convertedPoints[convertedPoints.length - 1][0] && + convertedPoints[0][1] === convertedPoints[convertedPoints.length - 1][1]) { + convertedPoints.pop() + } + return convertedPoints + } + + // مقداردهی اولیه با initialPolygon + useEffect(() => { + if (initialPolygon) { + const initialPoints = convertGeoJSONToPoints(initialPolygon) + if (initialPoints.length >= 3) { + setPoints(initialPoints) + setIsPolygonClosed(true) + onPolygonSelect(initialPolygon) + } else { + handleClear() + } + } else { + handleClear() + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [initialPolygon]) + + const convertToGeoJSON = (pts: LatLngTuple[]): GeoJSONPolygon | null => { + if (pts.length < 3) return null + + // تبدیل [lat, lng] به [lng, lat] و بستن Polygon با تکرار اولین نقطه + const coords = pts.map((pt) => [pt[1], pt[0]] as GeoJSONPoint) + + // در GeoJSON Polygon، اولین نقطه باید در انتها تکرار شود تا بسته شود + if (coords.length > 0 && + (coords[0][0] !== coords[coords.length - 1][0] || + coords[0][1] !== coords[coords.length - 1][1])) { + coords.push(coords[0]) + } + + const coordinates: GeoJSONPoint[][] = [coords] + + return { + type: 'Polygon', + coordinates, + } + } + + // بررسی فاصله بین دو نقطه (به درجه) + const getDistance = (pt1: LatLngTuple, pt2: LatLngTuple): number => { + const latDiff = pt1[0] - pt2[0] + const lngDiff = pt1[1] - pt2[1] + return Math.sqrt(latDiff * latDiff + lngDiff * lngDiff) + } + + const handleMapClick = (e: { latlng: LatLng }) => { + if (!isDrawing && points.length === 0) { + setIsDrawing(true) + } + + const newPoint: LatLngTuple = [e.latlng.lat, e.latlng.lng] + + // اگر حداقل 3 نقطه داریم و کاربر روی نقطه اول کلیک کرد (یا نزدیک به آن)، Polygon را ببند + if (points.length >= 3) { + const firstPoint = points[0] + const distance = getDistance(newPoint, firstPoint) + // اگر فاصله کمتر از 0.002 درجه باشد (تقریباً 200 متر)، به عنوان کلیک روی نقطه اول در نظر بگیریم + if (distance < 0.002) { + // Polygon را ببند و کامل کن (convertToGeoJSON خودش نقطه اول را اضافه می‌کند) + const geoJsonPolygon = convertToGeoJSON(points) + if (geoJsonPolygon) { + onPolygonSelect(geoJsonPolygon) + setIsDrawing(false) + setIsPolygonClosed(true) + // نقاط را نگه دار تا Polygon کامل نمایش داده شود + return + } + } + } + + // اگر Polygon قبلاً بسته شده بود، دوباره شروع کن + if (isPolygonClosed) { + setPoints([]) + setIsPolygonClosed(false) + setIsDrawing(true) + onPolygonSelect(null) + } + + // نقطه جدید را اضافه کن + const updatedPoints = [...points, newPoint] + setPoints(updatedPoints) + + // تا زمانی که Polygon بسته نشده، null بفرست + onPolygonSelect(null) + } + + const MapClickHandler = () => { + useMapEvents({ + click: handleMapClick, + }) + return null + } + + const polygonOptions = { + color: '#3B82F6', + fillColor: '#3B82F6', + fillOpacity: 0.2, + weight: 2, + } + + const polylineOptions = { + color: '#3B82F6', + weight: 2, + dashArray: '5, 10', + } + + // فقط وقتی حداقل 2 نقطه داریم، center را تغییر می‌دهیم + const centerPoint = points.length >= 2 + ? (() => { + const sum = points.reduce( + (acc, pt) => [acc[0] + pt[0], acc[1] + pt[1]], + [0, 0] as [number, number] + ) + return [sum[0] / points.length, sum[1] / points.length] as LatLngTuple + })() + : points.length === 1 + ? points[0] // با یک نقطه، center همان نقطه است + : mapCenter + + return ( +
+
+ + + {points.length >= 2 && ( + + )} + {points.length === 0 && ( + + )} + + {points.length >= 2 && !isPolygonClosed && ( + + )} + {/* فقط وقتی Polygon بسته شده، نمایش بده */} + {points.length >= 3 && isPolygonClosed && ( + + )} + +
+ +
+
+ {points.length === 0 + ? 'روی نقشه کلیک کنید تا نقاط ناحیه را انتخاب کنید' + : isPolygonClosed + ? `ناحیه کامل شده (${points.length} نقطه)` + : points.length < 3 + ? `نقاط انتخاب شده: ${points.length} (حداقل 3 نقطه لازم است)` + : `نقاط انتخاب شده: ${points.length} - برای بستن ناحیه روی نقطه اول کلیک کنید`} +
+ +
+ {points.length > 0 && ( + + )} +
+
+
+ ) +} + +export default ServiceAreaPicker \ No newline at end of file diff --git a/src/pages/settings/types/Types.ts b/src/pages/settings/types/Types.ts index c3259fa..8037047 100644 --- a/src/pages/settings/types/Types.ts +++ b/src/pages/settings/types/Types.ts @@ -1,5 +1,12 @@ import type { IResponse } from "@/types/response.types"; +export type GeoJSONPoint = [number, number]; + +export type GeoJSONPolygon = { + type: "Polygon"; + coordinates: GeoJSONPoint[][]; +}; + export type UpdateRestaurantType = { name: string; menuColor: string; @@ -10,6 +17,7 @@ export type UpdateRestaurantType = { latitude: number; longitude: number; description: string; + serviceArea?: GeoJSONPolygon; }; export type Restaurant = { @@ -19,19 +27,20 @@ export type Restaurant = { deletedAt: string | null; name: string; slug: string; - logo: string | null; - address: string | null; - menuColor: string | null; - latitude: number | null; - longitude: number | null; - serviceArea: unknown | null; + logo: string; + address: string; + menuColor: string; + latitude: number; + longitude: number; + serviceArea: GeoJSONPolygon; isActive: boolean; establishedYear: number | null; phoneNumber: string | null; - instagram: string | null; + phone: string; + instagram: string; telegram: string | null; whatsapp: string | null; - description: string | null; + description: string; seoTitle: string | null; seoDescription: string | null; tagNames: unknown | null;