Split filters and add clearing

dev
Platon Yasev 3 years ago
parent 581a312238
commit f73e799271

2
.gitignore vendored

@ -8,7 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
# dist
dist
# dist-ssr
*.local

File diff suppressed because one or more lines are too long

@ -1 +0,0 @@
function s(r,i){for(var o=0;o<i.length;o++){const e=i[o];if(typeof e!="string"&&!Array.isArray(e)){for(const t in e)if(t!=="default"&&!(t in r)){const n=Object.getOwnPropertyDescriptor(e,t);n&&Object.defineProperty(r,t,n.get?n:{enumerable:!0,get:()=>e[t]})}}}return Object.freeze(Object.defineProperty(r,Symbol.toStringTag,{value:"Module"}))}var a={},c=a.printMsg=function(){console.log("This is a message from the demo package")};const f=s({__proto__:null,printMsg:c,default:a},[a]);export{f as i};

File diff suppressed because one or more lines are too long

BIN
dist/favicon.ico vendored

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

16
dist/index.html vendored

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<link href="/vite.svg" rel="icon" type="image/svg+xml"/>
<link href="/favicon.ico" rel="icon"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>PostNet by Spatial</title>
<script crossorigin src="/assets/index.17466bb7.js" type="module"></script>
<link href="/assets/index.ca3dcc1b.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
</body>
</html>

1
dist/vite.svg vendored

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@ -7,15 +7,19 @@ import { MapPage } from "./pages/Map";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { enableMapSet } from "immer";
import { mountStoreDevtool } from "simple-zustand-devtools";
import { useFilters } from "./stores/useFilters";
import { usePointSelection } from "./stores/usePointSelection";
import { usePendingPointsFilters } from "./stores/usePendingPointsFilters";
import { useOnApprovalPointsFilters } from "./stores/useOnApprovalPointsFilters";
import { useWorkingPointsFilters } from "./stores/useWorkingPointsFilters";
const queryClient = new QueryClient();
enableMapSet();
if (import.meta.env.MODE === "development") {
mountStoreDevtool("Filters", useFilters);
mountStoreDevtool("PendingFilters", usePendingPointsFilters);
mountStoreDevtool("OnApprovalFilters", useOnApprovalPointsFilters);
mountStoreDevtool("WorkingFilters", useWorkingPointsFilters);
mountStoreDevtool("PointSelection", usePointSelection);
}

@ -5,16 +5,21 @@ import { MODES, STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants";
import { useMode } from "../../stores/useMode";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.cancelled];
export const CancelledPoints = () => {
const { isVisible } = useLayersVisibility();
const regionFilterExpression = useRegionFilterExpression();
const {
filters: { region },
} = useOnApprovalPointsFilters();
const regionFilterExpression = useRegionFilterExpression(region);
const { mode } = useMode();
const getFilter = () => {
if (mode === MODES.APPROVE_WORKING) {
if (mode === MODES.ON_APPROVAL) {
return regionFilterExpression
? ["all", statusExpression, regionFilterExpression]
: statusExpression;

@ -4,16 +4,16 @@ import { useLayersVisibility } from "../../stores/useLayersVisibility";
import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants";
import { useFilters } from "../../stores/useFilters";
import { useWorkingPointsFilters } from "../../stores/useWorkingPointsFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.working];
export const FilteredWorkingPoints = () => {
const { isVisible } = useLayersVisibility();
const regionFilterExpression = useRegionFilterExpression();
const {
filters: { deltaTraffic, factTraffic, age },
} = useFilters();
filters: { deltaTraffic, factTraffic, age, region },
} = useWorkingPointsFilters();
const regionFilterExpression = useRegionFilterExpression(region);
const deltaExpression = [
[">=", ["get", "delta_current"], deltaTraffic[0]],

@ -1,16 +1,12 @@
import { Points } from "./Points";
import { Layer, Source } from "react-map-gl";
import { aoLayer, rayonLayer, selectedRegionLayer } from "./layers-config";
import { useFilters } from "../../stores/useFilters";
import { aoLayer, rayonLayer } from "./layers-config";
import { BASE_URL } from "../../api";
import { PVZ } from "./PVZ";
import { OtherPostamates } from "./OtherPostamates";
import { SelectedRegion } from "./SelectedRegion";
export const Layers = () => {
const {
filters: { prediction, region },
} = useFilters();
return (
<>
<Source
@ -39,17 +35,7 @@ export const Layers = () => {
/>
</Source>
{region && region.geometry && (
<Source id="selected-region" type="geojson" data={region.geometry}>
<Layer
{...selectedRegionLayer}
layout={{
...selectedRegionLayer.layout,
visibility: region ? "visible" : "none",
}}
/>
</Source>
)}
<SelectedRegion />
<Source
id="rivals"
@ -60,7 +46,7 @@ export const Layers = () => {
<OtherPostamates />
</Source>
<Points prediction={prediction} />
<Points />
</>
);
};

@ -4,12 +4,16 @@ import { useLayersVisibility } from "../../stores/useLayersVisibility";
import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.approve];
const statusExpression = ["==", ["get", "status"], STATUSES.onApproval];
export const ApprovePoints = () => {
export const OnApprovalPoints = () => {
const { isVisible } = useLayersVisibility();
const regionFilterExpression = useRegionFilterExpression();
const {
filters: { region },
} = useOnApprovalPointsFilters();
const regionFilterExpression = useRegionFilterExpression(region);
const filter = regionFilterExpression
? ["all", statusExpression, regionFilterExpression]

@ -4,22 +4,22 @@ import {
unmatchInitialPointLayer,
} from "./layers-config";
import { useLayersVisibility } from "../../stores/useLayersVisibility";
import { useFilters } from "../../stores/useFilters";
import { usePointSelection } from "../../stores/usePointSelection";
import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants";
import { usePendingPointsFilters } from "../../stores/usePendingPointsFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.initial];
const statusExpression = ["==", ["get", "status"], STATUSES.pending];
const useFilterExpression = () => {
const { filters } = useFilters();
const { filters } = usePendingPointsFilters();
const { prediction, categories, region } = filters;
const { selection } = usePointSelection();
const includedArr = [...selection.included];
const excludedArr = [...selection.excluded];
const regionExpression = useRegionFilterExpression();
const regionExpression = useRegionFilterExpression(region);
const includedExpression = ["in", ["get", "id"], ["literal", includedArr]];
const excludedExpression = ["in", ["get", "id"], ["literal", excludedArr]];
@ -39,7 +39,7 @@ const useFilterExpression = () => {
["!", excludedExpression],
[
"any",
region
regionExpression
? ["all", ...predictionExpression, categoryExpression, regionExpression]
: ["all", ...predictionExpression, categoryExpression],
includedExpression,
@ -54,7 +54,7 @@ const useFilterExpression = () => {
"any",
[
"!",
region
regionExpression
? [
"all",
...predictionExpression,
@ -70,7 +70,7 @@ const useFilterExpression = () => {
return { match: matchFilterExpression, unmatch: unmatchFilterExpression };
};
export const InitialPoints = () => {
export const PendingPoints = () => {
const { isVisible } = useLayersVisibility();
const { match: matchFilterExpression, unmatch: unmatchFilterExpression } =
useFilterExpression();

@ -1,8 +1,8 @@
import { Source } from "react-map-gl";
import { BASE_URL } from "../../api";
import { useUpdateLayerCounter } from "../../stores/useUpdateLayerCounter";
import { InitialPoints } from "./InitialPoints";
import { ApprovePoints } from "./ApprovePoints";
import { PendingPoints } from "./PendingPoints";
import { OnApprovalPoints } from "./OnApprovalPoints";
import { WorkingPoints } from "./WorkingPoints";
import { FilteredWorkingPoints } from "./FilteredWorkingPoints";
import { CancelledPoints } from "./CancelledPoints";
@ -20,8 +20,8 @@ export const Points = () => {
`${BASE_URL}/martin/public.service_placementpoint/{z}/{x}/{y}.pbf`,
]}
>
<InitialPoints />
<ApprovePoints />
<PendingPoints />
<OnApprovalPoints />
<WorkingPoints />
<FilteredWorkingPoints />
<CancelledPoints />

@ -0,0 +1,52 @@
import { Layer, Source } from "react-map-gl";
import { selectedRegionLayer } from "./layers-config";
import { usePendingPointsFilters } from "../../stores/usePendingPointsFilters";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
import { useWorkingPointsFilters } from "../../stores/useWorkingPointsFilters";
import { useMode } from "../../stores/useMode";
import { MODES } from "../../config";
const SelectedRegionLayer = ({ data }) => {
return (
<Source id="selected-region" type="geojson" data={data}>
<Layer {...selectedRegionLayer} />
</Source>
);
};
export const SelectedRegion = () => {
const {
filters: { region: pendingRegion },
} = usePendingPointsFilters();
const {
filters: { region: onApprovalRegion },
} = useOnApprovalPointsFilters();
const {
filters: { region: workingRegion },
} = useWorkingPointsFilters();
const { mode } = useMode();
const shouldRenderPendingRegion =
mode === MODES.PENDING && pendingRegion?.geometry;
const shouldRenderOnApprovalRegion =
mode === MODES.ON_APPROVAL && onApprovalRegion?.geometry;
const shouldRenderWorkingRegion =
mode === MODES.WORKING && workingRegion?.geometry;
return (
<>
{shouldRenderPendingRegion && (
<SelectedRegionLayer data={pendingRegion.geometry} />
)}
{shouldRenderOnApprovalRegion && (
<SelectedRegionLayer data={onApprovalRegion.geometry} />
)}
{shouldRenderWorkingRegion && (
<SelectedRegionLayer data={workingRegion.geometry} />
)}
</>
);
};

@ -5,16 +5,21 @@ import { MODES, STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants";
import { useMode } from "../../stores/useMode";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
const statusExpression = ["==", ["get", "status"], STATUSES.working];
export const WorkingPoints = () => {
const { isVisible } = useLayersVisibility();
const regionFilterExpression = useRegionFilterExpression();
const {
filters: { region },
} = useOnApprovalPointsFilters();
const regionFilterExpression = useRegionFilterExpression(region);
const { mode } = useMode();
const getFilter = () => {
if (mode === MODES.APPROVE_WORKING) {
if (mode === MODES.ON_APPROVAL) {
return regionFilterExpression
? ["all", statusExpression, regionFilterExpression]
: statusExpression;

@ -1,4 +1,3 @@
import { useFilters } from "../../stores/useFilters";
import { useMemo } from "react";
const REGION_FIELD_MAPPER = {
@ -6,11 +5,7 @@ const REGION_FIELD_MAPPER = {
rayon: "rayon_id",
};
export const useRegionFilterExpression = () => {
const {
filters: { region },
} = useFilters();
export const useRegionFilterExpression = (region) => {
return useMemo(
() =>
region

@ -10,7 +10,7 @@ export const LayersVisibility = () => {
return (
<div className={"space-y-1 flex flex-col"}>
{mode === MODES.INITIAL && (
{mode === MODES.PENDING && (
<>
<Checkbox
className={"!ml-0"}

@ -31,14 +31,14 @@ export function Legend() {
<div>
<Title text={"Статус локации"} className="text-center" />
<div className="space-y-1">
{mode === MODES.INITIAL && (
{mode === MODES.PENDING && (
<>
<LegendPointItem name="К рассмотрению" color={INITIAL_COLOR} />
<LegendPointItem name="Работает" color={WORKING_COLOR} />
<LegendPointItem name="Отменен" color={CANCELLED_COLOR} />
</>
)}
{mode === MODES.APPROVE_WORKING && (
{mode === MODES.ON_APPROVAL && (
<>
<LegendPointItem
name="Согласование-установка"

@ -11,12 +11,8 @@ import { usePopup } from "../stores/usePopup";
import { useClickedPointConfig } from "../stores/useClickedPointConfig";
import { Legend } from "./Legend";
import { TableWrapper } from "../modules/Table/TableWrapper";
import { useFilters } from "../stores/useFilters";
import { useMode } from "../stores/useMode";
import {
MODE_TO_LAYER_VISIBILITY_MAPPER,
MODE_TO_STATUS_MAPPER,
} from "../config";
import { MODE_TO_LAYER_VISIBILITY_MAPPER } from "../config";
import { SidebarControl } from "./SidebarControl";
import { LayersControl } from "./LayersControl/LayersControl";
import { useTable } from "../stores/useTable";
@ -24,10 +20,6 @@ import { twMerge } from "tailwind-merge";
import { useLayersVisibility } from "../stores/useLayersVisibility";
import { LAYER_IDS } from "./Layers/constants";
import { Header } from "./Header";
import voyagerStyle from "./voyager.json";
import { fromJS } from "immutable";
const immutableVoyagerStyle = fromJS(voyagerStyle);
export const MapComponent = () => {
const mapRef = useRef(null);
@ -36,12 +28,10 @@ export const MapComponent = () => {
const { popup, setPopup } = usePopup();
const { setClickedPointConfig } = useClickedPointConfig();
const { setLayersVisibility } = useLayersVisibility();
const { setStatus } = useFilters();
const { mode } = useMode();
const { tableState, openTable } = useTable();
useEffect(() => {
setStatus(MODE_TO_STATUS_MAPPER[mode]);
setLayersVisibility(MODE_TO_LAYER_VISIBILITY_MAPPER[mode]);
setPopup(null);
setClickedPointConfig(null);

@ -21,7 +21,7 @@ const SingleFeaturePopup = ({ feature }) => {
return <FeatureProperties feature={feature} />;
}
if (mode === MODES.APPROVE_WORKING) {
if (mode === MODES.ON_APPROVAL) {
return <ApproveWorkingPointPopup feature={feature} />;
}
@ -29,7 +29,7 @@ const SingleFeaturePopup = ({ feature }) => {
return <WorkingPointPopup feature={feature} />;
}
if (mode === MODES.INITIAL && isInitialLayer)
if (mode === MODES.PENDING && isInitialLayer)
return <InitialPointPopup feature={feature} />;
return <FeatureProperties feature={feature} />;

@ -17,7 +17,7 @@ export const ApproveWorkingPointPopup = ({ feature }) => {
const { mutate: updateStatus } = useUpdateStatus({
onSuccess: () => {
queryClient.invalidateQueries(["approve-working-points"]);
queryClient.invalidateQueries(["on-approval-points"]);
queryClient.invalidateQueries(["clicked-point", featureId]);
},
});

@ -1,8 +1,8 @@
import axios from "axios";
import { useQuery } from "@tanstack/react-query";
import { STATUSES } from "./config";
import { useFilters } from "./stores/useFilters";
import { usePointSelection } from "./stores/usePointSelection";
import { usePendingPointsFilters } from "./stores/usePendingPointsFilters";
export const BASE_URL = "https://postnet-dev.selftech.ru";
@ -60,7 +60,7 @@ export const useGetTotalInitialPointsCount = () => {
const params = new URLSearchParams({
page: 1,
page_size: 1,
"status[]": [STATUSES.initial],
"status[]": [STATUSES.pending],
});
return await getPoints(params);
@ -69,8 +69,8 @@ export const useGetTotalInitialPointsCount = () => {
);
};
export const useGetFilteredInitialPointsCount = () => {
const { filters } = useFilters();
export const useGetFilteredPendingPointsCount = () => {
const { filters } = usePendingPointsFilters();
const { prediction, categories, region } = filters;
const {
selection: { included },
@ -85,7 +85,7 @@ export const useGetFilteredInitialPointsCount = () => {
page: 1,
page_size: 1,
"prediction_current[]": prediction,
"status[]": [STATUSES.initial],
"status[]": [STATUSES.pending],
"categories[]": categories,
"included[]": includedIds,
});

@ -0,0 +1,9 @@
import { Button } from "antd";
export const ClearFiltersButton = ({ onClick, disabled }) => {
return (
<Button block className={"mt-2"} onClick={onClick} disabled={disabled}>
Сбросить фильтры
</Button>
);
};

@ -23,8 +23,8 @@ export const ModeSelector = () => {
<Tooltip title="Отбор локаций для работы">
<Button
icon={<AIIcon />}
type={getType(MODES.INITIAL)}
onClick={() => handleClick(MODES.INITIAL)}
type={getType(MODES.PENDING)}
onClick={() => handleClick(MODES.PENDING)}
className="flex items-center justify-center"
size="large"
/>
@ -33,8 +33,8 @@ export const ModeSelector = () => {
<Tooltip title="Управление статусами локаций">
<Button
icon={<ApproveIcon />}
type={getType(MODES.APPROVE_WORKING)}
onClick={() => handleClick(MODES.APPROVE_WORKING)}
type={getType(MODES.ON_APPROVAL)}
onClick={() => handleClick(MODES.ON_APPROVAL)}
className="flex items-center justify-center"
size="large"
/>

@ -1,18 +1,15 @@
import { Empty, TreeSelect } from "antd";
import { Title } from "../../components/Title";
import { useEffect, useMemo, useState } from "react";
import { Title } from "./Title";
import { useMap } from "react-map-gl";
import getBbox from "@turf/bbox";
import { polygon as getPolygon } from "@turf/helpers";
import { api } from "../../api";
import { useFilters } from "../../stores/useFilters";
import { api } from "../api";
import parse from "wellknown";
import { useQuery } from "@tanstack/react-query";
const { TreeNode } = TreeSelect;
const normalizeRegions = (rawRegions) => {
if (!rawRegions) return {};
return rawRegions.reduce((acc, ao) => {
acc[ao.id] = ao;
acc[ao.id].type = "ao";
@ -26,43 +23,43 @@ const normalizeRegions = (rawRegions) => {
}, {});
};
export const RegionSelect = ({ disabled }) => {
const { map } = useMap();
const {
filters: { region },
setRegion,
} = useFilters();
const [data, setData] = useState([]);
const normalizedData = useMemo(() => normalizeRegions(data), [data]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const getRegions = async () => {
setLoading(true);
try {
const response = await api.get("/api/ao_rayons");
setData(response.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
const useGetRegions = () => {
return useQuery(
["regions"],
async () => {
const { data } = await api.get("/api/ao_rayons");
return data;
},
{
select: (rawRegions) => {
return {
raw: rawRegions,
normalized: normalizeRegions(rawRegions),
};
},
}
);
};
getRegions();
}, []);
export const RegionSelect = ({
disabled,
value: selectedRegionId,
onChange,
}) => {
const { map } = useMap();
const { data, isInitialLoading } = useGetRegions();
const onChange = (value) => {
const handleChange = (value) => {
if (!value) return;
const selectedRegion = normalizedData[value];
const selectedRegion = data.normalized[value];
const polygonWrapperGeom = parse(selectedRegion.polygon);
const polygon = getPolygon(polygonWrapperGeom.coordinates[0]);
const bbox = getBbox(polygon);
setRegion({ id: value, geometry: polygon, type: selectedRegion.type });
onChange({ id: value, geometry: polygon, type: selectedRegion.type });
map.fitBounds(
[
@ -75,7 +72,7 @@ export const RegionSelect = ({ disabled }) => {
);
};
const handleClear = () => setRegion(null);
const handleClear = () => onChange(null);
return (
<div>
@ -83,13 +80,13 @@ export const RegionSelect = ({ disabled }) => {
<TreeSelect
showSearch
style={{ width: "100%" }}
value={region?.id}
value={selectedRegionId}
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
placeholder="Выберите АО или район"
allowClear
treeDefaultExpandAll={false}
onChange={onChange}
loading={loading}
onChange={handleChange}
loading={isInitialLoading}
treeNodeFilterProp="title"
onClear={handleClear}
notFoundContent={
@ -100,7 +97,7 @@ export const RegionSelect = ({ disabled }) => {
}
disabled={disabled}
>
{data?.map((ao) => {
{data?.raw.map((ao) => {
return (
<TreeNode key={ao.id} value={ao.id} title={ao.name}>
{ao.rayons?.map((rayon) => (

@ -2,8 +2,11 @@ import { Select } from "antd";
import { STATUS_LABEL_MAPPER, STATUSES } from "../config";
const statusOptions = [
{ label: STATUS_LABEL_MAPPER[STATUSES.initial], value: STATUSES.initial },
{ label: STATUS_LABEL_MAPPER[STATUSES.approve], value: STATUSES.approve },
{ label: STATUS_LABEL_MAPPER[STATUSES.pending], value: STATUSES.pending },
{
label: STATUS_LABEL_MAPPER[STATUSES.onApproval],
value: STATUSES.onApproval,
},
{ label: STATUS_LABEL_MAPPER[STATUSES.working], value: STATUSES.working },
{ label: STATUS_LABEL_MAPPER[STATUSES.cancelled], value: STATUSES.cancelled },
];

@ -1,15 +1,15 @@
import { LAYER_IDS } from "./Map/Layers/constants";
export const STATUSES = {
initial: "Pending",
approve: "Installation",
pending: "Pending",
onApproval: "Installation",
working: "Working",
cancelled: "Cancelled",
};
export const STATUS_LABEL_MAPPER = {
[STATUSES.initial]: "К рассмотрению",
[STATUSES.approve]: "Согласование-установка",
[STATUSES.pending]: "К рассмотрению",
[STATUSES.onApproval]: "Согласование-установка",
[STATUSES.working]: "Работает",
[STATUSES.cancelled]: "Отменен",
};
@ -28,20 +28,20 @@ export const DISABLED_FILTER_TEXT =
"Фильтр заблокирован - было ручное редактирование";
export const MODES = {
INITIAL: "INITIAL",
APPROVE_WORKING: "APPROVE_WORKING",
PENDING: "INITIAL",
ON_APPROVAL: "ON_APPROVAL",
WORKING: "WORKING",
};
export const MODE_TO_STATUS_MAPPER = {
[MODES.INITIAL]: [STATUSES.initial],
[MODES.APPROVE_WORKING]: [STATUSES.approve, STATUSES.working],
[MODES.PENDING]: [STATUSES.pending],
[MODES.ON_APPROVAL]: [STATUSES.onApproval, STATUSES.working],
[MODES.WORKING]: [STATUSES.working],
};
export const MODE_TO_LAYER_VISIBILITY_MAPPER = {
[MODES.INITIAL]: [LAYER_IDS.initial, LAYER_IDS.working, LAYER_IDS.cancelled],
[MODES.APPROVE_WORKING]: [
[MODES.PENDING]: [LAYER_IDS.initial, LAYER_IDS.working, LAYER_IDS.cancelled],
[MODES.ON_APPROVAL]: [
LAYER_IDS.approve,
LAYER_IDS.working,
LAYER_IDS.cancelled,

@ -1,63 +0,0 @@
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,13 +0,0 @@
import { Logo } from "../../icons/Logo";
import { ModeSelector } from "../../components/ModeSelector";
export const Header = () => {
return (
<div className="mb-4 flex items-center justify-between">
<Logo />
<div className={"flex items-center gap-x-3"}>
<ModeSelector />
</div>
</div>
);
};

@ -0,0 +1,59 @@
import { Title } from "../../../components/Title";
import { Checkbox } from "antd";
import { LAYER_IDS } from "../../../Map/Layers/constants";
import { RegionSelect } from "../../../components/RegionSelect";
import { useOnApprovalPointsFilters } from "../../../stores/useOnApprovalPointsFilters";
import { useLayersVisibility } from "../../../stores/useLayersVisibility";
import { ClearFiltersButton } from "../../../components/ClearFiltersButton";
export const OnApprovalPointsFilters = () => {
const {
filters: { region },
setRegion,
clear,
} = useOnApprovalPointsFilters();
const { isVisible, toggleVisibility, showLayers } = useLayersVisibility();
const hasActiveFilters =
region ||
!isVisible[LAYER_IDS.approve] ||
!isVisible[LAYER_IDS.working] ||
!isVisible[LAYER_IDS.cancelled];
const clearFilters = () => {
clear();
showLayers([LAYER_IDS.approve, LAYER_IDS.working, LAYER_IDS.cancelled]);
};
return (
<div className="space-y-4">
<RegionSelect value={region?.id} onChange={setRegion} />
<div>
<Title text="Статусы" />
<div className="flex flex-col space-y-2">
<Checkbox
onChange={() => toggleVisibility(LAYER_IDS.approve)}
checked={isVisible[LAYER_IDS.approve]}
>
Согласование-установка
</Checkbox>
<Checkbox
className={"!ml-0"}
onChange={() => toggleVisibility(LAYER_IDS.working)}
checked={isVisible[LAYER_IDS.working]}
>
Работает
</Checkbox>
<Checkbox
className={"!ml-0"}
onChange={() => toggleVisibility(LAYER_IDS.cancelled)}
checked={isVisible[LAYER_IDS.cancelled]}
>
Отменен
</Checkbox>
</div>
</div>
{hasActiveFilters && <ClearFiltersButton onClick={clearFilters} />}
</div>
);
};

@ -1,7 +1,7 @@
import { Select } from "antd";
import { Title } from "../../../components/Title";
import { useFilters } from "../../../stores/useFilters";
import { CATEGORIES } from "../../../config";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
const options = Object.entries(CATEGORIES).map(([_key, value]) => {
return {
@ -11,7 +11,10 @@ const options = Object.entries(CATEGORIES).map(([_key, value]) => {
});
export const CategoriesSelect = ({ disabled }) => {
const { filters, setCategories } = useFilters();
const {
filters: { categories },
setCategories,
} = usePendingPointsFilters();
return (
<div>
@ -26,7 +29,7 @@ export const CategoriesSelect = ({ disabled }) => {
onChange={setCategories}
options={options}
allowClear={true}
value={filters.categories}
value={categories}
disabled={disabled}
/>
</div>

@ -0,0 +1,90 @@
import { DISABLED_FILTER_TEXT } from "../../../config";
import { Button, Tooltip } from "antd";
import { SelectedLocations } from "./SelectedLocations";
import { TakeToWorkButton } from "./TakeToWorkButton";
import { RegionSelect } from "../../../components/RegionSelect";
import { CategoriesSelect } from "./CategoriesSelect";
import { PredictionSlider } from "./PredictionSlider";
import { useEffect, useState } from "react";
import {
useHasManualEdits,
usePointSelection,
} from "../../../stores/usePointSelection";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
import { ClearFiltersButton } from "../../../components/ClearFiltersButton";
import { getDynamicActiveFilters } from "../utils";
export const PendingPointsFilters = ({ fullRange }) => {
const hasManualEdits = useHasManualEdits();
const { reset: resetPointSelection } = usePointSelection();
const { filters, setRegion, clear } = usePendingPointsFilters();
const [hover, setHover] = useState(false);
useEffect(() => {
const timer = setTimeout(() => setHover(false), 1500);
return () => clearTimeout(timer);
}, [hover]);
const handleMouseEnter = () => {
setHover(true);
};
const handleMouseLeave = () => {
setHover(false);
};
const activeDynamicFilters = getDynamicActiveFilters(filters, fullRange, [
"prediction",
]);
const clearFilters = () => clear(fullRange?.prediction);
const hasActiveFilters =
filters.region ||
activeDynamicFilters.prediction ||
filters.categories.length !== 0;
return (
<div className="flex flex-col flex-1 justify-between">
<div>
<Tooltip
title={DISABLED_FILTER_TEXT}
placement="right"
open={hasManualEdits && hover}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="space-y-5">
<RegionSelect
disabled={hasManualEdits}
value={filters.region?.id}
onChange={setRegion}
/>
<CategoriesSelect disabled={hasManualEdits} />
<PredictionSlider disabled={hasManualEdits} fullRange={fullRange} />
</div>
{hasActiveFilters && (
<ClearFiltersButton
onClick={clearFilters}
disabled={hasManualEdits}
/>
)}
</Tooltip>
{hasManualEdits ? (
<Button block className={"mt-2"} onClick={resetPointSelection}>
Отменить ручное редактирование
</Button>
) : null}
</div>
<div>
<SelectedLocations />
<TakeToWorkButton />
</div>
</div>
);
};

@ -1,25 +1,29 @@
import { SliderComponent as Slider } from "../../../components/SliderComponent";
import { INITIAL, useFilters } from "../../../stores/useFilters";
import { useEffect } from "react";
import {
INITIAL,
usePendingPointsFilters,
} from "../../../stores/usePendingPointsFilters";
export const PredictionSlider = ({ disabled, fullRange }) => {
const {
filters: { prediction },
setPrediction,
} = useFilters();
} = usePendingPointsFilters();
const handleAfterChange = (range) => setPrediction(range);
useEffect(() => {
if (!fullRange) return;
const min = fullRange.prediction_current[0];
const max = fullRange.prediction_current[1];
const min = fullRange.prediction[0];
const max = fullRange.prediction[1];
if (
const shouldSetFullRange =
prediction[0] === INITIAL.prediction[0] &&
prediction[1] === INITIAL.prediction[1]
) {
prediction[1] === INITIAL.prediction[1];
if (shouldSetFullRange) {
setPrediction([min, max]);
}
}, [fullRange]);
@ -29,8 +33,8 @@ export const PredictionSlider = ({ disabled, fullRange }) => {
title={"Прогнозный трафик"}
value={prediction}
onAfterChange={handleAfterChange}
min={fullRange?.prediction_current[0]}
max={fullRange?.prediction_current[1]}
min={fullRange?.prediction[0]}
max={fullRange?.prediction[1]}
range
disabled={disabled}
/>

@ -1,5 +1,5 @@
import {
useGetFilteredInitialPointsCount,
useGetFilteredPendingPointsCount,
useGetTotalInitialPointsCount,
} from "../../../api";
import { usePointSelection } from "../../../stores/usePointSelection";
@ -9,7 +9,7 @@ export const SelectedLocations = () => {
const { data: totalCount, isInitialLoading: isTotalLoading } =
useGetTotalInitialPointsCount();
const { data: filteredCount, isInitialLoading: isFilteredLoading } =
useGetFilteredInitialPointsCount();
useGetFilteredPendingPointsCount();
const showSpinner = isTotalLoading || isFilteredLoading;

@ -1,15 +1,15 @@
import { Alert, Button, Modal, Popover } from "antd";
import { useQueryClient } from "@tanstack/react-query";
import { useFilters } from "../../../stores/useFilters";
import { usePointSelection } from "../../../stores/usePointSelection";
import { STATUSES } from "../../../config";
import { useState } from "react";
import { useUpdateStatus } from "../../../hooks/useUpdateStatus";
import { ArrowRightOutlined } from "@ant-design/icons";
import { Title } from "../../../components/Title";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
export const TakeToWorkButton = () => {
const { filters } = useFilters();
const { filters } = usePendingPointsFilters();
const { prediction, categories, region } = filters;
const { selection } = usePointSelection();
const queryClient = useQueryClient();
@ -27,7 +27,7 @@ export const TakeToWorkButton = () => {
const takeToWork = () => {
const params = new URLSearchParams({
status: STATUSES.approve,
status: STATUSES.onApproval,
"prediction_current[]": prediction,
"categories[]": categories,
"included[]": [...selection.included],

@ -1,24 +1,51 @@
import { Button, Checkbox } from "antd";
import {
useHasManualEdits,
usePointSelection,
} from "../../stores/usePointSelection";
import { TakeToWorkButton } from "./InitialSidebar/TakeToWorkButton";
import { Filters } from "./Filters";
import { useMode } from "../../stores/useMode";
import { MODES } from "../../config";
import { twMerge } from "tailwind-merge";
import { SelectedLocations } from "./InitialSidebar/SelectedLocations";
import { forwardRef } from "react";
import { LAYER_IDS } from "../../Map/Layers/constants";
import { useLayersVisibility } from "../../stores/useLayersVisibility";
import { Title } from "../../components/Title";
import { MODES } from "../../config";
import { PendingPointsFilters } from "./PendingPointsFilters/PendingPointsFilters";
import { OnApprovalPointsFilters } from "./OnApprovalPointsFilters/OnApprovalPointsFilters";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../api";
import { WorkingPointsFilters } from "./WorkingPointsFilters/WorkingPointsFilters";
const useGetDataRange = () => {
return useQuery(
["max-min"],
async () => {
const { data } = await api.get(`/api/placement_points/filters/`);
return data;
},
{
select: (data) => {
return {
...data,
prediction: [data.prediction_current[0], data.prediction_current[1]],
deltaTraffic: [data.delta_current[0], data.delta_current[1]],
factTraffic: [data.fact[0], data.fact[1]],
age: [data.age_day[0], data.age_day[1]],
};
},
}
);
};
export const Sidebar = forwardRef(({ isCollapsed }, ref) => {
const hasManualEdits = useHasManualEdits();
const { mode } = useMode();
const { reset: resetSelection } = usePointSelection();
const { isVisible, toggleVisibility } = useLayersVisibility();
const { data: filtersValueRange } = useGetDataRange();
const getFilters = () => {
if (mode === MODES.PENDING) {
return <PendingPointsFilters fullRange={filtersValueRange} />;
}
if (mode === MODES.ON_APPROVAL) {
return <OnApprovalPointsFilters />;
}
return <WorkingPointsFilters fullRange={filtersValueRange} />;
};
return (
<div
@ -28,54 +55,7 @@ export const Sidebar = forwardRef(({ isCollapsed }, ref) => {
)}
ref={ref}
>
<div className="space-y-5 flex flex-col justify-between flex-1">
<div>
<Filters disabled={hasManualEdits} />
{mode === MODES.APPROVE_WORKING && (
<div className="mt-4">
<Title text="Статусы" />
<div className="flex flex-col space-y-2">
<Checkbox
onChange={() => toggleVisibility(LAYER_IDS.approve)}
checked={isVisible[LAYER_IDS.approve]}
>
Согласование-установка
</Checkbox>
<Checkbox
className={"!ml-0"}
onChange={() => toggleVisibility(LAYER_IDS.working)}
checked={isVisible[LAYER_IDS.working]}
>
Работает
</Checkbox>
<Checkbox
className={"!ml-0"}
onChange={() => toggleVisibility(LAYER_IDS.cancelled)}
checked={isVisible[LAYER_IDS.cancelled]}
>
Отменен
</Checkbox>
</div>
</div>
)}
{mode === MODES.INITIAL && hasManualEdits ? (
<Button block className={"mt-2"} onClick={resetSelection}>
Отменить ручное редактирование
</Button>
) : null}
</div>
<div>
{mode === MODES.INITIAL && (
<>
<SelectedLocations />
<TakeToWorkButton />
</>
)}
</div>
</div>
<div className="flex flex-col flex-1">{getFilters()}</div>
</div>
);
});

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

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

@ -1,20 +1,23 @@
import { INITIAL, useFilters } from "../../../stores/useFilters";
import { useEffect } from "react";
import { SliderComponent as Slider } from "../../../components/SliderComponent";
import {
INITIAL,
useWorkingPointsFilters,
} from "../../../stores/useWorkingPointsFilters";
export const FactTrafficSlider = ({ fullRange }) => {
const {
filters: { factTraffic },
setFactTraffic,
} = useFilters();
} = useWorkingPointsFilters();
const handleAfterChange = (range) => setFactTraffic(range);
useEffect(() => {
if (!fullRange) return;
const min = fullRange.fact[0];
const max = fullRange.fact[1];
const min = fullRange.factTraffic[0];
const max = fullRange.factTraffic[1];
if (
factTraffic[0] === INITIAL.factTraffic[0] &&

@ -0,0 +1,37 @@
import { RegionSelect } from "../../../components/RegionSelect";
import { useWorkingPointsFilters } from "../../../stores/useWorkingPointsFilters";
import { DeltaTrafficSlider } from "./DeltaSlider";
import { FactTrafficSlider } from "./FactTrafficSlider";
import { AgeSlider } from "./AgeSlider";
import { ClearFiltersButton } from "../../../components/ClearFiltersButton";
import { getDynamicActiveFilters } from "../utils";
export const WorkingPointsFilters = ({ fullRange }) => {
const { filters, setRegion, clear } = useWorkingPointsFilters();
const activeDynamicFilters = getDynamicActiveFilters(filters, fullRange, [
"deltaTraffic",
"factTraffic",
"age",
]);
const hasActiveFilters =
filters.region ||
activeDynamicFilters.deltaTraffic ||
activeDynamicFilters.factTraffic ||
activeDynamicFilters.age;
const handleClear = () => clear(fullRange);
return (
<div>
<RegionSelect value={filters.region?.id} onChange={setRegion} />
<div className={"space-y-12 mt-4"}>
<DeltaTrafficSlider fullRange={fullRange} />
<FactTrafficSlider fullRange={fullRange} />
<AgeSlider fullRange={fullRange} />
</div>
{hasActiveFilters && <ClearFiltersButton onClick={handleClear} />}
</div>
);
};

@ -0,0 +1,13 @@
export const getDynamicActiveFilters = (filters, fullRange, props) => {
if (!fullRange || !props) return false;
const result = {};
props.forEach((prop) => {
result[prop] =
filters[prop][0] !== fullRange[prop][0] ||
filters[prop][1] !== fullRange[prop][1];
});
return result;
};

@ -1,134 +0,0 @@
import { Table } from "../Table";
import { columns } from "../InitialTable/columns";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { getPoints } from "../../../api";
import { useCallback, useState } from "react";
import { PAGE_SIZE } from "../constants";
import { STATUSES } from "../../../config";
import { useMergeTableData } from "../useMergeTableData";
import { Button } from "antd";
import { useUpdateStatus } from "../../../hooks/useUpdateStatus";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportApproveAndWorkingData } from "./useExportApproveAndWorkingData";
import { useFilters } from "../../../stores/useFilters";
import { usePopup } from "../../../stores/usePopup";
import { StatusSelect } from "../../../components/StatusSelect";
const ChangeStatusButton = ({ selectedIds, selectedStatus }) => {
const queryClient = useQueryClient();
const { setPopup } = usePopup();
const { mutate: updateStatus } = useUpdateStatus({
onSuccess: () => {
queryClient.invalidateQueries(["approve-working-points"]);
setPopup(null);
},
});
const handleClick = (e) => {
e.stopPropagation();
const params = new URLSearchParams({
status: selectedStatus,
"location_ids[]": selectedIds,
});
updateStatus(params);
};
return (
<Button type="primary" onClick={handleClick}>
Обновить статус
</Button>
);
};
const Header = ({ selectedIds, onClearSelected }) => {
const [status, setStatus] = useState(STATUSES.initial);
const handleClear = (e) => {
e.stopPropagation();
onClearSelected();
};
return (
<HeaderWrapper
leftColumn={
selectedIds.length > 0 && (
<>
<StatusSelect value={status} onChange={setStatus} />
<ChangeStatusButton
selectedIds={selectedIds}
selectedStatus={status}
/>
</>
)
}
rightColumn={
selectedIds.length > 0 && (
<Button onClick={handleClear}>Очистить все</Button>
)
}
classes={{
leftColumn: "flex items-center gap-x-4",
rightColumn: "flex item-center gap-x-4",
}}
exportProvider={useExportApproveAndWorkingData}
/>
);
};
export const ApproveAndWorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1);
const [selectedIds, setSelectedIds] = useState([]);
const {
filters: { region },
} = useFilters();
const clearSelected = () => setSelectedIds([]);
const { data, isInitialLoading } = useQuery(
["approve-working-points", page, region],
async () => {
const params = new URLSearchParams({
page,
page_size: pageSize,
"status[]": [STATUSES.approve, STATUSES.working, STATUSES.cancelled],
});
return await getPoints(params, region);
},
{ keepPreviousData: true }
);
const { data: mergedData, isClickedPointLoading } = useMergeTableData(
data,
setPageSize
);
const handlePageChange = useCallback((page) => setPage(page), []);
const rowSelection = {
selectedRowKeys: selectedIds,
onChange: (selectedRowKeys) => setSelectedIds(selectedRowKeys),
hideSelectAll: true,
};
return (
<Table
header={
<Header selectedIds={selectedIds} onClearSelected={clearSelected} />
}
rowSelection={rowSelection}
data={mergedData}
onPageChange={handlePageChange}
page={page}
pageSize={pageSize}
isClickedPointLoading={isClickedPointLoading}
columns={columns}
fullWidth={fullWidth}
loading={isInitialLoading}
/>
);
};

@ -0,0 +1,33 @@
import { useQueryClient } from "@tanstack/react-query";
import { usePopup } from "../../../stores/usePopup";
import { useUpdateStatus } from "../../../hooks/useUpdateStatus";
import { Button } from "antd";
export const ChangeStatusButton = ({ selectedIds, selectedStatus }) => {
const queryClient = useQueryClient();
const { setPopup } = usePopup();
const { mutate: updateStatus } = useUpdateStatus({
onSuccess: () => {
queryClient.invalidateQueries(["on-approval-points"]);
setPopup(null);
},
});
const handleClick = (e) => {
e.stopPropagation();
const params = new URLSearchParams({
status: selectedStatus,
"location_ids[]": selectedIds,
});
updateStatus(params);
};
return (
<Button type="primary" onClick={handleClick}>
Обновить статус
</Button>
);
};

@ -0,0 +1,42 @@
import { useState } from "react";
import { STATUSES } from "../../../config";
import { HeaderWrapper } from "../HeaderWrapper";
import { StatusSelect } from "../../../components/StatusSelect";
import { Button } from "antd";
import { useExportOnApprovalData } from "./useExportOnApprovalData";
import { ChangeStatusButton } from "./ChangeStatusButton";
export const Header = ({ selectedIds, onClearSelected }) => {
const [status, setStatus] = useState(STATUSES.pending);
const handleClear = (e) => {
e.stopPropagation();
onClearSelected();
};
return (
<HeaderWrapper
leftColumn={
selectedIds.length > 0 && (
<>
<StatusSelect value={status} onChange={setStatus} />
<ChangeStatusButton
selectedIds={selectedIds}
selectedStatus={status}
/>
</>
)
}
rightColumn={
selectedIds.length > 0 && (
<Button onClick={handleClear}>Очистить все</Button>
)
}
classes={{
leftColumn: "flex items-center gap-x-4",
rightColumn: "flex item-center gap-x-4",
}}
exportProvider={useExportOnApprovalData}
/>
);
};

@ -0,0 +1,65 @@
import { Table } from "../Table";
import { columns } from "../InitialTable/columns";
import { useQuery } from "@tanstack/react-query";
import { getPoints } from "../../../api";
import { useCallback, useState } from "react";
import { PAGE_SIZE } from "../constants";
import { STATUSES } from "../../../config";
import { useMergeTableData } from "../useMergeTableData";
import { Header } from "./Header";
import { useOnApprovalPointsFilters } from "../../../stores/useOnApprovalPointsFilters";
export const OnApprovalTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1);
const [selectedIds, setSelectedIds] = useState([]);
const {
filters: { region },
} = useOnApprovalPointsFilters();
const clearSelected = () => setSelectedIds([]);
const { data, isInitialLoading } = useQuery(
["on-approval-points", page, region],
async () => {
const params = new URLSearchParams({
page,
page_size: pageSize,
"status[]": [STATUSES.onApproval, STATUSES.working, STATUSES.cancelled],
});
return await getPoints(params, region);
},
{ keepPreviousData: true }
);
const { data: mergedData, isClickedPointLoading } = useMergeTableData(
data,
setPageSize
);
const handlePageChange = useCallback((page) => setPage(page), []);
const rowSelection = {
selectedRowKeys: selectedIds,
onChange: (selectedRowKeys) => setSelectedIds(selectedRowKeys),
hideSelectAll: true,
};
return (
<Table
header={
<Header selectedIds={selectedIds} onClearSelected={clearSelected} />
}
rowSelection={rowSelection}
data={mergedData}
onPageChange={handlePageChange}
page={page}
pageSize={pageSize}
isClickedPointLoading={isClickedPointLoading}
columns={columns}
fullWidth={fullWidth}
loading={isInitialLoading}
/>
);
};

@ -2,18 +2,18 @@ import { useQuery } from "@tanstack/react-query";
import { STATUSES } from "../../../config";
import { exportPoints } from "../../../api";
import { handleExportSuccess } from "../ExportButton";
import { useFilters } from "../../../stores/useFilters";
import { useOnApprovalPointsFilters } from "../../../stores/useOnApprovalPointsFilters";
export const useExportApproveAndWorkingData = (enabled, onSettled) => {
export const useExportOnApprovalData = (enabled, onSettled) => {
const {
filters: { region },
} = useFilters();
} = useOnApprovalPointsFilters();
return useQuery(
["export-approve-working", region],
["export-on-approval", region],
async () => {
const params = new URLSearchParams({
"status[]": [STATUSES.approve, STATUSES.working],
"status[]": [STATUSES.onApproval, STATUSES.working],
});
return await exportPoints(params, region);

@ -2,10 +2,10 @@ import React, { useCallback, useState } from "react";
import { Table } from "../Table";
import { usePointSelection } from "../../../stores/usePointSelection";
import { useClickedPointConfig } from "../../../stores/useClickedPointConfig";
import { useInitialTableData } from "./useInitialTableData";
import { usePendingTableData } from "./usePendingTableData";
import { columns } from "./columns";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportInitialData } from "./useExportInitialData";
import { useExportPendingData } from "./useExportPendingData";
export const InitialTable = ({ fullWidth }) => {
const { selection, include, exclude } = usePointSelection();
@ -13,7 +13,7 @@ export const InitialTable = ({ fullWidth }) => {
const [page, setPage] = useState(1);
const { data, pageSize, isClickedPointLoading, isDataLoading } =
useInitialTableData(page, () => setPage(1));
usePendingTableData(page, () => setPage(1));
const getSelectedRowKeys = useCallback(() => {
const ids = data?.results.map((item) => item.id) ?? [];
@ -58,7 +58,7 @@ export const InitialTable = ({ fullWidth }) => {
isClickedPointLoading={isClickedPointLoading}
columns={columns}
fullWidth={fullWidth}
header={<HeaderWrapper exportProvider={useExportInitialData} />}
header={<HeaderWrapper exportProvider={useExportPendingData} />}
loading={isDataLoading}
/>
);

@ -1,11 +1,11 @@
import { useFilters } from "../../../stores/useFilters";
import { usePointSelection } from "../../../stores/usePointSelection";
import { useQuery } from "@tanstack/react-query";
import { exportPoints } from "../../../api";
import { handleExportSuccess } from "../ExportButton";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
export const useExportInitialData = (enabled, onSettled) => {
const { filters } = useFilters();
export const useExportPendingData = (enabled, onSettled) => {
const { filters } = usePendingPointsFilters();
const { prediction, status, categories, region } = filters;
const { selection } = usePointSelection();

@ -1,15 +1,15 @@
import { useState } from "react";
import { PAGE_SIZE } from "../constants";
import { useFilters } from "../../../stores/useFilters";
import { usePointSelection } from "../../../stores/usePointSelection";
import { useQuery } from "@tanstack/react-query";
import { getPoints } from "../../../api";
import { useMergeTableData } from "../useMergeTableData";
import { STATUSES } from "../../../config";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
export const useInitialTableData = (page, resetPage) => {
export const usePendingTableData = (page, resetPage) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const { filters } = useFilters();
const { filters } = usePendingPointsFilters();
const { prediction, categories, region } = filters;
const {
selection: { included },
@ -24,7 +24,7 @@ export const useInitialTableData = (page, resetPage) => {
page,
page_size: pageSize,
"prediction_current[]": prediction,
"status[]": [STATUSES.initial],
"status[]": [STATUSES.pending],
"categories[]": categories,
"included[]": includedIds,
});

@ -1,14 +1,14 @@
import { useMode } from "../../stores/useMode";
import { MODES } from "../../config";
import { InitialTable } from "./InitialTable/InitialTable";
import { ApproveAndWorkingTable } from "./ApproveAndWorkingTable/ApproveAndWorkingTable";
import { OnApprovalTable } from "./ApproveAndWorkingTable/OnApprovalTable";
import { WorkingTable } from "./WorkingTable/WorkingTable";
export const TableWrapper = ({ fullWidth }) => {
const { mode } = useMode();
if (mode === MODES.APPROVE_WORKING) {
return <ApproveAndWorkingTable fullWidth={fullWidth} />;
if (mode === MODES.ON_APPROVAL) {
return <OnApprovalTable fullWidth={fullWidth} />;
}
if (mode === MODES.WORKING) {

@ -7,15 +7,15 @@ import { useMergeTableData } from "../useMergeTableData";
import { Table } from "../Table";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportWorkingData } from "./useExportWorkingData";
import { useFilters } from "../../../stores/useFilters";
import { columns } from "./columns";
import { useWorkingPointsFilters } from "../../../stores/useWorkingPointsFilters";
export const WorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1);
const {
filters: { region, deltaTraffic, factTraffic, age },
} = useFilters();
} = useWorkingPointsFilters();
const { data, isInitialLoading } = useQuery(
["working-points", page, region, deltaTraffic, factTraffic, age],

@ -1,13 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { STATUSES } from "../../../config";
import { exportPoints } from "../../../api";
import { useFilters } from "../../../stores/useFilters";
import { handleExportSuccess } from "../ExportButton";
import { useWorkingPointsFilters } from "../../../stores/useWorkingPointsFilters";
export const useExportWorkingData = (enabled, onSettled) => {
const {
filters: { region, deltaTraffic, factTraffic, age },
} = useFilters();
} = useWorkingPointsFilters();
return useQuery(
["export-working", region, deltaTraffic, factTraffic, age],

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

@ -8,8 +8,8 @@ const INITIAL_STATE = {
[LAYER_IDS.working]: false,
[LAYER_IDS.filteredWorking]: false,
[LAYER_IDS.cancelled]: false,
[LAYER_IDS.pvz]: true,
[LAYER_IDS.other]: true,
[LAYER_IDS.pvz]: false,
[LAYER_IDS.other]: false,
};
const STATIC_LAYERS = [LAYER_IDS.pvz, LAYER_IDS.other];
@ -22,6 +22,13 @@ const store = (set) => ({
state.isVisible[layerId] = !state.isVisible[layerId];
}),
showLayers: (layerIds) =>
set((state) => {
layerIds.forEach((layerId) => {
state.isVisible[layerId] = true;
});
}),
setLayersVisibility: (visibleLayerIds) =>
set((state) => {
visibleLayerIds.forEach((layerId) => {

@ -3,7 +3,7 @@ import { immer } from "zustand/middleware/immer";
import { MODES } from "../config";
const store = (set) => ({
mode: MODES.INITIAL,
mode: MODES.PENDING,
setMode: (mode) => {
set((state) => {

@ -0,0 +1,22 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
const INITIAL = {
region: null,
};
const store = (set) => ({
filters: INITIAL,
setRegion: (value) =>
set((state) => {
state.filters.region = value;
}),
clear: () =>
set((state) => {
state.filters = INITIAL;
}),
});
export const useOnApprovalPointsFilters = create(immer(store));

@ -0,0 +1,42 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
export const INITIAL = {
prediction: [0, 0],
categories: [],
region: null,
};
const store = (set) => ({
filters: INITIAL,
setPrediction: (value) => {
set((state) => {
state.filters.prediction = value;
});
},
setCategories: (categories) =>
set((state) => {
state.filters.categories = categories;
}),
setRegion: (value) =>
set((state) => {
state.filters.region = value;
}),
clear: (predictionFullRange) =>
set((state) => {
if (!predictionFullRange) {
state.filters = INITIAL;
return state;
}
state.filters = {
...INITIAL,
prediction: [predictionFullRange[0], predictionFullRange[1]],
};
}),
});
export const usePendingPointsFilters = create(immer(store));

@ -1,24 +1,15 @@
import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { STATUSES } from "../config";
export const INITIAL = {
prediction: [0, 0],
categories: [],
region: null,
status: [STATUSES.initial],
deltaTraffic: [-1000, 1000],
deltaTraffic: [-10000, 10000],
factTraffic: [0, 0],
age: [0, 0],
};
const store = (set) => ({
filters: INITIAL,
setPrediction: (value) => {
set((state) => {
state.filters.prediction = value;
});
},
setDeltaTraffic: (value) => {
set((state) => {
@ -38,20 +29,25 @@ const store = (set) => ({
});
},
setCategories: (categories) =>
set((state) => {
state.filters.categories = categories;
}),
setRegion: (value) =>
set((state) => {
state.filters.region = value;
}),
setStatus: (newStatus) =>
clear: (fullRange) =>
set((state) => {
state.filters.status = newStatus;
if (!fullRange) {
state.filters = INITIAL;
return state;
}
state.filters = {
...INITIAL,
deltaTraffic: [fullRange.deltaTraffic[0], fullRange.deltaTraffic[1]],
factTraffic: [fullRange.factTraffic[0], fullRange.factTraffic[1]],
age: [fullRange.age[0], fullRange.age[1]],
};
}),
});
export const useFilters = create(immer(store));
export const useWorkingPointsFilters = create(immer(store));
Loading…
Cancel
Save