From 9ed33690cffa96d687d31f758a1c85178ded3b64 Mon Sep 17 00:00:00 2001
From: DjonniStorm
Date: Tue, 20 May 2025 00:00:45 +0400
Subject: [PATCH] =?UTF-8?q?feat:=20=D0=BF=D0=B5=D1=80=D0=B2=D0=B0=D1=8F=20?=
=?UTF-8?q?=D0=BF=D1=80=D0=BE=D1=81=D1=82=D0=B5=D0=BD=D1=8C=D0=BA=D0=B0?=
=?UTF-8?q?=D1=8F=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8F=20ui=20=D0=B4?=
=?UTF-8?q?=D0=BB=D1=8F=20=D0=BA=D0=BB=D0=B0=D0=B4=D0=BE=D0=B2=D1=89=D0=B8?=
=?UTF-8?q?=D0=BA=D0=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../DesignTimeDbContextFactory.cs | 14 +-
.../Models/CreditProgramCurrency.cs | 4 +-
.../BankWebApi/Adapters/StorekeeperAdapter.cs | 2 +-
.../Controllers/CreditProgramsController.cs | 2 -
.../Controllers/StorekeepersController.cs | 33 ++++
.../BankWebApi/Infrastructure/JwtProvider.cs | 2 +-
TheBank/bankui/bun.lockb | Bin 144949 -> 145411 bytes
TheBank/bankui/package.json | 3 +-
TheBank/bankui/src/App.tsx | 23 ++-
TheBank/bankui/src/api/api.ts | 18 +-
TheBank/bankui/src/api/client.ts | 29 ++-
.../components/features/CreditProgramForm.tsx | 82 +++-----
.../src/components/features/CurrencyForm.tsx | 172 +++++++++--------
.../src/components/features/PeriodForm.tsx | 160 +++++++++-------
.../src/components/features/ProfileForm.tsx | 180 ++++++++++++++++++
.../src/components/layout/DataTable.tsx | 14 +-
.../bankui/src/components/layout/Header.tsx | 41 ++--
.../src/components/pages/AuthStorekeeper.tsx | 53 ++++--
.../src/components/pages/Currencies.tsx | 73 +++++--
.../bankui/src/components/pages/Periods.tsx | 94 +++++++--
.../bankui/src/components/pages/Profile.tsx | 38 ++++
.../src/components/pages/Storekeepers.tsx | 6 +-
TheBank/bankui/src/hooks/useAuthCheck.ts | 38 ++++
TheBank/bankui/src/hooks/useStorekeepers.ts | 20 +-
TheBank/bankui/src/main.tsx | 17 +-
TheBank/bankui/src/store/workerStore.tsx | 36 ++++
26 files changed, 844 insertions(+), 310 deletions(-)
create mode 100644 TheBank/bankui/src/components/features/ProfileForm.tsx
create mode 100644 TheBank/bankui/src/components/pages/Profile.tsx
create mode 100644 TheBank/bankui/src/hooks/useAuthCheck.ts
create mode 100644 TheBank/bankui/src/store/workerStore.tsx
diff --git a/TheBank/BankDatabase/DesignTimeDbContextFactory.cs b/TheBank/BankDatabase/DesignTimeDbContextFactory.cs
index 2395aab..f3351f7 100644
--- a/TheBank/BankDatabase/DesignTimeDbContextFactory.cs
+++ b/TheBank/BankDatabase/DesignTimeDbContextFactory.cs
@@ -3,13 +3,13 @@ using Microsoft.EntityFrameworkCore.Design;
namespace BankDatabase;
-public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory
-{
- public BankDbContext CreateDbContext(string[] args)
- {
- return new BankDbContext(new ConfigurationDatabase());
- }
-}
+//public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory
+//{
+// //public BankDbContext CreateDbContext(string[] args)
+// //{
+// // return new BankDbContext(new ConfigurationDatabase());
+// //}
+//}
internal class ConfigurationDatabase : IConfigurationDatabase
{
diff --git a/TheBank/BankDatabase/Models/CreditProgramCurrency.cs b/TheBank/BankDatabase/Models/CreditProgramCurrency.cs
index c0f8a0f..f91465f 100644
--- a/TheBank/BankDatabase/Models/CreditProgramCurrency.cs
+++ b/TheBank/BankDatabase/Models/CreditProgramCurrency.cs
@@ -1,6 +1,4 @@
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace BankDatabase.Models;
+namespace BankDatabase.Models;
public class CreditProgramCurrency
{
diff --git a/TheBank/BankWebApi/Adapters/StorekeeperAdapter.cs b/TheBank/BankWebApi/Adapters/StorekeeperAdapter.cs
index 28f168d..305dd46 100644
--- a/TheBank/BankWebApi/Adapters/StorekeeperAdapter.cs
+++ b/TheBank/BankWebApi/Adapters/StorekeeperAdapter.cs
@@ -194,7 +194,7 @@ public class StorekeeperAdapter : IStorekeeperAdapter
token = _jwtProvider.GenerateToken(storekeeper);
- return StorekeeperOperationResponse.OK(token);
+ return StorekeeperOperationResponse.OK(_mapper.Map(storekeeper));
}
catch (Exception ex)
{
diff --git a/TheBank/BankWebApi/Controllers/CreditProgramsController.cs b/TheBank/BankWebApi/Controllers/CreditProgramsController.cs
index ad417ec..da7ca35 100644
--- a/TheBank/BankWebApi/Controllers/CreditProgramsController.cs
+++ b/TheBank/BankWebApi/Controllers/CreditProgramsController.cs
@@ -18,7 +18,6 @@ public class CreditProgramsController(ICreditProgramAdapter adapter) : Controlle
///
/// список кредитных программ
[HttpGet]
- [AllowAnonymous]
public IActionResult GetAllRecords()
{
return _adapter.GetList().GetResponse(Request, Response);
@@ -63,7 +62,6 @@ public class CreditProgramsController(ICreditProgramAdapter adapter) : Controlle
/// модель от пользователя
///
[HttpPost]
- [AllowAnonymous]
public IActionResult Register([FromBody] CreditProgramBindingModel model)
{
return _adapter.RegisterCreditProgram(model).GetResponse(Request, Response);
diff --git a/TheBank/BankWebApi/Controllers/StorekeepersController.cs b/TheBank/BankWebApi/Controllers/StorekeepersController.cs
index 2c12f7c..673b7b1 100644
--- a/TheBank/BankWebApi/Controllers/StorekeepersController.cs
+++ b/TheBank/BankWebApi/Controllers/StorekeepersController.cs
@@ -2,6 +2,7 @@
using BankContracts.BindingModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
+using System.Security.Claims;
namespace BankWebApi.Controllers;
@@ -67,6 +68,10 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
public IActionResult Login([FromBody] StorekeeperAuthBindingModel model)
{
var res = _adapter.Login(model, out string token);
+ if (string.IsNullOrEmpty(token))
+ {
+ return NotFound("User not found");
+ }
Response.Cookies.Append(AuthOptions.CookieName, token, new CookieOptions
{
@@ -78,4 +83,32 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
return res.GetResponse(Request, Response);
}
+
+ ///
+ /// Получение данных текущего кладовщика
+ ///
+ /// Данные кладовщика
+ [HttpGet("me")]
+ public IActionResult GetCurrentUser()
+ {
+ var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
+ if (string.IsNullOrEmpty(userId))
+ {
+ return Unauthorized();
+ }
+
+ var response = _adapter.GetElement(userId);
+ return response.GetResponse(Request, Response);
+ }
+
+ ///
+ /// Выход кладовщика
+ ///
+ ///
+ [HttpPost("logout")]
+ public IActionResult Logout()
+ {
+ Response.Cookies.Delete(AuthOptions.CookieName);
+ return Ok();
+ }
}
diff --git a/TheBank/BankWebApi/Infrastructure/JwtProvider.cs b/TheBank/BankWebApi/Infrastructure/JwtProvider.cs
index a95c1f2..3a8482e 100644
--- a/TheBank/BankWebApi/Infrastructure/JwtProvider.cs
+++ b/TheBank/BankWebApi/Infrastructure/JwtProvider.cs
@@ -12,7 +12,7 @@ public class JwtProvider : IJwtProvider
var token = new JwtSecurityToken(
issuer: AuthOptions.ISSUER,
audience: AuthOptions.AUDIENCE,
- claims: [new("id", dataModel.Id)],
+ claims: [new(ClaimTypes.NameIdentifier, dataModel.Id)],
expires: DateTime.UtcNow.Add(TimeSpan.FromDays(2)),
signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
diff --git a/TheBank/bankui/bun.lockb b/TheBank/bankui/bun.lockb
index 1b8f07138669cbe1fb488d8296bab1db9ec79ed3..aea057b387fbe8c950ae1c9072f3bc232ed2dc01 100644
GIT binary patch
delta 24439
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
delta 24389
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
yjXNivE9czbu>O1Mu4^tvylN{isK0*77PY~ store.user);
+ const { isLoading } = useAuthCheck();
+ const location = useLocation();
+
+ if (isLoading) {
+ return Loading...
;
+ }
+
+ if (!user) {
+ const redirect = encodeURIComponent(location.pathname + location.search);
+ return ;
+ }
+
return (
<>
- loading
}>
+ Loading...}>
diff --git a/TheBank/bankui/src/api/api.ts b/TheBank/bankui/src/api/api.ts
index 2fc7f55..ad97d2a 100644
--- a/TheBank/bankui/src/api/api.ts
+++ b/TheBank/bankui/src/api/api.ts
@@ -1,4 +1,10 @@
-import { getData, postData, putData } from './client';
+import {
+ getData,
+ getSingleData,
+ postData,
+ postLoginData,
+ putData,
+} from './client';
import type {
ClientBindingModel,
ClerkBindingModel,
@@ -115,7 +121,11 @@ export const storekeepersApi = {
getData(`api/Storekeepers/GetRecord/${id}`),
create: (data: StorekeeperBindingModel) =>
postData('api/Storekeepers/Register', data),
- update: (data: StorekeeperBindingModel) =>
- putData('api/Storekeepers/ChangeInfo', data),
- login: (data: LoginBindingModel) => postData('api/Storekeepers/login', data),
+ update: (data: StorekeeperBindingModel) => putData('api/Storekeepers', data),
+ // auth
+ login: (data: LoginBindingModel) =>
+ postLoginData('api/Storekeepers/login', data),
+ logout: () => postData('api/storekeepers/logout', {}),
+ getCurrentUser: () =>
+ getSingleData('api/storekeepers/me'),
};
diff --git a/TheBank/bankui/src/api/client.ts b/TheBank/bankui/src/api/client.ts
index 1584568..031a3c1 100644
--- a/TheBank/bankui/src/api/client.ts
+++ b/TheBank/bankui/src/api/client.ts
@@ -28,12 +28,39 @@ export async function postData(path: string, data: T) {
}
}
+export async function getSingleData(path: string): Promise {
+ const res = await fetch(`${API_URL}/${path}`, {
+ credentials: 'include',
+ });
+ if (!res.ok) {
+ throw new Error(`Не получается загрузить ${path}: ${res.statusText}`);
+ }
+ const data = (await res.json()) as T;
+ return data;
+}
+
+export async function postLoginData(path: string, data: T): Promise {
+ const res = await fetch(`${API_URL}/${path}`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ credentials: 'include',
+ body: JSON.stringify(data),
+ });
+ if (!res.ok) {
+ throw new Error(`Не получается загрузить ${path}: ${await res.text()}`);
+ }
+
+ const userData = (await res.json()) as T;
+ return userData;
+}
+
export async function putData(path: string, data: T) {
const res = await fetch(`${API_URL}/${path}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
- // mode: 'no-cors',
},
credentials: 'include',
body: JSON.stringify(data),
diff --git a/TheBank/bankui/src/components/features/CreditProgramForm.tsx b/TheBank/bankui/src/components/features/CreditProgramForm.tsx
index f101a2f..14783a3 100644
--- a/TheBank/bankui/src/components/features/CreditProgramForm.tsx
+++ b/TheBank/bankui/src/components/features/CreditProgramForm.tsx
@@ -19,26 +19,10 @@ import {
SelectValue,
} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
-import type {
- CreditProgramBindingModel,
- CurrencyBindingModel,
-} from '@/types/types';
-
-const storekeepers: { id: string; name: string }[] = [
- { id: crypto.randomUUID(), name: 'Кладовщик 1' },
- { id: crypto.randomUUID(), name: 'Кладовщик 2' },
-];
-
-const periods: { id: string; name: string }[] = [
- { id: crypto.randomUUID(), name: 'Период 1' },
- { id: crypto.randomUUID(), name: 'Период 2' },
-];
-
-const currencies: CurrencyBindingModel[] = [
- { id: crypto.randomUUID(), name: 'Доллар США', abbreviation: 'USD', cost: 1 },
- { id: crypto.randomUUID(), name: 'Евро', abbreviation: 'EUR', cost: 1.2 },
- { id: crypto.randomUUID(), name: 'Рубль', abbreviation: 'RUB', cost: 0.01 },
-];
+import type { CreditProgramBindingModel } from '@/types/types';
+import { useAuthStore } from '@/store/workerStore';
+import { usePeriods } from '@/hooks/usePeriods';
+import { useCurrencies } from '@/hooks/useCurrencies';
const formSchema = z.object({
id: z.string().optional(),
@@ -47,7 +31,6 @@ const formSchema = z.object({
maxCost: z.coerce
.number()
.min(0, 'Максимальная стоимость не может быть отрицательной'),
- storekeeperId: z.string().min(1, 'Выберите кладовщика'),
periodId: z.string().min(1, 'Выберите период'),
currencyCreditPrograms: z
.array(z.string())
@@ -70,12 +53,16 @@ export const CreditProgramForm = ({
name: '',
cost: 0,
maxCost: 0,
- storekeeperId: '',
periodId: '',
currencyCreditPrograms: [],
},
});
+ const { periods } = usePeriods();
+ const { currencies } = useCurrencies();
+
+ const storekeeper = useAuthStore((store) => store.user);
+
const handleSubmit = (data: FormValues) => {
const dataWithId = {
...data,
@@ -86,6 +73,7 @@ export const CreditProgramForm = ({
currencyCreditPrograms: data.currencyCreditPrograms.map((currencyId) => ({
currencyId,
})),
+ storekeeperId: storekeeper?.id,
};
onSubmit(payload);
@@ -145,30 +133,6 @@ export const CreditProgramForm = ({
)}
/>
- (
-
- Кладовщик
-
-
-
- )}
- />
- {periods.map((period) => (
-
- {period.name}
-
- ))}
+ {periods &&
+ periods.map((period) => (
+
+ {`${new Date(
+ period.startTime,
+ ).toLocaleDateString()} - ${new Date(
+ period.endTime,
+ ).toLocaleDateString()}`}
+
+ ))}
@@ -212,11 +181,12 @@ export const CreditProgramForm = ({
}}
className="w-full border rounded-md p-2 h-24"
>
- {currencies.map((currency) => (
-
- ))}
+ {currencies &&
+ currencies.map((currency) => (
+
+ ))}
diff --git a/TheBank/bankui/src/components/features/CurrencyForm.tsx b/TheBank/bankui/src/components/features/CurrencyForm.tsx
index 0454695..9a4a855 100644
--- a/TheBank/bankui/src/components/features/CurrencyForm.tsx
+++ b/TheBank/bankui/src/components/features/CurrencyForm.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -11,55 +11,88 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import type { CurrencyBindingModel } from '@/types/types';
-import { useStorekeepers } from '@/hooks/useStorekeepers'; // Импорт хука для кладовщиков
+import { useAuthStore } from '@/store/workerStore';
-const formSchema = z.object({
- id: z.string().optional(),
- name: z.string().min(1, 'Укажите название валюты'),
- abbreviation: z.string().min(1, 'Укажите аббревиатуру'),
- cost: z.coerce.number().min(0, 'Стоимость не может быть отрицательной'),
- storekeeperId: z.string().min(1, 'Выберите кладовщика'),
-});
-
-type FormValues = z.infer;
-
-type CurrencyFormProps = {
- onSubmit: (data: CurrencyBindingModel) => void;
+type BaseFormValues = {
+ id?: string;
+ name: string;
+ abbreviation: string;
+ cost: number;
};
-export const CurrencyForm = ({
+type EditFormValues = {
+ id?: string;
+ name?: string;
+ abbreviation?: string;
+ cost?: number;
+};
+
+const baseSchema = z.object({
+ id: z.string().optional(),
+ name: z.string({ message: 'Введите название' }),
+ abbreviation: z.string({ message: 'Введите аббревиатуру' }),
+ cost: z.coerce.number({ message: 'Введите стоимость' }),
+});
+
+const addSchema = baseSchema;
+
+const editSchema = z.object({
+ id: z.string().optional(),
+ name: z.string().min(1, 'Укажите название валюты').optional(),
+ abbreviation: z.string().min(1, 'Укажите аббревиатуру').optional(),
+ cost: z.coerce
+ .number()
+ .min(0, 'Стоимость не может быть отрицательной')
+ .optional(),
+});
+
+interface BaseCurrencyFormProps {
+ onSubmit: (data: CurrencyBindingModel) => void;
+ schema: z.ZodType;
+ defaultValues?: Partial;
+}
+
+const BaseCurrencyForm = ({
onSubmit,
-}: CurrencyFormProps): React.JSX.Element => {
- const form = useForm({
- resolver: zodResolver(formSchema),
+ schema,
+ defaultValues,
+}: BaseCurrencyFormProps): React.JSX.Element => {
+ const form = useForm({
+ resolver: zodResolver(schema),
defaultValues: {
- id: '',
- name: '',
- abbreviation: '',
- cost: 0,
- storekeeperId: '',
+ id: defaultValues?.id || '',
+ name: defaultValues?.name || '',
+ abbreviation: defaultValues?.abbreviation || '',
+ cost: defaultValues?.cost || 0,
},
});
- const {
- storekeepers,
- isLoading: isLoadingStorekeepers,
- error: storekeepersError,
- } = useStorekeepers(); // Получаем данные кладовщиков
+ useEffect(() => {
+ if (defaultValues) {
+ form.reset({
+ id: defaultValues.id || '',
+ name: defaultValues.name || '',
+ abbreviation: defaultValues.abbreviation || '',
+ cost: defaultValues.cost || 0,
+ });
+ }
+ }, [defaultValues, form]);
- const handleSubmit = (data: FormValues) => {
+ const storekeeper = useAuthStore((store) => store.user);
+
+ const handleSubmit = (data: BaseFormValues | EditFormValues) => {
+ // Если это форма редактирования, используем только заполненные поля
const payload: CurrencyBindingModel = {
- ...data,
id: data.id || crypto.randomUUID(),
+ storekeeperId: storekeeper?.id,
+ name: 'name' in data && data.name !== undefined ? data.name : '',
+ abbreviation:
+ 'abbreviation' in data && data.abbreviation !== undefined
+ ? data.abbreviation
+ : '',
+ cost: 'cost' in data && data.cost !== undefined ? data.cost : 0,
};
onSubmit(payload);
@@ -115,47 +148,6 @@ export const CurrencyForm = ({
)}
/>
- (
-
- Кладовщик
-
- {storekeepersError && (
- {storekeepersError.message}
- )}{' '}
- {/* Отображаем ошибку под полем */}
-
-
- )}
- />
@@ -163,3 +155,27 @@ export const CurrencyForm = ({
);
};
+
+export const CurrencyFormAdd = ({
+ onSubmit,
+}: {
+ onSubmit: (data: CurrencyBindingModel) => void;
+}): React.JSX.Element => {
+ return ;
+};
+
+export const CurrencyFormEdit = ({
+ onSubmit,
+ defaultValues,
+}: {
+ onSubmit: (data: CurrencyBindingModel) => void;
+ defaultValues: Partial;
+}): React.JSX.Element => {
+ return (
+
+ );
+};
diff --git a/TheBank/bankui/src/components/features/PeriodForm.tsx b/TheBank/bankui/src/components/features/PeriodForm.tsx
index e8bdf24..ec088fa 100644
--- a/TheBank/bankui/src/components/features/PeriodForm.tsx
+++ b/TheBank/bankui/src/components/features/PeriodForm.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -10,13 +10,6 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from '@/components/ui/select';
import { Button } from '@/components/ui/button';
import type { PeriodBindingModel } from '@/types/types';
import { Calendar } from '@/components/ui/calendar';
@@ -28,9 +21,21 @@ import {
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
-import { useStorekeepers } from '@/hooks/useStorekeepers';
+import { useAuthStore } from '@/store/workerStore';
-const formSchema = z.object({
+type BaseFormValues = {
+ id?: string;
+ startTime: Date;
+ endTime: Date;
+};
+
+type EditFormValues = {
+ id?: string;
+ startTime?: Date;
+ endTime?: Date;
+};
+
+const baseSchema = z.object({
id: z.string().optional(),
startTime: z.date({
required_error: 'Укажите время начала',
@@ -40,38 +45,70 @@ const formSchema = z.object({
required_error: 'Укажите время окончания',
invalid_type_error: 'Неверный формат даты',
}),
- storekeeperId: z.string().min(1, 'Выберите кладовщика'),
});
-type FormValues = z.infer;
+const addSchema = baseSchema;
-type PeriodFormProps = {
+const editSchema = z.object({
+ id: z.string().optional(),
+ startTime: z
+ .date({
+ required_error: 'Укажите время начала',
+ invalid_type_error: 'Неверный формат даты',
+ })
+ .optional(),
+ endTime: z
+ .date({
+ required_error: 'Укажите время окончания',
+ invalid_type_error: 'Неверный формат даты',
+ })
+ .optional(),
+});
+
+interface BasePeriodFormProps {
onSubmit: (data: PeriodBindingModel) => void;
-};
+ schema: z.ZodType;
+ defaultValues?: Partial;
+}
-export const PeriodForm = ({
+const BasePeriodForm = ({
onSubmit,
-}: PeriodFormProps): React.JSX.Element => {
- const form = useForm({
- resolver: zodResolver(formSchema),
+ schema,
+ defaultValues,
+}: BasePeriodFormProps): React.JSX.Element => {
+ const form = useForm({
+ resolver: zodResolver(schema),
defaultValues: {
- id: '',
- storekeeperId: '',
+ id: defaultValues?.id || '',
+ startTime: defaultValues?.startTime || new Date(),
+ endTime: defaultValues?.endTime || new Date(),
},
});
- const {
- storekeepers,
- isLoading: isLoadingStorekeepers,
- error: storekeepersError,
- } = useStorekeepers();
+ useEffect(() => {
+ if (defaultValues) {
+ form.reset({
+ id: defaultValues.id || '',
+ startTime: defaultValues.startTime || new Date(),
+ endTime: defaultValues.endTime || new Date(),
+ });
+ }
+ }, [defaultValues, form]);
- const handleSubmit = (data: FormValues) => {
+ const storekeeper = useAuthStore((store) => store.user);
+
+ const handleSubmit = (data: BaseFormValues | EditFormValues) => {
const payload: PeriodBindingModel = {
- ...data,
id: data.id || crypto.randomUUID(),
- startTime: data.startTime,
- endTime: data.endTime,
+ storekeeperId: storekeeper?.id,
+ startTime:
+ 'startTime' in data && data.startTime !== undefined
+ ? data.startTime
+ : new Date(),
+ endTime:
+ 'endTime' in data && data.endTime !== undefined
+ ? data.endTime
+ : new Date(),
};
onSubmit(payload);
@@ -164,46 +201,7 @@ export const PeriodForm = ({
)}
/>
- (
-
- Кладовщик
-
- {storekeepersError && (
- {storekeepersError.message}
- )}
-
-
- )}
- />
+
@@ -211,3 +209,27 @@ export const PeriodForm = ({
);
};
+
+export const PeriodFormAdd = ({
+ onSubmit,
+}: {
+ onSubmit: (data: PeriodBindingModel) => void;
+}): React.JSX.Element => {
+ return ;
+};
+
+export const PeriodFormEdit = ({
+ onSubmit,
+ defaultValues,
+}: {
+ onSubmit: (data: PeriodBindingModel) => void;
+ defaultValues: Partial;
+}): React.JSX.Element => {
+ return (
+
+ );
+};
diff --git a/TheBank/bankui/src/components/features/ProfileForm.tsx b/TheBank/bankui/src/components/features/ProfileForm.tsx
new file mode 100644
index 0000000..f3193a9
--- /dev/null
+++ b/TheBank/bankui/src/components/features/ProfileForm.tsx
@@ -0,0 +1,180 @@
+import React, { useEffect } 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 { StorekeeperBindingModel } from '@/types/types';
+
+// Схема для редактирования профиля (все поля опциональны)
+const profileEditSchema = z.object({
+ id: z.string().optional(),
+ name: z.string().optional(),
+ surname: z.string().optional(),
+ middleName: z.string().optional(),
+ login: z.string().optional(),
+ email: z.string().email('Неверный формат email').optional(),
+ phoneNumber: z.string().optional(),
+ // Пароль, вероятно, должен редактироваться отдельно, но добавим опционально
+ password: z.string().optional(),
+});
+
+type ProfileFormValues = z.infer;
+
+interface ProfileFormProps {
+ onSubmit: (data: Partial) => void;
+ defaultValues: ProfileFormValues;
+}
+
+export const ProfileForm = ({
+ onSubmit,
+ defaultValues,
+}: ProfileFormProps): React.JSX.Element => {
+ const form = useForm({
+ resolver: zodResolver(profileEditSchema),
+ defaultValues: defaultValues,
+ });
+
+ useEffect(() => {
+ if (defaultValues) {
+ form.reset(defaultValues);
+ }
+ }, [defaultValues, form]);
+
+ const handleSubmit = (data: ProfileFormValues) => {
+ const changedData: ProfileFormValues = {};
+ (Object.keys(data) as (keyof ProfileFormValues)[]).forEach((key) => {
+ changedData[key] = data[key];
+ if (data[key] !== defaultValues[key]) {
+ changedData[key] = data[key];
+ }
+ });
+
+ if (defaultValues.id) {
+ changedData.id = defaultValues.id;
+ }
+
+ onSubmit(changedData);
+ };
+
+ return (
+
+
+ );
+};
diff --git a/TheBank/bankui/src/components/layout/DataTable.tsx b/TheBank/bankui/src/components/layout/DataTable.tsx
index 32b8700..7b31f3e 100644
--- a/TheBank/bankui/src/components/layout/DataTable.tsx
+++ b/TheBank/bankui/src/components/layout/DataTable.tsx
@@ -12,6 +12,8 @@ import { Checkbox } from '../ui/checkbox';
type DataTableProps = {
data: T[];
columns: ColumnDef[];
+ selectedRow?: string;
+ onRowSelected: (id: string | undefined) => void;
};
export type ColumnDef = {
@@ -23,13 +25,11 @@ export type ColumnDef = {
export const DataTable = ({
data,
columns,
+ selectedRow,
+ onRowSelected,
}: DataTableProps): React.JSX.Element => {
- const [selectedRowId, setSelectedRowId] = React.useState<
- string | undefined
- >();
-
const handleRowSelect = (id: string) => {
- setSelectedRowId((prev) => (prev === id ? undefined : id));
+ onRowSelected(selectedRow === id ? undefined : id);
};
return (
@@ -59,12 +59,12 @@ export const DataTable =
({
handleRowSelect((item as any).id)}
aria-label="Select row"
/>
diff --git a/TheBank/bankui/src/components/layout/Header.tsx b/TheBank/bankui/src/components/layout/Header.tsx
index 23bd142..7be471c 100644
--- a/TheBank/bankui/src/components/layout/Header.tsx
+++ b/TheBank/bankui/src/components/layout/Header.tsx
@@ -17,6 +17,8 @@ import {
} from '../ui/dropdown-menu';
import { Avatar, AvatarFallback } from '../ui/avatar';
import { Button } from '../ui/button';
+import { useAuthStore } from '@/store/workerStore';
+import { useStorekeepers } from '@/hooks/useStorekeepers';
type NavOptionValue = {
name: string;
@@ -38,11 +40,6 @@ const navOptions = [
name: 'Просмотреть',
link: '/currencies',
},
- {
- id: 2,
- name: 'Создать',
- link: '',
- },
],
},
{
@@ -53,11 +50,6 @@ const navOptions = [
name: 'Просмотреть',
link: '/credit-programs',
},
- {
- id: 2,
- name: 'Создать',
- link: '',
- },
],
},
{
@@ -68,11 +60,6 @@ const navOptions = [
name: 'Просмотреть',
link: '/periods',
},
- {
- id: 2,
- name: 'Создать',
- link: '',
- },
],
},
{
@@ -88,6 +75,14 @@ const navOptions = [
];
export const Header = (): React.JSX.Element => {
+ const user = useAuthStore((store) => store.user);
+ const logout = useAuthStore((store) => store.logout);
+ const { logout: serverLogout } = useAuthStore();
+ const loggedOut = () => {
+ serverLogout();
+ logout();
+ };
+ const fullName = `${user?.name ?? ''} ${user?.surname ?? ''}`;
return (
);
@@ -127,9 +122,13 @@ const MenuOption = ({ item }: { item: NavOption }) => {
type ProfileIconProps = {
name: string;
+ logout: () => void;
};
-export const ProfileIcon = ({ name }: ProfileIconProps): React.JSX.Element => {
+export const ProfileIcon = ({
+ name,
+ logout,
+}: ProfileIconProps): React.JSX.Element => {
return (
@@ -144,13 +143,17 @@ export const ProfileIcon = ({ name }: ProfileIconProps): React.JSX.Element => {
-
+
Профиль
-
diff --git a/TheBank/bankui/src/components/pages/AuthStorekeeper.tsx b/TheBank/bankui/src/components/pages/AuthStorekeeper.tsx
index 1406ac9..b2b9b81 100644
--- a/TheBank/bankui/src/components/pages/AuthStorekeeper.tsx
+++ b/TheBank/bankui/src/components/pages/AuthStorekeeper.tsx
@@ -1,43 +1,47 @@
import { useStorekeepers } from '@/hooks/useStorekeepers';
import React from 'react';
-import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs';
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '../ui/tabs';
import { RegisterForm } from '../features/RegisterForm';
-import type { LoginBindingModel, StorekeeperBindingModel } from '@/types/types';
import { LoginForm } from '../features/LoginForm';
-import { Toaster } from '../ui/sonner';
import { toast } from 'sonner';
-import { useNavigate } from 'react-router-dom';
+import type { LoginBindingModel, StorekeeperBindingModel } from '@/types/types';
+
+type Forms = 'login' | 'register';
export const AuthStorekeeper = (): React.JSX.Element => {
- const navigate = useNavigate();
const {
createStorekeeper,
loginStorekeeper,
isLoginError,
loginError,
- isLoginSuccess,
+ isCreateError,
} = useStorekeepers();
+ const [currentForm, setCurrentForm] = React.useState('login');
+
const handleRegister = (data: StorekeeperBindingModel) => {
- console.log(data);
- createStorekeeper(data);
+ createStorekeeper(data, {
+ onSuccess: () => {
+ toast('Регистрация успешна! Войдите в систему.');
+ },
+ onError: (error) => {
+ toast(`Ошибка регистрации: ${error.message}`);
+ },
+ });
};
+
const handleLogin = (data: LoginBindingModel) => {
- console.log(data);
loginStorekeeper(data);
};
React.useEffect(() => {
if (isLoginError) {
- toast(`Ошибка ${loginError?.message}`);
+ toast(`Ошибка входа: ${loginError?.message}`);
}
- }, [isLoginError, loginError]);
-
- React.useEffect(() => {
- if (isLoginSuccess) {
- navigate('/storekeepers');
+ if (isCreateError) {
+ toast('Ошибка при регистрации');
}
- }, [isLoginSuccess]);
+ }, [isLoginError, loginError, isCreateError]);
return (
<>
@@ -45,10 +49,20 @@ export const AuthStorekeeper = (): React.JSX.Element => {
- Вход
- Регистрация
+ setCurrentForm('login')}
+ value="login"
+ >
+ Вход
+
+ setCurrentForm('register')}
+ value="register"
+ >
+ Регистрация
+
-
+
@@ -57,7 +71,6 @@ export const AuthStorekeeper = (): React.JSX.Element => {
-
>
);
};
diff --git a/TheBank/bankui/src/components/pages/Currencies.tsx b/TheBank/bankui/src/components/pages/Currencies.tsx
index 7f856c8..9c91a7a 100644
--- a/TheBank/bankui/src/components/pages/Currencies.tsx
+++ b/TheBank/bankui/src/components/pages/Currencies.tsx
@@ -4,12 +4,10 @@ import { DialogForm } from '../layout/DialogForm';
import { DataTable } from '../layout/DataTable';
import { useCurrencies } from '@/hooks/useCurrencies';
import { useStorekeepers } from '@/hooks/useStorekeepers';
-import type {
- CurrencyBindingModel,
- StorekeeperBindingModel,
-} from '@/types/types';
+import type { CurrencyBindingModel } from '@/types/types';
import type { ColumnDef } from '../layout/DataTable';
-import { CurrencyForm } from '../features/CurrencyForm';
+import { CurrencyFormAdd, CurrencyFormEdit } from '../features/CurrencyForm';
+import { toast } from 'sonner';
interface CurrencyTableData extends CurrencyBindingModel {
storekeeperName: string;
@@ -69,11 +67,41 @@ export const Currencies = (): React.JSX.Element => {
});
}, [currencies, storekeepers]);
- const [isDialogOpen, setIsDialogOpen] = React.useState(false);
+ const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false);
+ const [isEditDialogOpen, setIsEditDialogOpen] =
+ React.useState(false);
+ const [selectedItem, setSelectedItem] = React.useState<
+ CurrencyBindingModel | undefined
+ >();
const handleAdd = (data: CurrencyBindingModel) => {
- console.log(data);
createCurrency(data);
+ setIsAddDialogOpen(false);
+ };
+
+ const handleEdit = (data: CurrencyBindingModel) => {
+ if (selectedItem) {
+ updateCurrency({
+ ...selectedItem,
+ ...data,
+ });
+ setIsEditDialogOpen(false);
+ setSelectedItem(undefined);
+ }
+ };
+
+ const handleSelectItem = (id: string | undefined) => {
+ const item = currencies?.find((c) => c.id === id);
+ setSelectedItem(item);
+ };
+
+ const openEditForm = () => {
+ if (!selectedItem) {
+ toast('Выберите элемент для редактирования');
+ return;
+ }
+
+ setIsEditDialogOpen(true);
};
if (isLoading) {
@@ -92,22 +120,39 @@ export const Currencies = (): React.JSX.Element => {
{
- setIsDialogOpen(true);
+ setIsAddDialogOpen(true);
+ }}
+ onEditClick={() => {
+ openEditForm();
}}
- onEditClick={() => {}}
/>
title="Форма валюты"
description="Добавьте новую валюту"
- isOpen={isDialogOpen}
- onClose={() => setIsDialogOpen(false)}
- onSubmit={handleAdd}
+ isOpen={isAddDialogOpen}
+ onClose={() => setIsAddDialogOpen(false)}
>
-
+
+ {selectedItem && (
+
+ title="Форма валюты"
+ description="Измените валюту"
+ isOpen={isEditDialogOpen}
+ onClose={() => setIsEditDialogOpen(false)}
+ onSubmit={handleEdit}
+ >
+
+
+ )}
-
+ handleSelectItem(id)}
+ selectedRow={selectedItem?.id}
+ />
diff --git a/TheBank/bankui/src/components/pages/Periods.tsx b/TheBank/bankui/src/components/pages/Periods.tsx
index 31cc950..f12d030 100644
--- a/TheBank/bankui/src/components/pages/Periods.tsx
+++ b/TheBank/bankui/src/components/pages/Periods.tsx
@@ -4,19 +4,15 @@ import { DialogForm } from '../layout/DialogForm';
import { DataTable } from '../layout/DataTable';
import { usePeriods } from '@/hooks/usePeriods';
import { useStorekeepers } from '@/hooks/useStorekeepers';
-import type {
- PeriodBindingModel,
- StorekeeperBindingModel,
-} from '@/types/types';
+import type { PeriodBindingModel } from '@/types/types';
import type { ColumnDef } from '../layout/DataTable';
-import { PeriodForm } from '../features/PeriodForm';
+import { PeriodFormAdd, PeriodFormEdit } from '../features/PeriodForm';
+import { toast } from 'sonner';
-// Определяем расширенный тип для данных таблицы
interface PeriodTableData extends PeriodBindingModel {
storekeeperName: string;
}
-// Определяем столбцы
const columns: ColumnDef[] = [
{
accessorKey: 'id',
@@ -25,10 +21,12 @@ const columns: ColumnDef[] = [
{
accessorKey: 'startTime',
header: 'Время начала',
+ renderCell: (item) => new Date(item.startTime).toLocaleDateString(),
},
{
accessorKey: 'endTime',
header: 'Время окончания',
+ renderCell: (item) => new Date(item.endTime).toLocaleDateString(),
},
{
accessorKey: 'storekeeperName',
@@ -37,7 +35,8 @@ const columns: ColumnDef[] = [
];
export const Periods = (): React.JSX.Element => {
- const { isLoading, isError, error, periods, createPeriod } = usePeriods();
+ const { isLoading, isError, error, periods, createPeriod, updatePeriod } =
+ usePeriods();
const { storekeepers } = useStorekeepers();
const finalData = React.useMemo(() => {
@@ -60,11 +59,49 @@ export const Periods = (): React.JSX.Element => {
});
}, [periods, storekeepers]);
- const [isDialogOpen, setIsDialogOpen] = React.useState(false);
+ const [isAddDialogOpen, setIsAddDialogOpen] = React.useState(false);
+ const [isEditDialogOpen, setIsEditDialogOpen] =
+ React.useState(false);
+ const [selectedItem, setSelectedItem] = React.useState<
+ PeriodBindingModel | undefined
+ >();
const handleAdd = (data: PeriodBindingModel) => {
- console.log(data);
createPeriod(data);
+ setIsAddDialogOpen(false);
+ };
+
+ const handleEdit = (data: PeriodBindingModel) => {
+ if (selectedItem) {
+ updatePeriod({
+ ...selectedItem,
+ ...data,
+ });
+ setIsEditDialogOpen(false);
+ setSelectedItem(undefined);
+ }
+ };
+
+ const handleSelectItem = (id: string | undefined) => {
+ const item = periods?.find((p) => p.id === id);
+ if (item) {
+ setSelectedItem({
+ ...item,
+ startTime: new Date(item.startTime),
+ endTime: new Date(item.endTime),
+ });
+ } else {
+ setSelectedItem(undefined);
+ }
+ };
+
+ const openEditForm = () => {
+ if (!selectedItem) {
+ toast('Выберите элемент для редактирования');
+ return;
+ }
+
+ setIsEditDialogOpen(true);
};
if (isLoading) {
@@ -83,22 +120,43 @@ export const Periods = (): React.JSX.Element => {
{
- setIsDialogOpen(true);
+ setIsAddDialogOpen(true);
+ }}
+ onEditClick={() => {
+ openEditForm();
}}
- onEditClick={() => {}}
/>
- title="Форма"
- description="Описание"
- isOpen={isDialogOpen}
- onClose={() => setIsDialogOpen(false)}
+ title="Форма сроков"
+ description="Добавить сроки"
+ isOpen={isAddDialogOpen}
+ onClose={() => setIsAddDialogOpen(false)}
onSubmit={handleAdd}
>
-
+
+ {selectedItem && (
+
+ title="Форма сроков"
+ description="Изменить сроки"
+ isOpen={isEditDialogOpen}
+ onClose={() => setIsEditDialogOpen(false)}
+ onSubmit={handleEdit}
+ >
+
+
+ )}
-
+ handleSelectItem(id)}
+ selectedRow={selectedItem?.id}
+ />
diff --git a/TheBank/bankui/src/components/pages/Profile.tsx b/TheBank/bankui/src/components/pages/Profile.tsx
new file mode 100644
index 0000000..a7a0fa4
--- /dev/null
+++ b/TheBank/bankui/src/components/pages/Profile.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import { useAuthStore } from '@/store/workerStore';
+import { ProfileForm } from '../features/ProfileForm';
+import type { StorekeeperBindingModel } from '@/types/types';
+import { useStorekeepers } from '@/hooks/useStorekeepers';
+import { toast } from 'sonner';
+
+export const Profile = (): React.JSX.Element => {
+ const { user, updateUser } = useAuthStore();
+ const { updateStorekeeper, isUpdateError, updateError } = useStorekeepers();
+
+ React.useEffect(() => {
+ if (isUpdateError) {
+ toast(updateError?.message);
+ }
+ }, [isUpdateError, updateError]);
+
+ if (!user) {
+ return (
+
+ Загрузка данных пользователя...
+
+ );
+ }
+
+ const handleUpdate = (data: Partial) => {
+ console.log(data);
+ updateUser(data);
+ updateStorekeeper(data);
+ };
+
+ return (
+
+ Профиль пользователя
+
+
+ );
+};
diff --git a/TheBank/bankui/src/components/pages/Storekeepers.tsx b/TheBank/bankui/src/components/pages/Storekeepers.tsx
index a21dc31..a3ef139 100644
--- a/TheBank/bankui/src/components/pages/Storekeepers.tsx
+++ b/TheBank/bankui/src/components/pages/Storekeepers.tsx
@@ -53,7 +53,11 @@ export const Storekeepers = (): React.JSX.Element => {
return (
Кладовщики
-
+
);
};
diff --git a/TheBank/bankui/src/hooks/useAuthCheck.ts b/TheBank/bankui/src/hooks/useAuthCheck.ts
new file mode 100644
index 0000000..120fd16
--- /dev/null
+++ b/TheBank/bankui/src/hooks/useAuthCheck.ts
@@ -0,0 +1,38 @@
+import { useQuery } from '@tanstack/react-query';
+import { useAuthStore } from '@/store/workerStore';
+import { storekeepersApi } from '@/api/api';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { type StorekeeperBindingModel } from '@/types/types';
+import { useEffect } from 'react';
+
+export const useAuthCheck = () => {
+ const setAuth = useAuthStore((store) => store.setAuth);
+ const logout = useAuthStore((store) => store.logout);
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const {
+ isPending: isLoading,
+ error,
+ isError,
+ } = useQuery({
+ queryKey: ['authCheck'],
+ queryFn: async () => {
+ const userData = await storekeepersApi.getCurrentUser();
+ setAuth(userData);
+ return userData;
+ },
+ retry: false,
+ });
+
+ useEffect(() => {
+ if (isError) {
+ console.error('Auth check failed:', error?.message);
+ logout();
+ const redirect = encodeURIComponent(location.pathname + location.search);
+ navigate(`/auth?redirect=${redirect}`);
+ }
+ }, [isError, error, logout, navigate, location]);
+
+ return { isLoading, error };
+};
diff --git a/TheBank/bankui/src/hooks/useStorekeepers.ts b/TheBank/bankui/src/hooks/useStorekeepers.ts
index 08a5177..11c7018 100644
--- a/TheBank/bankui/src/hooks/useStorekeepers.ts
+++ b/TheBank/bankui/src/hooks/useStorekeepers.ts
@@ -1,8 +1,13 @@
import { storekeepersApi } 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 useStorekeepers = () => {
const queryClient = useQueryClient();
+ const setAuth = useAuthStore((store) => store.setAuth);
+ const navigate = useNavigate();
+ const location = useLocation();
const {
data: storekeepers,
@@ -21,7 +26,11 @@ export const useStorekeepers = () => {
},
});
- const { mutate: updateStorekeeper, isError: isUpdateError } = useMutation({
+ const {
+ mutate: updateStorekeeper,
+ isError: isUpdateError,
+ error: updateError,
+ } = useMutation({
mutationFn: storekeepersApi.update,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['storekeepers'] });
@@ -35,8 +44,12 @@ export const useStorekeepers = () => {
error: loginError,
} = useMutation({
mutationFn: storekeepersApi.login,
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['storekeepers'] });
+ onSuccess: (userData) => {
+ setAuth(userData);
+ const params = new URLSearchParams(location.search);
+ const redirect = params.get('redirect') || '/storekeepers';
+ navigate(redirect);
+ queryClient.invalidateQueries({ queryKey: ['storekeeper'] });
},
});
@@ -49,6 +62,7 @@ export const useStorekeepers = () => {
isUpdateError,
isLoginSuccess,
loginError,
+ updateError,
error,
createStorekeeper,
loginStorekeeper,
diff --git a/TheBank/bankui/src/main.tsx b/TheBank/bankui/src/main.tsx
index 888cf93..89e998e 100644
--- a/TheBank/bankui/src/main.tsx
+++ b/TheBank/bankui/src/main.tsx
@@ -2,13 +2,19 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
-import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+import {
+ createBrowserRouter,
+ Navigate,
+ RouterProvider,
+} from 'react-router-dom';
import { Currencies } from './components/pages/Currencies.tsx';
import { CreditPrograms } from './components/pages/CreditPrograms.tsx';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthStorekeeper } from './components/pages/AuthStorekeeper.tsx';
import { Storekeepers } from './components/pages/Storekeepers.tsx';
import { Periods } from './components/pages/Periods.tsx';
+import { Toaster } from './components/ui/sonner.tsx';
+import { Profile } from './components/pages/Profile.tsx';
const routes = createBrowserRouter([
{
@@ -31,6 +37,10 @@ const routes = createBrowserRouter([
path: '/periods',
element: ,
},
+ {
+ path: '/profile',
+ element: ,
+ },
],
errorElement: бля пизда рулям
,
},
@@ -38,6 +48,10 @@ const routes = createBrowserRouter([
path: '/auth',
element: ,
},
+ {
+ path: '*',
+ element: ,
+ },
]);
const queryClient = new QueryClient();
@@ -46,6 +60,7 @@ createRoot(document.getElementById('root')!).render(
+
,
);
diff --git a/TheBank/bankui/src/store/workerStore.tsx b/TheBank/bankui/src/store/workerStore.tsx
new file mode 100644
index 0000000..eafad0d
--- /dev/null
+++ b/TheBank/bankui/src/store/workerStore.tsx
@@ -0,0 +1,36 @@
+import { storekeepersApi } from '@/api/api';
+import type { StorekeeperBindingModel } from '@/types/types';
+import { create } from 'zustand';
+import { persist } from 'zustand/middleware';
+
+type AuthState = {
+ user?: StorekeeperBindingModel;
+ setAuth: (user: StorekeeperBindingModel) => void;
+ updateUser: (user: StorekeeperBindingModel) => void;
+ getUser: () => StorekeeperBindingModel | undefined;
+ logout: () => Promise;
+};
+
+export const useAuthStore = create()(
+ persist(
+ (set, get) => ({
+ user: undefined,
+ setAuth: (user: StorekeeperBindingModel) => {
+ set({ user: user });
+ },
+ updateUser: (user: StorekeeperBindingModel) => {
+ set({ user: user });
+ },
+ getUser: () => {
+ return get().user;
+ },
+ logout: async () => {
+ await storekeepersApi.logout();
+ set({ user: undefined });
+ },
+ }),
+ {
+ name: 'auth-storage',
+ },
+ ),
+);