Adjust to design

dev
Platon Yasev 3 years ago
parent 740c85390a
commit 525a63dfb0

@ -24,7 +24,7 @@
"nanostores": "^0.7.3", "nanostores": "^0.7.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-icons": "^4.6.0", "react-icons": "^4.8.0",
"react-map-gl": "^7.0.19", "react-map-gl": "^7.0.19",
"react-router-dom": "^6.8.1", "react-router-dom": "^6.8.1",
"scroll-into-view-if-needed": "^3.0.6", "scroll-into-view-if-needed": "^3.0.6",

@ -0,0 +1,17 @@
import { Button, Popover } from "antd";
import { BiLayer } from "react-icons/all";
import { LayersVisibility } from "./LayersVisibility";
export const LayersControl = () => {
return (
<Popover
content={<LayersVisibility />}
trigger="click"
placement={"leftBottom"}
>
<Button className="absolute bottom-[20px] right-[20px] flex items-center justify-center p-3">
<BiLayer className="w-4 h-4" />
</Button>
</Popover>
);
};

@ -0,0 +1,13 @@
import { Title } from "../components/Title";
export function Legend() {
return (
<div className="absolute bottom-[10px] left-[10px] text-xs text-grey z-10">
<Title
text={"Размер кружка пропорционален прогнозному трафику"}
className={"text-center mb-1"}
classNameText={"lowercase"}
/>
</div>
);
}

@ -1,6 +1,6 @@
import maplibregl from "maplibre-gl"; import maplibregl from "maplibre-gl";
import Map, { MapProvider } from "react-map-gl"; import Map, { MapProvider } from "react-map-gl";
import { useEffect, useRef } from "react"; import { useEffect, useRef, useState } from "react";
import { Sidebar } from "../modules/Sidebar/Sidebar"; import { Sidebar } from "../modules/Sidebar/Sidebar";
import { Layers } from "./Layers"; import { Layers } from "./Layers";
import { MapPopup } from "./Popup"; import { MapPopup } from "./Popup";
@ -9,12 +9,14 @@ import { SignOut } from "../SignOut";
import debounce from "lodash.debounce"; import debounce from "lodash.debounce";
import { usePopup } from "../stores/usePopup"; import { usePopup } from "../stores/usePopup";
import { useClickedPointConfig } from "../stores/useClickedPointConfig"; import { useClickedPointConfig } from "../stores/useClickedPointConfig";
import { Legend } from "../modules/Sidebar/Legend"; import { Legend } from "./Legend";
import { AddressSearch } from "../modules/Sidebar/AddressSearch"; import { AddressSearch } from "../modules/Sidebar/AddressSearch";
import { TableWrapper } from "../modules/Table/TableWrapper"; import { TableWrapper } from "../modules/Table/TableWrapper";
import { useFilters } from "../stores/useFilters"; import { useFilters } from "../stores/useFilters";
import { useMode } from "../stores/useMode"; import { useMode } from "../stores/useMode";
import { MODE_TO_STATUS_MAPPER } from "../config"; import { MODE_TO_STATUS_MAPPER } from "../config";
import { FiltersButton } from "../modules/Sidebar/FiltersButton";
import { LayersControl } from "./LayersControl/LayersControl";
export const MapComponent = () => { export const MapComponent = () => {
const mapRef = useRef(null); const mapRef = useRef(null);
@ -84,9 +86,16 @@ export const MapComponent = () => {
}; };
}, [mapContainerRef.current]); }, [mapContainerRef.current]);
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
const toggleCollapseSidebar = () =>
setIsSidebarCollapsed((prevState) => !prevState);
return ( return (
<MapProvider> <MapProvider>
<div ref={mapContainerRef} className="w-full flex-1"> <Sidebar isCollapsed={isSidebarCollapsed} />
<div className="flex-1 h-screen flex flex-col ">
<div ref={mapContainerRef} className="flex-1">
<Map <Map
mapLib={maplibregl} mapLib={maplibregl}
// mapStyle={`https://api.maptiler.com/maps/voyager/style.json?key=${MAP_TILER_KEY}`} // mapStyle={`https://api.maptiler.com/maps/voyager/style.json?key=${MAP_TILER_KEY}`}
@ -116,17 +125,22 @@ export const MapComponent = () => {
/> />
)} )}
<FiltersButton toggleCollapse={toggleCollapseSidebar} />
<Basemap /> <Basemap />
<Layers /> <Layers />
<Sidebar />
<AddressSearch /> <AddressSearch />
<Legend /> <Legend />
<SignOut /> <SignOut />
<LayersControl />
</Map> </Map>
</div> </div>
<TableWrapper /> <div className="w-full">
<TableWrapper fullWidth={isSidebarCollapsed} />
</div>
</div>
</MapProvider> </MapProvider>
); );
}; };

@ -20,26 +20,7 @@ export const pointLayer = {
type: "circle", type: "circle",
layout: {}, layout: {},
paint: { paint: {
"circle-color": [ "circle-color": "#a51eda",
"match",
["get", "category"],
CATEGORIES.kiosk,
pointColors[CATEGORIES.kiosk],
CATEGORIES.mfc,
pointColors[CATEGORIES.mfc],
CATEGORIES.library,
pointColors[CATEGORIES.library],
CATEGORIES.dk,
pointColors[CATEGORIES.dk],
CATEGORIES.sport,
pointColors[CATEGORIES.sport],
CATEGORIES.residential,
pointColors[CATEGORIES.residential],
CATEGORIES.retail,
pointColors[CATEGORIES.retail],
"black",
],
// "circle-color": "#a51eda",
"circle-radius": [ "circle-radius": [
"interpolate", "interpolate",
["linear"], ["linear"],

@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
#root { #root {
@apply h-screen w-screen flex flex-col overflow-hidden; @apply h-screen w-screen flex overflow-hidden;
} }
.mapboxgl-popup, .mapboxgl-popup,

@ -59,7 +59,7 @@ export const AddressSearch = () => {
}; };
return ( return (
<div className="absolute top-[20px] left-[360px]"> <div className="absolute top-[20px] left-[30px]">
<AutoComplete <AutoComplete
options={options} options={options}
style={{ style={{

@ -1,74 +0,0 @@
import { useFilters } from "../../stores/useFilters";
import { usePointSelection } from "../../stores/usePointSelection";
import { useQuery } from "@tanstack/react-query";
import { api } from "../../api";
import { Button } from "antd";
import { useState } from "react";
function download(filename, data) {
const downloadLink = window.document.createElement("a");
downloadLink.href = window.URL.createObjectURL(
new Blob([data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
})
);
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
const useExportData = (enabled, onSuccess, onSettled) => {
const { filters } = useFilters();
const { prediction, status, categories } = filters;
const { selection } = usePointSelection();
return useQuery(
["export", filters, selection],
async () => {
const params = new URLSearchParams({
"prediction_current[]": prediction,
"status[]": status,
"categories[]": categories,
"included[]": [...selection.included],
"excluded[]": [...selection.excluded],
});
const { data } = await api.get(
`/api/placement_points/to_excel?${params.toString()}`,
{ responseType: "arraybuffer" }
);
return data;
},
{ enabled, onSuccess, onSettled }
);
};
export const ExportButton = () => {
const [startExport, setStartExport] = useState(false);
useExportData(
startExport,
(data) => {
download("postamates.xlsx", data);
},
() => setStartExport(false)
);
const handleExport = () => {
setStartExport(true);
};
return (
<Button
type="primary"
block
className={"mt-2"}
onClick={handleExport}
loading={startExport}
disabled={startExport}
>
Экспорт данных
</Button>
);
};

@ -0,0 +1,12 @@
import { Button } from "antd";
import { MenuOutlined } from "@ant-design/icons";
export const FiltersButton = ({ toggleCollapse }) => {
return (
<Button
icon={<MenuOutlined />}
onClick={toggleCollapse}
className="border-l-0 rounded-bl-none rounded-tl-none absolute top-[100px]"
/>
);
};

@ -1,36 +1,51 @@
import { Radio } from "antd"; import { Button } from "antd";
import { MODES } from "../../config"; import { MODES } from "../../config";
import { useMode } from "../../stores/useMode"; import { useMode } from "../../stores/useMode";
import {
const modeOptions = [ GiGlowingArtifact,
{ IoAnalyticsSharp,
label: "ЛКР", TbTransform,
value: MODES.INITIAL, } from "react-icons/all";
},
{
label: "ЛВР",
value: MODES.APPROVE,
},
{
label: "РП",
value: MODES.WORKING,
},
];
export const Header = () => { export const Header = () => {
const { mode, setMode } = useMode(); const { mode, setMode } = useMode();
const handleModeChange = ({ target: { value } }) => setMode(value); const handleClick = (selectedMode) => {
setMode(selectedMode);
};
const getType = (currentMode) => {
if (currentMode === mode) return "primary";
return "default";
};
return ( return (
<div className="mb-4"> <div className="mb-4 flex items-center justify-between">
<Radio.Group LOGO
options={modeOptions} <div className={"flex items-center gap-x-3"}>
onChange={handleModeChange} <Button
value={mode} type={getType(MODES.INITIAL)}
optionType="button" className="flex items-center justify-center p-3"
buttonStyle="solid" onClick={() => handleClick(MODES.INITIAL)}
/> >
<GiGlowingArtifact />
</Button>
<Button
type={getType(MODES.APPROVE)}
className="flex items-center justify-center p-3"
onClick={() => handleClick(MODES.APPROVE)}
>
<TbTransform />
</Button>
<Button
type={getType(MODES.WORKING)}
className="flex items-center justify-center p-3"
onClick={() => handleClick(MODES.WORKING)}
>
<IoAnalyticsSharp />
</Button>
</div>
</div> </div>
); );
}; };

@ -1,60 +1,34 @@
import { Button } from "antd"; import { Select } from "antd";
import { twMerge } from "tailwind-merge";
import { Title } from "../../../components/Title"; import { Title } from "../../../components/Title";
import { useFilters } from "../../../stores/useFilters"; import { useFilters } from "../../../stores/useFilters";
import { CATEGORIES } from "../../../config"; import { CATEGORIES } from "../../../config";
const SelectItem = ({ name, isActive, onClick, disabled }) => { const options = Object.entries(CATEGORIES).map(([_key, value]) => {
return ( return {
<Button value,
block label: value,
type="text"
className={twMerge(
"text-left",
isActive && "bg-blue hover:bg-blue active:bg-blue focus:bg-blue"
)}
onClick={onClick}
disabled={disabled}
>
{name}
</Button>
);
}; };
});
export const CategoriesSelect = ({ disabled }) => { export const CategoriesSelect = ({ disabled }) => {
const { filters, setCategories } = useFilters(); const { filters, setCategories } = useFilters();
const handleClick = (category) => setCategories(category);
const clear = () => setCategories([]);
return ( return (
<div> <div>
<div className="flex justify-between items-center mb-1"> <Title text={"Категории"} />
<Title text={"Категория объекта размещения"} />
{filters.categories.length !== 0 && (
<Button
type="text"
className="text-grey text-xs p-0 hover:bg-transparent active:bg-transparent focus:bg-transparent h-fit"
onClick={clear}
disabled={disabled}
>
сбросить
</Button>
)}
</div>
<div className="space-y-2"> <Select
{Object.entries(CATEGORIES).map(([key, value]) => ( mode="tags"
<SelectItem style={{
key={key} width: "100%",
name={value} }}
isActive={filters.categories.includes(value)} placeholder="Выберите категории локаций"
onClick={() => handleClick(value)} onChange={setCategories}
options={options}
allowClear={true}
value={filters.categories}
disabled={disabled} disabled={disabled}
/> />
))}
</div>
</div> </div>
); );
}; };

@ -5,6 +5,7 @@ import { usePointSelection } from "../../../stores/usePointSelection";
import { STATUSES } from "../../../config"; import { STATUSES } from "../../../config";
import { useState } from "react"; import { useState } from "react";
import { useUpdateStatus } from "../../../hooks/useUpdateStatus"; import { useUpdateStatus } from "../../../hooks/useUpdateStatus";
import { ArrowRightOutlined } from "@ant-design/icons";
export const TakeToWorkButton = () => { export const TakeToWorkButton = () => {
const { filters } = useFilters(); const { filters } = useFilters();
@ -46,7 +47,8 @@ export const TakeToWorkButton = () => {
return ( return (
<> <>
<Button type="primary" block className={"mt-2"} onClick={takeToWork}> <Button type="primary" block className={"mt-2"} onClick={takeToWork}>
Взять в работу <span className="mr-1"> Взять в работу</span>
<ArrowRightOutlined />
</Button> </Button>
<Modal <Modal
title={" "} title={" "}

@ -1,29 +0,0 @@
import { Title } from "../../components/Title";
import { pointColors } from "../../Map/layers-config";
export function Legend() {
return (
<div className="absolute bottom-[20px] right-[20px] bg-white-background w-[250px] rounded-xl p-4 text-xs text-grey z-10">
<Title
text={"Категории объекта объекта размещения"}
className={"text-center"}
/>
<Title
text={"Размер кружка пропорционален прогнозному трафику"}
className={"text-center mb-1"}
classNameText={"lowercase"}
/>
<div className="space-y-2">
{Object.entries(pointColors).map(([label, color]) => (
<div className="flex gap-2 items-center" key={label}>
<span
className="rounded-xl w-3 h-3 inline-block"
style={{ backgroundColor: color }}
/>
<span>{label}</span>
</div>
))}
</div>
</div>
);
}

@ -27,7 +27,7 @@ const normalizeRegions = (rawRegions) => {
}; };
export const RegionSelect = ({ disabled }) => { export const RegionSelect = ({ disabled }) => {
const { current: map } = useMap(); const { map } = useMap();
const { const {
filters: { region }, filters: { region },
setRegion, setRegion,

@ -1,32 +1,32 @@
import { Button } from "antd"; import { Button } from "antd";
import { LayersVisibility } from "./LayersVisibility";
import { import {
useHasManualEdits, useHasManualEdits,
usePointSelection, usePointSelection,
} from "../../stores/usePointSelection"; } from "../../stores/usePointSelection";
import { TakeToWorkButton } from "./InitialSidebar/TakeToWorkButton"; import { TakeToWorkButton } from "./InitialSidebar/TakeToWorkButton";
import { Filters } from "./InitialSidebar/Filters"; import { Filters } from "./InitialSidebar/Filters";
import { ExportButton } from "./ExportButton";
import { Header } from "./Header"; import { Header } from "./Header";
import { useMode } from "../../stores/useMode"; import { useMode } from "../../stores/useMode";
import { MODES } from "../../config"; import { MODES } from "../../config";
import { twMerge } from "tailwind-merge";
export const Sidebar = () => { export const Sidebar = ({ isCollapsed }) => {
const hasManualEdits = useHasManualEdits(); const hasManualEdits = useHasManualEdits();
const { reset: resetSelection } = usePointSelection(); const { reset: resetSelection } = usePointSelection();
const { mode } = useMode(); const { mode } = useMode();
return ( return (
<div className="absolute top-[20px] left-[20px] bg-white-background w-[320px] rounded-xl p-3 max-h-[calc(100%-40px)] overflow-y-auto z-10"> <div
className={twMerge(
"bg-white-background h-screen p-3 overflow-y-auto shrink-0 border-solid border-border border-0 border-r-[1px] flex flex-col transition-all",
isCollapsed ? "basis-0 p-0 -translate-x-[320px]" : "basis-[320px] p-3"
)}
>
<Header /> <Header />
<div className="space-y-5"> <div className="space-y-5 flex flex-col justify-between flex-1">
<LayersVisibility />
<Filters disabled={hasManualEdits} /> <Filters disabled={hasManualEdits} />
<div> <div>
<ExportButton />
{mode === MODES.INITIAL && <TakeToWorkButton />}
{mode === MODES.INITIAL && hasManualEdits ? ( {mode === MODES.INITIAL && hasManualEdits ? (
<Button <Button
type="text" type="text"
@ -37,6 +37,8 @@ export const Sidebar = () => {
Отменить ручное редактирование Отменить ручное редактирование
</Button> </Button>
) : null} ) : null}
{mode === MODES.INITIAL && <TakeToWorkButton />}
</div> </div>
</div> </div>
</div> </div>

@ -8,6 +8,8 @@ import { STATUSES } from "../../../config";
import { useMergeTableData } from "../useMergeTableData"; import { useMergeTableData } from "../useMergeTableData";
import { Button, Select } from "antd"; import { Button, Select } from "antd";
import { useUpdateStatus } from "../../../hooks/useUpdateStatus"; import { useUpdateStatus } from "../../../hooks/useUpdateStatus";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportApproveAndWorkingData } from "./useExportApproveAndWorkingData";
const statusOptions = [ const statusOptions = [
{ label: STATUSES.approve, value: STATUSES.approve }, { label: STATUSES.approve, value: STATUSES.approve },
@ -42,7 +44,7 @@ const ChangeStatusButton = ({ selectedIds, selectedStatus }) => {
const { mutate: updateStatus } = useUpdateStatus({ const { mutate: updateStatus } = useUpdateStatus({
onSuccess: () => { onSuccess: () => {
queryClient.invalidateQueries(["approve-points"]); queryClient.invalidateQueries(["approve-working-points"]);
}, },
}); });
@ -73,10 +75,9 @@ const Header = ({ selectedIds, onClearSelected }) => {
}; };
return ( return (
<div className={"flex items-center w-full justify-between"}> <HeaderWrapper
<div className={"flex items-center gap-x-4"}> leftColumn={
<span className="py-[5px]">Таблица атрибутов</span> selectedIds.length > 0 && (
{selectedIds.length > 0 && (
<> <>
<StatusSelect value={status} onChange={setStatus} /> <StatusSelect value={status} onChange={setStatus} />
<ChangeStatusButton <ChangeStatusButton
@ -84,25 +85,32 @@ const Header = ({ selectedIds, onClearSelected }) => {
selectedStatus={status} selectedStatus={status}
/> />
</> </>
)} )
</div> }
{selectedIds.length > 0 && ( rightColumn={
selectedIds.length > 0 && (
<Button type="text" onClick={handleClear}> <Button type="text" onClick={handleClear}>
Очистить все Очистить все
</Button> </Button>
)} )
</div> }
classes={{
leftColumn: "flex items-center gap-x-4",
rightColumn: "flex item-center gap-x-4",
}}
exportProvider={useExportApproveAndWorkingData}
/>
); );
}; };
export const ApproveTable = () => { export const ApproveAndWorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE); const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [selectedIds, setSelectedIds] = useState([]); const [selectedIds, setSelectedIds] = useState([]);
const clearSelected = () => setSelectedIds([]); const clearSelected = () => setSelectedIds([]);
const { data } = useQuery(["approve-points"], async () => { const { data } = useQuery(["approve-working-points"], async () => {
const params = new URLSearchParams({ const params = new URLSearchParams({
page, page,
page_size: pageSize, page_size: pageSize,
@ -141,6 +149,7 @@ export const ApproveTable = () => {
pageSize={pageSize} pageSize={pageSize}
isClickedPointLoading={isClickedPointLoading} isClickedPointLoading={isClickedPointLoading}
columns={columns} columns={columns}
fullWidth={fullWidth}
/> />
); );
}; };

@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { download } from "../../../utils";
import { exportData } from "../utils";
import { STATUSES } from "../../../config";
export const useExportApproveAndWorkingData = (enabled, onSettled) => {
return useQuery(
["export-approve-working"],
async () => {
const params = new URLSearchParams({
"status[]": [STATUSES.approve, STATUSES.working],
});
return await exportData(params);
},
{
enabled,
onSuccess: (data) => {
download("postamates.xlsx", data);
},
onSettled,
}
);
};

@ -0,0 +1,20 @@
import { useState } from "react";
import { Button } from "antd";
import { DownloadOutlined } from "@ant-design/icons";
export const ExportButton = ({ provider }) => {
const [startExport, setStartExport] = useState(false);
provider(startExport, () => setStartExport(false));
const handleExport = (e) => {
e.stopPropagation();
setStartExport(true);
};
return (
<Button onClick={handleExport} loading={startExport} disabled={startExport}>
<DownloadOutlined />
</Button>
);
};

@ -0,0 +1,21 @@
import { ExportButton } from "./ExportButton";
export const HeaderWrapper = ({
leftColumn,
rightColumn,
exportProvider,
classes,
}) => {
return (
<div className={"flex items-center w-full justify-between"}>
<div className={classes?.leftColumn}>
<span className="py-[5px]">Таблица атрибутов</span>
{leftColumn}
</div>
<div className={classes?.rightColumn}>
{rightColumn}
{exportProvider && <ExportButton provider={exportProvider} />}
</div>
</div>
);
};

@ -1,11 +1,13 @@
import { useCallback, useState } from "react"; import React, { useCallback, 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";
import { useInitialTableData } from "./useInitialTableData"; import { useInitialTableData } from "./useInitialTableData";
import { columns } from "./columns"; import { columns } from "./columns";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportInitialData } from "./useExportInitialData";
export const InitialTable = () => { export const InitialTable = ({ fullWidth }) => {
const { selection, include, exclude } = usePointSelection(); const { selection, include, exclude } = usePointSelection();
const { clickedPointConfig } = useClickedPointConfig(); const { clickedPointConfig } = useClickedPointConfig();
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -54,6 +56,8 @@ export const InitialTable = () => {
pageSize={pageSize} pageSize={pageSize}
isClickedPointLoading={isClickedPointLoading} isClickedPointLoading={isClickedPointLoading}
columns={columns} columns={columns}
fullWidth={fullWidth}
header={<HeaderWrapper exportProvider={useExportInitialData} />}
/> />
); );
}; };

@ -0,0 +1,33 @@
import { useFilters } from "../../../stores/useFilters";
import { usePointSelection } from "../../../stores/usePointSelection";
import { useQuery } from "@tanstack/react-query";
import { download } from "../../../utils";
import { exportData } from "../utils";
export const useExportInitialData = (enabled, onSettled) => {
const { filters } = useFilters();
const { prediction, status, categories } = filters;
const { selection } = usePointSelection();
return useQuery(
["export-initial", filters, selection],
async () => {
const params = new URLSearchParams({
"prediction_current[]": prediction,
"status[]": status,
"categories[]": categories,
"included[]": [...selection.included],
"excluded[]": [...selection.excluded],
});
return await exportData(params);
},
{
enabled,
onSuccess: (data) => {
download("postamates.xlsx", data);
},
onSettled,
}
);
};

@ -3,6 +3,10 @@
} }
.ant-table { .ant-table {
@apply max-w-[calc(100vw-320px)];
}
.ant-table[data-fullwidth="true"] {
@apply max-w-[100vw]; @apply max-w-[100vw];
} }
@ -43,26 +47,34 @@
padding: 4px; padding: 4px;
} }
.ant-table-tbody > tr.ant-table-row-selected.scroll-row > td { .ant-collapse > .ant-collapse-item > .ant-collapse-header {
@apply bg-lime-200; @apply !items-center;
} }
.ant-table-tbody > tr.ant-table-row.scroll-row > td { .ant-collapse-header-text {
@apply bg-lime-200; @apply flex items-center;
} }
.ant-table-tbody > tr.ant-table-row-selected.scroll-row:hover > td { .ant-table-tbody > tr.ant-table-row-selected > td {
@apply bg-lime-200; background-color: transparent !important;
} }
.ant-table-tbody > tr.ant-table-row.scroll-row:hover > td { .ant-table-tbody > tr:not(.ant-table-placeholder):hover > td {
@apply bg-lime-200; @apply !bg-grey-light;
} }
.ant-collapse > .ant-collapse-item > .ant-collapse-header { .ant-table-tbody > tr.ant-table-row-selected.scroll-row > td {
@apply !items-center; @apply !bg-primary-light;
} }
.ant-collapse-header-text { .ant-table-tbody > tr.ant-table-row.scroll-row > td {
@apply flex items-center; @apply !bg-primary-light;
}
.ant-table-tbody > tr.ant-table-row-selected.scroll-row:hover > td {
@apply !bg-primary-light;
}
.ant-table-tbody > tr.ant-table-row.scroll-row:hover > td {
@apply !bg-primary-light;
} }

@ -6,6 +6,7 @@ import { useMap } from "react-map-gl";
import { useClickedPointConfig } from "../../stores/useClickedPointConfig"; import { useClickedPointConfig } from "../../stores/useClickedPointConfig";
import scrollIntoView from "scroll-into-view-if-needed"; import scrollIntoView from "scroll-into-view-if-needed";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { HeaderWrapper } from "./HeaderWrapper";
export const Table = React.memo( export const Table = React.memo(
({ ({
@ -18,6 +19,7 @@ export const Table = React.memo(
onPageChange, onPageChange,
columns, columns,
header, header,
fullWidth,
}) => { }) => {
const { clickedPointConfig } = useClickedPointConfig(); const { clickedPointConfig } = useClickedPointConfig();
const { map } = useMap(); const { map } = useMap();
@ -38,21 +40,11 @@ export const Table = React.memo(
}, [clickedPointConfig, data]); }, [clickedPointConfig, data]);
return ( return (
<div className="w-screen">
<Collapse> <Collapse>
<Collapse.Panel <Collapse.Panel key="1" header={header ? header : <HeaderWrapper />}>
key="1"
header={
header ? (
header
) : (
<span className="py-[5px]">Таблица атрибутов</span>
)
}
>
<AntdTable <AntdTable
ref={tableRef} ref={tableRef}
className="table" // className="max-w-full"
size="small" size="small"
locale={{ emptyText: <Empty description="Нет данных" /> }} locale={{ emptyText: <Empty description="Нет данных" /> }}
pagination={{ pagination={{
@ -73,10 +65,7 @@ export const Table = React.memo(
onClick: () => { onClick: () => {
const geometry = parse(record.geometry); const geometry = parse(record.geometry);
map.flyTo({ map.flyTo({
center: [ center: [geometry.coordinates[0], geometry.coordinates[1]],
geometry.coordinates[0],
geometry.coordinates[1],
],
zoom: 15, zoom: 15,
speed: 5, speed: 5,
}); });
@ -90,10 +79,10 @@ export const Table = React.memo(
record.location_id === clickedPointConfig?.id && "scroll-row" record.location_id === clickedPointConfig?.id && "scroll-row"
) )
} }
data-fullwidth={fullWidth}
/> />
</Collapse.Panel> </Collapse.Panel>
</Collapse> </Collapse>
</div>
); );
} }
); );

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

@ -6,8 +6,10 @@ import { api } from "../../../api";
import { useMergeTableData } from "../useMergeTableData"; import { useMergeTableData } from "../useMergeTableData";
import { Table } from "../Table"; import { Table } from "../Table";
import { columns } from "../InitialTable/columns"; import { columns } from "../InitialTable/columns";
import { HeaderWrapper } from "../HeaderWrapper";
import { useExportWorkingData } from "./useExportWorkingData";
export const WorkingTable = () => { export const WorkingTable = ({ fullWidth }) => {
const [pageSize, setPageSize] = useState(PAGE_SIZE); const [pageSize, setPageSize] = useState(PAGE_SIZE);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
@ -40,6 +42,8 @@ export const WorkingTable = () => {
pageSize={pageSize} pageSize={pageSize}
isClickedPointLoading={isClickedPointLoading} isClickedPointLoading={isClickedPointLoading}
columns={columns} columns={columns}
fullWidth={fullWidth}
header={<HeaderWrapper exportProvider={useExportWorkingData} />}
/> />
); );
}; };

@ -0,0 +1,24 @@
import { useQuery } from "@tanstack/react-query";
import { download } from "../../../utils";
import { exportData } from "../utils";
import { STATUSES } from "../../../config";
export const useExportWorkingData = (enabled, onSettled) => {
return useQuery(
["export-working"],
async () => {
const params = new URLSearchParams({
"status[]": [STATUSES.working],
});
return await exportData(params);
},
{
enabled,
onSuccess: (data) => {
download("postamates.xlsx", data);
},
onSettled,
}
);
};

@ -0,0 +1,10 @@
import { api } from "../../api";
export const exportData = async (params) => {
const { data } = await api.get(
`/api/placement_points/to_excel?${params.toString()}`,
{ responseType: "arraybuffer" }
);
return data;
};

@ -17,19 +17,9 @@ const store = (set) => ({
}); });
}, },
setCategories: (category) => setCategories: (categories) =>
set((state) => { set((state) => {
if (Array.isArray(category)) { state.filters.categories = categories;
state.filters.categories = [];
return;
}
if (state.filters.categories.includes(category)) {
state.filters.categories = state.filters.categories.filter(
(id) => id !== category
);
} else {
state.filters.categories.push(category);
}
}), }),
setRegion: (value) => setRegion: (value) =>

@ -0,0 +1,12 @@
export function download(filename, data) {
const downloadLink = window.document.createElement("a");
downloadLink.href = window.URL.createObjectURL(
new Blob([data], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
})
);
downloadLink.download = filename;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}

@ -5,11 +5,12 @@ module.exports = {
extend: { extend: {
colors: { colors: {
primary: "#CC2222FF", primary: "#CC2222FF",
'primary-light': "#ffc6c6", 'primary-light': "#ffe4e4",
blue: "rgba(167,201,236,0.57)", blue: "rgba(167,201,236,0.57)",
"white-background": "rgba(255, 255, 255, 0.9)", "white-background": "rgba(255, 255, 255, 0.9)",
grey: "rgba(0,0, 0, 0.5)", grey: "rgba(0,0, 0, 0.5)",
"grey-light": "rgba(239,239,239,0.9)", "grey-light": "rgba(239,239,239,0.9)",
border: '#d9d9d9'
}, },
}, },
}, },

@ -2244,10 +2244,10 @@ react-dom@^18.2.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-icons@^4.6.0: react-icons@^4.8.0:
version "4.7.1" version "4.8.0"
resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.7.1.tgz#0f4b25a5694e6972677cb189d2a72eabea7a8345" resolved "https://registry.yarnpkg.com/react-icons/-/react-icons-4.8.0.tgz#621e900caa23b912f737e41be57f27f6b2bff445"
integrity sha512-yHd3oKGMgm7zxo3EA7H2n7vxSoiGmHk5t6Ou4bXsfcgWyhfDKMpyKfhHR6Bjnn63c+YXBLBPUql9H4wPJM6sXw== integrity sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==
react-is@^16.12.0: react-is@^16.12.0:
version "16.13.1" version "16.13.1"

Loading…
Cancel
Save