feat: начало вьюхи клерка, готовы три таба, ни один не работает нормально, спасибо автомапперу

This commit is contained in:
2025-05-21 17:43:16 +04:00
parent b59bdf9f3d
commit 57f878a051
29 changed files with 1804 additions and 104 deletions

View File

@@ -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);
} }

View File

@@ -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)

View File

@@ -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);

View File

@@ -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>

View File

@@ -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>

View File

@@ -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
View File

@@ -0,0 +1 @@
VITE_API_URL=https://localhost:7204

View File

@@ -0,0 +1,2 @@
node_modules
obj

View File

@@ -0,0 +1,7 @@
{
"semi": true,
"jsxSingleQuote": false,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all"
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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

View 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}
/>
);
};

View 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}
/>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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',
}, },
], ],
}, },

View File

@@ -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(() => {

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@@ -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 (

View 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>
);
};

View File

@@ -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 };
}; };

View File

@@ -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,
}; };
}; };

View File

@@ -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>,
) );

View File

@@ -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 });
}, },
}), }),

View File

@@ -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"]
} }