Add working points filters

dev
Platon Yasev 3 years ago
parent 7ac62be115
commit e775c07c63

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

@ -4,16 +4,48 @@ import { useLayersVisibility } from "../../stores/useLayersVisibility";
import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants";
import { useFilters } from "../../stores/useFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.working];
export const WorkingPoints = () => {
const { isVisible } = useLayersVisibility();
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
? ["all", statusExpression, regionFilterExpression]
: statusExpression;
? [
"all",
statusExpression,
...deltaExpression,
...factExpression,
...ageExpression,
regionFilterExpression,
]
: [
"all",
statusExpression,
...deltaExpression,
...factExpression,
...ageExpression,
];
return (
<>

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

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

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

@ -83,3 +83,9 @@ export const residentialPointConfig = [
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 { FiltersIcon } from "../../icons/FiltersIcon";
import { FiltersIcon } from "../icons/FiltersIcon";
export const FiltersButton = ({ toggleCollapse }) => {
export const SidebarControl = ({ toggleCollapse }) => {
return (
<Button
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 { setAuth } from "./stores/auth";
import { api } from "./api";
import { setAuth } from "./stores/auth";
export function SignOut() {
const logOut = async () => {
await api.post("accounts/logout/");
setAuth(false);
};
return (
<div className="absolute top-[20px] right-[20px]">
<Button
icon={<LogoutOutlined />}
type="primary"
size="large"
title="Выйти"
onClick={async () => {
await api.post("accounts/logout/");
setAuth(false);
}}
/>
<Popover
content={
<Button type={"primary"} onClick={logOut}>
Выйти
</Button>
}
trigger="click"
placement={"bottomRight"}
>
<Button
icon={<LogoutOutlined />}
type="primary"
size="large"
title="Выйти"
/>
</Popover>
</div>
);
}

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

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

@ -34,6 +34,7 @@ export const SliderComponent = ({
disabled = false,
onMouseEnter,
onMouseLeave,
showZeroMark = false,
}) => {
const fullRangeMarks = {
[min]: <Mark value={min} />,
@ -72,13 +73,15 @@ export const SliderComponent = ({
onChange?.(value);
};
const finalMarks = showZeroMark ? { ...marks, 0: <Mark value={0} /> } : marks;
return (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<Title text={title} />
<Slider
range={range}
value={value}
marks={marks}
marks={finalMarks}
onChange={handleChange}
onAfterChange={handleAfterChange}
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 { INITIAL, useFilters } from "../../../stores/useFilters";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../../api";
import { useEffect } from "react";
export const PredictionSlider = ({ disabled }) => {
export const PredictionSlider = ({ disabled, fullRange }) => {
const {
filters: { prediction },
setPrediction,
} = useFilters();
const { data } = useQuery(["max-min"], async () => {
const { data } = await api.get(`/api/placement_points/filters/`);
return data;
});
const handleAfterChange = (range) => setPrediction(range);
useEffect(() => {
if (!data) return;
if (!fullRange) return;
const min = data.prediction_current[0];
const max = data.prediction_current[1];
const min = fullRange.prediction_current[0];
const max = fullRange.prediction_current[1];
if (
prediction[0] === INITIAL.prediction[0] &&
@ -30,15 +22,15 @@ export const PredictionSlider = ({ disabled }) => {
) {
setPrediction([min, max]);
}
}, [data]);
}, [fullRange]);
return (
<Slider
title={"Прогнозный трафик"}
value={prediction}
onAfterChange={handleAfterChange}
min={data?.prediction_current[0]}
max={data?.prediction_current[1]}
min={fullRange?.prediction_current[0]}
max={fullRange?.prediction_current[1]}
range
disabled={disabled}
/>

@ -4,7 +4,7 @@ import {
usePointSelection,
} from "../../stores/usePointSelection";
import { TakeToWorkButton } from "./InitialSidebar/TakeToWorkButton";
import { Filters } from "./InitialSidebar/Filters";
import { Filters } from "./Filters";
import { Header } from "./Header";
import { useMode } from "../../stores/useMode";
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 { useMergeTableData } from "../useMergeTableData";
import { Table } from "../Table";
import { columns } from "../InitialTable/columns";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportWorkingData } from "./useExportWorkingData";
import { useFilters } from "../../../stores/useFilters";
import { columns } from "./columns";
export const WorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1);
const {
filters: { region },
filters: { region, deltaTraffic, factTraffic, age },
} = useFilters();
const { data, isInitialLoading } = useQuery(
["working-points", page, region],
["working-points", page, region, deltaTraffic, factTraffic, age],
async () => {
const params = new URLSearchParams({
page,
page_size: pageSize,
"status[]": [STATUSES.working],
"delta_current[]": deltaTraffic,
"fact[]": factTraffic,
"age_day[]": age,
});
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) => {
const {
filters: { region },
filters: { region, deltaTraffic, factTraffic, age },
} = useFilters();
return useQuery(
["export-working", region],
["export-working", region, deltaTraffic, factTraffic, age],
async () => {
const params = new URLSearchParams({
"status[]": [STATUSES.working],
"delta_current[]": deltaTraffic,
"fact[]": factTraffic,
"age_day[]": age,
});
return await exportPoints(params, region);

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

@ -7,6 +7,9 @@ export const INITIAL = {
categories: [],
region: null,
status: [STATUSES.initial],
deltaTraffic: [-1000, 1000],
factTraffic: [0, 0],
age: [0, 0],
};
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) =>
set((state) => {
state.filters.categories = categories;

Loading…
Cancel
Save