Add working points filters

dev
Platon Yasev 3 years ago
parent 7ac62be115
commit e775c07c63

@ -1,7 +1,7 @@
import { AutoComplete, Input } from "antd"; import { AutoComplete, Input } from "antd";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { SearchOutlined } from "@ant-design/icons"; import { SearchOutlined } from "@ant-design/icons";
import { api } from "../../api"; import { api } from "../api";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useMap } from "react-map-gl"; import { useMap } from "react-map-gl";
import parse from "wellknown"; import parse from "wellknown";

@ -4,16 +4,48 @@ import { useLayersVisibility } from "../../stores/useLayersVisibility";
import { STATUSES } from "../../config"; import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression"; import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants"; import { LAYER_IDS } from "./constants";
import { useFilters } from "../../stores/useFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.working]; const statusExpression = ["==", ["get", "status"], STATUSES.working];
export const WorkingPoints = () => { export const WorkingPoints = () => {
const { isVisible } = useLayersVisibility(); const { isVisible } = useLayersVisibility();
const regionFilterExpression = useRegionFilterExpression(); const regionFilterExpression = useRegionFilterExpression();
const {
filters: { deltaTraffic, factTraffic, age },
} = useFilters();
const deltaExpression = [
[">=", ["get", "delta_current"], deltaTraffic[0]],
["<=", ["get", "delta_current"], deltaTraffic[1]],
];
const factExpression = [
[">=", ["get", "fact"], factTraffic[0]],
["<=", ["get", "fact"], factTraffic[1]],
];
const ageExpression = [
[">=", ["get", "age_day"], age[0]],
["<=", ["get", "age_day"], age[1]],
];
const filter = regionFilterExpression const filter = regionFilterExpression
? ["all", statusExpression, regionFilterExpression] ? [
: statusExpression; "all",
statusExpression,
...deltaExpression,
...factExpression,
...ageExpression,
regionFilterExpression,
]
: [
"all",
statusExpression,
...deltaExpression,
...factExpression,
...ageExpression,
];
return ( return (
<> <>

@ -17,7 +17,7 @@ import {
MODE_TO_LAYER_VISIBILITY_MAPPER, MODE_TO_LAYER_VISIBILITY_MAPPER,
MODE_TO_STATUS_MAPPER, MODE_TO_STATUS_MAPPER,
} from "../config"; } from "../config";
import { FiltersButton } from "../modules/Sidebar/FiltersButton"; import { SidebarControl } from "./SidebarControl";
import { LayersControl } from "./LayersControl/LayersControl"; import { LayersControl } from "./LayersControl/LayersControl";
import { useTable } from "../stores/useTable"; import { useTable } from "../stores/useTable";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
@ -153,7 +153,7 @@ export const MapComponent = () => {
<MapHeader isSidebarCollapsed={isSidebarCollapsed} /> <MapHeader isSidebarCollapsed={isSidebarCollapsed} />
<FiltersButton toggleCollapse={toggleCollapseSidebar} /> <SidebarControl toggleCollapse={toggleCollapseSidebar} />
<Basemap /> <Basemap />
<Layers /> <Layers />

@ -1,5 +1,5 @@
import { Logo } from "../icons/Logo"; import { Logo } from "../icons/Logo";
import { AddressSearch } from "../modules/Sidebar/AddressSearch"; import { AddressSearch } from "./AddressSearch";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { ModeSelector } from "../components/ModeSelector"; import { ModeSelector } from "../components/ModeSelector";

@ -1,15 +1,23 @@
import { CATEGORIES } from "../../../config"; import { CATEGORIES, STATUSES } from "../../../config";
import { popupConfig, residentialPointConfig } from "./config"; import {
popupConfig,
residentialPointConfig,
workingPointFields,
} from "./config";
import { Col, Row } from "antd"; import { Col, Row } from "antd";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export const FeatureProperties = ({ feature, dynamicStatus }) => { export const FeatureProperties = ({ feature, dynamicStatus }) => {
const isResidential = feature.properties.category === CATEGORIES.residential; const isResidential = feature.properties.category === CATEGORIES.residential;
const isWorking = feature.properties.status === STATUSES.working;
const config = isResidential ? residentialPointConfig : popupConfig; const config = isResidential ? residentialPointConfig : popupConfig;
const finalConfig = isWorking ? [...config, ...workingPointFields] : config;
return ( return (
<div> <div>
{config.map(({ field, name }) => { {finalConfig.map(({ field, name }) => {
const value = const value =
dynamicStatus && field === "status" dynamicStatus && field === "status"
? dynamicStatus ? dynamicStatus

@ -83,3 +83,9 @@ export const residentialPointConfig = [
field: "mat_nes", field: "mat_nes",
}, },
]; ];
export const workingPointFields = [
{ name: "Факт", field: "fact" },
{ name: "Расхождение с прогнозом", field: "delta_current" },
{ name: "Зрелость", field: "age_day" },
];

@ -1,7 +1,7 @@
import { Button } from "antd"; import { Button } from "antd";
import { FiltersIcon } from "../../icons/FiltersIcon"; import { FiltersIcon } from "../icons/FiltersIcon";
export const FiltersButton = ({ toggleCollapse }) => { export const SidebarControl = ({ toggleCollapse }) => {
return ( return (
<Button <Button
icon={<FiltersIcon width={16} height={16} />} icon={<FiltersIcon width={16} height={16} />}

@ -1,22 +1,33 @@
import { Button } from "antd"; import { Button, Popover } from "antd";
import { LogoutOutlined } from "@ant-design/icons"; import { LogoutOutlined } from "@ant-design/icons";
import { setAuth } from "./stores/auth";
import { api } from "./api"; import { api } from "./api";
import { setAuth } from "./stores/auth";
export function SignOut() { export function SignOut() {
const logOut = async () => {
await api.post("accounts/logout/");
setAuth(false);
};
return ( return (
<div className="absolute top-[20px] right-[20px]"> <div className="absolute top-[20px] right-[20px]">
<Button <Popover
icon={<LogoutOutlined />} content={
type="primary" <Button type={"primary"} onClick={logOut}>
size="large" Выйти
title="Выйти" </Button>
onClick={async () => { }
await api.post("accounts/logout/"); trigger="click"
placement={"bottomRight"}
setAuth(false); >
}} <Button
/> icon={<LogoutOutlined />}
type="primary"
size="large"
title="Выйти"
/>
</Popover>
</div> </div>
); );
} }

@ -92,6 +92,6 @@ export const useGetFilteredInitialPointsCount = () => {
return await getPoints(params, region); return await getPoints(params, region);
}, },
{ select: (data) => data.count } { select: (data) => data.count, keepPreviousData: true }
); );
}; };

@ -24,19 +24,19 @@ export const ModeSelector = () => {
icon={<AIIcon />} icon={<AIIcon />}
type={getType(MODES.INITIAL)} type={getType(MODES.INITIAL)}
onClick={() => handleClick(MODES.INITIAL)} onClick={() => handleClick(MODES.INITIAL)}
title="Локации к рассмотрению" title="Отбор локаций для работы"
/> />
<Button <Button
icon={<ApproveIcon />} icon={<ApproveIcon />}
type={getType(MODES.APPROVE_WORKING)} type={getType(MODES.APPROVE_WORKING)}
onClick={() => handleClick(MODES.APPROVE_WORKING)} onClick={() => handleClick(MODES.APPROVE_WORKING)}
title="Локации на согласовании" title="Управление статусами локаций"
/> />
<Button <Button
icon={<WorkingIcon />} icon={<WorkingIcon />}
type={getType(MODES.WORKING)} type={getType(MODES.WORKING)}
onClick={() => handleClick(MODES.WORKING)} onClick={() => handleClick(MODES.WORKING)}
title="Локации в работе" title="Мониторинг работающих постаматов"
/> />
</> </>
); );

@ -34,6 +34,7 @@ export const SliderComponent = ({
disabled = false, disabled = false,
onMouseEnter, onMouseEnter,
onMouseLeave, onMouseLeave,
showZeroMark = false,
}) => { }) => {
const fullRangeMarks = { const fullRangeMarks = {
[min]: <Mark value={min} />, [min]: <Mark value={min} />,
@ -72,13 +73,15 @@ export const SliderComponent = ({
onChange?.(value); onChange?.(value);
}; };
const finalMarks = showZeroMark ? { ...marks, 0: <Mark value={0} /> } : marks;
return ( return (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Title text={title} /> <Title text={title} />
<Slider <Slider
range={range} range={range}
value={value} value={value}
marks={marks} marks={finalMarks}
onChange={handleChange} onChange={handleChange}
onAfterChange={handleAfterChange} onAfterChange={handleAfterChange}
min={min} min={min}

@ -0,0 +1,63 @@
import { RegionSelect } from "./RegionSelect";
import { CategoriesSelect } from "./InitialSidebar/CategoriesSelect";
import { PredictionSlider } from "./InitialSidebar/PredictionSlider";
import { useEffect, useState } from "react";
import { Tooltip } from "antd";
import { DISABLED_FILTER_TEXT, MODES } from "../../config";
import { useMode } from "../../stores/useMode";
import { DeltaTrafficSlider } from "./WorkingFilters/DeltaSlider";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../api";
import { FactTrafficSlider } from "./WorkingFilters/FactTrafficSlider";
import { AgeSlider } from "./WorkingFilters/AgeSlider";
export const Filters = ({ disabled }) => {
const [hover, setHover] = useState(false);
const { mode } = useMode();
useEffect(() => {
const timer = setTimeout(() => setHover(false), 1500);
return () => clearTimeout(timer);
}, [hover]);
const { data: fullRange } = useQuery(["max-min"], async () => {
const { data } = await api.get(`/api/placement_points/filters/`);
return data;
});
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
return (
<Tooltip
title={DISABLED_FILTER_TEXT}
placement="right"
open={disabled && hover}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="space-y-5">
<RegionSelect disabled={disabled} />
{mode === MODES.INITIAL && (
<>
<CategoriesSelect disabled={disabled} />
<PredictionSlider disabled={disabled} fullRange={fullRange} />
</>
)}
{mode === MODES.WORKING && (
<div className={"space-y-12"}>
<DeltaTrafficSlider fullRange={fullRange} />
<FactTrafficSlider fullRange={fullRange} />
<AgeSlider fullRange={fullRange} />
</div>
)}
</div>
</Tooltip>
);
};

@ -1,45 +0,0 @@
import { RegionSelect } from "../RegionSelect";
import { CategoriesSelect } from "./CategoriesSelect";
import { PredictionSlider } from "./PredictionSlider";
import { useEffect, useState } from "react";
import { Tooltip } from "antd";
import { DISABLED_FILTER_TEXT, MODES } from "../../../config";
import { useMode } from "../../../stores/useMode";
export const Filters = ({ disabled }) => {
const [hover, setHover] = useState(false);
const { mode } = useMode();
useEffect(() => {
const timer = setTimeout(() => setHover(false), 1500);
return () => clearTimeout(timer);
}, [hover]);
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
return (
<Tooltip
title={DISABLED_FILTER_TEXT}
placement="right"
open={disabled && hover}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="space-y-5">
<RegionSelect disabled={disabled} />
{mode === MODES.INITIAL && (
<>
<CategoriesSelect disabled={disabled} />
<PredictionSlider disabled={disabled} />
</>
)}
</div>
</Tooltip>
);
};

@ -1,28 +1,20 @@
import { SliderComponent as Slider } from "../../../components/SliderComponent"; import { SliderComponent as Slider } from "../../../components/SliderComponent";
import { INITIAL, useFilters } from "../../../stores/useFilters"; import { INITIAL, useFilters } from "../../../stores/useFilters";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../../api";
import { useEffect } from "react"; import { useEffect } from "react";
export const PredictionSlider = ({ disabled }) => { export const PredictionSlider = ({ disabled, fullRange }) => {
const { const {
filters: { prediction }, filters: { prediction },
setPrediction, setPrediction,
} = useFilters(); } = useFilters();
const { data } = useQuery(["max-min"], async () => {
const { data } = await api.get(`/api/placement_points/filters/`);
return data;
});
const handleAfterChange = (range) => setPrediction(range); const handleAfterChange = (range) => setPrediction(range);
useEffect(() => { useEffect(() => {
if (!data) return; if (!fullRange) return;
const min = data.prediction_current[0]; const min = fullRange.prediction_current[0];
const max = data.prediction_current[1]; const max = fullRange.prediction_current[1];
if ( if (
prediction[0] === INITIAL.prediction[0] && prediction[0] === INITIAL.prediction[0] &&
@ -30,15 +22,15 @@ export const PredictionSlider = ({ disabled }) => {
) { ) {
setPrediction([min, max]); setPrediction([min, max]);
} }
}, [data]); }, [fullRange]);
return ( return (
<Slider <Slider
title={"Прогнозный трафик"} title={"Прогнозный трафик"}
value={prediction} value={prediction}
onAfterChange={handleAfterChange} onAfterChange={handleAfterChange}
min={data?.prediction_current[0]} min={fullRange?.prediction_current[0]}
max={data?.prediction_current[1]} max={fullRange?.prediction_current[1]}
range range
disabled={disabled} disabled={disabled}
/> />

@ -4,7 +4,7 @@ import {
usePointSelection, usePointSelection,
} from "../../stores/usePointSelection"; } from "../../stores/usePointSelection";
import { TakeToWorkButton } from "./InitialSidebar/TakeToWorkButton"; import { TakeToWorkButton } from "./InitialSidebar/TakeToWorkButton";
import { Filters } from "./InitialSidebar/Filters"; import { Filters } from "./Filters";
import { Header } from "./Header"; import { Header } from "./Header";
import { useMode } from "../../stores/useMode"; import { useMode } from "../../stores/useMode";
import { MODES } from "../../config"; import { MODES } from "../../config";

@ -0,0 +1,34 @@
import { INITIAL, useFilters } from "../../../stores/useFilters";
import { useEffect } from "react";
import { SliderComponent as Slider } from "../../../components/SliderComponent";
export const AgeSlider = ({ fullRange }) => {
const {
filters: { age },
setAge,
} = useFilters();
const handleAfterChange = (range) => setAge(range);
useEffect(() => {
if (!fullRange) return;
const min = fullRange.age_day[0];
const max = fullRange.age_day[1];
if (age[0] === INITIAL.age[0] && age[1] === INITIAL.age[1]) {
setAge([min, max]);
}
}, [fullRange, age]);
return (
<Slider
title={"Зреслость постамата, дней"}
value={age}
onAfterChange={handleAfterChange}
min={fullRange?.age_day[0]}
max={fullRange?.age_day[1]}
range
/>
);
};

@ -0,0 +1,38 @@
import { INITIAL, useFilters } from "../../../stores/useFilters";
import { useEffect } from "react";
import { SliderComponent as Slider } from "../../../components/SliderComponent";
export const DeltaTrafficSlider = ({ fullRange }) => {
const {
filters: { deltaTraffic },
setDeltaTraffic,
} = useFilters();
const handleAfterChange = (range) => setDeltaTraffic(range);
useEffect(() => {
if (!fullRange) return;
const min = fullRange.delta_current[0];
const max = fullRange.delta_current[1];
if (
deltaTraffic[0] === INITIAL.deltaTraffic[0] &&
deltaTraffic[1] === INITIAL.deltaTraffic[1]
) {
setDeltaTraffic([min, max]);
}
}, [fullRange, deltaTraffic]);
return (
<Slider
title={"Расхождение факта с прогнозом, %"}
value={deltaTraffic}
onAfterChange={handleAfterChange}
min={fullRange?.delta_current[0]}
max={fullRange?.delta_current[1]}
range
showZeroMark={true}
/>
);
};

@ -0,0 +1,37 @@
import { INITIAL, useFilters } from "../../../stores/useFilters";
import { useEffect } from "react";
import { SliderComponent as Slider } from "../../../components/SliderComponent";
export const FactTrafficSlider = ({ fullRange }) => {
const {
filters: { factTraffic },
setFactTraffic,
} = useFilters();
const handleAfterChange = (range) => setFactTraffic(range);
useEffect(() => {
if (!fullRange) return;
const min = fullRange.fact[0];
const max = fullRange.fact[1];
if (
factTraffic[0] === INITIAL.factTraffic[0] &&
factTraffic[1] === INITIAL.factTraffic[1]
) {
setFactTraffic([min, max]);
}
}, [fullRange, factTraffic]);
return (
<Slider
title={"Фактический трафик"}
value={factTraffic}
onAfterChange={handleAfterChange}
min={fullRange?.fact[0]}
max={fullRange?.fact[1]}
range
/>
);
};

@ -5,25 +5,28 @@ import { STATUSES } from "../../../config";
import { getPoints } from "../../../api"; import { getPoints } from "../../../api";
import { useMergeTableData } from "../useMergeTableData"; import { useMergeTableData } from "../useMergeTableData";
import { Table } from "../Table"; import { Table } from "../Table";
import { columns } from "../InitialTable/columns";
import { HeaderWrapper } from "../HeaderWrapper"; import { HeaderWrapper } from "../HeaderWrapper";
import { useExportWorkingData } from "./useExportWorkingData"; import { useExportWorkingData } from "./useExportWorkingData";
import { useFilters } from "../../../stores/useFilters"; import { useFilters } from "../../../stores/useFilters";
import { columns } from "./columns";
export const WorkingTable = ({ fullWidth }) => { export const WorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE); const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const { const {
filters: { region }, filters: { region, deltaTraffic, factTraffic, age },
} = useFilters(); } = useFilters();
const { data, isInitialLoading } = useQuery( const { data, isInitialLoading } = useQuery(
["working-points", page, region], ["working-points", page, region, deltaTraffic, factTraffic, age],
async () => { async () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page, page,
page_size: pageSize, page_size: pageSize,
"status[]": [STATUSES.working], "status[]": [STATUSES.working],
"delta_current[]": deltaTraffic,
"fact[]": factTraffic,
"age_day[]": age,
}); });
return await getPoints(params, region); return await getPoints(params, region);

@ -0,0 +1,57 @@
export const columns = [
{
title: "Адрес",
dataIndex: "address",
key: "address",
width: 200,
},
{
title: "Район",
dataIndex: "rayon",
key: "rayon",
width: "120px",
ellipsis: true,
},
{
title: "Округ",
dataIndex: "okrug",
key: "okrug",
width: "120px",
ellipsis: true,
},
{
title: "Название",
dataIndex: "name",
key: "name",
width: "120px",
ellipsis: true,
},
{
title: "Категория",
dataIndex: "category",
key: "category",
width: "120px",
ellipsis: true,
},
{
title: "Факт",
dataIndex: "fact",
key: "fact",
width: "120px",
ellipsis: true,
},
{
title: "Расхождение с прогнозом",
dataIndex: "delta_current",
key: "delta_current",
width: "120px",
ellipsis: true,
},
{
title: "Зрелость",
dataIndex: "age_day",
key: "age_day",
width: "120px",
ellipsis: true,
},
];

@ -6,14 +6,17 @@ import { handleExportSuccess } from "../ExportButton";
export const useExportWorkingData = (enabled, onSettled) => { export const useExportWorkingData = (enabled, onSettled) => {
const { const {
filters: { region }, filters: { region, deltaTraffic, factTraffic, age },
} = useFilters(); } = useFilters();
return useQuery( return useQuery(
["export-working", region], ["export-working", region, deltaTraffic, factTraffic, age],
async () => { async () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
"status[]": [STATUSES.working], "status[]": [STATUSES.working],
"delta_current[]": deltaTraffic,
"fact[]": factTraffic,
"age_day[]": age,
}); });
return await exportPoints(params, region); return await exportPoints(params, region);

@ -6,7 +6,7 @@ export const useGetClickedPoint = (enabled, onSuccess) => {
const { clickedPointConfig } = useClickedPointConfig(); const { clickedPointConfig } = useClickedPointConfig();
const { data, isInitialLoading, isFetching } = useQuery( const { data, isInitialLoading, isFetching } = useQuery(
["clicked-point", clickedPointConfig], ["clicked-point", clickedPointConfig?.id],
async () => { async () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
"location_ids[]": [clickedPointConfig.id], "location_ids[]": [clickedPointConfig.id],

@ -7,6 +7,9 @@ export const INITIAL = {
categories: [], categories: [],
region: null, region: null,
status: [STATUSES.initial], status: [STATUSES.initial],
deltaTraffic: [-1000, 1000],
factTraffic: [0, 0],
age: [0, 0],
}; };
const store = (set) => ({ const store = (set) => ({
@ -17,6 +20,24 @@ const store = (set) => ({
}); });
}, },
setDeltaTraffic: (value) => {
set((state) => {
state.filters.deltaTraffic = value;
});
},
setFactTraffic: (value) => {
set((state) => {
state.filters.factTraffic = value;
});
},
setAge: (value) => {
set((state) => {
state.filters.age = value;
});
},
setCategories: (categories) => setCategories: (categories) =>
set((state) => { set((state) => {
state.filters.categories = categories; state.filters.categories = categories;

Loading…
Cancel
Save