diff --git a/src/Map/MapComponent.jsx b/src/Map/MapComponent.jsx index 8a7c8a3..d9b3030 100644 --- a/src/Map/MapComponent.jsx +++ b/src/Map/MapComponent.jsx @@ -9,11 +9,13 @@ import { SignOut } from "../SignOut"; import debounce from "lodash.debounce"; import { Table } from "../modules/Table/Table"; import { usePopup } from "../stores/usePopup"; +import { useClickedPoint } from "../stores/useClickedPoint"; export const MapComponent = () => { const mapRef = useRef(null); const mapContainerRef = useRef(null); const { popup, setPopup } = usePopup(); + const { setClickedPoint } = useClickedPoint(); const handleClick = (event) => { if (!event.features) { @@ -94,7 +96,10 @@ export const MapComponent = () => { lat={popup.coordinates[1]} lng={popup.coordinates[0]} features={popup.features} - onClose={() => setPopup(null)} + onClose={() => { + setPopup(null); + setClickedPoint(null); + }} /> )} diff --git a/src/Map/Popup.jsx b/src/Map/Popup.jsx index 4fff60f..00f053a 100644 --- a/src/Map/Popup.jsx +++ b/src/Map/Popup.jsx @@ -2,24 +2,95 @@ import { Popup } from "react-map-gl"; import { Button, Col, Row } from "antd"; import { twMerge } from "tailwind-merge"; import { usePointSelection } from "../stores/usePointSelection"; -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { CATEGORIES } from "../config"; +import { useQuery } from "@tanstack/react-query"; +import { api } from "../api"; +import { useClickedPoint } from "../stores/useClickedPoint"; const popupConfig = [ { + name: "Id", field: "id", - name: "id", }, { - field: "name", + name: "Адрес", + field: "address", + }, + { + name: "Район", + field: "rayon_id", + }, + { + name: "Округ", + field: "okrug_id", + }, + { name: "Название", + field: "name", }, { + name: "Категория", field: "category", - name: "Тип", }, { + name: "Статус", + field: "status", + }, + { + name: "Прогнозный трафик", field: "prediction_current", + }, +]; + +const residentialPointConfig = [ + { + name: "Id", + field: "id", + }, + { + name: "Адрес", + field: "address", + }, + { + name: "Район", + field: "rayon_id", + }, + { + name: "Округ", + field: "okrug_id", + }, + { + name: "Название", + field: "name", + }, + { + name: "Категория", + field: "category", + }, + { + name: "Статус", + field: "status", + }, + { name: "Прогнозный трафик", + field: "prediction_current", + }, + { + name: "Кол-во квартир", + field: "flat_cnt", + }, + { + name: "Год постройки", + field: "year_bld", + }, + { + name: "Кол-во этажей", + field: "levels", + }, + { + name: "Материал стен", + field: "mat_nes", }, ]; @@ -28,9 +99,9 @@ const PopupWrapper = ({ lat, lng, onClose, children }) => { {children} @@ -39,17 +110,44 @@ const PopupWrapper = ({ lat, lng, onClose, children }) => { const SingleFeaturePopup = ({ feature, onSelect }) => { const { include, selection, exclude } = usePointSelection(); + const { setClickedPoint } = useClickedPoint(); const doesMatchFilter = feature.layer.id === "match-points"; + const featureId = feature.properties.id; + const locationId = feature.properties.location_id; + + const { data } = useQuery(["clicked-point", locationId], async () => { + const params = new URLSearchParams({ + "location_ids[]": [locationId], + }); + + const { data } = await api.get( + `/api/placement_points?${params.toString()}` + ); + + return data; + }); + + useEffect(() => { + if (!data) { + return; + } + + setClickedPoint(data.results[0]); + }, [data]); + + const isResidential = feature.properties.category === CATEGORIES.residential; + + const config = isResidential ? residentialPointConfig : popupConfig; const isSelected = - (doesMatchFilter || selection.included.has(feature.properties.id)) && - !selection.excluded.has(feature.properties.id); + (doesMatchFilter || selection.included.has(featureId)) && + !selection.excluded.has(featureId); const handleSelect = () => { if (isSelected) { - exclude(feature.properties.id); + exclude(featureId); } else { - include(feature.properties.id); + include(featureId); } onSelect(); }; @@ -57,13 +155,13 @@ const SingleFeaturePopup = ({ feature, onSelect }) => { return ( <>
- {popupConfig.map(({ field, name }) => { + {config.map(({ field, name }) => { return ( - + {name} - {feature.properties[field]} + {feature.properties[field]} ); })} diff --git a/src/config.js b/src/config.js index 0a276e3..f3d2637 100644 --- a/src/config.js +++ b/src/config.js @@ -1,20 +1,15 @@ -export const factorsNameMapper = { - people: "Численность населения в 2021 г.", - people2025: "Численность населения в 2025 г. (прогноз)", - stops_ot: "Остановки общественного транспорта", - routes_ot: "Маршруты общетвенного транспорта", - in_metro: "Входы в ближайшее метро в месяц", - out_metro: "Выходы из ближайшего метро в месяц", - tc: "Тогровые центры", - empls: "Рабочие места", - walkers: "Трафик населения", - schools: "Школы и детские сады", - parking: "Парковочные места", - pvz: "Пункты выдачи заказов", - gov_place: "Рекомендованные пункты размещения постаматов", - bike_park: "Городская аренда велосипедов, шт.", - products: "Продовольственные магазины", - nonprod: "Непродовольственные магазины", - service: "Пункты оказания бытовых услуг", - vuz: "ВУЗы и техникумы", +export const STATUSES = { + toWork: "К рассмотрению", + approve: "Согласование-установка", + working: "Работает", +}; + +export const CATEGORIES = { + kiosk: "Городской киоск", + mfc: "МФЦ", + library: "Библиотека", + sport: "Спортивный объект", + retail: "Ритейл", + residential: "Подъезд жилого дома", + culture: "Дом культуры/Клуб", }; diff --git a/src/index.css b/src/index.css index 008640b..86bbc70 100644 --- a/src/index.css +++ b/src/index.css @@ -21,6 +21,11 @@ @apply border-t-grey-light; } +.mapboxgl-popup-anchor-top .mapboxgl-popup-tip, +.maplibregl-popup-anchor-top .maplibregl-popup-tip { + @apply border-b-grey-light; +} + .ant-popover-inner { @apply bg-white-background rounded-xl max-h-[calc(100vh-100px)] overflow-y-auto; } diff --git a/src/modules/Sidebar/ObjectTypesSelect.jsx b/src/modules/Sidebar/ObjectTypesSelect.jsx index 2e48c51..bc08ffd 100644 --- a/src/modules/Sidebar/ObjectTypesSelect.jsx +++ b/src/modules/Sidebar/ObjectTypesSelect.jsx @@ -3,22 +3,7 @@ import { twMerge } from "tailwind-merge"; import { Title } from "../../components/Title"; import { useFilters } from "../../stores/useFilters"; import { useHasManualEdits } from "../../stores/usePointSelection"; - -//kiosk - городские киоски -// mfc - многофункциональные центры предоставления государственных и муниципальных услуг -// library - библиотеки -// dk - дома культуры и клубы -// sport - спортивные объекты - -const types = [ - "Городской киоск", - "МФЦ", - "Библиотека", - "Спортивный объект", - "Ритейл", - "Подъезд жилого дома", - "Дом культуры/Клуб", -]; +import { CATEGORIES } from "../../config"; const SelectItem = ({ name, isActive, onClick, disabled }) => { return ( @@ -48,7 +33,7 @@ export const ObjectTypesSelect = () => { return (
- + <Title text={"Категория объекта размещения"} /> {filters.categories.length !== 0 && ( <Button type="text" @@ -62,12 +47,12 @@ export const ObjectTypesSelect = () => { </div> <div className="space-y-2"> - {types.map((type) => ( + {Object.entries(CATEGORIES).map(([key, value]) => ( <SelectItem - key={type} - name={type} - isActive={filters.categories.includes(type)} - onClick={() => handleClick(type)} + key={key} + name={value} + isActive={filters.categories.includes(value)} + onClick={() => handleClick(value)} disabled={hasManualEdits} /> ))} diff --git a/src/modules/Sidebar/PredictionSlider.jsx b/src/modules/Sidebar/PredictionSlider.jsx index f5da42d..44cc5c9 100644 --- a/src/modules/Sidebar/PredictionSlider.jsx +++ b/src/modules/Sidebar/PredictionSlider.jsx @@ -8,11 +8,15 @@ import { useHasManualEdits } from "../../stores/usePointSelection"; export const PredictionSlider = () => { const { filters, setPrediction } = useFilters(); const hasManualEdits = useHasManualEdits(); - const { data } = useQuery(["max-min"], async () => { - const { data } = await api.get(`/api/placement_points/filters`); - - return data; - }); + const { data } = useQuery( + ["max-min"], + async () => { + const { data } = await api.get(`/api/placement_points/filters`); + + return data; + }, + { enabled: false } + ); const handleAfterChange = (range) => setPrediction(range); @@ -29,7 +33,7 @@ export const PredictionSlider = () => { return ( <Slider - title={"Прогнозный трафик, чел."} + title={"Прогнозный трафик"} value={filters.prediction} onAfterChange={handleAfterChange} min={200} diff --git a/src/modules/Sidebar/RegionSelect.jsx b/src/modules/Sidebar/RegionSelect.jsx index 28dbac3..9a439cd 100644 --- a/src/modules/Sidebar/RegionSelect.jsx +++ b/src/modules/Sidebar/RegionSelect.jsx @@ -37,9 +37,7 @@ export const RegionSelect = () => { const getRegions = async () => { setLoading(true); try { - const response = await api.get( - "https://postnet-dev.selftech.ru/api/ao_and_rayons" - ); + const response = await api.get("/api/ao_and_rayons"); setData(response.data); } catch (err) { console.error(err); diff --git a/src/modules/Sidebar/Sidebar.jsx b/src/modules/Sidebar/Sidebar.jsx index 59820b9..7d657ae 100644 --- a/src/modules/Sidebar/Sidebar.jsx +++ b/src/modules/Sidebar/Sidebar.jsx @@ -10,6 +10,7 @@ import { useHasManualEdits, usePointSelection, } from "../../stores/usePointSelection"; +import { TakeToWorkButton } from "./TakeToWorkButton"; function download(filename, data) { const downloadLink = window.document.createElement("a"); @@ -83,7 +84,7 @@ export const Sidebar = () => { }; return ( - <div className="absolute top-[20px] left-[20px] bg-white-background w-[300px] rounded-xl p-3 max-h-[calc(100%-40px)] overflow-y-auto z-10"> + <div className="absolute top-[20px] left-[20px] bg-white-background w-[320px] rounded-xl p-3 max-h-[calc(100%-40px)] overflow-y-auto z-10"> <div className="space-y-5"> <LayersVisibility /> <RegionSelect /> @@ -100,9 +101,7 @@ export const Sidebar = () => { > Экспорт данных </Button> - <Button type="primary" block className={"mt-2"} disabled={true}> - Взять в работу - </Button> + <TakeToWorkButton /> {hasManualEdits ? ( <Button type="text" diff --git a/src/modules/Sidebar/TakeToWorkButton.jsx b/src/modules/Sidebar/TakeToWorkButton.jsx new file mode 100644 index 0000000..1fd57f0 --- /dev/null +++ b/src/modules/Sidebar/TakeToWorkButton.jsx @@ -0,0 +1,40 @@ +import { Button } from "antd"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { api } from "../../api"; +import { useFilters } from "../../stores/useFilters"; +import { usePointSelection } from "../../stores/usePointSelection"; +import { STATUSES } from "../../config"; + +export const TakeToWorkButton = () => { + const { filters } = useFilters(); + const { prediction, categories } = filters; + const { selection } = usePointSelection(); + const queryClient = useQueryClient(); + + const { mutate } = useMutation({ + mutationFn: () => { + const params = new URLSearchParams({ + status: STATUSES.toWork, + "prediction_current[]": prediction, + "categories[]": categories, + "included[]": [...selection.included], + "excluded[]": [...selection.excluded], + }); + + return api.put( + `/api/placement_points/update_status?${params.toString()}` + ); + }, + onSuccess: () => queryClient.invalidateQueries(["table", 1, filters]), + }); + + const takeToWork = () => { + mutate(); + }; + + return ( + <Button type="primary" block className={"mt-2"} onClick={takeToWork}> + Взять в работу + </Button> + ); +}; diff --git a/src/modules/Table/Table.jsx b/src/modules/Table/Table.jsx index 4a8264d..43182eb 100644 --- a/src/modules/Table/Table.jsx +++ b/src/modules/Table/Table.jsx @@ -7,75 +7,61 @@ import parse from "wellknown"; import { useMap } from "react-map-gl"; import { usePointSelection } from "../../stores/usePointSelection"; import { useFilters } from "../../stores/useFilters"; +import { useClickedPoint } from "../../stores/useClickedPoint"; const columns = [ { title: "Id", dataIndex: "id", key: "id", - width: "20px", + width: 50, ellipsis: true, }, { - title: "Статус", - dataIndex: "status", - key: "status", + title: "Адрес", + dataIndex: "address", + key: "address", + width: 200, + }, + { + title: "Район", + dataIndex: "rayon", + key: "rayon", width: "120px", ellipsis: true, }, { - title: "Категория", - dataIndex: "category", - key: "category", + title: "Округ", + dataIndex: "okrug", + key: "okrug", width: "120px", ellipsis: true, }, { - title: "Прогноз", - dataIndex: "prediction_current", - key: "prediction_current", + title: "Название", + dataIndex: "name", + key: "name", width: "120px", ellipsis: true, }, - // { - // title: "Зрелость", - // dataIndex: "age", - // key: "age", - // width: "120px", - // ellipsis: true, - // }, - // { - // title: "План", - // dataIndex: "plan", - // key: "plan", - // width: "120px", - // ellipsis: true, - // }, - // { - // title: "Факт", - // dataIndex: "fact", - // key: "fact", - // width: "120px", - // ellipsis: true, - // }, - // { - // title: "Дельта", - // dataIndex: "delta", - // key: "delta", - // width: "120px", - // ellipsis: true, - // }, { - title: "АО", - dataIndex: "okrug", - key: "okrug", + title: "Категория", + dataIndex: "category", + key: "category", width: "120px", ellipsis: true, }, { - title: "Район", - dataIndex: "rayon", - key: "rayon", + title: "Статус", + dataIndex: "status", + key: "status", + width: "120px", + ellipsis: true, + }, + { + title: "Прогнозный трафик", + dataIndex: "prediction_current", + key: "prediction_current", width: "120px", ellipsis: true, }, @@ -91,6 +77,9 @@ export const Table = React.memo(({ height = 200 }) => { const { filters } = useFilters(); const { prediction, status, categories } = filters; const { include, selection, exclude } = usePointSelection(); + const { point } = useClickedPoint(); + + console.log(point); const { data } = useQuery( ["table", page, filters], diff --git a/src/stores/useClickedPoint.js b/src/stores/useClickedPoint.js new file mode 100644 index 0000000..78f5e38 --- /dev/null +++ b/src/stores/useClickedPoint.js @@ -0,0 +1,18 @@ +import { create } from "zustand"; +import { immer } from "zustand/middleware/immer"; + +const store = (set) => ({ + point: null, + + setClickedPoint: (point) => { + set((state) => { + if (!point) { + state.point = null; + return state; + } + state.point = point; + }); + }, +}); + +export const useClickedPoint = create(immer(store));