Merge branch 'feature/stage_two' into 'dev'

stage two

See merge request spatial/postamates_frontend!35
dev
Timofey Malinin 2 years ago
commit 3134a672c7

@ -6,6 +6,7 @@ import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants"; import { LAYER_IDS } from "./constants";
import { useMode } from "../../stores/useMode"; import { useMode } from "../../stores/useMode";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters"; import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
import { getSourceLayerName } from "../../api.js";
const statusExpression = ["==", ["get", "status"], STATUSES.cancelled]; const statusExpression = ["==", ["get", "status"], STATUSES.cancelled];
@ -34,7 +35,7 @@ export const CancelledPoints = () => {
{...cancelledPointLayer} {...cancelledPointLayer}
id={LAYER_IDS.cancelled} id={LAYER_IDS.cancelled}
source={"points"} source={"points"}
source-layer={"public.points_with_dist"} source-layer={getSourceLayerName()}
layout={{ layout={{
visibility: isVisible[LAYER_IDS.cancelled] ? "visible" : "none", visibility: isVisible[LAYER_IDS.cancelled] ? "visible" : "none",
}} }}

@ -5,6 +5,7 @@ import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression"; import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants"; import { LAYER_IDS } from "./constants";
import { useWorkingPointsFilters } from "../../stores/useWorkingPointsFilters"; import { useWorkingPointsFilters } from "../../stores/useWorkingPointsFilters";
import { getSourceLayerName } from "../../api.js";
const statusExpression = ["==", ["get", "status"], STATUSES.working]; const statusExpression = ["==", ["get", "status"], STATUSES.working];
@ -53,7 +54,7 @@ export const FilteredWorkingPoints = () => {
{...workingPointSymbolLayer} {...workingPointSymbolLayer}
id={LAYER_IDS.filteredWorking} id={LAYER_IDS.filteredWorking}
source={"points"} source={"points"}
source-layer={"public.points_with_dist"} source-layer={getSourceLayerName()}
layout={{ layout={{
...workingPointSymbolLayer.layout, ...workingPointSymbolLayer.layout,
visibility: isVisible[LAYER_IDS.filteredWorking] ? "visible" : "none", visibility: isVisible[LAYER_IDS.filteredWorking] ? "visible" : "none",

@ -5,6 +5,7 @@ import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression"; import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants"; import { LAYER_IDS } from "./constants";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters"; import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
import { getSourceLayerName } from "../../api.js";
const statusExpression = ["==", ["get", "status"], STATUSES.onApproval]; const statusExpression = ["==", ["get", "status"], STATUSES.onApproval];
@ -25,7 +26,7 @@ export const OnApprovalPoints = () => {
{...approvePointLayer} {...approvePointLayer}
id={LAYER_IDS.approve} id={LAYER_IDS.approve}
source={"points"} source={"points"}
source-layer={"public.points_with_dist"} source-layer={getSourceLayerName()}
layout={{ layout={{
visibility: isVisible[LAYER_IDS.approve] ? "visible" : "none", visibility: isVisible[LAYER_IDS.approve] ? "visible" : "none",
}} }}

@ -9,7 +9,8 @@ import { STATUSES } from "../../config";
import { useRegionFilterExpression } from "./useRegionFilterExpression"; import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants"; import { LAYER_IDS } from "./constants";
import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "../../stores/usePendingPointsFilters"; import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "../../stores/usePendingPointsFilters";
import {fieldHasChanged} from "../../utils.js"; import { fieldHasChanged } from "../../utils.js";
import { getSourceLayerName } from "../../api.js";
const statusExpression = ["==", ["get", "status"], STATUSES.pending]; const statusExpression = ["==", ["get", "status"], STATUSES.pending];
@ -97,7 +98,7 @@ export const PendingPoints = () => {
{...matchInitialPointLayer} {...matchInitialPointLayer}
id={LAYER_IDS["initial-unmatch"]} id={LAYER_IDS["initial-unmatch"]}
source={"points"} source={"points"}
source-layer={"public.points_with_dist"} source-layer={getSourceLayerName()}
layout={{ layout={{
...matchInitialPointLayer.layout, ...matchInitialPointLayer.layout,
visibility: isVisible[LAYER_IDS.initial] ? "visible" : "none", visibility: isVisible[LAYER_IDS.initial] ? "visible" : "none",
@ -109,7 +110,7 @@ export const PendingPoints = () => {
{...matchInitialPointLayer} {...matchInitialPointLayer}
id={LAYER_IDS["initial-match"]} id={LAYER_IDS["initial-match"]}
source={"points"} source={"points"}
source-layer={"public.points_with_dist"} source-layer={getSourceLayerName()}
layout={{ layout={{
...matchInitialPointLayer.layout, ...matchInitialPointLayer.layout,
visibility: isVisible[LAYER_IDS.initial] ? "visible" : "none", visibility: isVisible[LAYER_IDS.initial] ? "visible" : "none",

@ -1,14 +1,19 @@
import { Source } from "react-map-gl"; import { Source } from "react-map-gl";
import { BASE_URL } from "../../api"; import { BASE_URL, getSourceLayerName } from "../../api";
import { useUpdateLayerCounter } from "../../stores/useUpdateLayerCounter"; import { useUpdateLayerCounter } from "../../stores/useUpdateLayerCounter";
import { PendingPoints } from "./PendingPoints"; import { PendingPoints } from "./PendingPoints";
import { OnApprovalPoints } from "./OnApprovalPoints"; import { OnApprovalPoints } from "./OnApprovalPoints";
import { WorkingPoints } from "./WorkingPoints"; import { WorkingPoints } from "./WorkingPoints";
import { FilteredWorkingPoints } from "./FilteredWorkingPoints"; import { FilteredWorkingPoints } from "./FilteredWorkingPoints";
import { CancelledPoints } from "./CancelledPoints"; import { CancelledPoints } from "./CancelledPoints";
import {useEffect} from "react";
export const Points = () => { export const Points = () => {
const { updateCounter } = useUpdateLayerCounter(); const { updateCounter, toggleUpdateCounter } = useUpdateLayerCounter();
const layer = getSourceLayerName();
useEffect(() => {
toggleUpdateCounter();
}, [layer]);
return ( return (
<> <>
@ -17,7 +22,7 @@ export const Points = () => {
type="vector" type="vector"
key={`points-${updateCounter}`} key={`points-${updateCounter}`}
tiles={[ tiles={[
`${BASE_URL}/martin/public.points_with_dist/{z}/{x}/{y}.pbf`, `${BASE_URL}/martin/${layer}/{z}/{x}/{y}.pbf`,
]} ]}
> >
<PendingPoints /> <PendingPoints />

@ -6,6 +6,7 @@ import { useRegionFilterExpression } from "./useRegionFilterExpression";
import { LAYER_IDS } from "./constants"; import { LAYER_IDS } from "./constants";
import { useMode } from "../../stores/useMode"; import { useMode } from "../../stores/useMode";
import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters"; import { useOnApprovalPointsFilters } from "../../stores/useOnApprovalPointsFilters";
import {getSourceLayerName} from "../../api.js";
const statusExpression = ["==", ["get", "status"], STATUSES.working]; const statusExpression = ["==", ["get", "status"], STATUSES.working];
@ -34,7 +35,7 @@ export const WorkingPoints = () => {
{...workingPointSymbolLayer} {...workingPointSymbolLayer}
id={LAYER_IDS.working} id={LAYER_IDS.working}
source={"points"} source={"points"}
source-layer={"public.points_with_dist"} source-layer={getSourceLayerName()}
layout={{ layout={{
...workingPointSymbolLayer.layout, ...workingPointSymbolLayer.layout,
visibility: isVisible[LAYER_IDS.working] ? "visible" : "none", visibility: isVisible[LAYER_IDS.working] ? "visible" : "none",

@ -1,6 +1,29 @@
const POINT_SIZE = 5; const POINT_SIZE = 5;
const UNMATCH_POINT_SIZE = 3; const UNMATCH_POINT_SIZE = 3;
// const getPendingColor = () => {
// const { prediction } = usePendingPointsFilters();
//
// const getValue = (multiply) => {
// const difference = prediction[1] - prediction[0];
// const value = prediction[0] + (difference * multiply);
// return Math.floor(value);
// }
//
// return {
// property: "prediction_current",
// stops: [
// [getValue(0), "#FDEBF0"],
// [getValue(0.1), "#F8C7D8"],
// [getValue(0.25), "#F398BC"],
// [getValue(0.4), "#EE67A1"],
// [getValue(0.55), "#B64490"],
// [getValue(0.7), "#7E237E"],
// [getValue(0.85), "#46016C"],
// ],
// };
// }
export const PENDING_COLOR = { export const PENDING_COLOR = {
property: "prediction_current", property: "prediction_current",
stops: [ stops: [
@ -13,6 +36,7 @@ export const PENDING_COLOR = {
[251, "#46016C"], [251, "#46016C"],
], ],
}; };
export const CANCELLED_COLOR = "#CC2222"; export const CANCELLED_COLOR = "#CC2222";
export const APPROVE_COLOR = "#ff7d00"; export const APPROVE_COLOR = "#ff7d00";
export const WORKING_COLOR = "#006e01"; export const WORKING_COLOR = "#006e01";

@ -22,13 +22,12 @@ import { LAYER_IDS } from "./Layers/constants";
import { Header } from "./Header"; import { Header } from "./Header";
import { icons } from "../icons/icons-config"; import { icons } from "../icons/icons-config";
import { LastMLRun } from "./LastMLRun"; import { LastMLRun } from "./LastMLRun";
import { useGetPendingPointsRange, useOtherGroups, usePostamatesAndPvzGroups } from "../api.js"; import { useOtherGroups, usePostamatesAndPvzGroups } from "../api.js";
import { getFilteredGroups, transliterate } from "../utils.js"; import { getFilteredGroups, transliterate } from "../utils.js";
import { import {
CATEGORIES_MAP, CATEGORIES_MAP,
RANGE_FILTERS_KEYS, RANGE_FILTERS_KEYS,
RANGE_FILTERS_MAP, RANGE_FILTERS_MAP
usePendingPointsFilters
} from "../stores/usePendingPointsFilters.js"; } from "../stores/usePendingPointsFilters.js";
export const MapComponent = () => { export const MapComponent = () => {
@ -41,13 +40,6 @@ export const MapComponent = () => {
const { mode } = useMode(); const { mode } = useMode();
const { tableState, openTable } = useTable(); const { tableState, openTable } = useTable();
const { ranges, setRanges } = usePendingPointsFilters();
const { data: fullRange } = useGetPendingPointsRange();
useEffect(() => {
if (fullRange) setRanges({...ranges, ...fullRange});
}, [fullRange]);
const { data: postamatesAndPvzGroups } = usePostamatesAndPvzGroups(); const { data: postamatesAndPvzGroups } = usePostamatesAndPvzGroups();
const { data: otherGroups } = useOtherGroups(); const { data: otherGroups } = useOtherGroups();

@ -53,7 +53,6 @@ const MultipleFeaturesPopup = ({ features, points }) => {
const point = points.find(p => p.id = featureId); const point = points.find(p => p.id = featureId);
const isSelected = (doesMatchFilter(filters, ranges, feature) && !selection.excluded.has(featureId)) || const isSelected = (doesMatchFilter(filters, ranges, feature) && !selection.excluded.has(featureId)) ||
selection.included.has(featureId); selection.included.has(featureId);
console.log(isSelected, doesMatchFilter(filters, ranges, feature));
const handleSelect = () => { const handleSelect = () => {
if (isSelected) { if (isSelected) {
exclude(featureId); exclude(featureId);

@ -3,7 +3,8 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { STATUSES } from "./config"; import { STATUSES } from "./config";
import { usePointSelection } from "./stores/usePointSelection"; import { usePointSelection } from "./stores/usePointSelection";
import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "./stores/usePendingPointsFilters"; import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "./stores/usePendingPointsFilters";
import {fieldHasChanged} from "./utils.js"; import { fieldHasChanged } from "./utils.js";
import { useMode } from "./stores/useMode.js";
export const BASE_URL = import.meta.env.VITE_API_URL; export const BASE_URL = import.meta.env.VITE_API_URL;
@ -17,6 +18,18 @@ export const api = axios.create({
xsrfCookieName: "csrftoken", xsrfCookieName: "csrftoken",
}); });
export const getApiName = () => {
const {isImportMode} = useMode();
if (isImportMode) return "pre_placement_points";
return "placement_points";
}
export const getSourceLayerName = () => {
const {isImportMode} = useMode();
if (isImportMode) return "public.service_preplacementpoint";
return "public.points_with_dist";
}
const enrichParamsWithRegionFilter = (params, region) => { const enrichParamsWithRegionFilter = (params, region) => {
const resultParams = params ? params : new URLSearchParams(); const resultParams = params ? params : new URLSearchParams();
@ -33,11 +46,11 @@ const enrichParamsWithRegionFilter = (params, region) => {
return resultParams; return resultParams;
}; };
export const getPoints = async (params, region) => { export const getPoints = async (params, region, link = "placement_points") => {
const resultParams = enrichParamsWithRegionFilter(params, region); const resultParams = enrichParamsWithRegionFilter(params, region);
const { data } = await api.get( const { data } = await api.get(
`/api/placement_points/?${resultParams.toString()}` `/api/${link}/?${resultParams.toString()}`
); );
return data; return data;
@ -54,9 +67,33 @@ export const exportPoints = async (params, region) => {
return data; 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 = () => { export const useGetTotalInitialPointsCount = () => {
const link = getApiName();
return useQuery( return useQuery(
["all-initial-count"], ["all-initial-count", link],
async () => { async () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page: 1, page: 1,
@ -64,7 +101,7 @@ export const useGetTotalInitialPointsCount = () => {
"status[]": [STATUSES.pending], "status[]": [STATUSES.pending],
}); });
return await getPoints(params); return await getPoints(params, null, link);
}, },
{ select: (data) => data.count } { select: (data) => data.count }
); );
@ -114,12 +151,14 @@ export const useGetFilteredPendingPointsCount = () => {
return params; return params;
} }
const link = getApiName();
return useQuery( return useQuery(
["filtered-points", filters, includedIds], ["filtered-points", filters, link, includedIds],
async () => { async () => {
const params = getParams(); const params = getParams();
return await getPoints(params, region); return await getPoints(params, region, link);
}, },
{ select: (data) => data.count, keepPreviousData: true } { select: (data) => data.count, keepPreviousData: true }
); );
@ -204,18 +243,17 @@ export const useLastMLRun = () => {
); );
} }
export const useGetPendingPointsRange = () => { export const useGetPendingPointsRange = (link) => {
return useQuery( return useQuery(
["prediction-max-min"], ["prediction-max-min", link],
async () => { async () => {
const { data } = await api.get( const { data, isInitialLoading, isFetching } = await api.get(
`/api/placement_points/filters?status[]=${STATUSES.pending}` `/api/${link}/filters?status[]=${STATUSES.pending}`
); );
return {data, isLoading: isInitialLoading || isFetching};
return data;
}, },
{ {
select: (data) => { select: ({data, isLoading}) => {
const distToGroupsArr = data.dist_to_groups.map((groupRange) => { const distToGroupsArr = data.dist_to_groups.map((groupRange) => {
return { return {
[`d${groupRange.group_id}`]: [Math.floor(groupRange.dist[0]), Math.min(Math.ceil(groupRange.dist[1]), 4000)], [`d${groupRange.group_id}`]: [Math.floor(groupRange.dist[0]), Math.min(Math.ceil(groupRange.dist[1]), 4000)],
@ -230,9 +268,12 @@ export const useGetPendingPointsRange = () => {
}).filter(item => !!item); }).filter(item => !!item);
const ranges = Object.assign({}, ...rangesArr); const ranges = Object.assign({}, ...rangesArr);
return { return {
prediction: data.prediction_current, fullRange: {
prediction: data.prediction_current,
...ranges, ...ranges,
...distToGroups ...distToGroups
},
isLoading: isLoading
}; };
}, },
} }
@ -241,6 +282,7 @@ export const useGetPendingPointsRange = () => {
export const useGetPopupPoints = (features) => { export const useGetPopupPoints = (features) => {
const pointsIds = features.map(f => f.properties.id); const pointsIds = features.map(f => f.properties.id);
const link = getApiName();
const { data, isInitialLoading, isFetching } = useQuery( const { data, isInitialLoading, isFetching } = useQuery(
["popup_data", features], ["popup_data", features],
@ -250,7 +292,7 @@ export const useGetPopupPoints = (features) => {
}); });
const { data } = await api.get( const { data } = await api.get(
`/api/placement_points/?${params.toString()}` `/api/${link}/?${params.toString()}`
); );
return data.results; return data.results;

@ -6,7 +6,7 @@ import { ApproveIcon } from "../icons/ApproveIcon";
import { WorkingIcon } from "../icons/WorkingIcon"; import { WorkingIcon } from "../icons/WorkingIcon";
export const ModeSelector = () => { export const ModeSelector = () => {
const { mode, setMode } = useMode(); const { mode, setMode, isImportMode } = useMode();
const handleClick = (selectedMode) => { const handleClick = (selectedMode) => {
setMode(selectedMode); setMode(selectedMode);
@ -47,6 +47,7 @@ export const ModeSelector = () => {
onClick={() => handleClick(MODES.WORKING)} onClick={() => handleClick(MODES.WORKING)}
className="flex items-center justify-center" className="flex items-center justify-center"
size="large" size="large"
disabled={isImportMode}
/> />
</Tooltip> </Tooltip>
</> </>

@ -26,9 +26,9 @@
@apply bg-white-background rounded-xl max-h-[calc(100vh-100px)] overflow-y-auto; @apply bg-white-background rounded-xl max-h-[calc(100vh-100px)] overflow-y-auto;
} }
.ant-modal-header { /*.ant-modal-header {*/
border-bottom: none; /* border-bottom: none;*/
} /*}*/
.ant-select-multiple .ant-select-selection-item { .ant-select-multiple .ant-select-selection-item {
@apply !bg-rose; @apply !bg-rose;

@ -0,0 +1,91 @@
import { Button, Modal, Spin } from "antd";
import { useState } from "react";
import { useGetFilteredPendingPointsCount } from "../../api.js";
import { CheckCircleOutlined, InfoCircleOutlined, LoadingOutlined } from "@ant-design/icons";
import { useMode } from "../../stores/useMode.js";
export const AddPointsModal = ({isOpened, onClose}) => {
const {setImportMode} = useMode();
const [isLoading, setIsLoading] = useState(false);
const { data: filteredCount, isInitialLoading: isFilteredLoading } =
useGetFilteredPendingPointsCount();
const [isSuccess, setIsSuccess] = useState(false);
const onConfirm = () => {
setIsLoading(true);
setTimeout(() => {
setIsLoading(false);
setIsSuccess(true);
}, 2000);
}
const getFooter = () => {
if (isSuccess) return [
<Button
key="ok-button"
type="primary"
onClick={() => {
onClose();
setImportMode(false);
}}
disabled={isLoading}
>
Хорошо
</Button>,
];
return [
<Button
key="close-button"
type="default"
onClick={onClose}
>
Назад
</Button>,
<Button
key="ok-button"
type="primary"
onClick={() => onConfirm()}
disabled={isLoading}
>
Подтвердить
</Button>,
]
}
const getContent = () => {
if (isFilteredLoading) return <Spin />;
if (isLoading) return (
<div className="flex flex-col justify-center gap-2 items-center">
<Spin indicator={<LoadingOutlined style={{ fontSize: 32}} spin />} />
Добавляем точки...
</div>
);
if (isSuccess) return (
<div className="flex items-center justify-center font-bold gap-2">
<CheckCircleOutlined style={{ fontSize: 24, color: "#52C41A" }} />
Добавлено {filteredCount} новых точек
</div>
);
return (
<div className="flex flex-row gap-4">
<InfoCircleOutlined style={{ fontSize: 24, color: "#FFC53D" }} />
<div className="flex flex-col gap-2">
<p className="font-bold mb-0">Подтвердите добавление</p>
<p>В базу данных будет добавлено {filteredCount} новых точек.</p>
</div>
</div>
);
}
return (
<Modal
open={isOpened}
title="Добавить в базу"
onCancel={onClose}
width={400}
footer={getFooter()}
>
{getContent()}
</Modal>
);
}

@ -0,0 +1,45 @@
import { useLayoutEffect, useRef, useState } from "react";
import { uploadPointsFile } from "../../api.js";
import { Button, Upload } from "antd";
import { UploadOutlined } from "@ant-design/icons";
export const LoadingStage = ({setFileId}) => {
const ref = useRef(null);
const [isClicked, setIsClicked] = useState(true);
const onFileChange = async (options) => {
const { onSuccess, onError, file, onProgress } = options;
const config = {
onUploadProgress: event => {
const percent = Math.floor((event.loaded / event.total) * 100);
onProgress({ percent });
}
};
try {
const {id} = await uploadPointsFile(file, config);
onSuccess("Ok");
setFileId(id);
} catch (e) {
//
}
};
useLayoutEffect(() => {
if (ref && ref.current && !isClicked) {
ref.current.click();
setIsClicked(true);
}
}, [isClicked]);
return (
<>
<Upload
name='file'
accept='application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml'
customRequest={onFileChange}
>
<Button icon={<UploadOutlined />}>Выбрать файл .xlsx</Button>
</Upload>
</>
);
}

@ -0,0 +1,91 @@
import { Button, Modal, Spin } from "antd";
import { useState } from "react";
import { importPoints } from "../../api.js";
import { LoadingStage } from "./LoadingStage.jsx";
import { ReportStage } from "./ReportStage.jsx";
import { CheckCircleOutlined, LoadingOutlined } from "@ant-design/icons";
export const OnLoadModal = ({onClose, isOpened}) => {
const [fileId, setFileId] = useState();
const [report, setReport] = useState();
const [isImporting, setIsImporting] = useState(false);
const [isReportStage, setIsReportStage] = useState(false);
const onImportPoints = async () => {
setIsImporting(true);
try {
const { message } = await importPoints(fileId);
setReport(message);
} catch (e) {
console.log(e);
} finally {
setIsImporting(false);
}
}
const getFooter = () => {
if (isReportStage) return [
<Button
key="finish-button"
type="primary"
onClick={onClose}
>
Перейти к выбору
</Button>
]
if (report) return [
<Button
key="report-button"
type="primary"
onClick={() => setIsReportStage(true)}
>
Просмотреть отчет
</Button>
]
return [
<Button
key="close-button"
type="default"
onClick={onClose}
>
Отмена
</Button>,
<Button
key="ok-button"
type="primary"
onClick={() => onImportPoints()}
disabled={!fileId || isImporting}
>
Импортировать
</Button>,
]
}
const getContent = () => {
if (isImporting) return (
<div className="flex flex-col justify-center gap-2 items-center">
<Spin indicator={<LoadingOutlined style={{ fontSize: 64}} spin />} />
Импортируем точки...
</div>
);
if (isReportStage) return <ReportStage report={report} />;
if (report) return (
<div className="flex items-center justify-center font-bold gap-2">
<CheckCircleOutlined style={{ fontSize: 24, color: "#52C41A" }} />
Точки успешно импортированы
</div>
);
return <LoadingStage setFileId={setFileId} />;
}
return (
<Modal
open={isOpened}
title="Импорт точек"
onCancel={onClose}
width={400}
footer={getFooter()}
>
{getContent()}
</Modal>
);
}

@ -0,0 +1,34 @@
import { Col, Row } from "antd";
import { twMerge } from "tailwind-merge";
export const ReportStage = ({ report }) => {
return (
<>
<Row className={twMerge("p-1")}>
<Col className={"text-gray-600"} span={12}>
Всего точек:
</Col>
<Col span={12}>{report.total}</Col>
</Row>
<Row className={twMerge("p-1")}>
<Col className={"text-gray-600"} span={12}>
Совпадений:
</Col>
<Col span={12}>{report.matched}</Col>
</Row>
<Row className={twMerge("p-1")}>
<Col className={"text-gray-600"} span={12}>
Проблемные:
</Col>
<Col span={12}>{report.error}</Col>
</Row>
<Row className={twMerge("p-1")}>
<Col className={"text-gray-600"} span={12}>
Новые:
</Col>
<Col span={12}>{report.unmatched}</Col>
</Row>
</>
);
}

@ -0,0 +1,57 @@
import { useMode } from "../../stores/useMode.js";
import { Button } from "antd";
import { ImportOutlined } from "@ant-design/icons";
import { OnLoadModal } from "./OnLoadModal.jsx";
import { useState } from "react";
import { AddPointsModal } from "./AddPointsModal.jsx";
import { MODES } from "../../config.js";
export const ImportModeSidebarButtons = () => {
const { mode, isImportMode, setImportMode } = useMode();
const [loadModalOpen, setLoadModalOpen] = useState(false);
const [addPointsModalOpen, setAddPointsModalOpen] = useState(false);
const onCancel = () => {
setImportMode(false);
};
const onImport = () => {
setImportMode(true);
setLoadModalOpen(true);
};
if (isImportMode) {
return (
<div className="flex flex-row flex-0 gap-2 border-t-[1px] border-b-[1px]">
<Button type="default" onClick={onCancel}>
Отмена
</Button>
<Button type="primary" className="flex-1" onClick={() => setAddPointsModalOpen(true)}>
Добавить в базу
</Button>
{loadModalOpen &&
<OnLoadModal
isOpened={loadModalOpen}
onClose={() => setLoadModalOpen(false)}
/>
}
{addPointsModalOpen &&
<AddPointsModal
isOpened={addPointsModalOpen}
onClose={() => setAddPointsModalOpen(false)}
/>
}
</div>
);
}
return mode === MODES.PENDING && (
<div className="flex flex-col flex-0 border-t-[1px] border-b-[1px]">
<Button type="default" onClick={onImport}>
<ImportOutlined />
Импортировать
</Button>
</div>
);
}

@ -1,5 +1,5 @@
import { DISABLED_FILTER_TEXT } from "../../../config"; import { DISABLED_FILTER_TEXT } from "../../../config";
import { Button, Tooltip } from "antd"; import {Button, Spin, Tooltip} from "antd";
import { SelectedLocations } from "./SelectedLocations"; import { SelectedLocations } from "./SelectedLocations";
import { TakeToWorkButton } from "./TakeToWorkButton"; import { TakeToWorkButton } from "./TakeToWorkButton";
import { RegionSelect } from "../../../components/RegionSelect"; import { RegionSelect } from "../../../components/RegionSelect";
@ -13,13 +13,30 @@ import {
import {RANGE_FILTERS_KEYS, usePendingPointsFilters} from "../../../stores/usePendingPointsFilters"; import {RANGE_FILTERS_KEYS, usePendingPointsFilters} from "../../../stores/usePendingPointsFilters";
import { ClearFiltersButton } from "../../../components/ClearFiltersButton"; import { ClearFiltersButton } from "../../../components/ClearFiltersButton";
import { getDynamicActiveFilters } from "../utils"; import { getDynamicActiveFilters } from "../utils";
import { useCanEdit } from "../../../api"; import { getApiName, useCanEdit, useGetPendingPointsRange } from "../../../api";
import { AdvancedFiltersWrapper } from "./AdvancedFilters/AdvancedFiltersWrapper.jsx"; import { AdvancedFiltersWrapper } from "./AdvancedFilters/AdvancedFiltersWrapper.jsx";
export const PendingPointsFilters = () => { export const PendingPointsFilters = () => {
const hasManualEdits = useHasManualEdits(); const hasManualEdits = useHasManualEdits();
const { reset: resetPointSelection } = usePointSelection(); const { reset: resetPointSelection } = usePointSelection();
const { ranges, filters, setRegion, setFilterWithKey, setPrediction, setCategories } = usePendingPointsFilters(); const { ranges, filters, setRegion, setFilterWithKey, setPrediction, setCategories, setRanges } = usePendingPointsFilters();
const link = getApiName();
const { data } = useGetPendingPointsRange(link);
useEffect(() => {
const newRanges = data?.fullRange;
if (!newRanges) return;
RANGE_FILTERS_KEYS.map((key) => {
const gtChanged = ranges[key][0] !== newRanges[key][0];
const ltChanged = ranges[key][1] !== newRanges[key][1];
if (gtChanged || ltChanged) setFilterWithKey(newRanges[key], key);
});
const gtChanged = ranges.prediction[0] !== newRanges.prediction[0];
const ltChanged = ranges.prediction[1] !== newRanges.prediction[1];
if (gtChanged || ltChanged) setPrediction(newRanges.prediction);
setRanges({...ranges, ...newRanges});
}, [data]);
const [isSelectionEmpty, setIsSelectionEmpty] = useState(false); const [isSelectionEmpty, setIsSelectionEmpty] = useState(false);
@ -84,12 +101,16 @@ export const PendingPointsFilters = () => {
onChange={setRegion} onChange={setRegion}
/> />
<CategoriesSelect disabled={hasManualEdits} /> <CategoriesSelect disabled={hasManualEdits} />
<PredictionSlider {data?.isLoading ? <Spin /> :
disabled={hasManualEdits} <>
fullRange={ranges} <PredictionSlider
isLoading={false} disabled={hasManualEdits}
/> fullRange={ranges}
<AdvancedFiltersWrapper /> isLoading={false}
/>
<AdvancedFiltersWrapper />
</>
}
</div> </div>
{hasActiveFilters && ( {hasActiveFilters && (

@ -5,6 +5,7 @@ import { MODES } from "../../config";
import { PendingPointsFilters } from "./PendingPointsFilters/PendingPointsFilters"; import { PendingPointsFilters } from "./PendingPointsFilters/PendingPointsFilters";
import { OnApprovalPointsFilters } from "./OnApprovalPointsFilters/OnApprovalPointsFilters"; import { OnApprovalPointsFilters } from "./OnApprovalPointsFilters/OnApprovalPointsFilters";
import { WorkingPointsFilters } from "./WorkingPointsFilters/WorkingPointsFilters"; import { WorkingPointsFilters } from "./WorkingPointsFilters/WorkingPointsFilters";
import { ImportModeSidebarButtons } from "../ImportMode/SidebarButtons.jsx";
export const Sidebar = forwardRef(({ isCollapsed }, ref) => { export const Sidebar = forwardRef(({ isCollapsed }, ref) => {
const { mode } = useMode(); const { mode } = useMode();
@ -29,6 +30,7 @@ export const Sidebar = forwardRef(({ isCollapsed }, ref) => {
)} )}
ref={ref} ref={ref}
> >
<ImportModeSidebarButtons />
<div className="flex flex-col flex-1">{getFilters()}</div> <div className="flex flex-col flex-1">{getFilters()}</div>
</div> </div>
); );

@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react"; import React, {useCallback, useMemo, useState} from "react";
import { Table } from "../Table"; import { Table } from "../Table";
import { usePointSelection } from "../../../stores/usePointSelection"; import { usePointSelection } from "../../../stores/usePointSelection";
import { useClickedPointConfig } from "../../../stores/useClickedPointConfig"; import { useClickedPointConfig } from "../../../stores/useClickedPointConfig";
@ -9,6 +9,23 @@ import { useCanEdit } from "../../../api";
import { useColumns } from "../useColumns.jsx"; import { useColumns } from "../useColumns.jsx";
import { PAGE_SIZE } from "../constants.js"; import { PAGE_SIZE } from "../constants.js";
import { usePopup } from "../../../stores/usePopup.js"; import { usePopup } from "../../../stores/usePopup.js";
import { useMode } from "../../../stores/useMode.js";
const MATCHING_STATUS = {
Unmatched: {
name: 'Новая',
color: '#52c41a'
},
Error: {
name: 'Ошибка',
color: '#f5222d'
},
Matched: {
name: 'Совпадение',
color: '#2f54eb'
},
}
const tableKey = 'pendingTable'; const tableKey = 'pendingTable';
export const PendingTable = ({ fullWidth }) => { export const PendingTable = ({ fullWidth }) => {
@ -16,7 +33,29 @@ export const PendingTable = ({ fullWidth }) => {
const { clickedPointConfig, setClickedPointConfig } = useClickedPointConfig(); const { clickedPointConfig, setClickedPointConfig } = useClickedPointConfig();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(PAGE_SIZE); const [pageSize, setPageSize] = useState(PAGE_SIZE);
const {columns, orderColumns, sort, setSort} = useColumns([], tableKey); const { isImportMode } = useMode();
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 (
<div className={`bg-[${color}] bg-opacity-25 text-[${color}] rounded-md px-2 py-1 text-center border-solid border-[2px] border-[${color}]`}>
{name}
</div>
);
},
}] : [];
}, [isImportMode])
const {columns, orderColumns, sort, setSort} = useColumns(fields, tableKey);
const { setPopup } = usePopup(); const { setPopup } = usePopup();
const onSort = (sortDirection, key) => { const onSort = (sortDirection, key) => {

@ -1,5 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { getPoints } from "../../../api"; import { getApiName, getPoints } from "../../../api";
import { useMergeTableData } from "../useMergeTableData"; import { useMergeTableData } from "../useMergeTableData";
import { STATUSES } from "../../../config"; import { STATUSES } from "../../../config";
import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "../../../stores/usePendingPointsFilters"; import { RANGE_FILTERS_KEYS, usePendingPointsFilters } from "../../../stores/usePendingPointsFilters";
@ -13,6 +13,8 @@ export const usePendingTableData = (page, resetPage, pageSize, setPageSize, sort
region, region,
} = filters; } = filters;
const link = getApiName();
const getParams = () => { const getParams = () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page, page,
@ -46,11 +48,11 @@ export const usePendingTableData = (page, resetPage, pageSize, setPageSize, sort
} }
const {data, isInitialLoading, isFetching} = useQuery( const {data, isInitialLoading, isFetching} = useQuery(
["table", page, filters, sort], ["table", page, filters, sort, link],
async () => { async () => {
const params = getParams(); const params = getParams();
return await getPoints(params, region); return await getPoints(params, region, link);
}, },
{ {
keepPreviousData: true, keepPreviousData: true,

@ -38,6 +38,7 @@ export const TableSettings = ({orderColumns}) => {
<div className="flex flex-col" {...provided.droppableProps} ref={provided.innerRef}> <div className="flex flex-col" {...provided.droppableProps} ref={provided.innerRef}>
{columnsList.map((item, index) => { {columnsList.map((item, index) => {
const num = item.position; const num = item.position;
if (!orderColumns.defaultColumns[num]) return;
return ( return (
<Draggable key={`list-${num}`} draggableId={`list-${num}`} index={index}> <Draggable key={`list-${num}`} draggableId={`list-${num}`} index={index}>
{(provided) => ( {(provided) => (

@ -7,21 +7,12 @@ import { AddressSearch } from "../../Map/AddressSearch.jsx";
import { SearchOutlined } from "@ant-design/icons"; import { SearchOutlined } from "@ant-design/icons";
import { useTable } from "../../stores/useTable.js"; import { useTable } from "../../stores/useTable.js";
import useLocalStorage from "../../hooks/useLocalStorage.js"; import useLocalStorage from "../../hooks/useLocalStorage.js";
const DEFAULT_LENGTH = 39;
export const useColumns = (fields = [], key) => { export const useColumns = (fields = [], key) => {
const { data: regions } = useGetRegions(); const { data: regions } = useGetRegions();
const { const {
tableState: { fullScreen }, tableState: { fullScreen },
} = useTable(); } = useTable();
const [order, setOrder] = useLocalStorage(`${key}Order`, [...Array(DEFAULT_LENGTH).keys()].map((position) => {
return {
position,
show: true,
}
}));
const [sort, setSort] = useLocalStorage(`${key}Sort`, null); const [sort, setSort] = useLocalStorage(`${key}Sort`, null);
const defaultColumns = useMemo(() => { const defaultColumns = useMemo(() => {
@ -402,9 +393,17 @@ export const useColumns = (fields = [], key) => {
showSorterTooltip: false, showSorterTooltip: false,
}, },
...fields, ...fields,
]; ].filter(c => !!c);
}, [regions?.normalized, fields, fullScreen]); }, [regions?.normalized, fields, fullScreen]);
const [order, setOrder] = useLocalStorage(`${key}Order`, defaultColumns.map((column, index) => {
return {
key: column.key,
position: index,
show: true,
}
}));
const columns = useMemo(() => { const columns = useMemo(() => {
return order.flatMap((item) => !item.show ? [] : defaultColumns[item.position]) return order.flatMap((item) => !item.show ? [] : defaultColumns[item.position])
.map((column) => { .map((column) => {
@ -413,7 +412,7 @@ export const useColumns = (fields = [], key) => {
defaultSortOrder: sort.includes('-') ? 'descend' : 'ascend', defaultSortOrder: sort.includes('-') ? 'descend' : 'ascend',
}; };
return column; return column;
}); }).filter(c => !!c);
}, [defaultColumns, order, fullScreen]); }, [defaultColumns, order, fullScreen]);
return { return {

@ -5,12 +5,18 @@ import { persist } from "zustand/middleware";
const store = (set) => ({ const store = (set) => ({
mode: MODES.PENDING, mode: MODES.PENDING,
isImportMode: false,
setMode: (mode) => { setMode: (mode) => {
set((state) => { set((state) => {
state.mode = mode; state.mode = mode;
}); });
}, },
setImportMode: (value) => {
set((state) => {
state.isImportMode = value
});
}
}); });
export const useMode = create(persist(immer(store), { name: "postnet/mode" })); export const useMode = create(persist(immer(store), { name: "postnet/mode" }));

Loading…
Cancel
Save