feat: начало вьюхи клерка, готовы три таба, ни один не работает нормально, спасибо автомапперу
This commit is contained in:
@@ -24,13 +24,25 @@ public class ClientAdapter : IClientAdapter
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
var config = new MapperConfiguration(cfg =>
|
var config = new MapperConfiguration(cfg =>
|
||||||
{
|
{
|
||||||
|
// Mapping for Client
|
||||||
cfg.CreateMap<ClientBindingModel, ClientDataModel>();
|
cfg.CreateMap<ClientBindingModel, ClientDataModel>();
|
||||||
cfg.CreateMap<DepositDataModel, DepositViewModel>();
|
cfg.CreateMap<ClientDataModel, ClientViewModel>()
|
||||||
|
.ForMember(dest => dest.DepositClients, opt => opt.MapFrom(src => src.DepositClients))
|
||||||
|
.ForMember(dest => dest.CreditProgramClients, opt => opt.MapFrom(src => src.CreditProgramClients));
|
||||||
|
|
||||||
|
// Mapping for Deposit
|
||||||
|
cfg.CreateMap<DepositDataModel, DepositViewModel>()
|
||||||
|
.ForMember(dest => dest.DepositClients, opt => opt.MapFrom(src => src.Currencies)); // Adjust if Currencies is meant to map to DepositClients
|
||||||
|
|
||||||
|
// Mapping for ClientCreditProgram
|
||||||
cfg.CreateMap<ClientCreditProgramBindingModel, ClientCreditProgramDataModel>();
|
cfg.CreateMap<ClientCreditProgramBindingModel, ClientCreditProgramDataModel>();
|
||||||
cfg.CreateMap<ClientCreditProgramDataModel, ClientCreditProgramViewModel>();
|
cfg.CreateMap<ClientCreditProgramDataModel, ClientCreditProgramViewModel>();
|
||||||
|
|
||||||
|
// Mapping for DepositClient
|
||||||
cfg.CreateMap<DepositClientBindingModel, DepositClientDataModel>();
|
cfg.CreateMap<DepositClientBindingModel, DepositClientDataModel>();
|
||||||
cfg.CreateMap<DepositClientDataModel, DepositClientViewModel>();
|
cfg.CreateMap<DepositClientDataModel, DepositClientViewModel>();
|
||||||
});
|
});
|
||||||
|
|
||||||
_mapper = new Mapper(config);
|
_mapper = new Mapper(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ public class DepositAdapter : IDepositAdapter
|
|||||||
cfg.CreateMap<DepositDataModel, DepositViewModel>();
|
cfg.CreateMap<DepositDataModel, DepositViewModel>();
|
||||||
cfg.CreateMap<DepositCurrencyBindingModel, DepositCurrencyDataModel>();
|
cfg.CreateMap<DepositCurrencyBindingModel, DepositCurrencyDataModel>();
|
||||||
cfg.CreateMap<DepositCurrencyDataModel, DepositCurrencyViewModel>();
|
cfg.CreateMap<DepositCurrencyDataModel, DepositCurrencyViewModel>();
|
||||||
|
cfg.CreateMap<DepositClientBindingModel, DepositClientDataModel>()
|
||||||
|
.ConstructUsing(src => new DepositClientDataModel(src.DepositId, src.ClientId));
|
||||||
});
|
});
|
||||||
_mapper = new Mapper(config);
|
_mapper = new Mapper(config);
|
||||||
}
|
}
|
||||||
@@ -117,7 +119,7 @@ public class DepositAdapter : IDepositAdapter
|
|||||||
{
|
{
|
||||||
_logger.LogError(ex, "StorageException");
|
_logger.LogError(ex, "StorageException");
|
||||||
return DepositOperationResponse.BadRequest(
|
return DepositOperationResponse.BadRequest(
|
||||||
$"Error while working with data storage: {ex.InnerException!.Message}"
|
$"Error while working with data storage: {ex.InnerException?.Message}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace BankWebApi.Infrastructure;
|
namespace BankWebApi.Infrastructure;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// да пох на это
|
||||||
|
/// </summary>
|
||||||
public class PasswordHelper
|
public class PasswordHelper
|
||||||
{
|
{
|
||||||
public static string HashPassword(string password) => BCrypt.Net.BCrypt.HashPassword(password);
|
public static string HashPassword(string password) => BCrypt.Net.BCrypt.HashPassword(password);
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Шрек</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root" class="roboto"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ export const Periods = (): React.JSX.Element => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
|
{!selectedItem &&
|
||||||
<DialogForm<PeriodBindingModel>
|
<DialogForm<PeriodBindingModel>
|
||||||
title="Форма сроков"
|
title="Форма сроков"
|
||||||
description="Добавить сроки"
|
description="Добавить сроки"
|
||||||
@@ -134,8 +135,10 @@ export const Periods = (): React.JSX.Element => {
|
|||||||
onClose={() => setIsAddDialogOpen(false)}
|
onClose={() => setIsAddDialogOpen(false)}
|
||||||
onSubmit={handleAdd}
|
onSubmit={handleAdd}
|
||||||
>
|
>
|
||||||
<PeriodFormAdd onSubmit={handleAdd} />
|
<PeriodFormAdd />
|
||||||
</DialogForm>
|
</DialogForm>
|
||||||
|
|
||||||
|
}
|
||||||
{selectedItem && (
|
{selectedItem && (
|
||||||
<DialogForm<PeriodBindingModel>
|
<DialogForm<PeriodBindingModel>
|
||||||
title="Форма сроков"
|
title="Форма сроков"
|
||||||
@@ -145,7 +148,6 @@ export const Periods = (): React.JSX.Element => {
|
|||||||
onSubmit={handleEdit}
|
onSubmit={handleEdit}
|
||||||
>
|
>
|
||||||
<PeriodFormEdit
|
<PeriodFormEdit
|
||||||
onSubmit={handleEdit}
|
|
||||||
defaultValues={selectedItem}
|
defaultValues={selectedItem}
|
||||||
/>
|
/>
|
||||||
</DialogForm>
|
</DialogForm>
|
||||||
|
|||||||
@@ -4,15 +4,23 @@
|
|||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Roboto', 'Times New Roman', Times, serif; /* поменять */
|
font-family: 'Roboto', 'Times New Roman', Times, serif;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.roboto {
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
|
font-optical-sizing: auto;
|
||||||
|
font-weight: 100;
|
||||||
|
font-style: normal;
|
||||||
|
font-variation-settings:
|
||||||
|
"wdth" 100;
|
||||||
|
}
|
||||||
|
|
||||||
#root {
|
#root {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
1
TheBank/bankuiclerk/.env
Normal file
1
TheBank/bankuiclerk/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=https://localhost:7204
|
||||||
2
TheBank/bankuiclerk/.prettierignore
Normal file
2
TheBank/bankuiclerk/.prettierignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
obj
|
||||||
7
TheBank/bankuiclerk/.prettierrc
Normal file
7
TheBank/bankuiclerk/.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"jsxSingleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
<!doctype html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite + React + TS</title>
|
<title>Трудяги</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="roboto">
|
<body class="roboto">
|
||||||
text
|
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
import { useState } from 'react'
|
import { useAuthCheck } from '@/hooks/useAuthCheck';
|
||||||
import reactLogo from './assets/react.svg'
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
import viteLogo from '/vite.svg'
|
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||||
import './App.css'
|
import { Header } from '@/components/layout/Header';
|
||||||
|
import { Footer } from '@/components/layout/Footer';
|
||||||
|
import { Suspense } from 'react';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [count, setCount] = useState(0)
|
const user = useAuthStore((store) => store.user);
|
||||||
|
const { isLoading } = useAuthCheck();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
const redirect = encodeURIComponent(location.pathname + location.search);
|
||||||
|
return <Navigate to={`/auth?redirect=${redirect}`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<Header />
|
||||||
<a href="https://vite.dev" target="_blank">
|
<Suspense fallback={<p>Loading...</p>}>
|
||||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
{location.pathname === '/' && (
|
||||||
</a>
|
<main>Удобный сервис для работы клерков</main>
|
||||||
<a href="https://react.dev" target="_blank">
|
)}
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
{location.pathname !== '/' && <Outlet />}
|
||||||
</a>
|
</Suspense>
|
||||||
</div>
|
<Footer />
|
||||||
<h1>Vite + React</h1>
|
|
||||||
<div className="card">
|
|
||||||
<button onClick={() => setCount((count) => count + 1)}>
|
|
||||||
count is {count}
|
|
||||||
</button>
|
|
||||||
<p>
|
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<p className="read-the-docs">
|
|
||||||
Click on the Vite and React logos to learn more
|
|
||||||
</p>
|
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App
|
export default App;
|
||||||
|
|||||||
@@ -30,11 +30,15 @@ export const clientsApi = {
|
|||||||
|
|
||||||
// Clerks API
|
// Clerks API
|
||||||
export const clerksApi = {
|
export const clerksApi = {
|
||||||
getAll: () => getData<ClerkBindingModel>('api/Clerks/GetAllRecords'),
|
getAll: () => getData<ClerkBindingModel>('api/clerks'),
|
||||||
getById: (id: string) =>
|
getById: (id: string) =>
|
||||||
getData<ClerkBindingModel>(`api/Clerks/GetRecord/${id}`),
|
getData<ClerkBindingModel>(`api/Clerks/GetRecord/${id}`),
|
||||||
create: (data: ClerkBindingModel) => postData('api/Clerks/Register', data),
|
create: (data: ClerkBindingModel) => postData('api/Clerks/Register', data),
|
||||||
update: (data: ClerkBindingModel) => putData('api/Clerks/ChangeInfo', data),
|
update: (data: ClerkBindingModel) => putData('api/Clerks', data),
|
||||||
|
// auth
|
||||||
|
login: (data: LoginBindingModel) => postLoginData('api/Clerks/login', data),
|
||||||
|
logout: () => postData('api/clerks/logout', {}),
|
||||||
|
getCurrentUser: () => getSingleData<ClerkBindingModel>('api/clerks/me'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Credit Programs API
|
// Credit Programs API
|
||||||
|
|||||||
382
TheBank/bankuiclerk/src/components/features/ClientForm.tsx
Normal file
382
TheBank/bankuiclerk/src/components/features/ClientForm.tsx
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import type {
|
||||||
|
ClientBindingModel,
|
||||||
|
DepositClientBindingModel,
|
||||||
|
ClientCreditProgramBindingModel,
|
||||||
|
} from '@/types/types';
|
||||||
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
|
import { useDeposits } from '@/hooks/useDeposits';
|
||||||
|
import { useCreditPrograms } from '@/hooks/useCreditPrograms';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type BaseFormValues = {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
surname: string;
|
||||||
|
balance: number;
|
||||||
|
depositIds: string[];
|
||||||
|
creditProgramIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditFormValues = {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
surname?: string;
|
||||||
|
balance?: number;
|
||||||
|
depositIds?: string[];
|
||||||
|
creditProgramIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().min(1, 'Имя обязательно'),
|
||||||
|
surname: z.string().min(1, 'Фамилия обязательна'),
|
||||||
|
balance: z.coerce.number().min(0, 'Баланс не может быть отрицательным'),
|
||||||
|
depositIds: z.array(z.string()),
|
||||||
|
creditProgramIds: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addSchema = baseSchema;
|
||||||
|
|
||||||
|
const editSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().min(1, 'Имя обязательно').optional(),
|
||||||
|
surname: z.string().min(1, 'Фамилия обязательна').optional(),
|
||||||
|
balance: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, 'Баланс не может быть отрицательным')
|
||||||
|
.optional(),
|
||||||
|
depositIds: z.array(z.string()).optional(),
|
||||||
|
creditProgramIds: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface BaseClientFormProps {
|
||||||
|
onSubmit: (data: ClientBindingModel) => void;
|
||||||
|
schema: z.ZodType<BaseFormValues | EditFormValues>;
|
||||||
|
defaultValues?: Partial<ClientBindingModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseClientForm = ({
|
||||||
|
onSubmit,
|
||||||
|
schema,
|
||||||
|
defaultValues,
|
||||||
|
}: BaseClientFormProps): React.JSX.Element => {
|
||||||
|
const { deposits } = useDeposits();
|
||||||
|
const { creditPrograms } = useCreditPrograms();
|
||||||
|
|
||||||
|
const initialDepositIds = useMemo(
|
||||||
|
() =>
|
||||||
|
defaultValues?.depositClients
|
||||||
|
?.map((dc) => dc.depositId)
|
||||||
|
.filter((id): id is string => !!id) || [],
|
||||||
|
[defaultValues?.depositClients],
|
||||||
|
);
|
||||||
|
|
||||||
|
const initialCreditProgramIds = useMemo(
|
||||||
|
() =>
|
||||||
|
defaultValues?.creditProgramClients
|
||||||
|
?.map((ccp) => ccp.creditProgramId)
|
||||||
|
.filter((id): id is string => !!id) || [],
|
||||||
|
[defaultValues?.creditProgramClients],
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<BaseFormValues | EditFormValues>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
id: defaultValues?.id || '',
|
||||||
|
name: defaultValues?.name || '',
|
||||||
|
surname: defaultValues?.surname || '',
|
||||||
|
balance: defaultValues?.balance || 0,
|
||||||
|
depositIds: initialDepositIds,
|
||||||
|
creditProgramIds: initialCreditProgramIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (defaultValues) {
|
||||||
|
form.reset({
|
||||||
|
id: defaultValues.id || '',
|
||||||
|
name: defaultValues.name || '',
|
||||||
|
surname: defaultValues.surname || '',
|
||||||
|
balance: defaultValues.balance || 0,
|
||||||
|
depositIds: initialDepositIds,
|
||||||
|
creditProgramIds: initialCreditProgramIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [defaultValues, form, initialDepositIds, initialCreditProgramIds]);
|
||||||
|
|
||||||
|
const clerk = useAuthStore((store) => store.user);
|
||||||
|
|
||||||
|
const handleSubmit = (data: BaseFormValues | EditFormValues) => {
|
||||||
|
const clientId = data.id || crypto.randomUUID();
|
||||||
|
|
||||||
|
const depositClients: DepositClientBindingModel[] = (
|
||||||
|
'depositIds' in data && data.depositIds ? data.depositIds : []
|
||||||
|
).map((depositId) => {
|
||||||
|
const existingDepositClient = defaultValues?.depositClients?.find(
|
||||||
|
(dc) => dc.depositId === depositId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: existingDepositClient?.id,
|
||||||
|
clientId: clientId,
|
||||||
|
depositId: depositId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const creditProgramClients: ClientCreditProgramBindingModel[] = (
|
||||||
|
'creditProgramIds' in data && data.creditProgramIds
|
||||||
|
? data.creditProgramIds
|
||||||
|
: []
|
||||||
|
).map((creditProgramId) => {
|
||||||
|
const existingCreditProgramClient =
|
||||||
|
defaultValues?.creditProgramClients?.find(
|
||||||
|
(ccp) => ccp.creditProgramId === creditProgramId,
|
||||||
|
);
|
||||||
|
console.log(existingCreditProgramClient);
|
||||||
|
return {
|
||||||
|
id: existingCreditProgramClient?.id,
|
||||||
|
clientId: clientId,
|
||||||
|
creditProgramId: creditProgramId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload: ClientBindingModel = {
|
||||||
|
id: clientId,
|
||||||
|
clerkId: clerk?.id,
|
||||||
|
name: 'name' in data && data.name !== undefined ? data.name : '',
|
||||||
|
surname:
|
||||||
|
'surname' in data && data.surname !== undefined ? data.surname : '',
|
||||||
|
balance:
|
||||||
|
'balance' in data && data.balance !== undefined ? data.balance : 0,
|
||||||
|
depositClients: depositClients,
|
||||||
|
creditProgramClients: creditProgramClients,
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedDepositIds = form.watch('depositIds') || [];
|
||||||
|
const selectedCreditProgramIds = form.watch('creditProgramIds') || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
|
className="space-y-4 max-w-md mx-auto p-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="id"
|
||||||
|
render={({ field }) => <input type="hidden" {...field} />}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Имя</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Введите имя" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="surname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Фамилия</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Введите фамилию" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="balance"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Баланс</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="number" placeholder="Введите баланс" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="depositIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Вклады</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentValues = field.value || [];
|
||||||
|
if (!currentValues.includes(value)) {
|
||||||
|
field.onChange([...currentValues, value]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите вклады" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{deposits?.map((deposit) => (
|
||||||
|
<SelectItem
|
||||||
|
key={deposit.id}
|
||||||
|
value={deposit.id || ''}
|
||||||
|
className={cn(
|
||||||
|
selectedDepositIds.includes(deposit.id || '') &&
|
||||||
|
'bg-muted',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{`Вклад ${deposit.interestRate}% - ${deposit.cost}₽`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{field.value?.map((id) => {
|
||||||
|
const deposit = deposits?.find((d) => d.id === id);
|
||||||
|
return deposit ? (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
className="bg-secondary px-2 py-1 rounded-md text-sm flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>{`Вклад ${deposit.interestRate}% - ${deposit.cost}₽`}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(field.value?.filter((v) => v !== id));
|
||||||
|
}}
|
||||||
|
className="text-destructive hover:text-destructive/80"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="creditProgramIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Кредитные программы</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentValues = field.value || [];
|
||||||
|
if (!currentValues.includes(value)) {
|
||||||
|
field.onChange([...currentValues, value]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите кредитные программы" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{creditPrograms?.map((program) => (
|
||||||
|
<SelectItem
|
||||||
|
key={program.id}
|
||||||
|
value={program.id}
|
||||||
|
className={cn(
|
||||||
|
selectedCreditProgramIds.includes(program.id) &&
|
||||||
|
'bg-muted',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{`${program.name} - ${program.cost}₽`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{field.value?.map((id) => {
|
||||||
|
const program = creditPrograms?.find((p) => p.id === id);
|
||||||
|
return program ? (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
className="bg-secondary px-2 py-1 rounded-md text-sm flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>{`${program.name} - ${program.cost}₽`}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(field.value?.filter((v) => v !== id));
|
||||||
|
}}
|
||||||
|
className="text-destructive hover:text-destructive/80"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientFormAdd = ({
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
onSubmit: (data: ClientBindingModel) => void;
|
||||||
|
}): React.JSX.Element => {
|
||||||
|
return <BaseClientForm onSubmit={onSubmit} schema={addSchema} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ClientFormEdit = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: {
|
||||||
|
onSubmit: (data: ClientBindingModel) => void;
|
||||||
|
defaultValues: Partial<ClientBindingModel>;
|
||||||
|
}): React.JSX.Element => {
|
||||||
|
return (
|
||||||
|
<BaseClientForm
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
schema={editSchema}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
308
TheBank/bankuiclerk/src/components/features/DepositForm.tsx
Normal file
308
TheBank/bankuiclerk/src/components/features/DepositForm.tsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
import React, { useMemo } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import type {
|
||||||
|
DepositBindingModel,
|
||||||
|
DepositClientBindingModel,
|
||||||
|
} from '@/types/types';
|
||||||
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
|
import { useClients } from '@/hooks/useClients';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
type BaseFormValues = {
|
||||||
|
id?: string;
|
||||||
|
interestRate: number;
|
||||||
|
cost: number;
|
||||||
|
period: number;
|
||||||
|
clientIds: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditFormValues = {
|
||||||
|
id?: string;
|
||||||
|
interestRate?: number;
|
||||||
|
cost?: number;
|
||||||
|
period?: number;
|
||||||
|
clientIds?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
interestRate: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, 'Процентная ставка не может быть отрицательной'),
|
||||||
|
cost: z.coerce.number().min(0, 'Стоимость не может быть отрицательной'),
|
||||||
|
period: z.coerce.number().int().min(1, 'Срок вклада должен быть не менее 1'),
|
||||||
|
clientIds: z.array(z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addSchema = baseSchema;
|
||||||
|
|
||||||
|
const editSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
interestRate: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, 'Процентная ставка не может быть отрицательной')
|
||||||
|
.optional(),
|
||||||
|
cost: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0, 'Стоимость не может быть отрицательной')
|
||||||
|
.optional(),
|
||||||
|
period: z.coerce
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.min(1, 'Срок вклада должен быть не менее 1')
|
||||||
|
.optional(),
|
||||||
|
clientIds: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface BaseDepositFormProps {
|
||||||
|
onSubmit: (data: DepositBindingModel) => void;
|
||||||
|
schema: z.ZodType<BaseFormValues | EditFormValues>;
|
||||||
|
defaultValues?: Partial<DepositBindingModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseDepositForm = ({
|
||||||
|
onSubmit,
|
||||||
|
schema,
|
||||||
|
defaultValues,
|
||||||
|
}: BaseDepositFormProps): React.JSX.Element => {
|
||||||
|
const { clients } = useClients();
|
||||||
|
|
||||||
|
const initialClientIds = useMemo(
|
||||||
|
() =>
|
||||||
|
defaultValues?.depositClients
|
||||||
|
?.map((dc) => dc.clientId)
|
||||||
|
.filter((id): id is string => !!id) || [],
|
||||||
|
[defaultValues?.depositClients],
|
||||||
|
);
|
||||||
|
|
||||||
|
const form = useForm<BaseFormValues | EditFormValues>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
id: defaultValues?.id || '',
|
||||||
|
interestRate: defaultValues?.interestRate || 0,
|
||||||
|
cost: defaultValues?.cost || 0,
|
||||||
|
period: defaultValues?.period || 1,
|
||||||
|
clientIds: initialClientIds,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (defaultValues) {
|
||||||
|
form.reset({
|
||||||
|
id: defaultValues.id || '',
|
||||||
|
interestRate: defaultValues.interestRate || 0,
|
||||||
|
cost: defaultValues.cost || 0,
|
||||||
|
period: defaultValues.period || 1,
|
||||||
|
clientIds: initialClientIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [defaultValues, form, initialClientIds]);
|
||||||
|
|
||||||
|
const clerk = useAuthStore((store) => store.user);
|
||||||
|
|
||||||
|
const handleSubmit = (data: BaseFormValues | EditFormValues) => {
|
||||||
|
const depositId = data.id || crypto.randomUUID();
|
||||||
|
|
||||||
|
const depositClients: DepositClientBindingModel[] = (
|
||||||
|
'clientIds' in data && data.clientIds ? data.clientIds : []
|
||||||
|
).map((clientId) => {
|
||||||
|
const existingDepositClient = defaultValues?.depositClients?.find(
|
||||||
|
(dc) => dc.clientId === clientId,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: existingDepositClient?.id, // Use existing relationship ID if available
|
||||||
|
clientId: clientId,
|
||||||
|
depositId: depositId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload: DepositBindingModel = {
|
||||||
|
id: depositId,
|
||||||
|
clerkId: clerk?.id,
|
||||||
|
interestRate:
|
||||||
|
'interestRate' in data && data.interestRate !== undefined
|
||||||
|
? data.interestRate
|
||||||
|
: 0,
|
||||||
|
cost: 'cost' in data && data.cost !== undefined ? data.cost : 0,
|
||||||
|
period: 'period' in data && data.period !== undefined ? data.period : 1,
|
||||||
|
depositClients: depositClients,
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedClientIds = form.watch('clientIds') || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(handleSubmit)}
|
||||||
|
className="space-y-4 max-w-md mx-auto p-4"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="id"
|
||||||
|
render={({ field }) => <input type="hidden" {...field} />}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="interestRate"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Процентная ставка</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Введите процентную ставку"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="cost"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Стоимость</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Введите стоимость"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="period"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Срок вклада (месяцы)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Введите срок вклада"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="clientIds"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Клиенты</FormLabel>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const currentValues = field.value || [];
|
||||||
|
if (!currentValues.includes(value)) {
|
||||||
|
field.onChange([...currentValues, value]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите клиентов" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{clients?.map((client) => (
|
||||||
|
<SelectItem
|
||||||
|
key={client.id}
|
||||||
|
value={client.id || ''}
|
||||||
|
className={cn(
|
||||||
|
selectedClientIds.includes(client.id || '') &&
|
||||||
|
'bg-muted',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{`${client.name} ${client.surname}`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex flex-wrap gap-2 mt-2">
|
||||||
|
{field.value?.map((id) => {
|
||||||
|
const client = clients?.find((c) => c.id === id);
|
||||||
|
return client ? (
|
||||||
|
<div
|
||||||
|
key={id}
|
||||||
|
className="bg-secondary px-2 py-1 rounded-md text-sm flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<span>{`${client.name} ${client.surname}`}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
field.onChange(field.value?.filter((v) => v !== id));
|
||||||
|
}}
|
||||||
|
className="text-destructive hover:text-destructive/80"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DepositFormAdd = ({
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
onSubmit: (data: DepositBindingModel) => void;
|
||||||
|
}): React.JSX.Element => {
|
||||||
|
return <BaseDepositForm onSubmit={onSubmit} schema={addSchema} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DepositFormEdit = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: {
|
||||||
|
onSubmit: (data: DepositBindingModel) => void;
|
||||||
|
defaultValues: Partial<DepositBindingModel>;
|
||||||
|
}): React.JSX.Element => {
|
||||||
|
return (
|
||||||
|
<BaseDepositForm
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
schema={editSchema}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
82
TheBank/bankuiclerk/src/components/features/LoginForm.tsx
Normal file
82
TheBank/bankuiclerk/src/components/features/LoginForm.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { LoginBindingModel } from '@/types/types';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import React from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '../ui/form';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
|
||||||
|
interface LoginFormProps {
|
||||||
|
onSubmit: (data: LoginBindingModel) => void;
|
||||||
|
defaultValues?: Partial<LoginBindingModel>;
|
||||||
|
}
|
||||||
|
const loginFormSchema = z.object({
|
||||||
|
login: z.string().min(3, 'Логин должен быть не короче 3 символов'),
|
||||||
|
password: z.string().min(6, 'Пароль должен быть не короче 6 символов'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof loginFormSchema>;
|
||||||
|
|
||||||
|
export const LoginForm = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: LoginFormProps): React.JSX.Element => {
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(loginFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
login: defaultValues?.login || '',
|
||||||
|
password: defaultValues?.password || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (data: FormValues) => {
|
||||||
|
const payload: LoginBindingModel = {
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="login"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Логин</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Логин" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Пароль</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="Пароль" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Войти
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
164
TheBank/bankuiclerk/src/components/features/ProfileForm.tsx
Normal file
164
TheBank/bankuiclerk/src/components/features/ProfileForm.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import type { ClerkBindingModel } from '@/types/types';
|
||||||
|
|
||||||
|
interface ProfileFormValues extends ClerkBindingModel {}
|
||||||
|
|
||||||
|
interface ProfileFormProps {
|
||||||
|
onSubmit: (data: Partial<ClerkBindingModel>) => void;
|
||||||
|
defaultValues: ProfileFormValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileFormSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().min(1, 'Имя обязательно'),
|
||||||
|
surname: z.string().min(1, 'Фамилия обязательна'),
|
||||||
|
middleName: z.string().min(1, 'Отчество обязательно'),
|
||||||
|
login: z.string().min(3, 'Логин должен быть не короче 3 символов'),
|
||||||
|
password: z.string().min(6, 'Пароль должен быть не короче 6 символов'),
|
||||||
|
email: z.string().email('Введите корректный email'),
|
||||||
|
phoneNumber: z.string().min(10, 'Введите корректный номер телефона'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof profileFormSchema>;
|
||||||
|
|
||||||
|
export const ProfileForm = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: ProfileFormProps): React.JSX.Element => {
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(profileFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
id: defaultValues.id,
|
||||||
|
name: defaultValues.name,
|
||||||
|
surname: defaultValues.surname,
|
||||||
|
middleName: defaultValues.middleName,
|
||||||
|
login: defaultValues.login,
|
||||||
|
password: defaultValues.password,
|
||||||
|
email: defaultValues.email,
|
||||||
|
phoneNumber: defaultValues.phoneNumber,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (data: FormValues) => {
|
||||||
|
onSubmit(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="id"
|
||||||
|
render={({ field }) => <input type="hidden" {...field} />}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Имя</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Имя" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="surname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Фамилия</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Фамилия" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="middleName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Отчество</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Отчество" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="login"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Логин</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Логин" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Пароль</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="Пароль" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" placeholder="Email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phoneNumber"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Номер телефона</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Номер телефона" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Сохранить изменения
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
166
TheBank/bankuiclerk/src/components/features/RegisterForm.tsx
Normal file
166
TheBank/bankuiclerk/src/components/features/RegisterForm.tsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import type { ClerkBindingModel } from '@/types/types';
|
||||||
|
|
||||||
|
interface RegisterFormProps {
|
||||||
|
onSubmit: (data: ClerkBindingModel) => void;
|
||||||
|
defaultValues?: Partial<ClerkBindingModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerFormSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
name: z.string().min(1, 'Имя обязательно'),
|
||||||
|
surname: z.string().min(1, 'Фамилия обязательна'),
|
||||||
|
middleName: z.string().min(1, 'Отчество обязательно'),
|
||||||
|
login: z.string().min(3, 'Логин должен быть не короче 3 символов'),
|
||||||
|
password: z.string().min(6, 'Пароль должен быть не короче 6 символов'),
|
||||||
|
email: z.string().email('Введите корректный email'),
|
||||||
|
phoneNumber: z.string().min(10, 'Введите корректный номер телефона'),
|
||||||
|
});
|
||||||
|
|
||||||
|
type FormValues = z.infer<typeof registerFormSchema>;
|
||||||
|
|
||||||
|
export const RegisterForm = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: RegisterFormProps): React.JSX.Element => {
|
||||||
|
const form = useForm<FormValues>({
|
||||||
|
resolver: zodResolver(registerFormSchema),
|
||||||
|
defaultValues: {
|
||||||
|
id: defaultValues?.id || crypto.randomUUID(),
|
||||||
|
name: defaultValues?.name || '',
|
||||||
|
surname: defaultValues?.surname || '',
|
||||||
|
middleName: defaultValues?.middleName || '',
|
||||||
|
login: defaultValues?.login || '',
|
||||||
|
password: defaultValues?.password || '',
|
||||||
|
email: defaultValues?.email || '',
|
||||||
|
phoneNumber: defaultValues?.phoneNumber || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (data: FormValues) => {
|
||||||
|
const payload: ClerkBindingModel = {
|
||||||
|
...data,
|
||||||
|
id: data.id || crypto.randomUUID(),
|
||||||
|
};
|
||||||
|
onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form {...form}>
|
||||||
|
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="id"
|
||||||
|
render={({ field }) => <input type="hidden" {...field} />}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="name"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Имя</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Имя" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="surname"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Фамилия</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Фамилия" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="middleName"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Отчество</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Отчество" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="login"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Логин</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Логин" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Пароль</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="password" placeholder="Пароль" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="email" placeholder="Email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="phoneNumber"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Номер телефона</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Номер телефона" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Зарегистрировать
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,7 +18,6 @@ import {
|
|||||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import { useAuthStore } from '@/store/workerStore';
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
|
||||||
|
|
||||||
type NavOptionValue = {
|
type NavOptionValue = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -33,42 +32,42 @@ type NavOption = {
|
|||||||
|
|
||||||
const navOptions = [
|
const navOptions = [
|
||||||
{
|
{
|
||||||
name: 'Валюты',
|
name: 'Клиенты',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/currencies',
|
link: '/clients',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Кредитные программы',
|
name: 'Вклады',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/credit-programs',
|
link: '/deposits',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Сроки',
|
name: 'Пополнения',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/periods',
|
link: '/replenishments',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Кладовщики',
|
name: 'Клерки',
|
||||||
options: [
|
options: [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/storekeepers',
|
link: '/clerks',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
import { useClerks } from '@/hooks/useClerks';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
|
||||||
import { RegisterForm } from '../features/RegisterForm';
|
import { RegisterForm } from '../features/RegisterForm';
|
||||||
import { LoginForm } from '../features/LoginForm';
|
import { LoginForm } from '../features/LoginForm';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { LoginBindingModel, StorekeeperBindingModel } from '@/types/types';
|
import type { LoginBindingModel, ClerkBindingModel } from '@/types/types';
|
||||||
|
|
||||||
type Forms = 'login' | 'register';
|
type Forms = 'login' | 'register';
|
||||||
|
|
||||||
export const AuthStorekeeper = (): React.JSX.Element => {
|
export const AuthClerks = (): React.JSX.Element => {
|
||||||
const {
|
const { createClerk, loginClerk, isLoginError, loginError, isCreateError } =
|
||||||
createStorekeeper,
|
useClerks();
|
||||||
loginStorekeeper,
|
|
||||||
isLoginError,
|
|
||||||
loginError,
|
|
||||||
isCreateError,
|
|
||||||
} = useStorekeepers();
|
|
||||||
|
|
||||||
const [currentForm, setCurrentForm] = React.useState<Forms>('login');
|
const [currentForm, setCurrentForm] = React.useState<Forms>('login');
|
||||||
|
|
||||||
const handleRegister = (data: StorekeeperBindingModel) => {
|
const handleRegister = (data: ClerkBindingModel) => {
|
||||||
createStorekeeper(data, {
|
createClerk(data, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast('Регистрация успешна! Войдите в систему.');
|
toast('Регистрация успешна! Войдите в систему.');
|
||||||
},
|
},
|
||||||
@@ -31,7 +26,7 @@ export const AuthStorekeeper = (): React.JSX.Element => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleLogin = (data: LoginBindingModel) => {
|
const handleLogin = (data: LoginBindingModel) => {
|
||||||
loginStorekeeper(data);
|
loginClerk(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|||||||
62
TheBank/bankuiclerk/src/components/pages/Clerks.tsx
Normal file
62
TheBank/bankuiclerk/src/components/pages/Clerks.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { useClerks } from '@/hooks/useClerks';
|
||||||
|
import React from 'react';
|
||||||
|
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
||||||
|
import type { ClerkBindingModel } from '@/types/types';
|
||||||
|
|
||||||
|
const columns: ColumnDef<ClerkBindingModel>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Имя',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'surname',
|
||||||
|
header: 'Фамилия',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'middleName',
|
||||||
|
header: 'Отчество',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'login',
|
||||||
|
header: 'Логин',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'phoneNumber',
|
||||||
|
header: 'Телефон',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Clerks = (): React.JSX.Element => {
|
||||||
|
const { clerks, isLoading, error } = useClerks();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto py-10">
|
||||||
|
Ошибка загрузки данных: {error.message}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto py-10">
|
||||||
|
<h1 className="text-2xl font-bold mb-6">Клерки</h1>
|
||||||
|
<DataTable
|
||||||
|
data={clerks || []}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={console.log}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
170
TheBank/bankuiclerk/src/components/pages/Clients.tsx
Normal file
170
TheBank/bankuiclerk/src/components/pages/Clients.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import { useClients } from '@/hooks/useClients';
|
||||||
|
import { useClerks } from '@/hooks/useClerks';
|
||||||
|
import React from 'react';
|
||||||
|
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
||||||
|
import { AppSidebar } from '../layout/Sidebar';
|
||||||
|
import type { ClientBindingModel } from '@/types/types';
|
||||||
|
import { DialogForm } from '../layout/DialogForm';
|
||||||
|
import { ClientFormAdd, ClientFormEdit } from '../features/ClientForm';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
const columns: ColumnDef<ClientBindingModel>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'name',
|
||||||
|
header: 'Имя',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'surname',
|
||||||
|
header: 'Фамилия',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'balance',
|
||||||
|
header: 'Баланс',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'clerkName',
|
||||||
|
header: 'Клерк',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'deposits',
|
||||||
|
header: 'Вклады',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'creditPrograms',
|
||||||
|
header: 'Кредиты',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Clients = (): React.JSX.Element => {
|
||||||
|
const {
|
||||||
|
clients,
|
||||||
|
isLoading: isClientsLoading,
|
||||||
|
error: clientsError,
|
||||||
|
updateClient,
|
||||||
|
createClient,
|
||||||
|
} = useClients();
|
||||||
|
const {
|
||||||
|
clerks,
|
||||||
|
isLoading: isClerksLoading,
|
||||||
|
error: clerksError,
|
||||||
|
} = useClerks();
|
||||||
|
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] =
|
||||||
|
React.useState<boolean>(false);
|
||||||
|
const [selectedItem, setSelectedItem] = React.useState<
|
||||||
|
ClientBindingModel | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const finalData = React.useMemo(() => {
|
||||||
|
if (!clients || !clerks) return [];
|
||||||
|
|
||||||
|
return clients.map((client) => {
|
||||||
|
const clerk = clerks.find((c) => c.id === client.clerkId);
|
||||||
|
return {
|
||||||
|
...client,
|
||||||
|
clerkName: clerk ? `${clerk.name} ${clerk.surname}` : 'Неизвестно',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [clients, clerks]);
|
||||||
|
|
||||||
|
const handleAdd = (data: ClientBindingModel) => {
|
||||||
|
createClient(data);
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (data: ClientBindingModel) => {
|
||||||
|
if (selectedItem) {
|
||||||
|
updateClient({
|
||||||
|
...selectedItem,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectItem = (id: string | undefined) => {
|
||||||
|
const item = clients?.find((p) => p.id === id);
|
||||||
|
if (item) {
|
||||||
|
setSelectedItem({
|
||||||
|
...item,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditForm = () => {
|
||||||
|
if (!selectedItem) {
|
||||||
|
toast('Выберите элемент для редактирования');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isClientsLoading || isClerksLoading) {
|
||||||
|
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clientsError || clerksError) {
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto py-10">
|
||||||
|
Ошибка загрузки данных: {clientsError?.message || clerksError?.message}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex-1 flex relative">
|
||||||
|
<AppSidebar
|
||||||
|
onAddClick={() => {
|
||||||
|
setIsAddDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onEditClick={() => {
|
||||||
|
openEditForm();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
{!selectedItem && (
|
||||||
|
<DialogForm<ClientBindingModel>
|
||||||
|
title="Форма клиентов"
|
||||||
|
description="Добавить клиента"
|
||||||
|
isOpen={isAddDialogOpen}
|
||||||
|
onClose={() => setIsAddDialogOpen(false)}
|
||||||
|
onSubmit={handleAdd}
|
||||||
|
>
|
||||||
|
<ClientFormAdd />
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
|
{selectedItem && (
|
||||||
|
<DialogForm<ClientBindingModel>
|
||||||
|
title="Форма клиентов"
|
||||||
|
description="Изменить данные"
|
||||||
|
isOpen={isEditDialogOpen}
|
||||||
|
onClose={() => setIsEditDialogOpen(false)}
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
>
|
||||||
|
<ClientFormEdit
|
||||||
|
onSubmit={console.log}
|
||||||
|
defaultValues={selectedItem}
|
||||||
|
/>
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<DataTable
|
||||||
|
data={finalData}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={(id) => handleSelectItem(id)}
|
||||||
|
selectedRow={selectedItem?.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
180
TheBank/bankuiclerk/src/components/pages/Deposits.tsx
Normal file
180
TheBank/bankuiclerk/src/components/pages/Deposits.tsx
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
import { useDeposits } from '@/hooks/useDeposits';
|
||||||
|
import { useClerks } from '@/hooks/useClerks';
|
||||||
|
import React from 'react';
|
||||||
|
import { AppSidebar } from '../layout/Sidebar';
|
||||||
|
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
||||||
|
import type { DepositBindingModel, ClientBindingModel } from '@/types/types';
|
||||||
|
import { DialogForm } from '../layout/DialogForm';
|
||||||
|
import { DepositFormAdd, DepositFormEdit } from '../features/DepositForm';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
type DepositRowData = DepositBindingModel & {
|
||||||
|
clerkName: string;
|
||||||
|
clientsDisplay: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<DepositRowData>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'interestRate',
|
||||||
|
header: 'Процентная ставка',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'cost',
|
||||||
|
header: 'Стоимость',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'period',
|
||||||
|
header: 'Срок вклада',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'clerkName',
|
||||||
|
header: 'Клерк',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'clientsDisplay',
|
||||||
|
header: 'Клиенты',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Deposits = (): React.JSX.Element => {
|
||||||
|
const {
|
||||||
|
deposits,
|
||||||
|
isLoading: isDepositsLoading,
|
||||||
|
error: depositsError,
|
||||||
|
createDeposit,
|
||||||
|
updateDeposit,
|
||||||
|
} = useDeposits();
|
||||||
|
const {
|
||||||
|
clerks,
|
||||||
|
isLoading: isClerksLoading,
|
||||||
|
error: clerksError,
|
||||||
|
} = useClerks();
|
||||||
|
|
||||||
|
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] =
|
||||||
|
React.useState<boolean>(false);
|
||||||
|
const [selectedItem, setSelectedItem] = React.useState<
|
||||||
|
DepositBindingModel | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const finalData = React.useMemo(() => {
|
||||||
|
if (!deposits || !clerks) return [];
|
||||||
|
|
||||||
|
return deposits.map((deposit) => {
|
||||||
|
const clerk = clerks.find((c) => c.id === deposit.clerkId);
|
||||||
|
const clientsDisplay =
|
||||||
|
deposit.depositClients
|
||||||
|
?.map((dc) => {
|
||||||
|
const client = clerks?.find((c) => c.id === dc.clientId);
|
||||||
|
return client ? `${client.name} ${client.surname}` : dc.clientId;
|
||||||
|
})
|
||||||
|
.join(', ') || 'Нет клиентов';
|
||||||
|
|
||||||
|
return {
|
||||||
|
...deposit,
|
||||||
|
clerkName: clerk ? `${clerk.name} ${clerk.surname}` : 'Неизвестно',
|
||||||
|
clientsDisplay: clientsDisplay,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [deposits, clerks]);
|
||||||
|
|
||||||
|
const handleAdd = (data: DepositBindingModel) => {
|
||||||
|
createDeposit(data);
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (data: DepositBindingModel) => {
|
||||||
|
if (selectedItem) {
|
||||||
|
updateDeposit({
|
||||||
|
...selectedItem,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectItem = (id: string | undefined) => {
|
||||||
|
const item = deposits?.find((p) => p.id === id);
|
||||||
|
if (item) {
|
||||||
|
setSelectedItem({
|
||||||
|
...item,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditForm = () => {
|
||||||
|
if (!selectedItem) {
|
||||||
|
toast('Выберите элемент для редактирования');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDepositsLoading || isClerksLoading) {
|
||||||
|
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depositsError || clerksError) {
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto py-10">
|
||||||
|
Ошибка загрузки данных: {depositsError?.message || clerksError?.message}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex-1 flex relative">
|
||||||
|
<AppSidebar
|
||||||
|
onAddClick={() => {
|
||||||
|
setIsAddDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onEditClick={() => {
|
||||||
|
openEditForm();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
{!selectedItem && (
|
||||||
|
<DialogForm<DepositBindingModel>
|
||||||
|
title="Форма вкладов"
|
||||||
|
description="Добавить вклад"
|
||||||
|
isOpen={isAddDialogOpen}
|
||||||
|
onClose={() => setIsAddDialogOpen(false)}
|
||||||
|
onSubmit={handleAdd}
|
||||||
|
>
|
||||||
|
<DepositFormAdd onSubmit={handleAdd} />
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
|
{selectedItem && (
|
||||||
|
<DialogForm<DepositBindingModel>
|
||||||
|
title="Форма вкладов"
|
||||||
|
description="Изменить данные"
|
||||||
|
isOpen={isEditDialogOpen}
|
||||||
|
onClose={() => setIsEditDialogOpen(false)}
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
>
|
||||||
|
<DepositFormEdit
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
defaultValues={selectedItem}
|
||||||
|
/>
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<DataTable
|
||||||
|
data={finalData}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={(id) => handleSelectItem(id)}
|
||||||
|
selectedRow={selectedItem?.id}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useAuthStore } from '@/store/workerStore';
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
import { ProfileForm } from '../features/ProfileForm';
|
import { ProfileForm } from '../features/ProfileForm';
|
||||||
import type { StorekeeperBindingModel } from '@/types/types';
|
import type { ClerkBindingModel } from '@/types/types';
|
||||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
import { useClerks } from '@/hooks/useClerks';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
export const Profile = (): React.JSX.Element => {
|
export const Profile = (): React.JSX.Element => {
|
||||||
const { user, updateUser } = useAuthStore();
|
const { user, updateUser } = useAuthStore();
|
||||||
const { updateStorekeeper, isUpdateError, updateError } = useStorekeepers();
|
const { updateClerk, isUpdateError, updateError } = useClerks();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isUpdateError) {
|
if (isUpdateError) {
|
||||||
@@ -23,10 +23,10 @@ export const Profile = (): React.JSX.Element => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = (data: Partial<StorekeeperBindingModel>) => {
|
const handleUpdate = (data: Partial<ClerkBindingModel>) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
updateUser(data);
|
updateUser(data);
|
||||||
updateStorekeeper(data);
|
updateClerk(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
67
TheBank/bankuiclerk/src/components/pages/Replenishments.tsx
Normal file
67
TheBank/bankuiclerk/src/components/pages/Replenishments.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useReplenishments } from '@/hooks/useReplenishments';
|
||||||
|
import React from 'react';
|
||||||
|
import { AppSidebar } from '../layout/Sidebar';
|
||||||
|
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
||||||
|
import type { ReplenishmentBindingModel } from '@/types/types';
|
||||||
|
|
||||||
|
const columns: ColumnDef<ReplenishmentBindingModel>[] = [
|
||||||
|
{
|
||||||
|
accessorKey: 'id',
|
||||||
|
header: 'ID',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'amout',
|
||||||
|
header: 'Сумма',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'date',
|
||||||
|
header: 'Стоимость',
|
||||||
|
renderCell: (item) => new Date(item.date).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'depositName',
|
||||||
|
header: 'Вклад',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: 'clerkName',
|
||||||
|
header: 'Клерк',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Replenishments = (): React.JSX.Element => {
|
||||||
|
const { replenishments, isLoading, error } = useReplenishments();
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<main className="container mx-auto py-10">
|
||||||
|
Ошибка загрузки данных: {error.message}
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="flex-1 flex relative">
|
||||||
|
<AppSidebar
|
||||||
|
onAddClick={() => {
|
||||||
|
// setIsAddDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onEditClick={() => {
|
||||||
|
// openEditForm();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex-1 p-4">
|
||||||
|
<div>
|
||||||
|
<DataTable
|
||||||
|
data={replenishments || []}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={console.log}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,38 +1,30 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useAuthStore } from '@/store/workerStore';
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
import { storekeepersApi } from '@/api/api';
|
import { clerksApi } from '@/api/api';
|
||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { type ClerkBindingModel } from '@/types/types';
|
||||||
import { type StorekeeperBindingModel } from '@/types/types';
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
|
||||||
export const useAuthCheck = () => {
|
export const useAuthCheck = () => {
|
||||||
const setAuth = useAuthStore((store) => store.setAuth);
|
const setAuth = useAuthStore((store) => store.setAuth);
|
||||||
const logout = useAuthStore((store) => store.logout);
|
const logout = useAuthStore((store) => store.logout);
|
||||||
const navigate = useNavigate();
|
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isPending: isLoading,
|
isPending: isLoading,
|
||||||
error,
|
error,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery<StorekeeperBindingModel, Error>({
|
} = useQuery<ClerkBindingModel, Error>({
|
||||||
queryKey: ['authCheck'],
|
queryKey: ['authCheck'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const userData = await storekeepersApi.getCurrentUser();
|
const userData = await clerksApi.getCurrentUser();
|
||||||
setAuth(userData);
|
setAuth(userData);
|
||||||
return userData;
|
return userData;
|
||||||
},
|
},
|
||||||
retry: false,
|
retry: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
console.error('Auth check failed:', error?.message);
|
console.error('Auth check failed:', error?.message);
|
||||||
logout();
|
logout();
|
||||||
const redirect = encodeURIComponent(location.pathname + location.search);
|
|
||||||
navigate(`/auth?redirect=${redirect}`);
|
|
||||||
}
|
}
|
||||||
}, [isError, error, logout, navigate, location]);
|
|
||||||
|
|
||||||
return { isLoading, error };
|
return { isLoading, error };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,39 +1,71 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { clerksApi } from '@/api/api';
|
import { clerksApi } from '@/api/api';
|
||||||
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export const useClerks = () => {
|
export const useClerks = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const setAuth = useAuthStore((store) => store.setAuth);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: clerks,
|
data: clerks,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError: isGetAllError,
|
||||||
error,
|
error,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['clerks'],
|
queryKey: ['clerks'],
|
||||||
queryFn: clerksApi.getAll,
|
queryFn: clerksApi.getAll,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: createClerk } = useMutation({
|
const { mutate: createClerk, isError: isCreateError } = useMutation({
|
||||||
mutationFn: clerksApi.create,
|
mutationFn: clerksApi.create,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['clerks'] });
|
queryClient.invalidateQueries({ queryKey: ['clerks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: updateClerk } = useMutation({
|
const {
|
||||||
|
mutate: updateClerk,
|
||||||
|
isError: isUpdateError,
|
||||||
|
error: updateError,
|
||||||
|
} = useMutation({
|
||||||
mutationFn: clerksApi.update,
|
mutationFn: clerksApi.update,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['clerks'] });
|
queryClient.invalidateQueries({ queryKey: ['clerks'] });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
mutate: loginClerk,
|
||||||
|
isError: isLoginError,
|
||||||
|
isSuccess: isLoginSuccess,
|
||||||
|
error: loginError,
|
||||||
|
} = useMutation({
|
||||||
|
mutationFn: clerksApi.login,
|
||||||
|
onSuccess: (userData) => {
|
||||||
|
setAuth(userData);
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const redirect = params.get('redirect') || '/clerks';
|
||||||
|
navigate(redirect);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['clerk'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clerks,
|
clerks,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isGetAllError,
|
||||||
|
isCreateError,
|
||||||
|
isLoginError,
|
||||||
|
isUpdateError,
|
||||||
|
isLoginSuccess,
|
||||||
|
loginError,
|
||||||
|
updateError,
|
||||||
error,
|
error,
|
||||||
createClerk,
|
createClerk,
|
||||||
|
loginClerk,
|
||||||
updateClerk,
|
updateClerk,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,66 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import './index.css'
|
import './index.css';
|
||||||
import App from './App.tsx'
|
import App from './App.tsx';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
import {
|
||||||
|
createBrowserRouter,
|
||||||
|
Navigate,
|
||||||
|
RouterProvider,
|
||||||
|
} from 'react-router-dom';
|
||||||
|
import { Profile } from './components/pages/Profile.tsx';
|
||||||
|
import { AuthClerks } from './components/pages/AuthClerks.tsx';
|
||||||
|
import { Clerks } from './components/pages/Clerks.tsx';
|
||||||
|
import { Clients } from './components/pages/Clients.tsx';
|
||||||
|
import { Deposits } from './components/pages/Deposits.tsx';
|
||||||
|
import { Replenishments } from './components/pages/Replenishments.tsx';
|
||||||
|
|
||||||
|
const routes = createBrowserRouter([
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
element: <App />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
element: <Profile />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/clerks',
|
||||||
|
element: <Clerks />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/clients',
|
||||||
|
element: <Clients />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/deposits',
|
||||||
|
element: <Deposits />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/replenishments',
|
||||||
|
element: <Replenishments />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
errorElement: <p>бля пизда рулям</p>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/auth',
|
||||||
|
element: <AuthClerks />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <Navigate to="/" replace />,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RouterProvider router={routes} />
|
||||||
|
<Toaster />
|
||||||
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
);
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { storekeepersApi } from '@/api/api';
|
import { clerksApi } from '@/api/api';
|
||||||
import type { StorekeeperBindingModel } from '@/types/types';
|
import type { ClerkBindingModel } from '@/types/types';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
type AuthState = {
|
type AuthState = {
|
||||||
user?: StorekeeperBindingModel;
|
user?: ClerkBindingModel;
|
||||||
setAuth: (user: StorekeeperBindingModel) => void;
|
setAuth: (user: ClerkBindingModel) => void;
|
||||||
updateUser: (user: StorekeeperBindingModel) => void;
|
updateUser: (user: ClerkBindingModel) => void;
|
||||||
getUser: () => StorekeeperBindingModel | undefined;
|
getUser: () => ClerkBindingModel | undefined;
|
||||||
logout: () => Promise<void>;
|
logout: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,17 +15,17 @@ export const useAuthStore = create<AuthState>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
user: undefined,
|
user: undefined,
|
||||||
setAuth: (user: StorekeeperBindingModel) => {
|
setAuth: (user: ClerkBindingModel) => {
|
||||||
set({ user: user });
|
set({ user: user });
|
||||||
},
|
},
|
||||||
updateUser: (user: StorekeeperBindingModel) => {
|
updateUser: (user: ClerkBindingModel) => {
|
||||||
set({ user: user });
|
set({ user: user });
|
||||||
},
|
},
|
||||||
getUser: () => {
|
getUser: () => {
|
||||||
return get().user;
|
return get().user;
|
||||||
},
|
},
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
await storekeepersApi.logout();
|
await clerksApi.logout();
|
||||||
set({ user: undefined });
|
set({ user: undefined });
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -21,7 +21,11 @@
|
|||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"erasableSyntaxOnly": true,
|
"erasableSyntaxOnly": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user