Various fixes, add pagination
- add sending token when app is loading if present - set gcTime to Infinity in LoadingState to prevent rerender every 3 minutes - add pagination - add attaching users to address - add detaching users from address
This commit is contained in:
43
package-lock.json
generated
43
package-lock.json
generated
@@ -24,12 +24,15 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@tanstack/react-query": "^5.80.6",
|
"@tanstack/react-query": "^5.80.6",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@types/lodash": "^4.17.17",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.513.0",
|
"lucide-react": "^0.513.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
@@ -3365,6 +3368,39 @@
|
|||||||
"react": "^18 || ^19"
|
"react": "^18 || ^19"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/react-table": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/table-core": "8.21.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8",
|
||||||
|
"react-dom": ">=16.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/table-core": {
|
||||||
|
"version": "8.21.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
|
||||||
|
"integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tootallnate/once": {
|
"node_modules/@tootallnate/once": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||||
@@ -3504,6 +3540,12 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.17.tgz",
|
||||||
|
"integrity": "sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/ms": {
|
"node_modules/@types/ms": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||||
@@ -8760,7 +8802,6 @@
|
|||||||
"version": "4.17.21",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
|
|||||||
@@ -37,12 +37,15 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"@tanstack/react-query": "^5.80.6",
|
"@tanstack/react-query": "^5.80.6",
|
||||||
|
"@tanstack/react-table": "^8.21.3",
|
||||||
|
"@types/lodash": "^4.17.17",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"@types/react-router-dom": "^5.3.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"electron-store": "^8.2.0",
|
"electron-store": "^8.2.0",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"lucide-react": "^0.513.0",
|
"lucide-react": "^0.513.0",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { join } from 'path';
|
|||||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
||||||
import './tokenIpc';
|
import './tokenIpc';
|
||||||
import WebSocket from 'ws'
|
import WebSocket from 'ws'
|
||||||
|
import { getToken } from './store';
|
||||||
|
|
||||||
const WS_URL = import.meta.env.MAIN_VITE_WS_URL;
|
const WS_URL = import.meta.env.MAIN_VITE_WS_URL;
|
||||||
let ws: WebSocket;
|
let ws: WebSocket;
|
||||||
@@ -17,6 +18,10 @@ function connect(mainWindow: BrowserWindow) {
|
|||||||
ws.on("open", () => {
|
ws.on("open", () => {
|
||||||
console.log("Connected");
|
console.log("Connected");
|
||||||
reconnectAttempts = 0;
|
reconnectAttempts = 0;
|
||||||
|
|
||||||
|
const token = getToken();
|
||||||
|
if (token)
|
||||||
|
ws.send(token);
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("error", () => {
|
ws.on("error", () => {
|
||||||
@@ -32,6 +37,7 @@ function connect(mainWindow: BrowserWindow) {
|
|||||||
console.log("Message received:", str);
|
console.log("Message received:", str);
|
||||||
const splitted = str.split("::");
|
const splitted = str.split("::");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "msg":
|
case "msg":
|
||||||
mainWindow.webContents.send('ws-message-chat', [...splitted.slice(1)]);
|
mainWindow.webContents.send('ws-message-chat', [...splitted.slice(1)]);
|
||||||
|
|||||||
36
src/main/store.ts
Normal file
36
src/main/store.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { safeStorage } from 'electron';
|
||||||
|
import Store from 'electron-store';
|
||||||
|
|
||||||
|
type Token = {
|
||||||
|
access_token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const store = new Store<Token>({
|
||||||
|
defaults: {
|
||||||
|
access_token: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function setToken(token: string) {
|
||||||
|
if (!safeStorage.isEncryptionAvailable())
|
||||||
|
return;
|
||||||
|
|
||||||
|
const encrypted = safeStorage.encryptString(token);
|
||||||
|
store.set('access_token', encrypted.toString('base64'));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getToken() {
|
||||||
|
if (!safeStorage.isEncryptionAvailable())
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const encryptedBase64 = store.get('access_token');
|
||||||
|
if (!encryptedBase64)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const buffer = Buffer.from(encryptedBase64, 'base64');
|
||||||
|
return safeStorage.decryptString(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearToken() {
|
||||||
|
store.delete('access_token');
|
||||||
|
}
|
||||||
@@ -1,37 +1,14 @@
|
|||||||
import { ipcMain, safeStorage } from 'electron';
|
import { ipcMain } from "electron";
|
||||||
import Store from 'electron-store';
|
import { clearToken, getToken, setToken } from "./store";
|
||||||
|
|
||||||
|
|
||||||
type Token = {
|
|
||||||
access_token: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = new Store<Token>({
|
|
||||||
defaults: {
|
|
||||||
access_token: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('set-token', (_, token: string) => {
|
ipcMain.handle('set-token', (_, token: string) => {
|
||||||
if (!safeStorage.isEncryptionAvailable())
|
setToken(token);
|
||||||
return;
|
|
||||||
|
|
||||||
const encrypted = safeStorage.encryptString(token);
|
|
||||||
store.set('access_token', encrypted.toString('base64'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('get-token', () => {
|
ipcMain.handle('get-token', () => {
|
||||||
if (!safeStorage.isEncryptionAvailable())
|
return getToken();
|
||||||
return null;
|
|
||||||
|
|
||||||
const encryptedBase64 = store.get('access_token');
|
|
||||||
if (!encryptedBase64)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
const buffer = Buffer.from(encryptedBase64, 'base64');
|
|
||||||
return safeStorage.decryptString(buffer);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('clear-token', () => {
|
ipcMain.handle('clear-token', () => {
|
||||||
store.delete('access_token');
|
clearToken();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ const LoadingState = () => {
|
|||||||
queryKey: ["isAlive"],
|
queryKey: ["isAlive"],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
return checkServer();
|
return checkServer();
|
||||||
}
|
},
|
||||||
|
gcTime: Infinity
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -81,7 +82,6 @@ const Layout = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await window.api.sendWsMessage(token);
|
|
||||||
const data = await getEmployeeData(token);
|
const data = await getEmployeeData(token);
|
||||||
|
|
||||||
if (data instanceof Error) {
|
if (data instanceof Error) {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function LocationMarker({ initalMarker, onLocationSelect }: LocationMarkerProps)
|
|||||||
|
|
||||||
export const MapPicker = ({ center, zoom, className, initalMarker, onLocationSelect }: MapPickerComponentProps) => {
|
export const MapPicker = ({ center, zoom, className, initalMarker, onLocationSelect }: MapPickerComponentProps) => {
|
||||||
return (
|
return (
|
||||||
<MapContainer center={center} zoom={zoom} className={`h-[24rem] w-full ${className ?? ""}`}>
|
<MapContainer center={center} zoom={zoom} className={`h-[28rem] w-full ${className ?? ""}`}>
|
||||||
<TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
<TileLayer attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
|
||||||
<LocationMarker onLocationSelect={onLocationSelect} initalMarker={initalMarker}/>
|
<LocationMarker onLocationSelect={onLocationSelect} initalMarker={initalMarker}/>
|
||||||
|
|||||||
70
src/renderer/src/components/PaginationButtons.tsx
Normal file
70
src/renderer/src/components/PaginationButtons.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem } from "./ui/pagination";
|
||||||
|
import { range } from "lodash";
|
||||||
|
|
||||||
|
interface PaginationButtonsProps {
|
||||||
|
currentPage: number;
|
||||||
|
totalPages: number;
|
||||||
|
handleOnPageClick: (page: number) => void;
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const PaginationButtons = ({ currentPage, totalPages, handleOnPageClick, className }: PaginationButtonsProps) => {
|
||||||
|
const toShowButtons = 5;
|
||||||
|
const [showStartEllipsis, setShowStartEllipsis] = useState(false);
|
||||||
|
const [showEndEllipsis, setShowEndEllipsis] = useState(false);
|
||||||
|
const [currButtons, setCurrButtons] = useState<number[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (currentPage === 1)
|
||||||
|
setShowStartEllipsis(false);
|
||||||
|
if (totalPages <= toShowButtons) {
|
||||||
|
setCurrButtons(range(1, totalPages + 1));
|
||||||
|
}
|
||||||
|
else if (currentPage + toShowButtons - 1 < totalPages) {
|
||||||
|
if (currentPage !== 1)
|
||||||
|
setShowStartEllipsis(true);
|
||||||
|
setShowEndEllipsis(true);
|
||||||
|
setCurrButtons(range(currentPage, toShowButtons + currentPage));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
setCurrButtons(range(totalPages - toShowButtons, totalPages + 1));
|
||||||
|
setShowEndEllipsis(false);
|
||||||
|
}
|
||||||
|
}, [currentPage, totalPages]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pagination className={className ?? ""}>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<Button variant="ghost" onClick={() => handleOnPageClick(currentPage - 1)} disabled={currentPage <= 1}>Назад</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
{
|
||||||
|
showStartEllipsis &&
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
currButtons.map((val) => (
|
||||||
|
<PaginationItem key={val + 1}>
|
||||||
|
<Button variant={currentPage === val ? "outline" : "ghost"} onClick={() => handleOnPageClick(val)}>{val}</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
{
|
||||||
|
showEndEllipsis &&
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationEllipsis />
|
||||||
|
</PaginationItem>
|
||||||
|
}
|
||||||
|
<PaginationItem>
|
||||||
|
<Button variant="ghost" onClick={() => handleOnPageClick(currentPage + 1)} disabled={currentPage >= totalPages}>Далее</Button>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PaginationButtons;
|
||||||
127
src/renderer/src/components/ui/pagination.tsx
Normal file
127
src/renderer/src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
|
} from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@renderer/lib/utils"
|
||||||
|
import { Button, buttonVariants } from "@renderer/components/ui/button"
|
||||||
|
|
||||||
|
function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
|
||||||
|
return (
|
||||||
|
<nav
|
||||||
|
role="navigation"
|
||||||
|
aria-label="pagination"
|
||||||
|
data-slot="pagination"
|
||||||
|
className={cn("mx-auto flex w-full justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"ul">) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
data-slot="pagination-content"
|
||||||
|
className={cn("flex flex-row items-center gap-1", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||||
|
return <li data-slot="pagination-item" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaginationLinkProps = {
|
||||||
|
isActive?: boolean
|
||||||
|
} & Pick<React.ComponentProps<typeof Button>, "size"> &
|
||||||
|
React.ComponentProps<"a">
|
||||||
|
|
||||||
|
function PaginationLink({
|
||||||
|
className,
|
||||||
|
isActive,
|
||||||
|
size = "icon",
|
||||||
|
...props
|
||||||
|
}: PaginationLinkProps) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
aria-current={isActive ? "page" : undefined}
|
||||||
|
data-slot="pagination-link"
|
||||||
|
data-active={isActive}
|
||||||
|
className={cn(
|
||||||
|
buttonVariants({
|
||||||
|
variant: isActive ? "outline" : "ghost",
|
||||||
|
size,
|
||||||
|
}),
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationPrevious({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Вернутся назад"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
<span className="hidden sm:block">Назад</span>
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationNext({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof PaginationLink>) {
|
||||||
|
return (
|
||||||
|
<PaginationLink
|
||||||
|
aria-label="Следующая страница"
|
||||||
|
size="default"
|
||||||
|
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="hidden sm:block">Вперед</span>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</PaginationLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function PaginationEllipsis({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
data-slot="pagination-ellipsis"
|
||||||
|
className={cn("flex size-9 items-center justify-center", className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MoreHorizontalIcon className="size-4" />
|
||||||
|
<span className="sr-only">More pages</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Pagination,
|
||||||
|
PaginationContent,
|
||||||
|
PaginationLink,
|
||||||
|
PaginationItem,
|
||||||
|
PaginationPrevious,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationEllipsis,
|
||||||
|
}
|
||||||
114
src/renderer/src/components/ui/table.tsx
Normal file
114
src/renderer/src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@renderer/lib/utils"
|
||||||
|
|
||||||
|
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="table-container"
|
||||||
|
className="relative w-full overflow-x-auto"
|
||||||
|
>
|
||||||
|
<table
|
||||||
|
data-slot="table"
|
||||||
|
className={cn("w-full caption-bottom text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||||
|
return (
|
||||||
|
<thead
|
||||||
|
data-slot="table-header"
|
||||||
|
className={cn("[&_tr]:border-b", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||||
|
return (
|
||||||
|
<tbody
|
||||||
|
data-slot="table-body"
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||||
|
return (
|
||||||
|
<tfoot
|
||||||
|
data-slot="table-footer"
|
||||||
|
className={cn(
|
||||||
|
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
data-slot="table-row"
|
||||||
|
className={cn(
|
||||||
|
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||||
|
return (
|
||||||
|
<th
|
||||||
|
data-slot="table-head"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||||
|
return (
|
||||||
|
<td
|
||||||
|
data-slot="table-cell"
|
||||||
|
className={cn(
|
||||||
|
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableCaption({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"caption">) {
|
||||||
|
return (
|
||||||
|
<caption
|
||||||
|
data-slot="table-caption"
|
||||||
|
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableBody,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
TableCell,
|
||||||
|
TableCaption,
|
||||||
|
}
|
||||||
@@ -19,7 +19,7 @@ export const UserDto = z.object({
|
|||||||
middleName: z.string(),
|
middleName: z.string(),
|
||||||
creationDate: z.date(),
|
creationDate: z.date(),
|
||||||
phone: z.string(),
|
phone: z.string(),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const UserLoginDto = z.object({
|
export const UserLoginDto = z.object({
|
||||||
login: z.string()
|
login: z.string()
|
||||||
|
|||||||
8
src/renderer/src/models/utils.ts
Normal file
8
src/renderer/src/models/utils.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export interface PaginationDto<T> {
|
||||||
|
items: T[];
|
||||||
|
totalItems: number;
|
||||||
|
totalPages: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
hasNext: boolean;
|
||||||
|
};
|
||||||
157
src/renderer/src/pages/address.$id/AddressAttachUserModal.tsx
Normal file
157
src/renderer/src/pages/address.$id/AddressAttachUserModal.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { Button } from "@renderer/components/ui/button";
|
||||||
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from "@renderer/components/ui/dialog";
|
||||||
|
import { Checkbox } from "@renderer/components/ui/checkbox"
|
||||||
|
import { HiPlus } from "react-icons/hi2";
|
||||||
|
import UserDataTable from "./DataTable";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { UserDto } from "@renderer/models/user/types";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useUserStore } from "@renderer/hooks/userStore";
|
||||||
|
import { getUnattachedUsers } from "@renderer/services/user";
|
||||||
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Skeleton } from "@renderer/components/ui/skeleton";
|
||||||
|
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||||
|
import { attachUser } from "@renderer/services/address";
|
||||||
|
|
||||||
|
const columns: ColumnDef<UserDto>[] = [
|
||||||
|
{
|
||||||
|
id: "select",
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
aria-label="Select row"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
enableSorting: false,
|
||||||
|
enableHiding: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: "ID",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "login",
|
||||||
|
header: "Логин",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "lastName",
|
||||||
|
header: "Фамилия",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "firstName",
|
||||||
|
header: "Имя",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "middleName",
|
||||||
|
header: "Отчество",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "phone",
|
||||||
|
header: "Телефон",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
interface AddressAttachUserModalProps {
|
||||||
|
addressId: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const AddressAttachUserModal = ( { addressId }: AddressAttachUserModalProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const token = useUserStore((s) => s.token);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
const [selected, setSelected] = useState<UserDto | null>(null);
|
||||||
|
|
||||||
|
const currentPage = Number(searchParams.get("usersPage") ?? 1);
|
||||||
|
|
||||||
|
const { data: unattachedUsers, isLoading } = useQuery(
|
||||||
|
{
|
||||||
|
queryKey: ["unattachedUsers", currentPage],
|
||||||
|
queryFn: () => {
|
||||||
|
if (!token)
|
||||||
|
return null;
|
||||||
|
return getUnattachedUsers(token, currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLoading) {
|
||||||
|
if (unattachedUsers instanceof Error) {
|
||||||
|
toast.error("Произошла ошибка при загрузке пользователей", { description: unattachedUsers.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [unattachedUsers]);
|
||||||
|
|
||||||
|
const handleOpenChange = () => {
|
||||||
|
setOpen(!open);
|
||||||
|
searchParams.delete("usersPage");
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
searchParams.set("usersPage", page);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectedChange = (user: UserDto) => {
|
||||||
|
setSelected(user ?? null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!selected) {
|
||||||
|
toast.error("Выберите пользователя");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!token)
|
||||||
|
return;
|
||||||
|
const res = await attachUser(token, String(selected.id), addressId);
|
||||||
|
if (res instanceof Error) {
|
||||||
|
toast.error("Произошла ошибка при присоединении пользователя", { description: res.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success("Пользователь успешно присоединен");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["unattachedUsers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["address_users"] });
|
||||||
|
setOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isDisabled = !selected
|
||||||
|
|| isLoading
|
||||||
|
|| unattachedUsers instanceof Error
|
||||||
|
|| !unattachedUsers
|
||||||
|
|| unattachedUsers.items.length === 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button><HiPlus /></Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="min-w-3xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle asChild>
|
||||||
|
<h3 className="text-2xl">Присоединение пользователя</h3>
|
||||||
|
</DialogTitle>
|
||||||
|
<p className="text-muted-foreground text-sm">Выберите пользователя из таблицы ниже</p>
|
||||||
|
<p className="text-muted-foreground text-sm">Всего пользователей: {(unattachedUsers instanceof Error) ? 0 : unattachedUsers?.totalItems}</p>
|
||||||
|
</DialogHeader>
|
||||||
|
{
|
||||||
|
(unattachedUsers instanceof Error) || isLoading || !unattachedUsers ?
|
||||||
|
<Skeleton className="h-56"/> :
|
||||||
|
<>
|
||||||
|
<UserDataTable columns={columns} data={unattachedUsers.items} onSelectRow={handleSelectedChange}/>
|
||||||
|
{unattachedUsers.totalItems > 0 && <PaginationButtons currentPage={currentPage} totalPages={unattachedUsers.totalPages} handleOnPageClick={handlePageChange}/>}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
<DialogFooter><Button disabled={isDisabled} onClick={handleSubmit}>Присоединить</Button></DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddressAttachUserModal;
|
||||||
@@ -2,22 +2,27 @@ import { Button } from "@renderer/components/ui/button";
|
|||||||
import { Separator } from "@renderer/components/ui/separator";
|
import { Separator } from "@renderer/components/ui/separator";
|
||||||
import { Skeleton } from "@renderer/components/ui/skeleton";
|
import { Skeleton } from "@renderer/components/ui/skeleton";
|
||||||
import { useUserStore } from "@renderer/hooks/userStore";
|
import { useUserStore } from "@renderer/hooks/userStore";
|
||||||
import { deleteAddress, getAddressWithUsers } from "@renderer/services/address";
|
import { deleteAddress, getAddress, getAddressUsers } from "@renderer/services/address";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, } from "react";
|
import { useEffect, } from "react";
|
||||||
import { HiArrowPath, HiTrash } from "react-icons/hi2";
|
import { HiArrowPath, HiTrash } from "react-icons/hi2";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import UserCard from "./UserCard";
|
import UserCard from "./UserCard";
|
||||||
import { MapWithMarker } from "@renderer/components/Map";
|
import { MapWithMarker } from "@renderer/components/Map";
|
||||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@renderer/components/ui/alert-dialog";
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@renderer/components/ui/alert-dialog";
|
||||||
import AddressUpdateModal from "./AddressUpdateModal";
|
import AddressUpdateModal from "./AddressUpdateModal";
|
||||||
|
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||||
|
import AddressAttachUserModal from "./AddressAttachUserModal";
|
||||||
|
|
||||||
const AddressPage = () => {
|
const AddressPage = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const token = useUserStore((s) => s.token);
|
const token = useUserStore((s) => s.token);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const currentPage = Number(searchParams.get("page") ?? 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -25,34 +30,57 @@ const AddressPage = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: address, isLoading, isFetching, error } = useQuery({
|
const { data: address, isLoading: isLoadingAddress, isFetching: isFetchingAddress, error: addressError } = useQuery({
|
||||||
queryKey: ["address", params.id!],
|
queryKey: ["address", params.id!],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (!token)
|
if (!token)
|
||||||
return null;
|
return null;
|
||||||
if (!params.id)
|
if (!params.id)
|
||||||
return null;
|
return null;
|
||||||
return getAddressWithUsers(token, params.id);
|
return getAddress(token, params.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
const { data: users, isLoading, isFetching, error } = useQuery({
|
||||||
|
queryKey: ["address_users", params.id, currentPage],
|
||||||
|
queryFn: () => {
|
||||||
|
if (!token)
|
||||||
|
return null;
|
||||||
|
if (!params.id)
|
||||||
|
return null;
|
||||||
|
return getAddressUsers(token, params.id, currentPage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading || !isFetching) {
|
if (!isLoading || !isFetching) {
|
||||||
|
if (!users) {
|
||||||
|
queryClient.refetchQueries({ queryKey: ["address_users", params.id] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isLoadingAddress || !isFetchingAddress) {
|
||||||
if (!address) {
|
if (!address) {
|
||||||
queryClient.refetchQueries({ queryKey: ["address", params.id] });
|
queryClient.refetchQueries({ queryKey: ["address", params.id] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isLoading) {
|
if (!isLoading || !isLoadingAddress) {
|
||||||
|
if (users instanceof Error) {
|
||||||
|
toast.error("Произошла ошибка во время загрузки пользователей адреса", { description: users.message });
|
||||||
|
}
|
||||||
if (address instanceof Error) {
|
if (address instanceof Error) {
|
||||||
toast.error("Произошла ошибка во время загрузки адреса", { description: address.message });
|
toast.error("Произошла ошибка во время загрузки адреса", { description: address.message });
|
||||||
}
|
}
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error("Произошла ошибка во время загрузки адреса", { description: error.message });
|
toast.error("Произошла ошибка во время загрузки пользователей адреса", { description: error.message });
|
||||||
|
}
|
||||||
|
if (addressError) {
|
||||||
|
toast.error("Произошла ошибка во время загрузки адреса", { description: addressError.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [address]);
|
}, [address]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await queryClient.refetchQueries({ queryKey: ["address", params.id] });
|
await queryClient.refetchQueries({ queryKey: ["address", params.id] });
|
||||||
|
await queryClient.refetchQueries({ queryKey: ["address_users", params.id] });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@@ -69,6 +97,11 @@ const AddressPage = () => {
|
|||||||
navigate('/addresses');
|
navigate('/addresses');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
searchParams.set("page", page);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-16 mt-12 space-y-6">
|
<div className="w-full px-16 mt-12 space-y-6">
|
||||||
{
|
{
|
||||||
@@ -112,12 +145,21 @@ const AddressPage = () => {
|
|||||||
<MapWithMarker center={[address.latitude, address.longitude]} zoom={16} markerCoords={[address.latitude, address.longitude]} className="rounded-lg z-5"/>
|
<MapWithMarker center={[address.latitude, address.longitude]} zoom={16} markerCoords={[address.latitude, address.longitude]} className="rounded-lg z-5"/>
|
||||||
</div>
|
</div>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="space-y-2">
|
<div className="space-y-4 mb-8">
|
||||||
<div className="flex items-center space-x-2 mb-6">
|
<div className="flex items-center space-x-4 mb-6">
|
||||||
<p className="text-xl font-semibold">Пользователи: {address.users?.length}</p>
|
<p className="text-xl font-semibold">Пользователи</p>
|
||||||
|
<AddressAttachUserModal addressId={params.id!} />
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
address.users?.map((user) => (
|
!(users instanceof Error) && users &&
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
users.items.length === 0 ?
|
||||||
|
<p className="text-lg font-semibold text-center text-muted-foreground">По данному адресу нет привязанных пользователей</p> :
|
||||||
|
<PaginationButtons className="mb-8" currentPage={currentPage} totalPages={users.totalPages} handleOnPageClick={handlePageChange}/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
users.items.map((user) => (
|
||||||
<UserCard
|
<UserCard
|
||||||
key={user.id}
|
key={user.id}
|
||||||
id={user.id}
|
id={user.id}
|
||||||
@@ -128,6 +170,9 @@ const AddressPage = () => {
|
|||||||
creationDate={user.creationDate.toLocaleString()} />
|
creationDate={user.creationDate.toLocaleString()} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { MapPicker } from "@renderer/components/Map";
|
|||||||
import { Button } from "@renderer/components/ui/button";
|
import { Button } from "@renderer/components/ui/button";
|
||||||
import { DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from "@renderer/components/ui/dialog";
|
import { DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from "@renderer/components/ui/dialog";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useReducer, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { HiPencilSquare } from "react-icons/hi2";
|
import { HiPencilSquare } from "react-icons/hi2";
|
||||||
import { addressReducer, State as AddressState } from "../addresses/AddressReducer";
|
|
||||||
import { useDebounce } from "use-debounce";
|
import { useDebounce } from "use-debounce";
|
||||||
import { AddressNamesDto } from "@renderer/models/address/types";
|
import { AddressNamesDto } from "@renderer/models/address/types";
|
||||||
import { getAddressByLocation, updateAddress } from "@renderer/services/address";
|
import { getAddressByLocation, updateAddress } from "@renderer/services/address";
|
||||||
@@ -28,28 +27,9 @@ interface AddressUpdateModalProps {
|
|||||||
const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber, latitude, longitude }: AddressUpdateModalProps) => {
|
const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber, latitude, longitude }: AddressUpdateModalProps) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const initState: AddressState = {
|
|
||||||
values: {
|
|
||||||
city: city,
|
|
||||||
cityDistrict: cityDistrict,
|
|
||||||
houseNumber: houseNumber,
|
|
||||||
road: road,
|
|
||||||
latitude: latitude,
|
|
||||||
longitude: longitude
|
|
||||||
},
|
|
||||||
errors: {
|
|
||||||
city: [],
|
|
||||||
cityDistrict: [],
|
|
||||||
houseNumber: [],
|
|
||||||
road: [],
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(addressReducer, initState);
|
|
||||||
const [latLng, setLatLng] = useState<[number, number] | null>(null);
|
const [latLng, setLatLng] = useState<[number, number] | null>(null);
|
||||||
const [debouncedLatLng] = useDebounce(latLng, 300);
|
const [debouncedLatLng] = useDebounce(latLng, 300);
|
||||||
const [nominatimAddress, setNominatimAddress] = useState<AddressNamesDto | Error>(state.values);
|
const [nominatimAddress, setNominatimAddress] = useState<AddressNamesDto | Error>({ city, cityDistrict, road, houseNumber });
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -60,29 +40,28 @@ const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber,
|
|||||||
setNominatimAddress(address);
|
setNominatimAddress(address);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "latitude", value: debouncedLatLng[0] });
|
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "longitude", value: debouncedLatLng[1] });
|
|
||||||
}
|
}
|
||||||
}, [debouncedLatLng]);
|
}, [debouncedLatLng]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (nominatimAddress && !(nominatimAddress instanceof Error) && debouncedLatLng) {
|
if (nominatimAddress && debouncedLatLng) {
|
||||||
|
if (!(nominatimAddress instanceof Error)) {
|
||||||
if (nominatimAddress.city !== city || nominatimAddress.cityDistrict !== cityDistrict || nominatimAddress.road !== road || nominatimAddress.houseNumber !== houseNumber) {
|
if (nominatimAddress.city !== city || nominatimAddress.cityDistrict !== cityDistrict || nominatimAddress.road !== road || nominatimAddress.houseNumber !== houseNumber) {
|
||||||
setNominatimAddress(new Error("Невозможно изменить метку на другой дом"));
|
setNominatimAddress(new Error("Невозможно изменить метку на другой дом"));
|
||||||
}
|
}
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "city", value: nominatimAddress.city });
|
}
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "cityDistrict", value: nominatimAddress.cityDistrict });
|
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "houseNumber", value: nominatimAddress.houseNumber });
|
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "road", value: nominatimAddress.road });
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}, [nominatimAddress]);
|
}, [nominatimAddress]);
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
|
if (!debouncedLatLng)
|
||||||
|
return;
|
||||||
|
|
||||||
const body = AddressUpdateDto.safeParse({
|
const body = AddressUpdateDto.safeParse({
|
||||||
latitude: state.values.latitude,
|
latitude: debouncedLatLng[0],
|
||||||
longitude: state.values.longitude
|
longitude: debouncedLatLng[1]
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!body.success) {
|
if (!body.success) {
|
||||||
@@ -102,15 +81,14 @@ const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber,
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const doesHaveErrors = Object.values(state.errors).some((errs) => errs.length > 0) || (nominatimAddress instanceof Error);
|
const doesHaveErrors = nominatimAddress instanceof Error;
|
||||||
const doesNotHasValues = Object.values(state.values).some((value) => value === "");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button><HiPencilSquare />Изменить метку</Button>
|
<Button><HiPencilSquare />Изменить метку</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-3xl">
|
<DialogContent className="min-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle asChild>
|
<DialogTitle asChild>
|
||||||
<h3 className="text-2xl">Изменение метки адреса</h3>
|
<h3 className="text-2xl">Изменение метки адреса</h3>
|
||||||
@@ -124,7 +102,7 @@ const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber,
|
|||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="outline">Отмена</Button>
|
<Button type="button" variant="outline">Отмена</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
<Button type="submit" onClick={handleSubmit} disabled={isLoading || doesHaveErrors || doesNotHasValues}>{isLoading && <Loader2Icon className="animate-spin" />}Сохранить</Button>
|
<Button type="submit" onClick={handleSubmit} disabled={isLoading || doesHaveErrors}>{isLoading && <Loader2Icon className="animate-spin" />}Сохранить</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
75
src/renderer/src/pages/address.$id/DataTable.tsx
Normal file
75
src/renderer/src/pages/address.$id/DataTable.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@renderer/components/ui/table";
|
||||||
|
import { UserDto } from "@renderer/models/user/types";
|
||||||
|
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
interface UserDataTableProps {
|
||||||
|
onSelectRow: (user: UserDto) => void
|
||||||
|
columns: ColumnDef<UserDto>[]
|
||||||
|
data: UserDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserDataTable = ( { columns, data, onSelectRow } : UserDataTableProps) => {
|
||||||
|
const table = useReactTable({
|
||||||
|
enableMultiRowSelection: false,
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel()
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onSelectRow(table.getSelectedRowModel().rows[0]?.original);
|
||||||
|
}, [table.getState().rowSelection]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{
|
||||||
|
headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id}>
|
||||||
|
{
|
||||||
|
header.isPlaceholder ? null :
|
||||||
|
flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</TableHead>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{
|
||||||
|
table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id}>
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
Пользователей нет
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserDataTable;
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@renderer/components/ui/alert-dialog";
|
||||||
import { Button } from "@renderer/components/ui/button";
|
import { Button } from "@renderer/components/ui/button";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@renderer/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@renderer/components/ui/card";
|
||||||
|
import { useUserStore } from "@renderer/hooks/userStore";
|
||||||
|
import { detachUser } from "@renderer/services/address";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { HiCalendarDays, HiMiniPhone, HiMinus } from "react-icons/hi2";
|
import { HiCalendarDays, HiMiniPhone, HiMinus } from "react-icons/hi2";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface UserCardProps {
|
interface UserCardProps {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -12,6 +17,23 @@ interface UserCardProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const UserCard = ({ id, firstName, lastName, middleName, phone, creationDate }: UserCardProps) => {
|
const UserCard = ({ id, firstName, lastName, middleName, phone, creationDate }: UserCardProps) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const token = useUserStore((s) => s.token);
|
||||||
|
|
||||||
|
const handleDetach = async () => {
|
||||||
|
if (!token)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const res = await detachUser(token, String(id));
|
||||||
|
if (res instanceof Error) {
|
||||||
|
toast.error("Произошла ошибка при отвязке пользователя", { description: res.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toast.success("Пользователь успешно отвязан");
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["unattachedUsers"] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["address_users"] });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -28,7 +50,21 @@ const UserCard = ({ id, firstName, lastName, middleName, phone, creationDate }:
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
<AlertDialog>
|
||||||
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive"><HiMinus />Отвязать</Button>
|
<Button variant="destructive"><HiMinus />Отвязать</Button>
|
||||||
|
</AlertDialogTrigger>
|
||||||
|
<AlertDialogContent>
|
||||||
|
<AlertDialogHeader>
|
||||||
|
<AlertDialogTitle>Вы уверены?</AlertDialogTitle>
|
||||||
|
<AlertDialogDescription>При отвязке пользователя чат с ним будет удален</AlertDialogDescription>
|
||||||
|
</AlertDialogHeader>
|
||||||
|
<AlertDialogFooter>
|
||||||
|
<AlertDialogCancel>Отменить</AlertDialogCancel>
|
||||||
|
<AlertDialogAction onClick={handleDetach}>Отвязать</AlertDialogAction>
|
||||||
|
</AlertDialogFooter>
|
||||||
|
</AlertDialogContent>
|
||||||
|
</AlertDialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,11 +62,13 @@ const AddressCreateModalButton = ({ token }: AddressCreateModalButtonProps) => {
|
|||||||
}, [debouncedLatLng]);
|
}, [debouncedLatLng]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (address && !(address instanceof Error) && debouncedLatLng) {
|
if (address && debouncedLatLng) {
|
||||||
|
if (!(address instanceof Error)) {
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "city", value: address.city });
|
dispatch({ type: "UPDATE_FIELD", field: "city", value: address.city });
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "cityDistrict", value: address.cityDistrict });
|
dispatch({ type: "UPDATE_FIELD", field: "cityDistrict", value: address.cityDistrict });
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "houseNumber", value: address.houseNumber });
|
dispatch({ type: "UPDATE_FIELD", field: "houseNumber", value: address.houseNumber });
|
||||||
dispatch({ type: "UPDATE_FIELD", field: "road", value: address.road });
|
dispatch({ type: "UPDATE_FIELD", field: "road", value: address.road });
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [address]);
|
}, [address]);
|
||||||
@@ -112,7 +114,7 @@ const AddressCreateModalButton = ({ token }: AddressCreateModalButtonProps) => {
|
|||||||
toast.success("Адрес успешно добавлен");
|
toast.success("Адрес успешно добавлен");
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
await queryClient.invalidateQueries({ queryKey: ["addresses"] });
|
await queryClient.refetchQueries({ queryKey: ["addresses"] });
|
||||||
}
|
}
|
||||||
|
|
||||||
const doesHaveErrors = Object.values(state.errors).some((errs) => errs.length > 0) || (address instanceof Error);
|
const doesHaveErrors = Object.values(state.errors).some((errs) => errs.length > 0) || (address instanceof Error);
|
||||||
@@ -123,7 +125,7 @@ const AddressCreateModalButton = ({ token }: AddressCreateModalButtonProps) => {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button><HiPlus />Добавить</Button>
|
<Button><HiPlus />Добавить</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-w-3xl">
|
<DialogContent className="min-w-3xl">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<h3 className="text-2xl">Добавить адрес</h3>
|
<h3 className="text-2xl">Добавить адрес</h3>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useUserStore } from "@renderer/hooks/userStore";
|
import { useUserStore } from "@renderer/hooks/userStore";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
@@ -10,42 +10,54 @@ import { Skeleton } from "@renderer/components/ui/skeleton";
|
|||||||
import { Button } from "@renderer/components/ui/button";
|
import { Button } from "@renderer/components/ui/button";
|
||||||
import { HiArrowPath } from "react-icons/hi2";
|
import { HiArrowPath } from "react-icons/hi2";
|
||||||
import AddressCreateModalButton from "./AddressCreateModal";
|
import AddressCreateModalButton from "./AddressCreateModal";
|
||||||
|
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||||
|
|
||||||
|
|
||||||
const AddressesPage = () => {
|
const AddressesPage = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const token = useUserStore((s) => s.token);
|
const token = useUserStore((s) => s.token);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const currentPage = Number(searchParams.get("page") ?? 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
const { data: addresses, isLoading, isFetching, error } = useQuery(
|
|
||||||
|
const { data: page, isLoading, isFetching, error } = useQuery(
|
||||||
{
|
{
|
||||||
queryKey: ["addresses"],
|
queryKey: ["addresses", currentPage],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (!token)
|
if (!token)
|
||||||
return null;
|
return null;
|
||||||
return getAddresses(token);
|
return getAddresses(token, currentPage);
|
||||||
},
|
},
|
||||||
},);
|
},);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading || !isFetching) {
|
if (!isLoading || !isFetching) {
|
||||||
if (!addresses) {
|
if (!page) {
|
||||||
queryClient.refetchQueries({ queryKey: ["addresses"] });
|
queryClient.refetchQueries({ queryKey: ["addresses", currentPage] });
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error("Произошла ошибка во время загрузки адресов", { description: error.message });
|
toast.error("Произошла ошибка во время загрузки адресов", { description: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [addresses]);
|
}, [page]);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
await queryClient.refetchQueries({ queryKey: ["addresses"] });
|
await queryClient.refetchQueries({ queryKey: ["addresses", currentPage] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
searchParams.set("page", page);
|
||||||
|
setSearchParams(searchParams);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -56,9 +68,13 @@ const AddressesPage = () => {
|
|||||||
<Button size="icon" variant="ghost" onClick={handleRefresh}><HiArrowPath /></Button>
|
<Button size="icon" variant="ghost" onClick={handleRefresh}><HiArrowPath /></Button>
|
||||||
</div>
|
</div>
|
||||||
<AddressCreateModalButton token={token!}/>
|
<AddressCreateModalButton token={token!}/>
|
||||||
|
{
|
||||||
|
!(page instanceof Error) && page &&
|
||||||
|
<PaginationButtons currentPage={currentPage} totalPages={page.totalPages} handleOnPageClick={handlePageChange}/>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
{isLoading || isFetching ?
|
isLoading || isFetching ?
|
||||||
<>
|
<>
|
||||||
<Skeleton className="h-52 w-full" />
|
<Skeleton className="h-52 w-full" />
|
||||||
<Skeleton className="h-52 w-full" />
|
<Skeleton className="h-52 w-full" />
|
||||||
@@ -66,11 +82,9 @@ const AddressesPage = () => {
|
|||||||
<Skeleton className="h-52 w-full" />
|
<Skeleton className="h-52 w-full" />
|
||||||
<Skeleton className="h-52 w-full" />
|
<Skeleton className="h-52 w-full" />
|
||||||
</> :
|
</> :
|
||||||
addresses instanceof Error ?
|
!(page instanceof Error) && page &&
|
||||||
null
|
|
||||||
:
|
|
||||||
<>
|
<>
|
||||||
{addresses?.map((address) => (
|
{page?.items?.map((address) => (
|
||||||
<AddressCard key={address?.id}
|
<AddressCard key={address?.id}
|
||||||
id={address?.id}
|
id={address?.id}
|
||||||
city={address?.city}
|
city={address?.city}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ const LoginPage = () => {
|
|||||||
}
|
}
|
||||||
await window.api.setToken(res.access_token);
|
await window.api.setToken(res.access_token);
|
||||||
setToken(res.access_token);
|
setToken(res.access_token);
|
||||||
|
await window.api.sendWsMessage(res.access_token);
|
||||||
|
|
||||||
toast.success("Вы успешно вошли в аккаунт");
|
toast.success("Вы успешно вошли в аккаунт");
|
||||||
navigate('/');
|
navigate('/');
|
||||||
|
|||||||
@@ -47,10 +47,6 @@ const ProfilePage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isLoading) {
|
if (!isLoading) {
|
||||||
if (!companyData) {
|
|
||||||
toast.error("Произошла ошибка во время загрузки данных УК");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (companyData instanceof Error) {
|
if (companyData instanceof Error) {
|
||||||
toast.error("Произошла ошибка во время загрузки данных УК", { description: companyData.message });
|
toast.error("Произошла ошибка во время загрузки данных УК", { description: companyData.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,26 +5,30 @@ import { getAllRequests } from "@renderer/services/address";
|
|||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { HiArrowPath } from "react-icons/hi2";
|
import { HiArrowPath } from "react-icons/hi2";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import RequestCard from "./RequestCard";
|
import RequestCard from "./RequestCard";
|
||||||
|
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||||
|
|
||||||
const RequestsPage = () => {
|
const RequestsPage = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const token = useUserStore((s) => s.token);
|
const token = useUserStore((s) => s.token);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const currentPage = Number(searchParams.get("page") ?? 1);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token)
|
if (!token)
|
||||||
navigate('/login');
|
navigate('/login');
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { data: requests, isLoading, isFetching } = useQuery(
|
const { data: page, isLoading, isFetching } = useQuery(
|
||||||
{
|
{
|
||||||
queryKey: ["requests"],
|
queryKey: ["requests", currentPage],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
if (!token)
|
if (!token)
|
||||||
return null;
|
return null;
|
||||||
return getAllRequests(token);
|
return getAllRequests(token, currentPage);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -33,6 +37,11 @@ const RequestsPage = () => {
|
|||||||
await queryClient.refetchQueries({ queryKey: ["requests"] });
|
await queryClient.refetchQueries({ queryKey: ["requests"] });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page) => {
|
||||||
|
searchParams.set("page", page);
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-16 mt-12 mb-6 space-y-4">
|
<div className="w-full px-16 mt-12 mb-6 space-y-4">
|
||||||
<div className="space-y-6 mb-6">
|
<div className="space-y-6 mb-6">
|
||||||
@@ -40,6 +49,16 @@ const RequestsPage = () => {
|
|||||||
<h1 className="text-3xl font-semibold">Заявки на присоединение</h1>
|
<h1 className="text-3xl font-semibold">Заявки на присоединение</h1>
|
||||||
<Button size="icon" variant="ghost" onClick={handleRefresh}><HiArrowPath /></Button>
|
<Button size="icon" variant="ghost" onClick={handleRefresh}><HiArrowPath /></Button>
|
||||||
</div>
|
</div>
|
||||||
|
{
|
||||||
|
!(page instanceof Error) && page &&
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
page.totalItems === 0 ?
|
||||||
|
<p className="text-lg font-semibold text-center text-muted-foreground">Новых заявок на присоединение пока что нет</p>:
|
||||||
|
<PaginationButtons currentPage={currentPage} totalPages={page.totalPages} handleOnPageClick={handlePageChange}/>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
{
|
{
|
||||||
isLoading || isFetching ?
|
isLoading || isFetching ?
|
||||||
@@ -50,8 +69,10 @@ const RequestsPage = () => {
|
|||||||
<Skeleton className="h-53 w-full" />
|
<Skeleton className="h-53 w-full" />
|
||||||
<Skeleton className="h-53 w-full" />
|
<Skeleton className="h-53 w-full" />
|
||||||
</> :
|
</> :
|
||||||
requests instanceof Error ? null :
|
!(page instanceof Error) && page &&
|
||||||
requests?.map((request) => (
|
<>
|
||||||
|
{
|
||||||
|
page?.items?.map((request) => (
|
||||||
<RequestCard
|
<RequestCard
|
||||||
key={request.id}
|
key={request.id}
|
||||||
address={request.address!}
|
address={request.address!}
|
||||||
@@ -60,6 +81,9 @@ const RequestsPage = () => {
|
|||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
</>
|
||||||
|
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AddressCreateDto, AddressDto as AddressDtoType, AddressNamesDto, Addres
|
|||||||
import { UserDto } from "@renderer/models/user/dtos";
|
import { UserDto } from "@renderer/models/user/dtos";
|
||||||
import { AttachRequestDto } from "@renderer/models/attachrequest/dtos";
|
import { AttachRequestDto } from "@renderer/models/attachrequest/dtos";
|
||||||
import { AttachRequestDto as AttachRequestDtoType } from "@renderer/models/attachrequest/types";
|
import { AttachRequestDto as AttachRequestDtoType } from "@renderer/models/attachrequest/types";
|
||||||
|
import { PaginationDto } from "@renderer/models/utils";
|
||||||
|
import { UserDto as UserDtoType } from "@renderer/models/user/types";
|
||||||
|
|
||||||
const API_URL = `${BASE_URL}/address`;
|
const API_URL = `${BASE_URL}/address`;
|
||||||
const NOMINATIM_URL = "https://nominatim.openstreetmap.org/reverse?format=jsonv2"
|
const NOMINATIM_URL = "https://nominatim.openstreetmap.org/reverse?format=jsonv2"
|
||||||
@@ -26,8 +28,9 @@ export async function getAddressByLocation(location: [number, number]) : Promise
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllRequests(token: string): Promise<Error | AttachRequestDtoType[]> {
|
export async function getAllRequests(token: string, page: number): Promise<Error | PaginationDto<AttachRequestDtoType>> {
|
||||||
const data = await requestWrapper(`${API_URL}/attach_requests`, {
|
const params = new URLSearchParams({ page: page.toString() }).toString();
|
||||||
|
const data = await requestWrapper(`${API_URL}/attach_requests?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -38,7 +41,13 @@ export async function getAllRequests(token: string): Promise<Error | AttachReque
|
|||||||
if (data instanceof Error) {
|
if (data instanceof Error) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
const requests = data.map((item: any) => {
|
const totalItems = data.total_items;
|
||||||
|
const totalPages = data.total_pages;
|
||||||
|
const pageSize = data.page_size;
|
||||||
|
const hasNext = data.has_next;
|
||||||
|
const items = data.items;
|
||||||
|
|
||||||
|
const requests = items.map((item: any) => {
|
||||||
const a = AddressDto.safeParse({
|
const a = AddressDto.safeParse({
|
||||||
id: item.address.id,
|
id: item.address.id,
|
||||||
city: item.address.city,
|
city: item.address.city,
|
||||||
@@ -75,11 +84,19 @@ export async function getAllRequests(token: string): Promise<Error | AttachReque
|
|||||||
if (requests === undefined) {
|
if (requests === undefined) {
|
||||||
return new Error("Произошла ошибка при получении заявок");
|
return new Error("Произошла ошибка при получении заявок");
|
||||||
}
|
}
|
||||||
return requests;
|
return {
|
||||||
|
totalItems,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
hasNext,
|
||||||
|
items: requests
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAddresses(token: string): Promise<Error | AddressDtoType[]> {
|
export async function getAddresses(token: string, page: number): Promise<Error | PaginationDto<AddressDtoType>> {
|
||||||
const data = await requestWrapper(`${API_URL}/company_addresses`, {
|
const params = new URLSearchParams({ page: page.toString() }).toString();
|
||||||
|
const data = await requestWrapper(`${API_URL}/company_addresses?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -96,7 +113,13 @@ export async function getAddresses(token: string): Promise<Error | AddressDtoTyp
|
|||||||
}
|
}
|
||||||
return new Error(data.detail);
|
return new Error(data.detail);
|
||||||
}
|
}
|
||||||
const addresses = data.map((item: any) => {
|
const totalItems = data.total_items;
|
||||||
|
const totalPages = data.total_pages;
|
||||||
|
const pageSize = data.page_size;
|
||||||
|
const hasNext = data.has_next;
|
||||||
|
const items = data.items;
|
||||||
|
|
||||||
|
const addresses = items.map((item: any) => {
|
||||||
const a = AddressDto.safeParse({
|
const a = AddressDto.safeParse({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
city: item.city,
|
city: item.city,
|
||||||
@@ -112,10 +135,17 @@ export async function getAddresses(token: string): Promise<Error | AddressDtoTyp
|
|||||||
if (addresses === undefined) {
|
if (addresses === undefined) {
|
||||||
return new Error("Произошла ошибка при получении адресов");
|
return new Error("Произошла ошибка при получении адресов");
|
||||||
}
|
}
|
||||||
return addresses;
|
return {
|
||||||
|
totalItems,
|
||||||
|
totalPages,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
hasNext,
|
||||||
|
items: addresses
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAddressWithUsers(token: string, address_id: string): Promise<Error | AddressDtoType> {
|
export async function getAddress(token: string, address_id: string): Promise<Error | AddressDtoType> {
|
||||||
const params = new URLSearchParams({ address_id }).toString();
|
const params = new URLSearchParams({ address_id }).toString();
|
||||||
const data = await requestWrapper(`${API_URL}/get?${params}`, {
|
const data = await requestWrapper(`${API_URL}/get?${params}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -124,7 +154,6 @@ export async function getAddressWithUsers(token: string, address_id: string): Pr
|
|||||||
"Authorization": `Bearer ${token}`
|
"Authorization": `Bearer ${token}`
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data instanceof Error) {
|
if (data instanceof Error) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -134,7 +163,38 @@ export async function getAddressWithUsers(token: string, address_id: string): Pr
|
|||||||
}
|
}
|
||||||
return new Error(data.detail);
|
return new Error(data.detail);
|
||||||
}
|
}
|
||||||
const users = data.users.map((item: any) => {
|
const a = AddressDto.safeParse({
|
||||||
|
id: data.id,
|
||||||
|
city: data.city,
|
||||||
|
cityDistrict: data.city_district,
|
||||||
|
road: data.road,
|
||||||
|
houseNumber: data.house_number,
|
||||||
|
latitude: data.latitude,
|
||||||
|
longitude: data.longitude,
|
||||||
|
});
|
||||||
|
if (!a.success) {
|
||||||
|
return new Error("Произошла ошибка при получении адреса", { cause: a.error });
|
||||||
|
}
|
||||||
|
return a.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAddressUsers(token: string, address_id: string, page: number): Promise<Error | PaginationDto<UserDtoType>> {
|
||||||
|
const params = new URLSearchParams({ address_id: address_id, page: page.toString() }).toString();
|
||||||
|
const usersData = await requestWrapper(`${API_URL}/get/users?${params}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalItems = usersData.total_items;
|
||||||
|
const totalPages = usersData.total_pages;
|
||||||
|
const pageSize = usersData.page_size;
|
||||||
|
const hasNext = usersData.has_next;
|
||||||
|
const items = usersData.items;
|
||||||
|
|
||||||
|
const users = items.map((item: any) => {
|
||||||
const u = UserDto.safeParse({
|
const u = UserDto.safeParse({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
login: item.login,
|
login: item.login,
|
||||||
@@ -149,20 +209,14 @@ export async function getAddressWithUsers(token: string, address_id: string): Pr
|
|||||||
if (users === undefined) {
|
if (users === undefined) {
|
||||||
return new Error("Произошла ошибка при получении пользователей");
|
return new Error("Произошла ошибка при получении пользователей");
|
||||||
}
|
}
|
||||||
const a = AddressDto.safeParse({
|
return {
|
||||||
id: data.id,
|
totalItems,
|
||||||
city: data.city,
|
totalPages,
|
||||||
cityDistrict: data.city_district,
|
page,
|
||||||
road: data.road,
|
pageSize,
|
||||||
houseNumber: data.house_number,
|
hasNext,
|
||||||
latitude: data.latitude,
|
items: users
|
||||||
longitude: data.longitude,
|
};
|
||||||
users: users
|
|
||||||
});
|
|
||||||
if (a.data === undefined) {
|
|
||||||
return new Error("Произошла ошибка при получении адреса");
|
|
||||||
}
|
|
||||||
return a.data;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAddress(token: string, body: AddressCreateDto) {
|
export async function createAddress(token: string, body: AddressCreateDto) {
|
||||||
@@ -209,7 +263,7 @@ export async function updateAddress(token: string, body: AddressUpdateDto, addre
|
|||||||
longitude: body.longitude
|
longitude: body.longitude
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
console.log(data);
|
|
||||||
if (data instanceof Error) {
|
if (data instanceof Error) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,49 @@
|
|||||||
import requestWrapper from "@renderer/utils/requestWrapper";
|
import requestWrapper from "@renderer/utils/requestWrapper";
|
||||||
import { BASE_URL } from "@renderer/utils/constants";
|
import { BASE_URL } from "@renderer/utils/constants";
|
||||||
import { UserEmployeeDto, EmployeeUpdateDto } from "@renderer/models/user/types";
|
import { UserEmployeeDto, EmployeeUpdateDto, UserDto } from "@renderer/models/user/types";
|
||||||
|
import { PaginationDto } from "@renderer/models/utils";
|
||||||
|
|
||||||
const API_URL = `${BASE_URL}/user`;
|
const API_URL = `${BASE_URL}/user`;
|
||||||
|
|
||||||
|
export async function getUnattachedUsers(token: string, page: number): Promise<PaginationDto<UserDto> | Error> {
|
||||||
|
const params = new URLSearchParams({ page: page.toString() }).toString();
|
||||||
|
const data = await requestWrapper(`${API_URL}/all_unattached?${params}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Authorization": `Bearer ${token}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (data instanceof Error) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalItems = data.total_items;
|
||||||
|
const totalPages = data.total_pages;
|
||||||
|
const pageSize = data.page_size;
|
||||||
|
const hasNext = data.has_next;
|
||||||
|
const items = data.items;
|
||||||
|
|
||||||
|
const users = items.map((user: any) => ({
|
||||||
|
id: user.id,
|
||||||
|
login: user.login,
|
||||||
|
firstName: user.first_name,
|
||||||
|
lastName: user.last_name,
|
||||||
|
middleName: user.middle_name,
|
||||||
|
creationDate: new Date(user.creation_date),
|
||||||
|
phone: user.phone,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
totalItems,
|
||||||
|
totalPages,
|
||||||
|
pageSize,
|
||||||
|
hasNext,
|
||||||
|
items: users
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function getEmployeeData(token: string): Promise<UserEmployeeDto | Error>{
|
export async function getEmployeeData(token: string): Promise<UserEmployeeDto | Error>{
|
||||||
const data = await requestWrapper(`${API_URL}/profile_data`, {
|
const data = await requestWrapper(`${API_URL}/profile_data`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -16,7 +56,7 @@ export async function getEmployeeData(token: string): Promise<UserEmployeeDto |
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.detail) {
|
if (data?.detail) {
|
||||||
if (data.detail.error === "NotExists" && data.detail.class_name === "User") {
|
if (data.detail.error === "NotExists" && data.detail.class_name === "User") {
|
||||||
return new Error("Пользователь не найден");
|
return new Error("Пользователь не найден");
|
||||||
}
|
}
|
||||||
@@ -55,7 +95,7 @@ export async function updateEmployeeData(token: string, body: EmployeeUpdateDto)
|
|||||||
if (data instanceof Error) {
|
if (data instanceof Error) {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
if (data.detail) {
|
if (data?.detail) {
|
||||||
if (data.detail.error === "DatabaseError") {
|
if (data.detail.error === "DatabaseError") {
|
||||||
return new Error("Произошла ошибка в базе данных");
|
return new Error("Произошла ошибка в базе данных");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user