@@ -46,11 +51,45 @@ const LegendColorRampItem = ({ colors, name }) => {
);
};
-export function Legend() {
- const { mode } = useMode();
+const LegendGroupItem = ({item, color}) => {
+ return (
+
+ )
+}
+
+export function Legend({ postGroups, otherGroups }) {
+ const {mode} = useMode();
return (
-
+
{mode === MODES.PENDING && (
@@ -59,10 +98,11 @@ export function Legend() {
colors={pendingColors}
name="Локации к рассмотрению"
/>
-
+
>
)}
@@ -71,28 +111,33 @@ export function Legend() {
-
+
>
)}
{mode === MODES.WORKING && (
<>
-
+
>
)}
-
-
+ {postGroups?.map((item) => {
+ return
+ })}
+
+
+ {otherGroups?.map((item) => {
+ return
+ })}
);
diff --git a/src/Map/MapComponent.jsx b/src/Map/MapComponent.jsx
index 7e16d4d..b1a1158 100644
--- a/src/Map/MapComponent.jsx
+++ b/src/Map/MapComponent.jsx
@@ -1,6 +1,6 @@
import maplibregl from "maplibre-gl";
import Map, { MapProvider } from "react-map-gl";
-import { useEffect, useRef, useState } from "react";
+import { useEffect, useMemo, useRef, useState } from "react";
import { Sidebar } from "../modules/Sidebar/Sidebar";
import { Layers } from "./Layers/Layers";
import { MapPopup } from "./Popup/Popup";
@@ -21,6 +21,14 @@ import { useLayersVisibility } from "../stores/useLayersVisibility";
import { LAYER_IDS } from "./Layers/constants";
import { Header } from "./Header";
import { icons } from "../icons/icons-config";
+import { LastMLRun } from "./LastMLRun";
+import { useOtherGroups, usePostamatesAndPvzGroups } from "../api.js";
+import { getFilteredGroups, transliterate } from "../utils.js";
+import {
+ CATEGORIES_MAP,
+ RANGE_FILTERS_KEYS,
+ RANGE_FILTERS_MAP
+} from "../stores/usePendingPointsFilters.js";
export const MapComponent = () => {
const mapRef = useRef(null);
@@ -32,6 +40,51 @@ export const MapComponent = () => {
const { mode } = useMode();
const { tableState, openTable } = useTable();
+ const { data: postamatesAndPvzGroups } = usePostamatesAndPvzGroups();
+ const { data: otherGroups } = useOtherGroups();
+
+ const filteredPostamatesGroups = useMemo(() => {
+ return getFilteredGroups(postamatesAndPvzGroups);
+ }, [postamatesAndPvzGroups]);
+
+ const filteredOtherGroups = useMemo(() => {
+ return getFilteredGroups(otherGroups);
+ }, [otherGroups]);
+
+ const mapIcons = useMemo(() => {
+ const res = [];
+
+ [...filteredOtherGroups, ...filteredPostamatesGroups].map((category) => {
+ category.groups.map((group) => {
+ res.push({name: transliterate(group.name + group.id), url: group.image});
+ })
+ });
+ return [...res, ...icons];
+ }, [icons, filteredPostamatesGroups, filteredOtherGroups]);
+
+ const mapLayersIds = useMemo(() => {
+ const res = []
+
+ filteredPostamatesGroups.map((item) => {
+ RANGE_FILTERS_MAP[`category${item.id}`] = {
+ name: CATEGORIES_MAP[item.name],
+ };
+ item.groups.map((groupItem) => {
+ if (!RANGE_FILTERS_KEYS.includes(`d${groupItem.id}`)) RANGE_FILTERS_KEYS.push(`d${groupItem.id}`);
+ RANGE_FILTERS_MAP[`category${item.id}`][`d${groupItem.id}`] = groupItem.name;
+ res.push(LAYER_IDS.pvz + groupItem.id);
+ });
+ });
+
+ filteredOtherGroups.map((item) => {
+ item.groups.map((groupItem) => {
+ res.push(LAYER_IDS.other + groupItem.id);
+ })
+ });
+
+ return res;
+ }, [filteredPostamatesGroups, filteredOtherGroups]);
+
useEffect(() => {
setLayersVisibility(MODE_TO_LAYER_VISIBILITY_MAPPER[mode]);
setPopup(null);
@@ -131,21 +184,21 @@ export const MapComponent = () => {
LAYER_IDS.working,
LAYER_IDS.filteredWorking,
LAYER_IDS.cancelled,
- LAYER_IDS.pvz,
- LAYER_IDS.other,
+ ...mapLayersIds,
]}
onClick={handleClick}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
onLoad={() => {
- icons.map((icon) => {
+ mapIcons.map((icon) => {
const img = new Image(
- icon.size?.width || 24,
- icon.size?.height || 24
+ icon.size?.width || 64,
+ icon.size?.height || 64
);
img.onload = () =>
- mapRef.current.addImage(icon.name, img, { sdf: true });
+ mapRef.current.addImage(icon.name, img);
img.src = icon.url;
+ img.crossOrigin = "Anonymous";
});
}}
id="map"
@@ -165,11 +218,12 @@ export const MapComponent = () => {
-
+
-
+
+
-
+
diff --git a/src/Map/PointChart.jsx b/src/Map/PointChart.jsx
new file mode 100644
index 0000000..a3c306d
--- /dev/null
+++ b/src/Map/PointChart.jsx
@@ -0,0 +1,168 @@
+import { Line } from "react-chartjs-2";
+import {
+ Chart as ChartJS,
+ CategoryScale,
+ LinearScale,
+ BarElement,
+ Title,
+ Tooltip as ChartTooltip,
+ Legend, PointElement, LineElement, BarController,
+} from 'chart.js';
+import { useQuery } from "@tanstack/react-query";
+import { api } from "../api.js";
+
+ChartJS.register(
+ CategoryScale,
+ BarController,
+ PointElement,
+ LineElement,
+ LinearScale,
+ BarElement,
+ Title,
+ ChartTooltip,
+ Legend
+);
+
+const GRAPH_LABELS_MAP= {
+ target_dist_shap: "Расстояние до ближайшего постамата Мой постамат",
+ target_post_cnt_shap: "Кол-во точек Мой постамат*",
+ target_cnt_ao_mean_shap: "Средний трафик в точках Мой постамат в АО",
+ rival_pvz_cnt_shap: "Кол-во ПВЗ*",
+ rival_post_cnt_shap: "Кол-во постаматов конкурентов *",
+ metro_dist_shap: "Расстояние до метро",
+ property_price_bargains_shap: "Цена сделок жилой недвижимости*",
+ property_price_offers_shap: "Цена предложений жилой недвижимости*",
+ property_mean_floor_shap: "Средняя этажность застройки*",
+ property_era_shap: "Эпоха жилой недвижимости*",
+ flats_cnt_shap: "Кол-во квартир*",
+ popul_home_shap: "Численность проживающего населения*",
+ popul_job_shap: "Численность работающего населения*",
+ yndxfood_sum_shap: "Сумма заказов Яндекс.Еда*",
+ yndxfood_cnt_shap: "Кол-во заказов Яндекс.Еда*",
+ school_cnt_shap: "Кол-во школ*",
+ kindergar_cnt_shap: "Кол-во детсадов*",
+ public_stop_cnt_shap: "Кол-во остановок общ. транспорта*",
+ sport_center_cnt_shap: "Кол-во спортивных центров*",
+ pharmacy_cnt_shap: "Кол-во аптек*",
+ supermarket_cnt_shap: "Кол-во супермаркетов*",
+ supermarket_premium_cnt_shap: "Кол-во премиальных супермаркетов*",
+ clinic_cnt_shap: "Кол-во поликлиник*",
+ bank_cnt_shap: "Кол-во банков*",
+ reca_cnt_shap: "Кол-во точек общепита*",
+ lab_cnt_shap: "Кол-во лабораторий*",
+ culture_cnt_shap: "Кол-во объектов культуры*",
+ attraction_cnt_shap: "Кол-во достопримечательностей*",
+ mfc_cnt_shap: "Кол-во МФЦ*",
+ bc_cnt_shap: "Кол-во бизнес-центров*",
+ tc_cnt_shap: "Кол-во торговых центров*",
+ business_activity_shap: "Бизнес активность",
+}
+export const PointChart = ({ point }) => {
+ const { data: meanData } = useQuery(
+ ["mean-data"],
+ async () => {
+ const { data } = await api.get(
+ `/api/avg_bi_values/`
+ );
+
+ return data;
+ },
+ {
+ refetchOnWindowFocus: false,
+ refetchOnMount: false
+ }
+ );
+
+ const options = {
+ indexAxis: 'y',
+ elements: {
+ bar: {
+ borderWidth: 0,
+ borderRadius: 5,
+ pointStyle: 'circle'
+ },
+ },
+ plugins: {
+ legend: {
+ display: false
+ },
+ tooltip: {
+ displayColors: false,
+ yAlign: "top",
+ callbacks: {
+ label: function(context) {
+ const label = []
+ const shap_key = Object.keys(GRAPH_LABELS_MAP).find(key => GRAPH_LABELS_MAP[key] === context.label);
+ const key = shap_key.substring(0, shap_key.length - 5)
+ if (context.datasetIndex === 0) label.push("Значение: " + point[key]);
+ if (context.parsed.x !== null) {
+ let labelText = "";
+ if (context.datasetIndex === 0) labelText = "Вклад в прогноз, %: ";
+ if (context.datasetIndex === 1) labelText = "Минимальный вклад в прогноз, %: ";
+ if (context.datasetIndex === 2) labelText = "Максимальный вклад в прогноз, %: ";
+ label.push(labelText + context.parsed.x);
+ }
+ return label;
+ },
+ body: () => {
+ return "Вклад в прогноз, %:"
+ }
+ }
+ },
+ },
+ scales: {
+ y: {
+ stacked: true,
+ },
+ x: {
+ title: {
+ display: true,
+ text: 'Вклад в прогноз, %',
+ },
+ grid: {
+ color: function(context) {
+ if (context.tick.value === 0) {
+ return "#000000";
+ }
+
+ return "#E5E5E5";
+ },
+ },
+ }
+ }
+ };
+
+ const labels = Object.keys(GRAPH_LABELS_MAP).sort((a, b) => {
+ if (Math.abs(point[a]) < Math.abs(point[b])) return 1;
+ else return -1;
+ }).slice(0, 15);
+
+ const data = {
+ labels: labels.map((l) => GRAPH_LABELS_MAP[l]),
+ datasets: [
+ {
+ data: labels.map((l) => point[l]),
+ backgroundColor: labels.map((l) => point[l]).map(v => v <= 0 ? '#CC2500' : '#278211'),
+ hoverBackgroundColor: labels.map((l) => point[l]).map(v => v <= 0 ? '#F22C00' : '#2DB20C'),
+ type: 'line',
+ showLine: false,
+ },
+ {
+ data: labels.map((l) => meanData ? meanData[`min_${l}`] : 0),
+ backgroundColor: "#cccccc",
+ hoverBackgroundColor: "#aaaaaa",
+ type: 'bar',
+ showLine: false,
+ },
+ {
+ data: labels.map((l) => meanData ? meanData[`max_${l}`] : 0),
+ backgroundColor: "#cccccc",
+
+ hoverBackgroundColor: "#aaaaaa",
+ type: 'bar',
+ showLine: false,
+ },
+ ],
+ };
+ return
+}
\ No newline at end of file
diff --git a/src/Map/Popup/Popup.jsx b/src/Map/Popup/Popup.jsx
index f024c3d..aece2c2 100644
--- a/src/Map/Popup/Popup.jsx
+++ b/src/Map/Popup/Popup.jsx
@@ -1,4 +1,4 @@
-import { Button } from "antd";
+import { Button, Spin, Tooltip } from "antd";
import { CATEGORIES, MODES, STATUSES } from "../../config";
import { useMode } from "../../stores/useMode";
import { LAYER_IDS } from "../Layers/constants";
@@ -8,84 +8,129 @@ import { OnApprovalPointPopup } from "./mode-popup/OnApprovalPointPopup";
import { WorkingPointPopup } from "./mode-popup/WorkingPointPopup";
import { FeatureProperties } from "./mode-popup/FeatureProperties";
import { usePopup } from "../../stores/usePopup.js";
+import { PanoramaIcon } from "../../icons/PanoramaIcon";
+import { useGetPopupPoints } from "../../api.js";
+import { doesMatchFilter } from "../../utils.js";
+import Checkbox from "antd/es/checkbox/Checkbox";
+import { usePointSelection } from "../../stores/usePointSelection.js";
+import { usePendingPointsFilters } from "../../stores/usePendingPointsFilters.js";
-const SingleFeaturePopup = ({ feature }) => {
+const SingleFeaturePopup = ({ feature, point }) => {
const { mode } = useMode();
const isRivals =
- feature.layer?.id === LAYER_IDS.pvz ||
- feature.layer?.id === LAYER_IDS.other;
+ feature.layer?.id.includes(LAYER_IDS.pvz) ||
+ feature.layer?.id.includes(LAYER_IDS.other);
const isPendingPoint = feature.properties.status === STATUSES.pending;
const isWorkingPoint = feature.properties.status === STATUSES.working;
if (isRivals) {
- return
;
+ return
;
}
if (mode === MODES.ON_APPROVAL && !isPendingPoint) {
- return
;
+ return
;
}
if (mode === MODES.WORKING && isWorkingPoint) {
- return
;
+ return
;
}
if (mode === MODES.PENDING && isPendingPoint)
- return
;
+ return
;
- return
;
+ return
;
};
-const MultipleFeaturesPopup = ({ features }) => {
+const MultipleFeaturesPopup = ({ features, points }) => {
const { setPopup } = usePopup();
+ const { selection, include, exclude } = usePointSelection();
+ const { filters, ranges } = usePendingPointsFilters();
return (
{features.map((feature) => {
+ const featureId = feature.properties.id;
+ const point = points.find(p => p.id === featureId);
+ const isSelected = (doesMatchFilter(filters, ranges, feature) && !selection.excluded.has(featureId)) ||
+ selection.included.has(featureId);
+ const handleSelect = () => {
+ if (isSelected) {
+ exclude(featureId);
+ } else {
+ include(featureId);
+ }
+ };
return (
-
);
};
+const YandexPanoramaLink = ({ lat, lng }) => {
+ const link = `https://yandex.ru/maps/?panorama[point]=${lng},${lat}`
+ return (
+
+ );
+}
+
export const MapPopup = ({ features, lat, lng, onClose }) => {
+ const {data: points, isLoading} = useGetPopupPoints(features);
+
const getContent = () => {
if (features.length === 1) {
- return
;
+ return
;
}
- return ;
+ return ;
};
return (
- {getContent()}
+
+ {isLoading ? : getContent()}
);
};
diff --git a/src/Map/Popup/PopupWrapper.jsx b/src/Map/Popup/PopupWrapper.jsx
index 73e9869..03202b1 100644
--- a/src/Map/Popup/PopupWrapper.jsx
+++ b/src/Map/Popup/PopupWrapper.jsx
@@ -7,7 +7,7 @@ export const PopupWrapper = ({ lat, lng, onClose, children }) => {
latitude={lat}
onClose={onClose}
closeOnClick={false}
- style={{ minWidth: "300px" }}
+ style={{ minWidth: "330px" }}
>
{children}
diff --git a/src/Map/Popup/mode-popup/FeatureProperties.jsx b/src/Map/Popup/mode-popup/FeatureProperties.jsx
index 3f2830c..f674e92 100644
--- a/src/Map/Popup/mode-popup/FeatureProperties.jsx
+++ b/src/Map/Popup/mode-popup/FeatureProperties.jsx
@@ -1,35 +1,95 @@
import { CATEGORIES, STATUSES } from "../../../config";
import {
commonPopupConfig,
- residentialPopupConfig,
+ residentialPopupFields,
rivalsConfig,
workingPointFields,
} from "./config";
import { Col, Row } from "antd";
import { twMerge } from "tailwind-merge";
import { LAYER_IDS } from "../../Layers/constants";
-import { isNil } from "../../../utils.js";
+import {getFilteredGroups, isNil} from "../../../utils.js";
import { useGetRegions } from "../../../components/RegionSelect.jsx";
+import {useOtherGroups, usePostamatesAndPvzGroups} from "../../../api.js";
+import {useMemo} from "react";
+import { TrafficModal } from "../../TrafficModal.jsx";
-export const FeatureProperties = ({ feature, dynamicStatus, postamatId }) => {
+const getRivalsName = (feature) => {
+ const { data: postamatesAndPvzGroups } = usePostamatesAndPvzGroups();
+ const { data: otherGroups } = useOtherGroups();
+
+ const filteredPostamatesCategories = useMemo(() => {
+ return getFilteredGroups(postamatesAndPvzGroups);
+ }, [postamatesAndPvzGroups]);
+
+ const filteredOtherCategories = useMemo(() => {
+ return getFilteredGroups(otherGroups);
+ }, [otherGroups]);
+
+ const filteredPostamatesGroups = useMemo(() => {
+ if (!filteredPostamatesCategories) return [];
+ return filteredPostamatesCategories
+ .map((category) => {
+ return [...category.groups]
+ }).flat();
+ }, [filteredPostamatesCategories]);
+
+ const filteredOtherGroups = useMemo(() => {
+ if (!filteredOtherCategories) return [];
+ return filteredOtherCategories
+ .map((category) => {
+ return [...category.groups]
+ }).flat();
+ }, [filteredOtherCategories]);
+
+ const isOther = feature.layer?.id.includes(LAYER_IDS.other);
+ const name = isOther ?
+ filteredOtherCategories.find(c => c.id === feature.properties.category_id)?.name :
+ filteredPostamatesCategories.find(c => c.id === feature.properties.category_id)?.name;
+
+ const groupName = isOther ?
+ filteredOtherGroups.find(c => c.id === feature.properties.group_id)?.name :
+ filteredPostamatesGroups.find(c => c.id === feature.properties.group_id)?.name;
+
+ return {
+ name,
+ groupName
+ }
+}
+
+export const FeatureProperties = ({ feature, dynamicStatus, postamatId, point }) => {
const { data } = useGetRegions();
const isResidential = feature.properties.category === CATEGORIES.residential;
const isWorking = feature.properties.status === STATUSES.working;
+ const { name, groupName } = getRivalsName(feature);
+
const isRivals =
- feature.layer?.id === LAYER_IDS.pvz ||
- feature.layer?.id === LAYER_IDS.other;
+ feature.layer?.id.includes(LAYER_IDS.pvz) ||
+ feature.layer?.id.includes(LAYER_IDS.other);
const getConfig = () => {
if (isRivals) {
return rivalsConfig;
}
- const config = isResidential ? residentialPopupConfig : commonPopupConfig;
- return isWorking ? [...config, ...workingPointFields] : config;
+ const config = isWorking ? [...commonPopupConfig, ...workingPointFields] : commonPopupConfig;
+ return isResidential ? [...config, ...residentialPopupFields] : config;
};
const getValue = ({ field, render, empty, type, fallbackField }) => {
- let value = feature.properties[field];
+ let value = point ? point[field] : feature.properties[field];
+
+ if (field === "prediction_current") {
+ value = ;
+ }
+
+ if (field === "category_id") {
+ value = name;
+ }
+
+ if (field === "group_id") {
+ value = groupName;
+ }
if (field === "status" && dynamicStatus) {
value = dynamicStatus;
@@ -40,7 +100,8 @@ export const FeatureProperties = ({ feature, dynamicStatus, postamatId }) => {
}
if (type === "region") {
- value = value ? value : feature.properties[fallbackField];
+ const valueProvider = point ? point : feature
+ value = value ? value : valueProvider[fallbackField];
value = render(value, data?.normalized);
} else {
value = render ? render(value) : value;
diff --git a/src/Map/Popup/mode-popup/OnApprovalPointPopup.jsx b/src/Map/Popup/mode-popup/OnApprovalPointPopup.jsx
index e9b07ae..47c78fa 100644
--- a/src/Map/Popup/mode-popup/OnApprovalPointPopup.jsx
+++ b/src/Map/Popup/mode-popup/OnApprovalPointPopup.jsx
@@ -10,12 +10,12 @@ import { Button, InputNumber } from "antd";
import { STATUSES } from "../../../config";
import { isNil } from "../../../utils.js";
-export const OnApprovalPointPopup = ({ feature }) => {
+export const OnApprovalPointPopup = ({ feature, point }) => {
const featureId = feature.properties.id;
const { setClickedPointConfig } = useClickedPointConfig();
const { status: initialStatus, postamat_id: initialPostamatId } =
- feature.properties;
+ point;
const [status, setStatus] = useState(initialStatus);
const [postamatId, setPostamatId] = useState(initialPostamatId);
@@ -92,6 +92,7 @@ export const OnApprovalPointPopup = ({ feature }) => {
<>
diff --git a/src/Map/Popup/mode-popup/PendingPointPopup.jsx b/src/Map/Popup/mode-popup/PendingPointPopup.jsx
index 68cee6e..261bddc 100644
--- a/src/Map/Popup/mode-popup/PendingPointPopup.jsx
+++ b/src/Map/Popup/mode-popup/PendingPointPopup.jsx
@@ -5,50 +5,17 @@ import { FeatureProperties } from "./FeatureProperties";
import { Button } from "antd";
import { useCanEdit } from "../../../api";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
+import { doesMatchFilter } from "../../../utils.js";
-export const PendingPointPopup = ({ feature }) => {
+export const PendingPointPopup = ({ feature, point }) => {
const { include, selection, exclude } = usePointSelection();
const { setClickedPointConfig } = useClickedPointConfig();
- const { filters } = usePendingPointsFilters();
- const doesMatchFilter = () => {
- const { prediction, categories, region } = filters;
- const {
- prediction_current,
- category,
- area,
- district,
- area_id,
- district_id,
- } = feature.properties;
-
- const doesMatchPredictionFilter =
- prediction_current >= prediction[0] &&
- prediction_current <= prediction[1];
-
- const doesMatchCategoriesFilter =
- categories.length > 0 ? categories.includes(category) : true;
-
- const doesMatchRegionFilter = () => {
- if (!region) return true;
-
- if (region.type === "ao") {
- return (district ?? district_id) === region.id;
- } else {
- return (area ?? area_id) === region.id;
- }
- };
-
- return (
- doesMatchPredictionFilter &&
- doesMatchCategoriesFilter &&
- doesMatchRegionFilter()
- );
- };
+ const { filters, ranges } = usePendingPointsFilters();
const featureId = feature.properties.id;
const isSelected =
- (doesMatchFilter() && !selection.excluded.has(featureId)) ||
+ (doesMatchFilter(filters, ranges, feature) && !selection.excluded.has(featureId)) ||
selection.included.has(featureId);
useEffect(
@@ -68,7 +35,7 @@ export const PendingPointPopup = ({ feature }) => {
return (
<>
-
+
{canEdit && (
{
+export const WorkingPointPopup = ({ feature, point }) => {
const featureId = feature.properties.id;
const { setClickedPointConfig } = useClickedPointConfig();
useEffect(() => setClickedPointConfig(featureId), [feature]);
- return ;
+ return ;
};
diff --git a/src/Map/Popup/mode-popup/config.js b/src/Map/Popup/mode-popup/config.js
index 2c6df83..502d3ce 100644
--- a/src/Map/Popup/mode-popup/config.js
+++ b/src/Map/Popup/mode-popup/config.js
@@ -41,8 +41,7 @@ export const commonPopupConfig = [
},
];
-export const residentialPopupConfig = [
- ...commonPopupConfig,
+export const residentialPopupFields = [
{
name: "Кол-во квартир",
field: "flat_cnt",
@@ -59,22 +58,24 @@ export const residentialPopupConfig = [
name: "Материал стен",
field: "mat_nes",
},
+]
+
+export const residentialPopupConfig = [
+ ...commonPopupConfig,
+ ...residentialPopupFields
];
+
+
export const workingPointFields = [
{ name: "План", field: "plan_current" },
{ name: "Факт", field: "fact" },
{ name: "Расхождение с прогнозом", field: "delta_current" },
{ name: "Зрелость", field: "age_day" },
- { name: "id постамата", field: "postamat_id", empty: "Не указан" },
+ { name: "id локации", field: "postamat_id", empty: "Не указан" },
];
-const RIVALS_MAPPER = {
- pvz: "ПВЗ",
- post: "Постамат",
-};
-
export const rivalsConfig = [
- { name: "Инфо", field: "info" },
- { name: "Категория", field: "type", render: (value) => RIVALS_MAPPER[value] },
+ { name: "Категория", field: "category_id" },
+ { name: "Группа", field: "group_id" },
];
diff --git a/src/Map/TrafficModal.jsx b/src/Map/TrafficModal.jsx
new file mode 100644
index 0000000..6f48607
--- /dev/null
+++ b/src/Map/TrafficModal.jsx
@@ -0,0 +1,79 @@
+import { Button, Col, Divider, Modal, Popover, Row, Tooltip } from "antd";
+import { useState } from "react";
+import { BiStats } from "react-icons/all.js";
+import { twMerge } from "tailwind-merge";
+import { PointChart } from "./PointChart.jsx";
+
+const ChartHelp = () => {
+ return (
+
+ График показывает топ-15 факторов, которые оказывают наибольшее влияние на прогнозный трафик в определённой точке.
+ Факторы могут оказывать положительное или отрицательное влияние.
+ Чем больше влияния оказывает фактор на прогнозный трафик, тем ближе его значение к 100% (-100%).
+
+ )
+}
+
+export const TrafficModal = ({point}) => {
+ const [isOpened, setIsOpened] = useState(false);
+
+ const getFooter = () => {
+ return [
+ setIsOpened(false)}
+ >
+ Закрыть
+ ,
+ ]
+ }
+
+ return (
+
+ {point.prediction_current}
+
+ setIsOpened(true)}>
+
+
+
+
setIsOpened(false)}
+ width={800}
+ footer={getFooter()}
+ style={{ top: "15px" }}
+ >
+
+
+
+
+ Адрес точки:
+
+ {point.address}
+
+
+
+ Прогнозный траффик:
+
+ {point.prediction_current}
+
+
+
+
+
* - в окрестности
+
}
+ trigger="click"
+ placement="leftBottom"
+ color="#ffffff"
+ >
+ Как читать график?
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/SignOut.jsx b/src/SignOut.jsx
index 280e378..f342779 100644
--- a/src/SignOut.jsx
+++ b/src/SignOut.jsx
@@ -13,7 +13,7 @@ export function SignOut() {
};
const { data } = useQuery(["profile"], async () => {
- const { data } = await api.get("/accounts/profile");
+ const { data } = await api.get("/accounts/profile/");
return data;
});
diff --git a/src/api.js b/src/api.js
index 6c6378a..4ffbe88 100644
--- a/src/api.js
+++ b/src/api.js
@@ -2,7 +2,11 @@ import axios from "axios";
import { useMutation, useQuery } from "@tanstack/react-query";
import { STATUSES } from "./config";
import { usePointSelection } from "./stores/usePointSelection";
-import { usePendingPointsFilters } from "./stores/usePendingPointsFilters";
+import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "./stores/usePendingPointsFilters";
+import { appendFiltersInUse } from "./utils.js";
+import { useMode } from "./stores/useMode.js";
+import { useMemo } from "react";
+import { useUpdateLayerCounter } from "./stores/useUpdateLayerCounter.js";
export const BASE_URL = import.meta.env.VITE_API_URL;
@@ -16,6 +20,18 @@ export const api = axios.create({
xsrfCookieName: "csrftoken",
});
+export const useDbTableName = () => {
+ const {isImportMode} = useMode();
+ if (isImportMode) return "pre_placement_points";
+ return "placement_points";
+}
+
+export const useSourceLayerName = () => {
+ const {isImportMode} = useMode();
+ if (isImportMode) return "public.prepoints_with_dist";
+ return "public.points_with_dist";
+}
+
const enrichParamsWithRegionFilter = (params, region) => {
const resultParams = params ? params : new URLSearchParams();
@@ -32,73 +48,164 @@ const enrichParamsWithRegionFilter = (params, region) => {
return resultParams;
};
-export const getPoints = async (params, region) => {
+export const getPoints = async (params, region, dbTable = "placement_points", signal) => {
const resultParams = enrichParamsWithRegionFilter(params, region);
const { data } = await api.get(
- `/api/placement_points?${resultParams.toString()}`
+ `/api/${dbTable}/?${resultParams.toString()}`, {signal}
);
return data;
};
-export const exportPoints = async (params, region) => {
+export const exportPoints = async (params, region, dbTable = "placement_points") => {
const resultParams = enrichParamsWithRegionFilter(params, region);
const { data } = await api.get(
- `/api/placement_points/to_excel?${resultParams.toString()}`,
+ `/api/${dbTable}/to_excel/?${resultParams.toString()}`,
{ responseType: "arraybuffer" }
);
return data;
};
+export const downloadImportTemplate = async () => {
+ const { data } = await api.get(
+ '/api/pre_placement_points/download_template/',
+ { responseType: "arraybuffer" }
+ );
+
+ return data;
+};
+
+export const uploadPointsFile = async (file, config) => {
+ const formData = new FormData();
+ formData.append("file", file);
+ const { data } = await api.post(
+ `/api/pre_placement_points/load_matching_file/`,
+ formData,
+ config
+ );
+
+ return data;
+};
+
+export const importPoints = async (id) => {
+ const formData = new FormData();
+ formData.append("id", id);
+ const { data } = await api.post(
+ `/api/pre_placement_points/start_matching/`,
+ formData
+ );
+
+ return data;
+};
+
export const useGetTotalInitialPointsCount = () => {
+ const dbTable = useDbTableName();
+ const { updateCounter } = useUpdateLayerCounter();
return useQuery(
- ["all-initial-count"],
- async () => {
+ ["all-initial-count", dbTable, updateCounter],
+ async ({signal}) => {
const params = new URLSearchParams({
page: 1,
page_size: 1,
- "status[]": [STATUSES.pending],
});
+ params.append("status[]", STATUSES.pending);
- return await getPoints(params);
+ return await getPoints(params, null, dbTable, signal);
},
- { select: (data) => data.count }
+ {
+ select: (data) => data.count,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false
+ }
);
};
-export const useGetFilteredPendingPointsCount = () => {
- const { filters } = usePendingPointsFilters();
- const { prediction, categories, region } = filters;
+export const useGetFilteredPendingPointsCount = (isMerge) => {
+ const { filters, ranges } = usePendingPointsFilters();
+ const { updateCounter } = useUpdateLayerCounter();
+ const {
+ categories,
+ region,
+ } = filters;
const {
selection: { included },
} = usePointSelection();
const includedIds = [...included];
+ const getParams = () => {
+ const params = new URLSearchParams({
+ page: 1,
+ page_size: 1,
+ "categories[]": categories,
+ "included[]": includedIds,
+ });
+ params.append("status[]", STATUSES.pending);
+
+ appendFiltersInUse(params, filters, ranges);
+
+ return params;
+ }
+
+ const getMergeParams = () => {
+ return new URLSearchParams({
+ "matching_status": "New",
+ });
+ }
+
+ const dbTable = useDbTableName();
return useQuery(
- ["filtered-points", filters, includedIds],
+ ["filtered-points", filters, dbTable, includedIds, updateCounter],
+ async ({signal}) => {
+ const params = isMerge ? getMergeParams() : getParams();
+
+ return await getPoints(params, region, dbTable, signal);
+ },
+ {
+ select: (data) => data.count,
+ keepPreviousData: true,
+ refetchOnWindowFocus: false,
+ refetchOnMount: false
+ }
+ );
+};
+
+export const useGetPointsToMergeCount = () => {
+ const getMergeParams = () => {
+ return new URLSearchParams({
+ "matching_status": "New",
+ });
+ }
+
+ const dbTable = useDbTableName();
+
+ return useQuery(
+ ["filtered-points", dbTable],
async () => {
- const params = new URLSearchParams({
- page: 1,
- page_size: 1,
- "prediction_current[]": prediction,
- "status[]": [STATUSES.pending],
- "categories[]": categories,
- "included[]": includedIds,
- });
+ const params = getMergeParams();
- return await getPoints(params, region);
+ return await getPoints(params, null, dbTable);
},
{ select: (data) => data.count, keepPreviousData: true }
);
};
+export const useMergePointsToDb = () => {
+ return useMutation({
+ mutationFn: () => {
+ return api.post(
+ `/api/pre_placement_points/move_points/`
+ );
+ },
+ });
+};
+
export const useGetPermissions = () => {
return useQuery(["permissions"], async () => {
- const { data } = await api.get("/api/me");
+ const { data } = await api.get("/api/me/");
if (data?.groups?.includes("Редактор")) {
return "editor";
@@ -108,18 +215,163 @@ export const useGetPermissions = () => {
});
};
+const TASK_STATUSES = {
+ finished: "Перерасчет ML завершен"
+}
export const useCanEdit = () => {
const { data } = useGetPermissions();
+ const { data: statusData } = useLastMLRun();
+
+ const hasFinishedUpdate = useMemo(() => {
+ return statusData?.task_status === TASK_STATUSES.finished
+ }, [statusData]);
- return data === "editor";
+ return data === "editor" && hasFinishedUpdate;
};
export const useUpdatePostamatId = () => {
return useMutation({
mutationFn: (params) => {
return api.put(
- `/api/placement_points/update_postamat_id?${params.toString()}`
+ `/api/placement_points/update_postamat_id/?${params.toString()}`
);
},
});
};
+
+export const getLastMLRun = async () => {
+ const { data } = await api.get(
+ `/api/placement_points/last_time_ml_run/`
+ );
+
+ return data;
+};
+
+export const startML = async () => {
+ const { data } = await api.get(
+ `/api/placement_points/start/`
+ );
+
+ return data;
+};
+
+export const getPostamatesAndPvzGroups = async () => {
+ const { data } = await api.get(
+ `/api/postamate_and_pvz_groups/`
+ );
+
+ return data;
+};
+
+export const usePostamatesAndPvzGroups = () => {
+ return useQuery(
+ ['groups'],
+ async () => {
+ return await getPostamatesAndPvzGroups();
+ },
+ );
+};
+
+export const getOtherGroups = async () => {
+ const { data } = await api.get(
+ `/api/other_object_groups/`
+ );
+
+ return data;
+};
+
+export const useOtherGroups = () => {
+ return useQuery(
+ ['other_groups'],
+ async () => {
+ return await getOtherGroups();
+ },
+ );
+};
+
+export const useLastMLRun = () => {
+ return useQuery(
+ ['last_time'],
+ async () => {
+ return await getLastMLRun();
+ },
+ {
+ refetchInterval: 5000,
+ }
+ );
+}
+
+export const useGetPendingPointsRange = (dbTable) => {
+ const { isImportMode } = useMode();
+ const statusFilter = isImportMode ? '' : `?status[]=${STATUSES.pending}`;
+
+ return useQuery(
+ ["prediction-max-min", dbTable],
+ async () => {
+ const { data, isInitialLoading, isFetching } = await api.get(
+ `/api/${dbTable}/filters/${statusFilter}`
+ );
+ return {data, isLoading: isInitialLoading || isFetching};
+ },
+ {
+ select: ({data, isLoading}) => {
+ const distToGroupsArr = data.dist_to_groups.map((groupRange) => {
+ return {
+ [`d${groupRange.group_id}`]: [Math.floor(groupRange.dist[0]), Math.min(Math.ceil(groupRange.dist[1]), 4000)],
+ }
+ });
+ const distToGroups = Object.assign({}, ...distToGroupsArr);
+ const rangesArr = RANGE_FILTERS_KEYS.map((key) => {
+ if ((/d[0-9]/.test(key))) return;
+ return {
+ [key]: [Math.floor(data[key][0]), Math.min(Math.ceil(data[key][1]), 4000)]
+ }
+ }).filter(item => !!item);
+ const ranges = Object.assign({}, ...rangesArr);
+ return {
+ fullRange: {
+ prediction: data.prediction_current,
+ ...ranges,
+ ...distToGroups
+ },
+ isLoading: isLoading
+ };
+ },
+ }
+ );
+};
+
+export const useGetPopupPoints = (features) => {
+ const pointsIds = features.map(f => f.properties.id);
+ const dbTable = useDbTableName();
+
+ const { data, isInitialLoading, isFetching } = useQuery(
+ ["popup_data", features],
+ async () => {
+ const params = new URLSearchParams({
+ "location_ids[]": pointsIds,
+ });
+
+ const { data } = await api.get(
+ `/api/${dbTable}/?${params.toString()}`
+ );
+
+ return data.results;
+ },
+ {
+ refetchOnWindowFocus: false,
+ refetchOnMount: false
+ }
+ );
+
+ return { data, isLoading: isInitialLoading || isFetching };
+};
+
+export const deletePoint = async (id) => {
+ const formData = new FormData();
+ formData.append("ids", id);
+ await api.delete(
+ `/api/pre_placement_points/delete_points/`,
+ { data: formData }
+ )
+}
diff --git a/src/assets/logopng.png b/src/assets/logopng.png
new file mode 100644
index 0000000..4a838dc
Binary files /dev/null and b/src/assets/logopng.png differ
diff --git a/src/components/ModeSelector.jsx b/src/components/ModeSelector.jsx
index 6b67075..74bcbb4 100644
--- a/src/components/ModeSelector.jsx
+++ b/src/components/ModeSelector.jsx
@@ -6,7 +6,7 @@ import { ApproveIcon } from "../icons/ApproveIcon";
import { WorkingIcon } from "../icons/WorkingIcon";
export const ModeSelector = () => {
- const { mode, setMode } = useMode();
+ const { mode, setMode, isImportMode } = useMode();
const handleClick = (selectedMode) => {
setMode(selectedMode);
@@ -47,6 +47,7 @@ export const ModeSelector = () => {
onClick={() => handleClick(MODES.WORKING)}
className="flex items-center justify-center"
size="large"
+ disabled={isImportMode}
/>
>
diff --git a/src/components/RegionSelect.jsx b/src/components/RegionSelect.jsx
index bdb46a0..e02f7ef 100644
--- a/src/components/RegionSelect.jsx
+++ b/src/components/RegionSelect.jsx
@@ -27,7 +27,7 @@ export const useGetRegions = () => {
return useQuery(
["regions"],
async () => {
- const { data } = await api.get("/api/ao_rayons");
+ const { data } = await api.get("/api/ao_rayons/");
return data;
},
{
@@ -37,6 +37,8 @@ export const useGetRegions = () => {
normalized: normalizeRegions(rawRegions),
};
},
+ refetchOnWindowFocus: false,
+ refetchOnMount: false
}
);
};
diff --git a/src/components/Title.jsx b/src/components/Title.jsx
index c9c2f7c..4ac0255 100644
--- a/src/components/Title.jsx
+++ b/src/components/Title.jsx
@@ -3,11 +3,11 @@ import { twMerge } from "tailwind-merge";
const { Text } = Typography;
-export const Title = ({ text, className, classNameText }) => {
+export const Title = ({ text, className, classNameText, type = "secondary" }) => {
return (
{text}
diff --git a/src/hooks/useLocalStorage.js b/src/hooks/useLocalStorage.js
new file mode 100644
index 0000000..01f6d35
--- /dev/null
+++ b/src/hooks/useLocalStorage.js
@@ -0,0 +1,25 @@
+import { useState, useEffect } from "react";
+
+const useLocalStorage = (key, defaultValue) => {
+ const [value, setValue] = useState(() => {
+ let currentValue;
+
+ try {
+ currentValue = JSON.parse(
+ localStorage.getItem(key) || String(defaultValue)
+ );
+ } catch (error) {
+ currentValue = defaultValue;
+ }
+
+ return currentValue;
+ });
+
+ useEffect(() => {
+ localStorage.setItem(key, JSON.stringify(value));
+ }, [value, key]);
+
+ return [value, setValue];
+};
+
+export default useLocalStorage;
\ No newline at end of file
diff --git a/src/hooks/useUpdateStatus.js b/src/hooks/useUpdateStatus.js
index 2254506..84f84fb 100644
--- a/src/hooks/useUpdateStatus.js
+++ b/src/hooks/useUpdateStatus.js
@@ -1,14 +1,15 @@
import { useMutation } from "@tanstack/react-query";
-import { api } from "../api";
+import { api, useDbTableName } from "../api";
import { useUpdateLayerCounter } from "../stores/useUpdateLayerCounter";
export const useUpdateStatus = ({ onSuccess }) => {
const { toggleUpdateCounter } = useUpdateLayerCounter();
+ const dbTable = useDbTableName();
return useMutation({
mutationFn: (params) => {
return api.put(
- `/api/placement_points/update_status?${params.toString()}`
+ `/api/${dbTable}/update_status/?${params.toString()}`
);
},
onSuccess: () => {
diff --git a/src/icons/Logo.jsx b/src/icons/Logo.jsx
index 44a6ff6..78beaec 100644
--- a/src/icons/Logo.jsx
+++ b/src/icons/Logo.jsx
@@ -1,23 +1,42 @@
-export const Logo = ({ width = 40, height = 40, fill }) => {
+import logo from "../assets/logopng.png";
+
+export const Logo = ({ width = 40, height = 40 }) => {
return (
-
{hasActiveFilters && (
diff --git a/src/modules/Sidebar/PendingPointsFilters/SelectedLocations.jsx b/src/modules/Sidebar/PendingPointsFilters/SelectedLocations.jsx
index 0e67cfb..c1e6921 100644
--- a/src/modules/Sidebar/PendingPointsFilters/SelectedLocations.jsx
+++ b/src/modules/Sidebar/PendingPointsFilters/SelectedLocations.jsx
@@ -9,7 +9,7 @@ import { useEffect } from "react";
export const SelectedLocations = ({ onSelectedChange }) => {
const { data: totalCount, isInitialLoading: isTotalLoading } =
useGetTotalInitialPointsCount();
- const { data: filteredCount, isInitialLoading: isFilteredLoading } =
+ const { data: filteredCount, isInitialLoading: isFilteredLoading, isFetching: isFilteredFetching } =
useGetFilteredPendingPointsCount();
const {
@@ -21,7 +21,7 @@ export const SelectedLocations = ({ onSelectedChange }) => {
[filteredCount, excluded]
);
- const showSpinner = isTotalLoading || isFilteredLoading;
+ const showSpinner = isTotalLoading || isFilteredLoading || isFilteredFetching;
return (
diff --git a/src/modules/Sidebar/PendingPointsFilters/TakeToWorkButton.jsx b/src/modules/Sidebar/PendingPointsFilters/TakeToWorkButton.jsx
index 64bf172..5f78c5d 100644
--- a/src/modules/Sidebar/PendingPointsFilters/TakeToWorkButton.jsx
+++ b/src/modules/Sidebar/PendingPointsFilters/TakeToWorkButton.jsx
@@ -7,9 +7,10 @@ import { useUpdateStatus } from "../../../hooks/useUpdateStatus";
import { ArrowRightOutlined } from "@ant-design/icons";
import { Title } from "../../../components/Title";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
+import { appendFiltersInUse } from "../../../utils.js";
export const TakeToWorkButton = ({ disabled }) => {
- const { filters } = usePendingPointsFilters();
+ const { filters, ranges } = usePendingPointsFilters();
const { prediction, categories, region } = filters;
const { selection } = usePointSelection();
const queryClient = useQueryClient();
@@ -34,6 +35,8 @@ export const TakeToWorkButton = ({ disabled }) => {
"excluded[]": [...selection.excluded],
});
+ appendFiltersInUse(params, filters, ranges);
+
if (region) {
if (region.type === "ao") {
params.append("district[]", region.id);
diff --git a/src/modules/Sidebar/Sidebar.jsx b/src/modules/Sidebar/Sidebar.jsx
index 24469f0..e9ebb46 100644
--- a/src/modules/Sidebar/Sidebar.jsx
+++ b/src/modules/Sidebar/Sidebar.jsx
@@ -5,6 +5,7 @@ import { MODES } from "../../config";
import { PendingPointsFilters } from "./PendingPointsFilters/PendingPointsFilters";
import { OnApprovalPointsFilters } from "./OnApprovalPointsFilters/OnApprovalPointsFilters";
import { WorkingPointsFilters } from "./WorkingPointsFilters/WorkingPointsFilters";
+import { ImportModeSidebarButtons } from "../ImportMode/SidebarButtons.jsx";
export const Sidebar = forwardRef(({ isCollapsed }, ref) => {
const { mode } = useMode();
@@ -29,6 +30,7 @@ export const Sidebar = forwardRef(({ isCollapsed }, ref) => {
)}
ref={ref}
>
+
{getFilters()}
);
diff --git a/src/modules/Sidebar/WorkingPointsFilters/WorkingPointsFilters.jsx b/src/modules/Sidebar/WorkingPointsFilters/WorkingPointsFilters.jsx
index 4101979..95de144 100644
--- a/src/modules/Sidebar/WorkingPointsFilters/WorkingPointsFilters.jsx
+++ b/src/modules/Sidebar/WorkingPointsFilters/WorkingPointsFilters.jsx
@@ -7,15 +7,18 @@ import { ClearFiltersButton } from "../../../components/ClearFiltersButton";
import { getDynamicActiveFilters } from "../utils";
import { Spin } from "antd";
import { useQuery } from "@tanstack/react-query";
-import { api } from "../../../api.js";
+import { api, useDbTableName } from "../../../api.js";
import { STATUSES } from "../../../config.js";
+import { useEffect } from "react";
+import { workingFilterHasChanged } from "../../../utils.js";
const useGetDataRange = () => {
+ const dbTable = useDbTableName();
return useQuery(
["working-max-min"],
async () => {
const { data } = await api.get(
- `/api/placement_points/filters?status[]=${STATUSES.working}`
+ `/api/${dbTable}/filters?status[]=${STATUSES.working}`
);
return data;
@@ -33,11 +36,20 @@ const useGetDataRange = () => {
};
export const WorkingPointsFilters = () => {
- const { filters, setRegion, clear } = useWorkingPointsFilters();
+ const { filters, ranges, setRegion, setAge, setDeltaTraffic, setRanges, setFactTraffic, clear } = useWorkingPointsFilters();
const { data: fullRange, isInitialLoading: isFullRangeLoading } =
useGetDataRange();
+ useEffect(() => {
+ if (!fullRange) return;
+ const newRanges = fullRange;
+ if (workingFilterHasChanged(newRanges.deltaTraffic, ranges, "deltaTraffic")) setDeltaTraffic(fullRange.deltaTraffic);
+ if (workingFilterHasChanged(newRanges.factTraffic, ranges, "factTraffic")) setFactTraffic(fullRange.deltaTraffic);
+ if (workingFilterHasChanged(newRanges.age, ranges, "age")) setAge(fullRange.deltaTraffic);
+ setRanges({...newRanges});
+ }, [fullRange]);
+
const activeDynamicFilters = getDynamicActiveFilters(filters, fullRange, [
"deltaTraffic",
"factTraffic",
diff --git a/src/modules/Table/HeaderWrapper.jsx b/src/modules/Table/HeaderWrapper.jsx
index de1a562..f068fc7 100644
--- a/src/modules/Table/HeaderWrapper.jsx
+++ b/src/modules/Table/HeaderWrapper.jsx
@@ -3,6 +3,7 @@ import { Button, Tooltip } from "antd";
import { useTable } from "../../stores/useTable";
import { FullscreenExitOutlined, FullscreenOutlined } from "@ant-design/icons";
import { useEffect, useState } from "react";
+import { TableSettings } from "./TableSettings";
const ToggleFullScreenButton = () => {
const {
@@ -51,6 +52,7 @@ export const HeaderWrapper = ({
rightColumn,
exportProvider,
classes,
+ orderColumns
}) => {
return (
@@ -61,6 +63,7 @@ export const HeaderWrapper = ({
{rightColumn}
diff --git a/src/modules/Table/OnApprovalTable/Header.jsx b/src/modules/Table/OnApprovalTable/Header.jsx
index 7513e82..206bf11 100644
--- a/src/modules/Table/OnApprovalTable/Header.jsx
+++ b/src/modules/Table/OnApprovalTable/Header.jsx
@@ -10,6 +10,7 @@ export const Header = ({
selectedIds,
onClearSelected,
onOpenMakeWorkingModal,
+ orderColumns
}) => {
const [status, setStatus] = useState(STATUSES.pending);
@@ -42,6 +43,7 @@ export const Header = ({
leftColumn: "flex items-center gap-x-4",
rightColumn: "flex item-center gap-x-4",
}}
+ orderColumns={orderColumns}
exportProvider={useExportOnApprovalData}
/>
);
diff --git a/src/modules/Table/OnApprovalTable/OnApprovalTable.jsx b/src/modules/Table/OnApprovalTable/OnApprovalTable.jsx
index c7bd27b..54115d5 100644
--- a/src/modules/Table/OnApprovalTable/OnApprovalTable.jsx
+++ b/src/modules/Table/OnApprovalTable/OnApprovalTable.jsx
@@ -1,6 +1,6 @@
import { Table } from "../Table";
import { useQuery } from "@tanstack/react-query";
-import { getPoints, useCanEdit } from "../../../api";
+import { getPoints, useCanEdit, useDbTableName } from "../../../api";
import { useCallback, useState } from "react";
import { PAGE_SIZE } from "../constants";
import { STATUSES } from "../../../config";
@@ -19,6 +19,8 @@ const extraCols = [
key: "postamat_id",
width: "70px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
];
@@ -31,13 +33,20 @@ export const OnApprovalTable = ({ fullWidth }) => {
} = useOnApprovalPointsFilters();
const [isMakeWorkingModalOpened, setIsMakeWorkingModalOpened] =
useState(false);
- const columns = useColumns(extraCols);
+ const { columns, orderColumns, sort, setSort } = useColumns(extraCols, 'onApprovalTableOrder');
const { isVisible } = useLayersVisibility();
+ const dbTable = useDbTableName();
+
+ const onSort = (sortDirection, key) => {
+ if (sortDirection === `ascend`) setSort(key);
+ if (sortDirection === `descend`) setSort(`-${key}`);
+ if (!sortDirection) setSort(null);
+ }
const clearSelected = () => setSelectedIds([]);
- const { data, isInitialLoading } = useQuery(
- ["on-approval-points", page, region, isVisible],
+ const { data, isInitialLoading, isFetching } = useQuery(
+ ["on-approval-points", page, region, isVisible, sort],
async () => {
const statuses = [];
@@ -60,13 +69,14 @@ export const OnApprovalTable = ({ fullWidth }) => {
statuses.length > 0
? statuses
: [STATUSES.onApproval, STATUSES.working, STATUSES.cancelled],
+ ordering: sort,
});
if (statuses.length === 0) {
return { count: 0, results: [] };
}
- return await getPoints(params, region);
+ return await getPoints(params, region, dbTable);
},
{ keepPreviousData: true }
);
@@ -94,6 +104,7 @@ export const OnApprovalTable = ({ fullWidth }) => {
selectedIds={selectedIds}
onClearSelected={clearSelected}
onOpenMakeWorkingModal={() => setIsMakeWorkingModalOpened(true)}
+ orderColumns={orderColumns}
/>
}
rowSelection={canEdit ? rowSelection : undefined}
@@ -104,7 +115,10 @@ export const OnApprovalTable = ({ fullWidth }) => {
isClickedPointLoading={isClickedPointLoading}
columns={columns}
fullWidth={fullWidth}
- loading={isInitialLoading}
+ onChange={(val, filter, sorter) => {
+ onSort(sorter.order, sorter.columnKey);
+ }}
+ loading={isInitialLoading || isFetching}
/>
{isMakeWorkingModalOpened && (
{
const {
filters: { region },
} = useOnApprovalPointsFilters();
+ const dbTable = useDbTableName();
return useQuery(
["export-on-approval", region],
@@ -16,7 +17,7 @@ export const useExportOnApprovalData = (enabled, onSettled) => {
"status[]": [STATUSES.onApproval, STATUSES.working],
});
- return await exportPoints(params, region);
+ return await exportPoints(params, region, dbTable);
},
{
enabled,
diff --git a/src/modules/Table/PendingTable/PendingTable.jsx b/src/modules/Table/PendingTable/PendingTable.jsx
index db74dd6..2f3af31 100644
--- a/src/modules/Table/PendingTable/PendingTable.jsx
+++ b/src/modules/Table/PendingTable/PendingTable.jsx
@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from "react";
+import React, {useCallback, useState} from "react";
import { Table } from "../Table";
import { usePointSelection } from "../../../stores/usePointSelection";
import { useClickedPointConfig } from "../../../stores/useClickedPointConfig";
@@ -9,20 +9,31 @@ import { useCanEdit } from "../../../api";
import { useColumns } from "../useColumns.jsx";
import { PAGE_SIZE } from "../constants.js";
import { usePopup } from "../../../stores/usePopup.js";
+import { usePendingTableFields } from "./usePendingTableFields.jsx";
+const tableKey = 'pendingTable';
export const PendingTable = ({ fullWidth }) => {
const { selection, include, exclude } = usePointSelection();
const { clickedPointConfig, setClickedPointConfig } = useClickedPointConfig();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(PAGE_SIZE);
- const columns = useColumns();
+
+ const fields = usePendingTableFields();
+ const {columns, orderColumns, sort, setSort} = useColumns(fields, tableKey);
const { setPopup } = usePopup();
+ const onSort = (sortDirection, key) => {
+ if (sortDirection === `ascend`) setSort(key);
+ if (sortDirection === `descend`) setSort(`-${key}`);
+ if (!sortDirection) setSort(null);
+ }
+
const { data, isClickedPointLoading, isDataLoading } = usePendingTableData(
page,
() => setPage(1),
pageSize,
- setPageSize
+ setPageSize,
+ sort
);
const resetPageSize = () => setPageSize(PAGE_SIZE);
@@ -79,7 +90,10 @@ export const PendingTable = ({ fullWidth }) => {
isClickedPointLoading={isClickedPointLoading}
columns={columns}
fullWidth={fullWidth}
- header={}
+ onChange={(val, filter, sorter) => {
+ onSort(sorter.order, sorter.columnKey);
+ }}
+ header={}
loading={isDataLoading}
/>
);
diff --git a/src/modules/Table/PendingTable/useExportPendingData.js b/src/modules/Table/PendingTable/useExportPendingData.js
index 014f7aa..7ab1b1e 100644
--- a/src/modules/Table/PendingTable/useExportPendingData.js
+++ b/src/modules/Table/PendingTable/useExportPendingData.js
@@ -1,38 +1,47 @@
import { usePointSelection } from "../../../stores/usePointSelection";
import { useQuery } from "@tanstack/react-query";
-import { exportPoints } from "../../../api";
+import { exportPoints, useDbTableName } from "../../../api";
import { handleExportSuccess } from "../ExportButton";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
import { STATUSES } from "../../../config.js";
+import { appendFiltersInUse } from "../../../utils.js";
export const useExportPendingData = (enabled, onSettled) => {
- const { filters } = usePendingPointsFilters();
- const { prediction, categories, region } = filters;
+ const { filters, ranges } = usePendingPointsFilters();
+ const { categories, region } = filters;
const { selection } = usePointSelection();
const includedArr = [...selection.included];
const excludedArr = [...selection.excluded];
+ const dbTable = useDbTableName();
return useQuery(
["export-initial", filters, selection],
async () => {
- const params = new URLSearchParams({
- "prediction_current[]": prediction,
- "status[]": [STATUSES.pending],
- });
- if (categories.length) {
- params.append("categories[]", categories);
- }
+ const getParams = () => {
+ const params = new URLSearchParams({
+ "status[]": [STATUSES.pending],
+ });
- if (includedArr.length) {
- params.append("included[]", includedArr);
- }
+ appendFiltersInUse(params, filters, ranges);
+ params.append("status[]", [STATUSES.pending, STATUSES.cancelled].join(","));
+
+ if (categories.length) {
+ params.append("categories[]", categories);
+ }
+
+ if (includedArr.length) {
+ params.append("included[]", includedArr);
+ }
+
+ if (excludedArr.length) {
+ params.append("excluded[]", excludedArr);
+ }
- if (excludedArr.length) {
- params.append("excluded[]", excludedArr);
+ return params;
}
- return await exportPoints(params, region);
+ return await exportPoints(getParams(), region, dbTable);
},
{
enabled,
diff --git a/src/modules/Table/PendingTable/usePendingTableData.js b/src/modules/Table/PendingTable/usePendingTableData.js
index 1d7619c..d0a52e6 100644
--- a/src/modules/Table/PendingTable/usePendingTableData.js
+++ b/src/modules/Table/PendingTable/usePendingTableData.js
@@ -1,25 +1,41 @@
import { useQuery } from "@tanstack/react-query";
-import { getPoints } from "../../../api";
+import { useDbTableName, getPoints } from "../../../api";
import { useMergeTableData } from "../useMergeTableData";
import { STATUSES } from "../../../config";
import { usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
+import { appendFiltersInUse } from "../../../utils.js";
+import { useUpdateLayerCounter } from "../../../stores/useUpdateLayerCounter.js";
-export const usePendingTableData = (page, resetPage, pageSize, setPageSize) => {
- const { filters } = usePendingPointsFilters();
- const { prediction, categories, region } = filters;
-
- const { data, isInitialLoading } = useQuery(
- ["table", page, filters],
- async () => {
- const params = new URLSearchParams({
- page,
- page_size: pageSize,
- "prediction_current[]": prediction,
- "status[]": [STATUSES.pending],
- "categories[]": categories,
- });
-
- return await getPoints(params, region);
+export const usePendingTableData = (page, resetPage, pageSize, setPageSize, sort) => {
+ const { filters, ranges } = usePendingPointsFilters();
+ const { updateCounter } = useUpdateLayerCounter();
+ const {
+ categories,
+ region,
+ } = filters;
+
+ const dbTable = useDbTableName();
+
+ const getParams = () => {
+ const params = new URLSearchParams({
+ page,
+ page_size: pageSize,
+ "categories[]": categories,
+ ordering: sort,
+ });
+
+ appendFiltersInUse(params, filters, ranges);
+ params.append("status[]", [STATUSES.pending, STATUSES.cancelled].join(","))
+
+ return params;
+ }
+
+ const {data, isInitialLoading, isFetching} = useQuery(
+ ["table", page, filters, sort, dbTable, updateCounter],
+ async ({signal}) => {
+ const params = getParams();
+
+ return await getPoints(params, region, dbTable, signal);
},
{
keepPreviousData: true,
@@ -28,10 +44,12 @@ export const usePendingTableData = (page, resetPage, pageSize, setPageSize) => {
resetPage();
}
},
+ refetchOnWindowFocus: false,
+ refetchOnMount: false
}
);
- const { data: mergedData, isClickedPointLoading } = useMergeTableData(
+ const {data: mergedData, isClickedPointLoading} = useMergeTableData(
data,
setPageSize
);
@@ -40,6 +58,6 @@ export const usePendingTableData = (page, resetPage, pageSize, setPageSize) => {
data: mergedData,
pageSize,
isClickedPointLoading,
- isDataLoading: isInitialLoading,
+ isDataLoading: isInitialLoading || isFetching,
};
};
diff --git a/src/modules/Table/PendingTable/usePendingTableFields.jsx b/src/modules/Table/PendingTable/usePendingTableFields.jsx
new file mode 100644
index 0000000..3e31073
--- /dev/null
+++ b/src/modules/Table/PendingTable/usePendingTableFields.jsx
@@ -0,0 +1,75 @@
+import { useMode } from "../../../stores/useMode.js";
+import React, { useMemo } from "react";
+import { Button } from "antd";
+import { BiTrash } from "react-icons/all.js";
+import { deletePoint } from "../../../api.js";
+import { useUpdateLayerCounter } from "../../../stores/useUpdateLayerCounter.js";
+
+const MATCHING_STATUS = {
+ New: {
+ name: 'Новая',
+ color: 'import_status_new'
+ },
+ Error: {
+ name: 'Ошибка геокодирования',
+ color: 'import_status_error'
+ },
+ Matched: {
+ name: 'Совпадение',
+ color: 'import_status_matched'
+ },
+
+}
+
+export const usePendingTableFields = () => {
+ const { isImportMode } = useMode();
+ const { toggleUpdateCounter } = useUpdateLayerCounter();
+ const deleteRow = async (e, id) => {
+ e.stopPropagation();
+ try {
+ await deletePoint(id);
+ toggleUpdateCounter();
+ } catch (e) {
+ //
+ }
+ };
+
+ const fields = useMemo(() => {
+ return isImportMode ? [
+ {
+ title: "Статус импорта",
+ dataIndex: "matching_status",
+ key: "matching_status",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ render: (_, record) => {
+ if (!record.matching_status) return;
+ const name = MATCHING_STATUS[record.matching_status].name;
+ const color = MATCHING_STATUS[record.matching_status].color;
+ return (
+
+ {name}
+
+ );
+ },
+ },
+ {
+ title: "Удалить",
+ key: "del",
+ width: "60px",
+ ellipsis: true,
+ render: (_, record) => {
+ if (!record.id) return;
+ return (
+ deleteRow(event, record.id)}>
+
+
+ );
+ },
+ }
+ ] : [];
+ }, [isImportMode]);
+ return fields;
+}
\ No newline at end of file
diff --git a/src/modules/Table/Table.css b/src/modules/Table/Table.css
index d3c24b4..750ca2f 100644
--- a/src/modules/Table/Table.css
+++ b/src/modules/Table/Table.css
@@ -14,6 +14,10 @@
cursor: pointer;
}
+.table__wrapper__fullScreen .ant-table-container {
+ height: calc(100vh - 98px);
+}
+
.table__title {
padding: 0 1rem;
display: flex;
diff --git a/src/modules/Table/Table.jsx b/src/modules/Table/Table.jsx
index 07aa1f6..a6539a9 100644
--- a/src/modules/Table/Table.jsx
+++ b/src/modules/Table/Table.jsx
@@ -22,6 +22,7 @@ export const Table = React.memo(
header,
fullWidth,
loading,
+ onChange
}) => {
const { clickedPointConfig, setClickedPointConfig } =
useClickedPointConfig();
@@ -56,7 +57,10 @@ export const Table = React.memo(
>
}}
pagination={{
pageSize,
@@ -66,8 +70,10 @@ export const Table = React.memo(
showSizeChanger: false,
position: "bottomCenter",
}}
+ showHeader={data?.results && data.results.length > 0}
dataSource={data?.results}
columns={columns}
+ onChange={onChange}
rowKey="id"
scroll={SCROLL}
sticky={true}
diff --git a/src/modules/Table/TableSettings.jsx b/src/modules/Table/TableSettings.jsx
new file mode 100644
index 0000000..81fa9fb
--- /dev/null
+++ b/src/modules/Table/TableSettings.jsx
@@ -0,0 +1,79 @@
+import { useEffect, useState } from "react";
+import {Button, Checkbox, Dropdown} from "antd";
+import {SettingOutlined} from "@ant-design/icons";
+import {DragDropContext, Draggable, Droppable} from "react-beautiful-dnd";
+
+export const TableSettings = ({orderColumns}) => {
+ const [columnsList, setColumnsList] = useState(orderColumns.order);
+ useEffect(() => {
+ setColumnsList(orderColumns.order);
+ }, [orderColumns]);
+
+ const handleDrop = (droppedItem) => {
+ // Ignore drop outside droppable container
+ if (!droppedItem.destination) return;
+ var updatedList = [...columnsList];
+ // Remove dragged item
+ const [reorderedItem] = updatedList.splice(droppedItem.source.index, 1);
+ // Add dropped item
+ updatedList.splice(droppedItem.destination.index, 0, reorderedItem);
+ // Update State
+ setColumnsList(updatedList);
+ orderColumns.setOrder(updatedList);
+ };
+
+ const hideColumn = (columnIndex) => {
+ const updatedList = columnsList.map((item, index) => {
+ if (columnIndex === index) return {...item, show: !item.show};
+ return item;
+ });
+ setColumnsList(updatedList);
+ orderColumns.setOrder(updatedList);
+ }
+
+ const columnsListRender = () => {
+ return (
+ e.stopPropagation()} className='z-10 bg-white-background rounded-xl p-3 space-y-3'
+ style={{ maxHeight: "80vh", overflowY: "scroll", margin: "24px 0 24px" }}>
+
+
+ {(provided) => (
+
+ {columnsList.map((item, index) => {
+ const num = item.position;
+ if (!orderColumns.defaultColumns[num]) return;
+ return (
+
+ {(provided) => (
+
+
hideColumn(index)} checked={item.show} />
+
+ { orderColumns.defaultColumns[num].name || orderColumns.defaultColumns[num].title }
+
+
+ )}
+
+ );
+ })}
+ {provided.placeholder}
+
+ )}
+
+
+
+ )
+ }
+
+ return (
+ columnsListRender()}
+ >
+ e.stopPropagation()}
+ >
+
+
+
+ );
+};
diff --git a/src/modules/Table/WorkingTable/WorkingTable.jsx b/src/modules/Table/WorkingTable/WorkingTable.jsx
index 89b5f8b..7ad286a 100644
--- a/src/modules/Table/WorkingTable/WorkingTable.jsx
+++ b/src/modules/Table/WorkingTable/WorkingTable.jsx
@@ -10,16 +10,23 @@ import { useExportWorkingData } from "./useExportWorkingData";
import { useWorkingPointsFilters } from "../../../stores/useWorkingPointsFilters";
import { useColumns } from "./useColumns.jsx";
+const tableKey = 'workingTable'
export const WorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1);
const {
filters: { region, deltaTraffic, factTraffic, age },
} = useWorkingPointsFilters();
- const columns = useColumns();
+ const {columns, orderColumns, sort, setSort} = useColumns(tableKey);
- const { data, isInitialLoading } = useQuery(
- ["working-points", page, region, deltaTraffic, factTraffic, age],
+ const onSort = (sortDirection, key) => {
+ if (sortDirection === `ascend`) setSort(key);
+ if (sortDirection === `descend`) setSort(`-${key}`);
+ if (!sortDirection) setSort(null);
+ }
+
+ const { data, isInitialLoading, isFetching } = useQuery(
+ ["working-points", page, region, deltaTraffic, factTraffic, age, sort],
async () => {
const params = new URLSearchParams({
page,
@@ -28,6 +35,7 @@ export const WorkingTable = ({ fullWidth }) => {
"delta_current[]": deltaTraffic,
"fact[]": factTraffic,
"age_day[]": age,
+ ordering: sort
});
return await getPoints(params, region);
@@ -51,8 +59,11 @@ export const WorkingTable = ({ fullWidth }) => {
isClickedPointLoading={isClickedPointLoading}
columns={columns}
fullWidth={fullWidth}
- header={}
- loading={isInitialLoading}
+ onChange={(val, filter, sorter) => {
+ onSort(sorter.order, sorter.columnKey);
+ }}
+ header={}
+ loading={isInitialLoading || isFetching}
/>
);
};
diff --git a/src/modules/Table/WorkingTable/useColumns.jsx b/src/modules/Table/WorkingTable/useColumns.jsx
index d988161..7fea0d1 100644
--- a/src/modules/Table/WorkingTable/useColumns.jsx
+++ b/src/modules/Table/WorkingTable/useColumns.jsx
@@ -1,38 +1,52 @@
-import { useGetRegions } from "../../../components/RegionSelect.jsx";
-import { useMemo } from "react";
-import { getRegionNameById } from "../../../Map/Popup/mode-popup/config.js";
-import { SearchOutlined } from "@ant-design/icons";
-import { Button, Popover } from "antd";
-import { AddressSearch } from "../../../Map/AddressSearch.jsx";
-import { useTable } from "../../../stores/useTable.js";
+import {useGetRegions} from "../../../components/RegionSelect.jsx";
+import {useMemo} from "react";
+import {getRegionNameById} from "../../../Map/Popup/mode-popup/config.js";
+import {SearchOutlined} from "@ant-design/icons";
+import {Button, Popover} from "antd";
+import {AddressSearch} from "../../../Map/AddressSearch.jsx";
+import {useTable} from "../../../stores/useTable.js";
+import useLocalStorage from "../../../hooks/useLocalStorage.js";
-export const useColumns = () => {
- const { data: regions } = useGetRegions();
+const DEFAULT_LENGTH = 11;
+export const useColumns = (key) => {
+ const {data: regions} = useGetRegions();
const {
- tableState: { fullScreen },
+ tableState: {fullScreen},
} = useTable();
- return useMemo(() => {
+ const [order, setOrder] = useLocalStorage(`${key}Order`, [...Array(DEFAULT_LENGTH).keys()].map((position) => {
+ return {
+ position,
+ show: true,
+ }
+ }));
+
+ const [sort, setSort] = useLocalStorage(`${key}Sort`, null);
+
+ const defaultColumns = useMemo(() => {
return [
{
title: fullScreen ? (
Адрес
}
+ content={
}
trigger="click"
placement={"right"}
>
-
-
+ e.stopPropagation()}>
+
) : (
"Адрес"
),
+ name: "Адрес",
dataIndex: "address",
key: "address",
+ sorter: true,
+ showSorterTooltip: false,
width: 200,
},
{
@@ -41,6 +55,8 @@ export const useColumns = () => {
key: "area",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
render: (_, record) => {
return getRegionNameById(record.area, regions?.normalized);
},
@@ -54,6 +70,8 @@ export const useColumns = () => {
render: (_, record) => {
return getRegionNameById(record.district, regions?.normalized);
},
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Название",
@@ -61,6 +79,8 @@ export const useColumns = () => {
key: "name",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Категория",
@@ -68,6 +88,8 @@ export const useColumns = () => {
key: "category",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "План",
@@ -75,6 +97,8 @@ export const useColumns = () => {
key: "plan_current",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Факт",
@@ -82,6 +106,8 @@ export const useColumns = () => {
key: "fact",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Расхождение с прогнозом",
@@ -89,6 +115,8 @@ export const useColumns = () => {
key: "delta_current",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Зрелость",
@@ -96,12 +124,16 @@ export const useColumns = () => {
key: "age_day",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Дата начала работы",
dataIndex: "start_date",
key: "start_date",
width: "120px",
+ sorter: true,
+ showSorterTooltip: false,
render: (value) => {
if (!value) return "Нет данных";
@@ -115,7 +147,31 @@ export const useColumns = () => {
key: "postamat_id",
width: "70px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
];
}, [regions?.normalized, fullScreen]);
+
+ const columns = useMemo(() => {
+ return order.flatMap((item) => !item.show ? [] : defaultColumns[item.position])
+ .map((column) => {
+ if (sort && sort.includes(column.key)) return {
+ ...column,
+ defaultSortOrder: sort.includes('-') ? 'descend' : 'ascend',
+ };
+ return column;
+ });
+ }, [defaultColumns, order, fullScreen]);
+
+ return {
+ columns,
+ orderColumns: {
+ defaultColumns,
+ order,
+ setOrder,
+ },
+ sort,
+ setSort
+ };
};
diff --git a/src/modules/Table/useColumns.jsx b/src/modules/Table/useColumns.jsx
index e5a3614..f30b15f 100644
--- a/src/modules/Table/useColumns.jsx
+++ b/src/modules/Table/useColumns.jsx
@@ -1,19 +1,22 @@
import { STATUS_LABEL_MAPPER } from "../../config";
-import { useMemo } from "react";
+import { useEffect, useMemo } from "react";
import { useGetRegions } from "../../components/RegionSelect.jsx";
import { getRegionNameById } from "../../Map/Popup/mode-popup/config.js";
import { Button, Popover } from "antd";
import { AddressSearch } from "../../Map/AddressSearch.jsx";
import { SearchOutlined } from "@ant-design/icons";
import { useTable } from "../../stores/useTable.js";
-
-export const useColumns = (fields = []) => {
+import useLocalStorage from "../../hooks/useLocalStorage.js";
+import { TrafficModal } from "../../Map/TrafficModal.jsx";
+export const useColumns = (fields = [], key) => {
const { data: regions } = useGetRegions();
const {
tableState: { fullScreen },
} = useTable();
- return useMemo(() => {
+ const [sort, setSort] = useLocalStorage(`${key}Sort`, null);
+
+ const defaultColumns = useMemo(() => {
return [
{
title: fullScreen ? (
@@ -24,7 +27,7 @@ export const useColumns = (fields = []) => {
trigger="click"
placement={"right"}
>
-
+ e.stopPropagation()} >
@@ -32,9 +35,12 @@ export const useColumns = (fields = []) => {
) : (
"Адрес"
),
+ name: "Адрес",
dataIndex: "address",
key: "address",
width: 200,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Район",
@@ -42,6 +48,8 @@ export const useColumns = (fields = []) => {
key: "area",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
render: (_, record) => {
return getRegionNameById(record.area, regions?.normalized);
},
@@ -52,6 +60,8 @@ export const useColumns = (fields = []) => {
key: "district",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
render: (_, record) => {
return getRegionNameById(record.district, regions?.normalized);
},
@@ -62,6 +72,8 @@ export const useColumns = (fields = []) => {
key: "name",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Категория",
@@ -69,6 +81,8 @@ export const useColumns = (fields = []) => {
key: "category",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
{
title: "Статус",
@@ -76,6 +90,8 @@ export const useColumns = (fields = []) => {
key: "status",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
render: (_, record) => {
return STATUS_LABEL_MAPPER[record.status];
},
@@ -86,8 +102,346 @@ export const useColumns = (fields = []) => {
key: "prediction_current",
width: "120px",
ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ render: (_, record) =>
+ },
+ {
+ title: "Кол-во подъездов в жилом доме",
+ dataIndex: "doors",
+ key: "doors",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Класс энероэффективности жилого дома",
+ dataIndex: "enrg_cls",
+ key: "enrg_cls",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во квартир в подъезде жилого дома",
+ dataIndex: "flat_cnt",
+ key: "flat_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Год постройки жилого дома",
+ dataIndex: "year_bld",
+ key: "year_bld",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во этажей жилого дома",
+ dataIndex: "levels",
+ key: "levels",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Материал стен жилого дома",
+ dataIndex: "mat_nes",
+ key: "mat_nes",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во постаматов других сетей в окрестности 500м (далее аналогично)",
+ dataIndex: "rival_post_cnt",
+ key: "rival_post_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во ПВЗ",
+ dataIndex: "rival_pvz_cnt",
+ key: "rival_pvz_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во постаматов Мой постамат",
+ dataIndex: "target_post_cnt",
+ key: "target_post_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во квартир в окрестности",
+ dataIndex: "flats_cnt",
+ key: "flats_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во достопримечательностей",
+ dataIndex: "attraction_cnt",
+ key: "attraction_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во банков",
+ dataIndex: "bank_cnt",
+ key: "bank_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во торговых центров",
+ dataIndex: "tc_cnt",
+ key: "tc_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во бизнес-центров",
+ dataIndex: "bc_cnt",
+ key: "bc_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во клиник",
+ dataIndex: "clinic_cnt",
+ key: "clinic_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во объектов культуры (театры, музей и тд)",
+ dataIndex: "culture_cnt",
+ key: "culture_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во спортивных центров",
+ dataIndex: "sport_center_cnt",
+ key: "sport_center_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во лабораторий",
+ dataIndex: "lab_cnt",
+ key: "lab_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во школ",
+ dataIndex: "school_cnt",
+ key: "school_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во детских садов",
+ dataIndex: "kindergar_cnt",
+ key: "kindergar_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во МФЦ",
+ dataIndex: "mfc_cnt",
+ key: "mfc_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во аптек",
+ dataIndex: "pharmacy_cnt",
+ key: "pharmacy_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во остановок ОТ",
+ dataIndex: "public_stop_cnt",
+ key: "public_stop_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во объектов из HORECA",
+ dataIndex: "reca_cnt",
+ key: "reca_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во супермаркетов",
+ dataIndex: "supermarket_cnt",
+ key: "supermarket_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Кол-во премиальных супермаркетов",
+ dataIndex: "supermarket_premium_cnt",
+ key: "supermarket_premium_cnt",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Расстояние до постамата Мой постамата",
+ dataIndex: "target_dist",
+ key: "target_dist",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Расстояние до метро",
+ dataIndex: "metro_dist",
+ key: "metro_dist",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Стоимость жилой недвижимости ",
+ dataIndex: "property_price_bargains",
+ key: "property_price_bargains",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Бизнес-активность",
+ dataIndex: "business_activity",
+ key: "business_activity",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Эра постройки жилой недвижимости",
+ dataIndex: "property_era",
+ key: "property_era",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
+ },
+ {
+ title: "Средняя этажность застройки",
+ dataIndex: "property_mean_floor",
+ key: "property_mean_floor",
+ width: "120px",
+ ellipsis: true,
+ sorter: true,
+ showSorterTooltip: false,
},
...fields,
- ];
+ ].filter(Boolean);
}, [regions?.normalized, fields, fullScreen]);
+
+ const [order, setOrder] = useLocalStorage(`${key}Order`, defaultColumns.map((column, index) => {
+ return {
+ key: column.key,
+ position: index,
+ show: true,
+ }
+ }));
+
+ useEffect(() => {
+ const newColumns = defaultColumns.filter((column) => {
+ return !order.find(c => c.key === column.key);
+ });
+ const newOrderColumns = newColumns.map((column, index) => {
+ return {
+ key: column.key,
+ position: defaultColumns.length - index - 1,
+ show: true,
+ }
+ });
+ setOrder([
+ ...order,
+ ...newOrderColumns
+ ]);
+ }, [defaultColumns]);
+
+ const columns = useMemo(() => {
+ return order.flatMap((item) => !item.show ? [] : defaultColumns[item.position])
+ .map((column) => {
+ if (sort && sort.includes(column.key)) return {
+ ...column,
+ defaultSortOrder: sort.includes('-') ? 'descend' : 'ascend',
+ };
+ return column;
+ }).filter(Boolean);
+ }, [defaultColumns, order, fullScreen]);
+
+ return {
+ columns,
+ orderColumns: {
+ defaultColumns,
+ order,
+ setOrder,
+ },
+ sort,
+ setSort
+ };
};
diff --git a/src/modules/Table/useMergeTableData.js b/src/modules/Table/useMergeTableData.js
index 4580513..25691f9 100644
--- a/src/modules/Table/useMergeTableData.js
+++ b/src/modules/Table/useMergeTableData.js
@@ -47,7 +47,7 @@ export const useMergeTableData = (fullData, onPageSizeChange) => {
onPageSizeChange(PAGE_SIZE + 1);
setMergedData({
- count: fullData.count + 1,
+ count: fullData?.count + 1,
results: [clickedPointData.results[0], ...fullData.results],
});
}, [clickedPointData, fullData]);
diff --git a/src/stores/useLayersVisibility.js b/src/stores/useLayersVisibility.js
index 3c9819d..60e52f0 100644
--- a/src/stores/useLayersVisibility.js
+++ b/src/stores/useLayersVisibility.js
@@ -5,10 +5,10 @@ import { persist } from "zustand/middleware";
const INITIAL_STATE = {
[LAYER_IDS.initial]: true,
- [LAYER_IDS.approve]: false,
- [LAYER_IDS.working]: false,
+ [LAYER_IDS.approve]: true,
+ [LAYER_IDS.working]: true,
[LAYER_IDS.filteredWorking]: false,
- [LAYER_IDS.cancelled]: false,
+ [LAYER_IDS.cancelled]: true,
[LAYER_IDS.pvz]: true,
[LAYER_IDS.other]: true,
};
diff --git a/src/stores/useMode.js b/src/stores/useMode.js
index c339f62..9ab7a5a 100644
--- a/src/stores/useMode.js
+++ b/src/stores/useMode.js
@@ -5,12 +5,18 @@ import { persist } from "zustand/middleware";
const store = (set) => ({
mode: MODES.PENDING,
+ isImportMode: false,
setMode: (mode) => {
set((state) => {
state.mode = mode;
});
},
+ setImportMode: (value) => {
+ set((state) => {
+ state.isImportMode = value
+ });
+ }
});
export const useMode = create(persist(immer(store), { name: "postnet/mode" }));
diff --git a/src/stores/usePendingPointsFilters.js b/src/stores/usePendingPointsFilters.js
index ff8ab77..35f4d00 100644
--- a/src/stores/usePendingPointsFilters.js
+++ b/src/stores/usePendingPointsFilters.js
@@ -2,14 +2,102 @@ import { create } from "zustand";
import { immer } from "zustand/middleware/immer";
import { persist } from "zustand/middleware";
+export const RANGE_FILTERS_KEYS = [
+ 'doors',
+ 'flat_cnt',
+ 'rival_post_cnt',
+ 'rival_pvz_cnt',
+ 'target_post_cnt',
+ 'flats_cnt',
+ 'tc_cnt',
+ 'culture_cnt',
+ 'mfc_cnt',
+ 'public_stop_cnt',
+ 'supermarket_cnt',
+ 'target_dist',
+ 'metro_dist',
+]
+
+export const RANGE_FILTERS_MAP = {
+ common: {
+ name: "Общие",
+ doors: "Кол-во подъездов в жилом доме",
+ flat_cnt: "Кол-во квартир в подъезде жилого дома",
+ },
+ objects_dist: {
+ name: "Кол-во объектов в окрестности 500м",
+ rival_post_cnt: "Кол-во постаматов других сетей",
+ rival_pvz_cnt: "Кол-во ПВЗ",
+ target_post_cnt: "Кол-во постаматов Мой постамат",
+ flats_cnt: "Кол-во квартир в окрестности",
+ tc_cnt: "Кол-во торговых центров",
+ culture_cnt: "Кол-во объектов культуры (театры, музей и тд)",
+ mfc_cnt: "Кол-во МФЦ",
+ public_stop_cnt: "Кол-во остановок ОТ",
+ supermarket_cnt: "Кол-во супермаркетов",
+ target_dist: "Расстояние до постамата Мой постамат",
+ metro_dist: "Расстояние до метро",
+ },
+}
+
+export const CATEGORIES_MAP = {
+ "ПВЗ": "Расстояние до ПВЗ сети",
+ "Постаматы прочих сетей": "Расстояние до постамата сети",
+}
+
export const INITIAL = {
prediction: [0, 0],
categories: [],
region: null,
+ doors__gt: 0,
+ doors__lt: 0,
+ flat_cnt__gt: 0,
+ flat_cnt__lt: 5000,
+ rival_post_cnt__gt: 0,
+ rival_post_cnt__lt: 5000,
+ rival_pvz_cnt__gt: 0,
+ rival_pvz_cnt__lt: 5000,
+ target_post_cnt__gt: 0,
+ target_post_cnt__lt: 5000,
+ flats_cnt__gt: 0,
+ flats_cnt__lt: 5000,
+ tc_cnt__gt: 0,
+ tc_cnt__lt: 5000,
+ culture_cnt__gt: 0,
+ culture_cnt__lt: 5000,
+ mfc_cnt__gt: 0,
+ mfc_cnt__lt: 5000,
+ public_stop_cnt__gt: 0,
+ public_stop_cnt__lt: 5000,
+ supermarket_cnt__gt: 0,
+ supermarket_cnt__lt: 5000,
+ target_dist__gt: 0,
+ target_dist__lt: 5000,
+ metro_dist__gt: 0,
+ metro_dist__lt: 5000,
};
+const INITIAL_RANGES = {
+ prediction: [0, 0],
+ doors: [0, 0],
+ flat_cnt: [0, 5000],
+ rival_post_cnt: [0, 5000],
+ rival_pvz_cnt: [0, 5000],
+ target_post_cnt: [0, 5000],
+ flats_cnt: [0, 5000],
+ tc_cnt: [0, 5000],
+ culture_cnt: [0, 5000],
+ mfc_cnt: [0, 5000],
+ public_stop_cnt: [0, 5000],
+ supermarket_cnt: [0, 5000],
+ target_dist: [0, 5000],
+ metro_dist: [0, 5000],
+}
+
const store = (set) => ({
filters: INITIAL,
+ ranges: INITIAL_RANGES,
+
setPrediction: (value) => {
set((state) => {
state.filters.prediction = value;
@@ -26,6 +114,17 @@ const store = (set) => ({
state.filters.region = value;
}),
+ setFilterWithKey: (value, key) =>
+ set((state) => {
+ state.filters[`${key}__gt`] = value[0];
+ state.filters[`${key}__lt`] = value[1];
+ }),
+
+ setRanges: (value) =>
+ set((state) => {
+ state.ranges = value;
+ }),
+
clear: (fullRange) =>
set((state) => {
if (!fullRange) {
diff --git a/src/stores/useWorkingPointsFilters.js b/src/stores/useWorkingPointsFilters.js
index 63955a3..9e108f7 100644
--- a/src/stores/useWorkingPointsFilters.js
+++ b/src/stores/useWorkingPointsFilters.js
@@ -9,8 +9,16 @@ export const INITIAL = {
age: [-1, 0],
};
+export const INITIAL_RANGES = {
+ region: null,
+ deltaTraffic: [-10000, 10000],
+ factTraffic: [-100, 0],
+ age: [-1, 0],
+};
+
const store = (set) => ({
filters: INITIAL,
+ ranges: INITIAL_RANGES,
setDeltaTraffic: (value) => {
set((state) => {
@@ -35,6 +43,11 @@ const store = (set) => ({
state.filters.region = value;
}),
+ setRanges: (value) =>
+ set((state) => {
+ state.ranges = value;
+ }),
+
clear: (fullRange) =>
set((state) => {
if (!fullRange) {
diff --git a/src/utils.js b/src/utils.js
index cc59667..d5fd698 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,3 +1,5 @@
+import {RANGE_FILTERS_KEYS} from "./stores/usePendingPointsFilters.js";
+
export function download(filename, data) {
const downloadLink = window.document.createElement("a");
downloadLink.href = window.URL.createObjectURL(
@@ -11,5 +13,127 @@ export function download(filename, data) {
document.body.removeChild(downloadLink);
}
+var chars = {"Ё":"YO","Й":"I","Ц":"TS","У":"U","К":"K","Е":"E","Н":"N","Г":"G","Ш":"SH","Щ":"SCH","З":"Z","Х":"H","Ъ":"'","ё":"yo","й":"i","ц":"ts","у":"u","к":"k","е":"e","н":"n","г":"g","ш":"sh","щ":"sch","з":"z","х":"h","ъ":"'","Ф":"F","Ы":"I","В":"V","А":"A","П":"P","Р":"R","О":"O","Л":"L","Д":"D","Ж":"ZH","Э":"E","ф":"f","ы":"i","в":"v","а":"a","п":"p","р":"r","о":"o","л":"l","д":"d","ж":"zh","э":"e","Я":"Ya","Ч":"CH","С":"S","М":"M","И":"I","Т":"T","Ь":"'","Б":"B","Ю":"YU","я":"ya","ч":"ch","с":"s","м":"m","и":"i","т":"t","ь":"'","б":"b","ю":"yu"," ":""};
+
+export function transliterate(word){
+ return word.split('').map(function (char) {
+ return chars[char] || char;
+ }).join("");
+}
+
+export function getFilteredGroups(categories) {
+ if (!categories) return [];
+ return categories
+ .filter((category) => category.visible)
+ .map((category) => {
+ return {
+ ...category,
+ groups: [...category.groups.filter((group) => group.visible)],
+ }
+ })
+}
+
+export function fieldHasChanged(filters, ranges, filterKey) {
+ const r = ranges[filterKey]
+ const gtValue = filters[`${filterKey}__gt`];
+ const gtInitial = r ? r[0] : 0 ;
+ const ltValue = filters[`${filterKey}__lt`];
+ const ltInitial =r ? r[1] : 0;
+
+ const result = !(gtValue === gtInitial && ltValue === ltInitial)
+
+ return {
+ result,
+ gtValue,
+ ltValue,
+ }
+}
+
+export const doesMatchFilter = (filters, ranges, feature) => {
+ const { prediction, categories, region } = filters;
+ const {
+ prediction_current,
+ category,
+ area,
+ district,
+ area_id,
+ district_id,
+ } = feature.properties;
+
+ const doesMatchPredictionFilter =
+ prediction_current >= prediction[0] &&
+ prediction_current <= prediction[1];
+
+ const doesMatchCategoriesFilter =
+ categories.length > 0 ? categories.includes(category) : true;
+
+ const doesMatchOtherFilters = () => {
+ let res = true;
+ RANGE_FILTERS_KEYS.map((filterKey) => {
+ if (fieldHasChanged(filters, ranges, filterKey).result && res) {
+ res =
+ feature.properties[filterKey] >= filters[`${filterKey}__gt`] &&
+ feature.properties[filterKey] <= filters[`${filterKey}__lt`];
+ }
+
+ });
+
+ return res;
+ }
+
+ const doesMatchRegionFilter = () => {
+ if (!region) return true;
+
+ if (region.type === "ao") {
+ return (district ?? district_id) === region.id;
+ } else {
+ return (area ?? area_id) === region.id;
+ }
+ };
+
+ return (
+ doesMatchPredictionFilter &&
+ doesMatchCategoriesFilter &&
+ doesMatchRegionFilter() &&
+ doesMatchOtherFilters()
+ );
+};
+
+export const appendFiltersInUse = (params, filters, ranges) => {
+ RANGE_FILTERS_KEYS.map((filterKey) => {
+ if (!fieldHasChanged(filters, ranges, filterKey).result) return;
+ if (/d[0-9]/.test(filterKey)) {
+ params.append('dist_to_group__gt', [
+ filterKey.split('d')[1],
+ filters[`${filterKey}__gt`] - 1
+ ].join(','));
+ params.append('dist_to_group__lt', [
+ filterKey.split('d')[1],
+ filters[`${filterKey}__lt`] + 1
+ ].join(','));
+ } else {
+ params.append(`${filterKey}__gt`, filters[`${filterKey}__gt`] - 1);
+ params.append(`${filterKey}__lt`, filters[`${filterKey}__lt`] + 1);
+ }
+ });
+ if (predictionHasChanged(filters, ranges)) {
+ params.append("prediction_current[]", filters.prediction);
+ }
+}
+
+export const predictionHasChanged = (filters, ranges) => {
+ const gtChanged = ranges.prediction[0] !== filters.prediction[0];
+ const ltChanged = ranges.prediction[1] !== filters.prediction[1];
+ return gtChanged || ltChanged;
+}
+
+export const workingFilterHasChanged = (filter, ranges, fieldKey) => {
+ if (!ranges[fieldKey]) return false;
+ const gtChanged = ranges[fieldKey][0] !== filter[0];
+ const ltChanged = ranges[fieldKey][1] !== filter[1];
+ return gtChanged || ltChanged;
+}
+
+
export const isNil = (value) =>
value === undefined || value === null || value === "";