Add TakeToWork action

dev
Platon Yasev 3 years ago
parent c13d404776
commit cbe5aebb41

@ -9,11 +9,13 @@ import { SignOut } from "../SignOut";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import { Table } from "../modules/Table/Table"; import { Table } from "../modules/Table/Table";
import { usePopup } from "../stores/usePopup"; import { usePopup } from "../stores/usePopup";
import { useClickedPoint } from "../stores/useClickedPoint";
export const MapComponent = () => { export const MapComponent = () => {
const mapRef = useRef(null); const mapRef = useRef(null);
const mapContainerRef = useRef(null); const mapContainerRef = useRef(null);
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
const { setClickedPoint } = useClickedPoint();
const handleClick = (event) => { const handleClick = (event) => {
if (!event.features) { if (!event.features) {
@ -94,7 +96,10 @@ export const MapComponent = () => {
lat={popup.coordinates[1]} lat={popup.coordinates[1]}
lng={popup.coordinates[0]} lng={popup.coordinates[0]}
features={popup.features} features={popup.features}
onClose={() => setPopup(null)} onClose={() => {
setPopup(null);
setClickedPoint(null);
}}
/> />
)} )}

@ -2,24 +2,95 @@ import { Popup } from "react-map-gl";
import { Button, Col, Row } from "antd"; import { Button, Col, Row } from "antd";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { usePointSelection } from "../stores/usePointSelection"; 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 = [ const popupConfig = [
{ {
name: "Id",
field: "id", field: "id",
name: "id",
}, },
{ {
field: "name", name: "Адрес",
field: "address",
},
{
name: "Район",
field: "rayon_id",
},
{
name: "Округ",
field: "okrug_id",
},
{
name: "Название", name: "Название",
field: "name",
}, },
{ {
name: "Категория",
field: "category", field: "category",
name: "Тип",
}, },
{ {
name: "Статус",
field: "status",
},
{
name: "Прогнозный трафик",
field: "prediction_current", 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: "Прогнозный трафик", 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 }) => {
<Popup <Popup
longitude={lng} longitude={lng}
latitude={lat} latitude={lat}
anchor="bottom"
onClose={onClose} onClose={onClose}
closeOnClick={false} closeOnClick={false}
style={{ minWidth: "300px" }}
> >
{children} {children}
</Popup> </Popup>
@ -39,17 +110,44 @@ const PopupWrapper = ({ lat, lng, onClose, children }) => {
const SingleFeaturePopup = ({ feature, onSelect }) => { const SingleFeaturePopup = ({ feature, onSelect }) => {
const { include, selection, exclude } = usePointSelection(); const { include, selection, exclude } = usePointSelection();
const { setClickedPoint } = useClickedPoint();
const doesMatchFilter = feature.layer.id === "match-points"; 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 = const isSelected =
(doesMatchFilter || selection.included.has(feature.properties.id)) && (doesMatchFilter || selection.included.has(featureId)) &&
!selection.excluded.has(feature.properties.id); !selection.excluded.has(featureId);
const handleSelect = () => { const handleSelect = () => {
if (isSelected) { if (isSelected) {
exclude(feature.properties.id); exclude(featureId);
} else { } else {
include(feature.properties.id); include(featureId);
} }
onSelect(); onSelect();
}; };
@ -57,13 +155,13 @@ const SingleFeaturePopup = ({ feature, onSelect }) => {
return ( return (
<> <>
<div> <div>
{popupConfig.map(({ field, name }) => { {config.map(({ field, name }) => {
return ( return (
<Row className={twMerge("p-1")} key={field}> <Row className={twMerge("p-1")} key={field}>
<Col className={"font-semibold"} span={15}> <Col className={"font-semibold"} span={12}>
{name} {name}
</Col> </Col>
<Col span={9}>{feature.properties[field]}</Col> <Col span={12}>{feature.properties[field]}</Col>
</Row> </Row>
); );
})} })}

@ -1,20 +1,15 @@
export const factorsNameMapper = { export const STATUSES = {
people: "Численность населения в 2021 г.", toWork: "К рассмотрению",
people2025: "Численность населения в 2025 г. (прогноз)", approve: "Согласование-установка",
stops_ot: "Остановки общественного транспорта", working: "Работает",
routes_ot: "Маршруты общетвенного транспорта", };
in_metro: "Входы в ближайшее метро в месяц",
out_metro: "Выходы из ближайшего метро в месяц", export const CATEGORIES = {
tc: "Тогровые центры", kiosk: "Городской киоск",
empls: "Рабочие места", mfc: "МФЦ",
walkers: "Трафик населения", library: "Библиотека",
schools: "Школы и детские сады", sport: "Спортивный объект",
parking: "Парковочные места", retail: "Ритейл",
pvz: "Пункты выдачи заказов", residential: "Подъезд жилого дома",
gov_place: "Рекомендованные пункты размещения постаматов", culture: "Дом культуры/Клуб",
bike_park: "Городская аренда велосипедов, шт.",
products: "Продовольственные магазины",
nonprod: "Непродовольственные магазины",
service: "Пункты оказания бытовых услуг",
vuz: "ВУЗы и техникумы",
}; };

@ -21,6 +21,11 @@
@apply border-t-grey-light; @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 { .ant-popover-inner {
@apply bg-white-background rounded-xl max-h-[calc(100vh-100px)] overflow-y-auto; @apply bg-white-background rounded-xl max-h-[calc(100vh-100px)] overflow-y-auto;
} }

@ -3,22 +3,7 @@ import { twMerge } from "tailwind-merge";
import { Title } from "../../components/Title"; import { Title } from "../../components/Title";
import { useFilters } from "../../stores/useFilters"; import { useFilters } from "../../stores/useFilters";
import { useHasManualEdits } from "../../stores/usePointSelection"; import { useHasManualEdits } from "../../stores/usePointSelection";
import { CATEGORIES } from "../../config";
//kiosk - городские киоски
// mfc - многофункциональные центры предоставления государственных и муниципальных услуг
// library - библиотеки
// dk - дома культуры и клубы
// sport - спортивные объекты
const types = [
"Городской киоск",
"МФЦ",
"Библиотека",
"Спортивный объект",
"Ритейл",
"Подъезд жилого дома",
"Дом культуры/Клуб",
];
const SelectItem = ({ name, isActive, onClick, disabled }) => { const SelectItem = ({ name, isActive, onClick, disabled }) => {
return ( return (
@ -48,7 +33,7 @@ export const ObjectTypesSelect = () => {
return ( return (
<div> <div>
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
<Title text={"Тип объекта размещения"} /> <Title text={"Категория объекта размещения"} />
{filters.categories.length !== 0 && ( {filters.categories.length !== 0 && (
<Button <Button
type="text" type="text"
@ -62,12 +47,12 @@ export const ObjectTypesSelect = () => {
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{types.map((type) => ( {Object.entries(CATEGORIES).map(([key, value]) => (
<SelectItem <SelectItem
key={type} key={key}
name={type} name={value}
isActive={filters.categories.includes(type)} isActive={filters.categories.includes(value)}
onClick={() => handleClick(type)} onClick={() => handleClick(value)}
disabled={hasManualEdits} disabled={hasManualEdits}
/> />
))} ))}

@ -8,11 +8,15 @@ import { useHasManualEdits } from "../../stores/usePointSelection";
export const PredictionSlider = () => { export const PredictionSlider = () => {
const { filters, setPrediction } = useFilters(); const { filters, setPrediction } = useFilters();
const hasManualEdits = useHasManualEdits(); const hasManualEdits = useHasManualEdits();
const { data } = useQuery(["max-min"], async () => { const { data } = useQuery(
["max-min"],
async () => {
const { data } = await api.get(`/api/placement_points/filters`); const { data } = await api.get(`/api/placement_points/filters`);
return data; return data;
}); },
{ enabled: false }
);
const handleAfterChange = (range) => setPrediction(range); const handleAfterChange = (range) => setPrediction(range);
@ -29,7 +33,7 @@ export const PredictionSlider = () => {
return ( return (
<Slider <Slider
title={"Прогнозный трафик, чел."} title={"Прогнозный трафик"}
value={filters.prediction} value={filters.prediction}
onAfterChange={handleAfterChange} onAfterChange={handleAfterChange}
min={200} min={200}

@ -37,9 +37,7 @@ export const RegionSelect = () => {
const getRegions = async () => { const getRegions = async () => {
setLoading(true); setLoading(true);
try { try {
const response = await api.get( const response = await api.get("/api/ao_and_rayons");
"https://postnet-dev.selftech.ru/api/ao_and_rayons"
);
setData(response.data); setData(response.data);
} catch (err) { } catch (err) {
console.error(err); console.error(err);

@ -10,6 +10,7 @@ import {
useHasManualEdits, useHasManualEdits,
usePointSelection, usePointSelection,
} from "../../stores/usePointSelection"; } from "../../stores/usePointSelection";
import { TakeToWorkButton } from "./TakeToWorkButton";
function download(filename, data) { function download(filename, data) {
const downloadLink = window.document.createElement("a"); const downloadLink = window.document.createElement("a");
@ -83,7 +84,7 @@ export const Sidebar = () => {
}; };
return ( 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"> <div className="space-y-5">
<LayersVisibility /> <LayersVisibility />
<RegionSelect /> <RegionSelect />
@ -100,9 +101,7 @@ export const Sidebar = () => {
> >
Экспорт данных Экспорт данных
</Button> </Button>
<Button type="primary" block className={"mt-2"} disabled={true}> <TakeToWorkButton />
Взять в работу
</Button>
{hasManualEdits ? ( {hasManualEdits ? (
<Button <Button
type="text" type="text"

@ -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>
);
};

@ -7,75 +7,61 @@ import parse from "wellknown";
import { useMap } from "react-map-gl"; import { useMap } from "react-map-gl";
import { usePointSelection } from "../../stores/usePointSelection"; import { usePointSelection } from "../../stores/usePointSelection";
import { useFilters } from "../../stores/useFilters"; import { useFilters } from "../../stores/useFilters";
import { useClickedPoint } from "../../stores/useClickedPoint";
const columns = [ const columns = [
{ {
title: "Id", title: "Id",
dataIndex: "id", dataIndex: "id",
key: "id", key: "id",
width: "20px", width: 50,
ellipsis: true, ellipsis: true,
}, },
{ {
title: "Статус", title: "Адрес",
dataIndex: "status", dataIndex: "address",
key: "status", key: "address",
width: 200,
},
{
title: "Район",
dataIndex: "rayon",
key: "rayon",
width: "120px", width: "120px",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "Категория", title: "Округ",
dataIndex: "category", dataIndex: "okrug",
key: "category", key: "okrug",
width: "120px", width: "120px",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "Прогноз", title: "Название",
dataIndex: "prediction_current", dataIndex: "name",
key: "prediction_current", key: "name",
width: "120px", width: "120px",
ellipsis: true, 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: "АО", title: "Категория",
dataIndex: "okrug", dataIndex: "category",
key: "okrug", key: "category",
width: "120px", width: "120px",
ellipsis: true, ellipsis: true,
}, },
{ {
title: "Район", title: "Статус",
dataIndex: "rayon", dataIndex: "status",
key: "rayon", key: "status",
width: "120px",
ellipsis: true,
},
{
title: "Прогнозный трафик",
dataIndex: "prediction_current",
key: "prediction_current",
width: "120px", width: "120px",
ellipsis: true, ellipsis: true,
}, },
@ -91,6 +77,9 @@ export const Table = React.memo(({ height = 200 }) => {
const { filters } = useFilters(); const { filters } = useFilters();
const { prediction, status, categories } = filters; const { prediction, status, categories } = filters;
const { include, selection, exclude } = usePointSelection(); const { include, selection, exclude } = usePointSelection();
const { point } = useClickedPoint();
console.log(point);
const { data } = useQuery( const { data } = useQuery(
["table", page, filters], ["table", page, filters],

@ -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));
Loading…
Cancel
Save