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",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.513.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-icons": "^5.5.0",
|
||||
@@ -3365,6 +3368,39 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
@@ -3504,6 +3540,12 @@
|
||||
"@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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
|
||||
@@ -8760,7 +8802,6 @@
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
|
||||
@@ -37,12 +37,15 @@
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.80.6",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/lodash": "^4.17.17",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"electron-store": "^8.2.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.513.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react-icons": "^5.5.0",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { join } from 'path';
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils';
|
||||
import './tokenIpc';
|
||||
import WebSocket from 'ws'
|
||||
import { getToken } from './store';
|
||||
|
||||
const WS_URL = import.meta.env.MAIN_VITE_WS_URL;
|
||||
let ws: WebSocket;
|
||||
@@ -17,6 +18,10 @@ function connect(mainWindow: BrowserWindow) {
|
||||
ws.on("open", () => {
|
||||
console.log("Connected");
|
||||
reconnectAttempts = 0;
|
||||
|
||||
const token = getToken();
|
||||
if (token)
|
||||
ws.send(token);
|
||||
});
|
||||
|
||||
ws.on("error", () => {
|
||||
@@ -29,9 +34,10 @@ function connect(mainWindow: BrowserWindow) {
|
||||
|
||||
ws.on("message", (msg: any) => {
|
||||
const str: string = msg.toString();
|
||||
console.log("Message received: ", str);
|
||||
console.log("Message received:", str);
|
||||
const splitted = str.split("::");
|
||||
const type = splitted[0];
|
||||
|
||||
switch (type) {
|
||||
case "msg":
|
||||
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 Store from 'electron-store';
|
||||
|
||||
|
||||
type Token = {
|
||||
access_token: string;
|
||||
}
|
||||
|
||||
const store = new Store<Token>({
|
||||
defaults: {
|
||||
access_token: '',
|
||||
},
|
||||
});
|
||||
import { ipcMain } from "electron";
|
||||
import { clearToken, getToken, setToken } from "./store";
|
||||
|
||||
ipcMain.handle('set-token', (_, token: string) => {
|
||||
if (!safeStorage.isEncryptionAvailable())
|
||||
return;
|
||||
|
||||
const encrypted = safeStorage.encryptString(token);
|
||||
store.set('access_token', encrypted.toString('base64'));
|
||||
setToken(token);
|
||||
});
|
||||
|
||||
ipcMain.handle('get-token', () => {
|
||||
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);
|
||||
return getToken();
|
||||
});
|
||||
|
||||
ipcMain.handle('clear-token', () => {
|
||||
store.delete('access_token');
|
||||
clearToken();
|
||||
});
|
||||
|
||||
@@ -29,7 +29,8 @@ const LoadingState = () => {
|
||||
queryKey: ["isAlive"],
|
||||
queryFn: () => {
|
||||
return checkServer();
|
||||
}
|
||||
},
|
||||
gcTime: Infinity
|
||||
}
|
||||
);
|
||||
|
||||
@@ -81,7 +82,6 @@ const Layout = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
await window.api.sendWsMessage(token);
|
||||
const data = await getEmployeeData(token);
|
||||
|
||||
if (data instanceof Error) {
|
||||
|
||||
@@ -43,7 +43,7 @@ function LocationMarker({ initalMarker, onLocationSelect }: LocationMarkerProps)
|
||||
|
||||
export const MapPicker = ({ center, zoom, className, initalMarker, onLocationSelect }: MapPickerComponentProps) => {
|
||||
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'
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"/>
|
||||
<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(),
|
||||
creationDate: z.date(),
|
||||
phone: z.string(),
|
||||
})
|
||||
});
|
||||
|
||||
export const UserLoginDto = z.object({
|
||||
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 { Skeleton } from "@renderer/components/ui/skeleton";
|
||||
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 { useEffect, } from "react";
|
||||
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 UserCard from "./UserCard";
|
||||
import { MapWithMarker } from "@renderer/components/Map";
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from "@renderer/components/ui/alert-dialog";
|
||||
import AddressUpdateModal from "./AddressUpdateModal";
|
||||
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||
import AddressAttachUserModal from "./AddressAttachUserModal";
|
||||
|
||||
const AddressPage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const params = useParams();
|
||||
const token = useUserStore((s) => s.token);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const currentPage = Number(searchParams.get("page") ?? 1);
|
||||
|
||||
useEffect(() => {
|
||||
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!],
|
||||
queryFn: () => {
|
||||
if (!token)
|
||||
return null;
|
||||
if (!params.id)
|
||||
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(() => {
|
||||
if (!isLoading || !isFetching) {
|
||||
if (!users) {
|
||||
queryClient.refetchQueries({ queryKey: ["address_users", params.id] });
|
||||
}
|
||||
}
|
||||
if (!isLoadingAddress || !isFetchingAddress) {
|
||||
if (!address) {
|
||||
queryClient.refetchQueries({ queryKey: ["address", params.id] });
|
||||
}
|
||||
}
|
||||
if (!isLoading) {
|
||||
if (!isLoading || !isLoadingAddress) {
|
||||
if (users instanceof Error) {
|
||||
toast.error("Произошла ошибка во время загрузки пользователей адреса", { description: users.message });
|
||||
}
|
||||
if (address instanceof Error) {
|
||||
toast.error("Произошла ошибка во время загрузки адреса", { description: address.message });
|
||||
}
|
||||
if (error) {
|
||||
toast.error("Произошла ошибка во время загрузки адреса", { description: error.message });
|
||||
toast.error("Произошла ошибка во время загрузки пользователей адреса", { description: error.message });
|
||||
}
|
||||
if (addressError) {
|
||||
toast.error("Произошла ошибка во время загрузки адреса", { description: addressError.message });
|
||||
}
|
||||
}
|
||||
}, [address]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await queryClient.refetchQueries({ queryKey: ["address", params.id] });
|
||||
await queryClient.refetchQueries({ queryKey: ["address_users", params.id] });
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
@@ -69,6 +97,11 @@ const AddressPage = () => {
|
||||
navigate('/addresses');
|
||||
}
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.set("page", page);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
<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"/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2 mb-6">
|
||||
<p className="text-xl font-semibold">Пользователи: {address.users?.length}</p>
|
||||
<div className="space-y-4 mb-8">
|
||||
<div className="flex items-center space-x-4 mb-6">
|
||||
<p className="text-xl font-semibold">Пользователи</p>
|
||||
<AddressAttachUserModal addressId={params.id!} />
|
||||
</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
|
||||
key={user.id}
|
||||
id={user.id}
|
||||
@@ -128,6 +170,9 @@ const AddressPage = () => {
|
||||
creationDate={user.creationDate.toLocaleString()} />
|
||||
))
|
||||
}
|
||||
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import { MapPicker } from "@renderer/components/Map";
|
||||
import { Button } from "@renderer/components/ui/button";
|
||||
import { DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTrigger } from "@renderer/components/ui/dialog";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect, useReducer, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { HiPencilSquare } from "react-icons/hi2";
|
||||
import { addressReducer, State as AddressState } from "../addresses/AddressReducer";
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { AddressNamesDto } from "@renderer/models/address/types";
|
||||
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 [open, setOpen] = useState(false);
|
||||
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 [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);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -60,29 +40,28 @@ const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber,
|
||||
setNominatimAddress(address);
|
||||
}
|
||||
);
|
||||
dispatch({ type: "UPDATE_FIELD", field: "latitude", value: debouncedLatLng[0] });
|
||||
dispatch({ type: "UPDATE_FIELD", field: "longitude", value: debouncedLatLng[1] });
|
||||
}
|
||||
}, [debouncedLatLng]);
|
||||
|
||||
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) {
|
||||
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);
|
||||
}
|
||||
|
||||
}, [nominatimAddress]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!debouncedLatLng)
|
||||
return;
|
||||
|
||||
const body = AddressUpdateDto.safeParse({
|
||||
latitude: state.values.latitude,
|
||||
longitude: state.values.longitude
|
||||
latitude: debouncedLatLng[0],
|
||||
longitude: debouncedLatLng[1]
|
||||
});
|
||||
|
||||
if (!body.success) {
|
||||
@@ -102,15 +81,14 @@ const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber,
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const doesHaveErrors = Object.values(state.errors).some((errs) => errs.length > 0) || (nominatimAddress instanceof Error);
|
||||
const doesNotHasValues = Object.values(state.values).some((value) => value === "");
|
||||
const doesHaveErrors = nominatimAddress instanceof Error;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button><HiPencilSquare />Изменить метку</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogContent className="min-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle asChild>
|
||||
<h3 className="text-2xl">Изменение метки адреса</h3>
|
||||
@@ -124,7 +102,7 @@ const AddressUpdateModal = ({ id, token, city, cityDistrict, road, houseNumber,
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline">Отмена</Button>
|
||||
</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>
|
||||
</DialogContent>
|
||||
</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 { 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 { toast } from "sonner";
|
||||
|
||||
interface UserCardProps {
|
||||
id: number;
|
||||
@@ -12,6 +17,23 @@ interface 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 (
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
@@ -28,7 +50,21 @@ const UserCard = ({ id, firstName, lastName, middleName, phone, creationDate }:
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive"><HiMinus />Отвязать</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Вы уверены?</AlertDialogTitle>
|
||||
<AlertDialogDescription>При отвязке пользователя чат с ним будет удален</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Отменить</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDetach}>Отвязать</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -62,11 +62,13 @@ const AddressCreateModalButton = ({ token }: AddressCreateModalButtonProps) => {
|
||||
}, [debouncedLatLng]);
|
||||
|
||||
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: "cityDistrict", value: address.cityDistrict });
|
||||
dispatch({ type: "UPDATE_FIELD", field: "houseNumber", value: address.houseNumber });
|
||||
dispatch({ type: "UPDATE_FIELD", field: "road", value: address.road });
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [address]);
|
||||
@@ -112,7 +114,7 @@ const AddressCreateModalButton = ({ token }: AddressCreateModalButtonProps) => {
|
||||
toast.success("Адрес успешно добавлен");
|
||||
setIsLoading(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);
|
||||
@@ -123,7 +125,7 @@ const AddressCreateModalButton = ({ token }: AddressCreateModalButtonProps) => {
|
||||
<DialogTrigger asChild>
|
||||
<Button><HiPlus />Добавить</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogContent className="min-w-3xl">
|
||||
<DialogHeader>
|
||||
<h3 className="text-2xl">Добавить адрес</h3>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { useUserStore } from "@renderer/hooks/userStore";
|
||||
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 { HiArrowPath } from "react-icons/hi2";
|
||||
import AddressCreateModalButton from "./AddressCreateModal";
|
||||
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||
|
||||
|
||||
const AddressesPage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const token = useUserStore((s) => s.token);
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const currentPage = Number(searchParams.get("page") ?? 1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
navigate('/login');
|
||||
}
|
||||
}, []);
|
||||
const { data: addresses, isLoading, isFetching, error } = useQuery(
|
||||
|
||||
const { data: page, isLoading, isFetching, error } = useQuery(
|
||||
{
|
||||
queryKey: ["addresses"],
|
||||
queryKey: ["addresses", currentPage],
|
||||
queryFn: () => {
|
||||
if (!token)
|
||||
return null;
|
||||
return getAddresses(token);
|
||||
return getAddresses(token, currentPage);
|
||||
},
|
||||
},);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading || !isFetching) {
|
||||
if (!addresses) {
|
||||
queryClient.refetchQueries({ queryKey: ["addresses"] });
|
||||
if (!page) {
|
||||
queryClient.refetchQueries({ queryKey: ["addresses", currentPage] });
|
||||
}
|
||||
|
||||
}
|
||||
if (!isLoading) {
|
||||
if (error) {
|
||||
toast.error("Произошла ошибка во время загрузки адресов", { description: error.message });
|
||||
}
|
||||
}
|
||||
}, [addresses]);
|
||||
}, [page]);
|
||||
|
||||
const handleRefresh = async () => {
|
||||
await queryClient.refetchQueries({ queryKey: ["addresses"] });
|
||||
await queryClient.refetchQueries({ queryKey: ["addresses", currentPage] });
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.set("page", page);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -56,9 +68,13 @@ const AddressesPage = () => {
|
||||
<Button size="icon" variant="ghost" onClick={handleRefresh}><HiArrowPath /></Button>
|
||||
</div>
|
||||
<AddressCreateModalButton token={token!}/>
|
||||
{
|
||||
!(page instanceof Error) && page &&
|
||||
<PaginationButtons currentPage={currentPage} totalPages={page.totalPages} handleOnPageClick={handlePageChange}/>
|
||||
}
|
||||
</div>
|
||||
|
||||
{isLoading || isFetching ?
|
||||
{
|
||||
isLoading || isFetching ?
|
||||
<>
|
||||
<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" />
|
||||
</> :
|
||||
addresses instanceof Error ?
|
||||
null
|
||||
:
|
||||
!(page instanceof Error) && page &&
|
||||
<>
|
||||
{addresses?.map((address) => (
|
||||
{page?.items?.map((address) => (
|
||||
<AddressCard key={address?.id}
|
||||
id={address?.id}
|
||||
city={address?.city}
|
||||
|
||||
@@ -37,6 +37,7 @@ const LoginPage = () => {
|
||||
}
|
||||
await window.api.setToken(res.access_token);
|
||||
setToken(res.access_token);
|
||||
await window.api.sendWsMessage(res.access_token);
|
||||
|
||||
toast.success("Вы успешно вошли в аккаунт");
|
||||
navigate('/');
|
||||
|
||||
@@ -47,10 +47,6 @@ const ProfilePage = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
if (!companyData) {
|
||||
toast.error("Произошла ошибка во время загрузки данных УК");
|
||||
return;
|
||||
}
|
||||
if (companyData instanceof Error) {
|
||||
toast.error("Произошла ошибка во время загрузки данных УК", { description: companyData.message });
|
||||
}
|
||||
|
||||
@@ -5,26 +5,30 @@ import { getAllRequests } from "@renderer/services/address";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useEffect } from "react";
|
||||
import { HiArrowPath } from "react-icons/hi2";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import RequestCard from "./RequestCard";
|
||||
import PaginationButtons from "@renderer/components/PaginationButtons";
|
||||
|
||||
const RequestsPage = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const token = useUserStore((s) => s.token);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
const currentPage = Number(searchParams.get("page") ?? 1);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token)
|
||||
navigate('/login');
|
||||
}, []);
|
||||
|
||||
const { data: requests, isLoading, isFetching } = useQuery(
|
||||
const { data: page, isLoading, isFetching } = useQuery(
|
||||
{
|
||||
queryKey: ["requests"],
|
||||
queryKey: ["requests", currentPage],
|
||||
queryFn: () => {
|
||||
if (!token)
|
||||
return null;
|
||||
return getAllRequests(token);
|
||||
return getAllRequests(token, currentPage);
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -33,6 +37,11 @@ const RequestsPage = () => {
|
||||
await queryClient.refetchQueries({ queryKey: ["requests"] });
|
||||
};
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
searchParams.set("page", page);
|
||||
setSearchParams(searchParams);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full px-16 mt-12 mb-6 space-y-4">
|
||||
<div className="space-y-6 mb-6">
|
||||
@@ -40,6 +49,16 @@ const RequestsPage = () => {
|
||||
<h1 className="text-3xl font-semibold">Заявки на присоединение</h1>
|
||||
<Button size="icon" variant="ghost" onClick={handleRefresh}><HiArrowPath /></Button>
|
||||
</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>
|
||||
{
|
||||
isLoading || isFetching ?
|
||||
@@ -50,8 +69,10 @@ const RequestsPage = () => {
|
||||
<Skeleton className="h-53 w-full" />
|
||||
<Skeleton className="h-53 w-full" />
|
||||
</> :
|
||||
requests instanceof Error ? null :
|
||||
requests?.map((request) => (
|
||||
!(page instanceof Error) && page &&
|
||||
<>
|
||||
{
|
||||
page?.items?.map((request) => (
|
||||
<RequestCard
|
||||
key={request.id}
|
||||
address={request.address!}
|
||||
@@ -60,6 +81,9 @@ const RequestsPage = () => {
|
||||
/>
|
||||
))
|
||||
}
|
||||
</>
|
||||
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,6 +5,8 @@ import { AddressCreateDto, AddressDto as AddressDtoType, AddressNamesDto, Addres
|
||||
import { UserDto } from "@renderer/models/user/dtos";
|
||||
import { AttachRequestDto } from "@renderer/models/attachrequest/dtos";
|
||||
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 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[]> {
|
||||
const data = await requestWrapper(`${API_URL}/attach_requests`, {
|
||||
export async function getAllRequests(token: string, page: number): Promise<Error | PaginationDto<AttachRequestDtoType>> {
|
||||
const params = new URLSearchParams({ page: page.toString() }).toString();
|
||||
const data = await requestWrapper(`${API_URL}/attach_requests?${params}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -38,7 +41,13 @@ export async function getAllRequests(token: string): Promise<Error | AttachReque
|
||||
if (data instanceof Error) {
|
||||
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({
|
||||
id: item.address.id,
|
||||
city: item.address.city,
|
||||
@@ -75,11 +84,19 @@ export async function getAllRequests(token: string): Promise<Error | AttachReque
|
||||
if (requests === undefined) {
|
||||
return new Error("Произошла ошибка при получении заявок");
|
||||
}
|
||||
return requests;
|
||||
return {
|
||||
totalItems,
|
||||
totalPages,
|
||||
page,
|
||||
pageSize,
|
||||
hasNext,
|
||||
items: requests
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAddresses(token: string): Promise<Error | AddressDtoType[]> {
|
||||
const data = await requestWrapper(`${API_URL}/company_addresses`, {
|
||||
export async function getAddresses(token: string, page: number): Promise<Error | PaginationDto<AddressDtoType>> {
|
||||
const params = new URLSearchParams({ page: page.toString() }).toString();
|
||||
const data = await requestWrapper(`${API_URL}/company_addresses?${params}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -96,7 +113,13 @@ export async function getAddresses(token: string): Promise<Error | AddressDtoTyp
|
||||
}
|
||||
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({
|
||||
id: item.id,
|
||||
city: item.city,
|
||||
@@ -112,10 +135,17 @@ export async function getAddresses(token: string): Promise<Error | AddressDtoTyp
|
||||
if (addresses === undefined) {
|
||||
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 data = await requestWrapper(`${API_URL}/get?${params}`, {
|
||||
method: "GET",
|
||||
@@ -124,7 +154,6 @@ export async function getAddressWithUsers(token: string, address_id: string): Pr
|
||||
"Authorization": `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (data instanceof Error) {
|
||||
return data;
|
||||
}
|
||||
@@ -134,7 +163,38 @@ export async function getAddressWithUsers(token: string, address_id: string): Pr
|
||||
}
|
||||
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({
|
||||
id: item.id,
|
||||
login: item.login,
|
||||
@@ -149,20 +209,14 @@ export async function getAddressWithUsers(token: string, address_id: string): Pr
|
||||
if (users === undefined) {
|
||||
return new Error("Произошла ошибка при получении пользователей");
|
||||
}
|
||||
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,
|
||||
users: users
|
||||
});
|
||||
if (a.data === undefined) {
|
||||
return new Error("Произошла ошибка при получении адреса");
|
||||
}
|
||||
return a.data;
|
||||
return {
|
||||
totalItems,
|
||||
totalPages,
|
||||
page,
|
||||
pageSize,
|
||||
hasNext,
|
||||
items: users
|
||||
};
|
||||
}
|
||||
|
||||
export async function createAddress(token: string, body: AddressCreateDto) {
|
||||
@@ -209,7 +263,7 @@ export async function updateAddress(token: string, body: AddressUpdateDto, addre
|
||||
longitude: body.longitude
|
||||
})
|
||||
});
|
||||
console.log(data);
|
||||
|
||||
if (data instanceof Error) {
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,49 @@
|
||||
import requestWrapper from "@renderer/utils/requestWrapper";
|
||||
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`;
|
||||
|
||||
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>{
|
||||
const data = await requestWrapper(`${API_URL}/profile_data`, {
|
||||
method: "GET",
|
||||
@@ -16,7 +56,7 @@ export async function getEmployeeData(token: string): Promise<UserEmployeeDto |
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data.detail) {
|
||||
if (data?.detail) {
|
||||
if (data.detail.error === "NotExists" && data.detail.class_name === "User") {
|
||||
return new Error("Пользователь не найден");
|
||||
}
|
||||
@@ -55,7 +95,7 @@ export async function updateEmployeeData(token: string, body: EmployeeUpdateDto)
|
||||
if (data instanceof Error) {
|
||||
return data;
|
||||
}
|
||||
if (data.detail) {
|
||||
if (data?.detail) {
|
||||
if (data.detail.error === "DatabaseError") {
|
||||
return new Error("Произошла ошибка в базе данных");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user