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 1b8f071..aea057b 100644
Binary files a/TheBank/bankui/bun.lockb and b/TheBank/bankui/bun.lockb differ
diff --git a/TheBank/bankui/package.json b/TheBank/bankui/package.json
index b9d4ce5..e5a277a 100644
--- a/TheBank/bankui/package.json
+++ b/TheBank/bankui/package.json
@@ -39,7 +39,8 @@
"tailwind-merge": "^3.3.0",
"tailwindcss": "^4.1.7",
"tw-animate-css": "^1.3.0",
- "zod": "^3.24.4"
+ "zod": "^3.24.4",
+ "zustand": "^5.0.4"
},
"devDependencies": {
"@eslint/js": "^9.25.0",
diff --git a/TheBank/bankui/src/App.tsx b/TheBank/bankui/src/App.tsx
index f719fd7..1a781d2 100644
--- a/TheBank/bankui/src/App.tsx
+++ b/TheBank/bankui/src/App.tsx
@@ -1,13 +1,28 @@
+import { useAuthCheck } from '@/hooks/useAuthCheck';
+import { useAuthStore } from '@/store/workerStore';
+import { Navigate, Outlet, useLocation } from 'react-router-dom';
+import { Header } from '@/components/layout/Header';
+import { Footer } from '@/components/layout/Footer';
import { Suspense } from 'react';
-import { Footer } from './components/layout/Footer';
-import { Header } from './components/layout/Header';
-import { Outlet } from 'react-router-dom';
function App() {
+ const user = useAuthStore((store) => 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',
+ },
+ ),
+);