feat: первая простенькая версия ui для кладовщика
This commit is contained in:
@@ -3,13 +3,13 @@ using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace BankDatabase;
|
||||
|
||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<BankDbContext>
|
||||
{
|
||||
public BankDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
return new BankDbContext(new ConfigurationDatabase());
|
||||
}
|
||||
}
|
||||
//public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<BankDbContext>
|
||||
//{
|
||||
// //public BankDbContext CreateDbContext(string[] args)
|
||||
// //{
|
||||
// // return new BankDbContext(new ConfigurationDatabase());
|
||||
// //}
|
||||
//}
|
||||
|
||||
internal class ConfigurationDatabase : IConfigurationDatabase
|
||||
{
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
|
||||
namespace BankDatabase.Models;
|
||||
namespace BankDatabase.Models;
|
||||
|
||||
public class CreditProgramCurrency
|
||||
{
|
||||
|
||||
@@ -194,7 +194,7 @@ public class StorekeeperAdapter : IStorekeeperAdapter
|
||||
|
||||
token = _jwtProvider.GenerateToken(storekeeper);
|
||||
|
||||
return StorekeeperOperationResponse.OK(token);
|
||||
return StorekeeperOperationResponse.OK(_mapper.Map<StorekeeperViewModel>(storekeeper));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,6 @@ public class CreditProgramsController(ICreditProgramAdapter adapter) : Controlle
|
||||
/// </summary>
|
||||
/// <returns>список кредитных программ</returns>
|
||||
[HttpGet]
|
||||
[AllowAnonymous]
|
||||
public IActionResult GetAllRecords()
|
||||
{
|
||||
return _adapter.GetList().GetResponse(Request, Response);
|
||||
@@ -63,7 +62,6 @@ public class CreditProgramsController(ICreditProgramAdapter adapter) : Controlle
|
||||
/// <param name="model">модель от пользователя</param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
[AllowAnonymous]
|
||||
public IActionResult Register([FromBody] CreditProgramBindingModel model)
|
||||
{
|
||||
return _adapter.RegisterCreditProgram(model).GetResponse(Request, Response);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получение данных текущего кладовщика
|
||||
/// </summary>
|
||||
/// <returns>Данные кладовщика</returns>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выход кладовщика
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpPost("logout")]
|
||||
public IActionResult Logout()
|
||||
{
|
||||
Response.Cookies.Delete(AuthOptions.CookieName);
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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 <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
const redirect = encodeURIComponent(location.pathname + location.search);
|
||||
return <Navigate to={`/auth?redirect=${redirect}`} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Suspense fallback={<p>loading</p>}>
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
<Footer />
|
||||
|
||||
@@ -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<StorekeeperBindingModel>(`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<StorekeeperBindingModel>('api/storekeepers/me'),
|
||||
};
|
||||
|
||||
@@ -28,12 +28,39 @@ export async function postData<T>(path: string, data: T) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSingleData<T>(path: string): Promise<T> {
|
||||
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<T>(path: string, data: T): Promise<T> {
|
||||
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<T>(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),
|
||||
|
||||
@@ -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 = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storekeeperId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Кладовщик</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Выберите кладовщика" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{storekeepers.map((storekeeper) => (
|
||||
<SelectItem key={storekeeper.id} value={storekeeper.id}>
|
||||
{storekeeper.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="periodId"
|
||||
@@ -182,9 +146,14 @@ export const CreditProgramForm = ({
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{periods.map((period) => (
|
||||
{periods &&
|
||||
periods.map((period) => (
|
||||
<SelectItem key={period.id} value={period.id}>
|
||||
{period.name}
|
||||
{`${new Date(
|
||||
period.startTime,
|
||||
).toLocaleDateString()} - ${new Date(
|
||||
period.endTime,
|
||||
).toLocaleDateString()}`}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -212,7 +181,8 @@ export const CreditProgramForm = ({
|
||||
}}
|
||||
className="w-full border rounded-md p-2 h-24"
|
||||
>
|
||||
{currencies.map((currency) => (
|
||||
{currencies &&
|
||||
currencies.map((currency) => (
|
||||
<option key={currency.id} value={currency.id}>
|
||||
{currency.name}
|
||||
</option>
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
|
||||
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<BaseFormValues | EditFormValues>;
|
||||
defaultValues?: Partial<BaseFormValues | EditFormValues>;
|
||||
}
|
||||
|
||||
const BaseCurrencyForm = ({
|
||||
onSubmit,
|
||||
}: CurrencyFormProps): React.JSX.Element => {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
schema,
|
||||
defaultValues,
|
||||
}: BaseCurrencyFormProps): React.JSX.Element => {
|
||||
const form = useForm<BaseFormValues | EditFormValues>({
|
||||
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 = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storekeeperId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Кладовщик</FormLabel>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger disabled={isLoadingStorekeepers}>
|
||||
{' '}
|
||||
{/* Отключаем выбор, пока данные загружаются */}
|
||||
<SelectValue placeholder="Выберите кладовщика" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{isLoadingStorekeepers ? ( // Индикатор загрузки
|
||||
<SelectItem value="loading" disabled>
|
||||
Загрузка...
|
||||
</SelectItem>
|
||||
) : storekeepersError ? ( // Сообщение об ошибке
|
||||
<SelectItem value="error" disabled>
|
||||
Ошибка загрузки
|
||||
</SelectItem>
|
||||
) : (
|
||||
// Реальные данные
|
||||
storekeepers?.map((storekeeper) => (
|
||||
<SelectItem key={storekeeper.id} value={storekeeper.id}>
|
||||
{storekeeper.name} {storekeeper.surname}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{storekeepersError && (
|
||||
<FormMessage>{storekeepersError.message}</FormMessage>
|
||||
)}{' '}
|
||||
{/* Отображаем ошибку под полем */}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" className="w-full">
|
||||
Сохранить
|
||||
</Button>
|
||||
@@ -163,3 +155,27 @@ export const CurrencyForm = ({
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const CurrencyFormAdd = ({
|
||||
onSubmit,
|
||||
}: {
|
||||
onSubmit: (data: CurrencyBindingModel) => void;
|
||||
}): React.JSX.Element => {
|
||||
return <BaseCurrencyForm onSubmit={onSubmit} schema={addSchema} />;
|
||||
};
|
||||
|
||||
export const CurrencyFormEdit = ({
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: {
|
||||
onSubmit: (data: CurrencyBindingModel) => void;
|
||||
defaultValues: Partial<BaseFormValues>;
|
||||
}): React.JSX.Element => {
|
||||
return (
|
||||
<BaseCurrencyForm
|
||||
onSubmit={onSubmit}
|
||||
schema={editSchema}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<typeof formSchema>;
|
||||
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<BaseFormValues | EditFormValues>;
|
||||
defaultValues?: Partial<BaseFormValues | EditFormValues>;
|
||||
}
|
||||
|
||||
export const PeriodForm = ({
|
||||
const BasePeriodForm = ({
|
||||
onSubmit,
|
||||
}: PeriodFormProps): React.JSX.Element => {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
schema,
|
||||
defaultValues,
|
||||
}: BasePeriodFormProps): React.JSX.Element => {
|
||||
const form = useForm<BaseFormValues | EditFormValues>({
|
||||
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 = ({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="storekeeperId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Кладовщик</FormLabel>
|
||||
<Select
|
||||
onValueChange={(value) => field.onChange(value)}
|
||||
value={field.value}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectTrigger disabled={isLoadingStorekeepers}>
|
||||
<SelectValue placeholder="Выберите кладовщика" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{isLoadingStorekeepers ? (
|
||||
<SelectItem value="loading" disabled>
|
||||
Загрузка...
|
||||
</SelectItem>
|
||||
) : storekeepersError ? (
|
||||
<SelectItem value="error" disabled>
|
||||
Ошибка загрузки
|
||||
</SelectItem>
|
||||
) : (
|
||||
storekeepers?.map((storekeeper) => (
|
||||
<SelectItem key={storekeeper.id} value={storekeeper.id}>
|
||||
{storekeeper.name} {storekeeper.surname}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{storekeepersError && (
|
||||
<FormMessage>{storekeepersError.message}</FormMessage>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" className="w-full">
|
||||
Сохранить
|
||||
</Button>
|
||||
@@ -211,3 +209,27 @@ export const PeriodForm = ({
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export const PeriodFormAdd = ({
|
||||
onSubmit,
|
||||
}: {
|
||||
onSubmit: (data: PeriodBindingModel) => void;
|
||||
}): React.JSX.Element => {
|
||||
return <BasePeriodForm onSubmit={onSubmit} schema={addSchema} />;
|
||||
};
|
||||
|
||||
export const PeriodFormEdit = ({
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: {
|
||||
onSubmit: (data: PeriodBindingModel) => void;
|
||||
defaultValues: Partial<BaseFormValues>;
|
||||
}): React.JSX.Element => {
|
||||
return (
|
||||
<BasePeriodForm
|
||||
onSubmit={onSubmit}
|
||||
schema={editSchema}
|
||||
defaultValues={defaultValues}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
180
TheBank/bankui/src/components/features/ProfileForm.tsx
Normal file
180
TheBank/bankui/src/components/features/ProfileForm.tsx
Normal file
@@ -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<typeof profileEditSchema>;
|
||||
|
||||
interface ProfileFormProps {
|
||||
onSubmit: (data: Partial<StorekeeperBindingModel>) => void;
|
||||
defaultValues: ProfileFormValues;
|
||||
}
|
||||
|
||||
export const ProfileForm = ({
|
||||
onSubmit,
|
||||
defaultValues,
|
||||
}: ProfileFormProps): React.JSX.Element => {
|
||||
const form = useForm<ProfileFormValues>({
|
||||
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 (
|
||||
<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="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="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Email" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phoneNumber"
|
||||
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>
|
||||
);
|
||||
};
|
||||
@@ -12,6 +12,8 @@ import { Checkbox } from '../ui/checkbox';
|
||||
type DataTableProps<T> = {
|
||||
data: T[];
|
||||
columns: ColumnDef<T>[];
|
||||
selectedRow?: string;
|
||||
onRowSelected: (id: string | undefined) => void;
|
||||
};
|
||||
|
||||
export type ColumnDef<T> = {
|
||||
@@ -23,13 +25,11 @@ export type ColumnDef<T> = {
|
||||
export const DataTable = <T extends {}>({
|
||||
data,
|
||||
columns,
|
||||
selectedRow,
|
||||
onRowSelected,
|
||||
}: DataTableProps<T>): 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 (
|
||||
<div className="rounded-md border">
|
||||
@@ -59,12 +59,12 @@ export const DataTable = <T extends {}>({
|
||||
<TableRow
|
||||
key={(item as any).id || index}
|
||||
data-state={
|
||||
selectedRowId === (item as any).id ? 'selected' : undefined
|
||||
selectedRow === (item as any).id ? 'selected' : undefined
|
||||
}
|
||||
>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={selectedRowId === (item as any).id}
|
||||
checked={selectedRow === (item as any).id}
|
||||
onCheckedChange={() => handleRowSelect((item as any).id)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<header className="flex w-full p-2 justify-between">
|
||||
<nav className="text-black">
|
||||
@@ -98,7 +93,7 @@ export const Header = (): React.JSX.Element => {
|
||||
</Menubar>
|
||||
</nav>
|
||||
<div>
|
||||
<ProfileIcon name={'Евгений Эгов'} />
|
||||
<ProfileIcon name={fullName || 'Евгений Эгов'} logout={loggedOut} />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
@@ -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 (
|
||||
<div>
|
||||
<DropdownMenu>
|
||||
@@ -144,13 +143,17 @@ export const ProfileIcon = ({ name }: ProfileIconProps): React.JSX.Element => {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/" className="block w-full text-left">
|
||||
<Link to="/profile" className="block w-full text-left">
|
||||
Профиль
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Button variant="outline" className="block w-full text-left">
|
||||
<Button
|
||||
onClick={logout}
|
||||
variant="outline"
|
||||
className="block w-full text-left"
|
||||
>
|
||||
Выйти
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -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<Forms>('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 => {
|
||||
<div>
|
||||
<Tabs defaultValue="login" className="w-[400px]">
|
||||
<TabsList>
|
||||
<TabsTrigger value="login">Вход</TabsTrigger>
|
||||
<TabsTrigger value="register">Регистрация</TabsTrigger>
|
||||
<TabsTrigger
|
||||
onClick={() => setCurrentForm('login')}
|
||||
value="login"
|
||||
>
|
||||
Вход
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
onClick={() => setCurrentForm('register')}
|
||||
value="register"
|
||||
>
|
||||
Регистрация
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="login">
|
||||
<TabsContent value={currentForm}>
|
||||
<LoginForm onSubmit={handleLogin} />
|
||||
</TabsContent>
|
||||
<TabsContent value="register">
|
||||
@@ -57,7 +71,6 @@ export const AuthStorekeeper = (): React.JSX.Element => {
|
||||
</Tabs>
|
||||
</div>
|
||||
</main>
|
||||
<Toaster />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<boolean>(false);
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] =
|
||||
React.useState<boolean>(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 => {
|
||||
<main className="flex-1 flex relative">
|
||||
<AppSidebar
|
||||
onAddClick={() => {
|
||||
setIsDialogOpen(true);
|
||||
setIsAddDialogOpen(true);
|
||||
}}
|
||||
onEditClick={() => {
|
||||
openEditForm();
|
||||
}}
|
||||
onEditClick={() => {}}
|
||||
/>
|
||||
<div className="flex-1 p-4">
|
||||
<DialogForm<CurrencyBindingModel>
|
||||
title="Форма валюты"
|
||||
description="Добавьте новую валюту"
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
onSubmit={handleAdd}
|
||||
isOpen={isAddDialogOpen}
|
||||
onClose={() => setIsAddDialogOpen(false)}
|
||||
>
|
||||
<CurrencyForm />
|
||||
<CurrencyFormAdd onSubmit={handleAdd} />
|
||||
</DialogForm>
|
||||
{selectedItem && (
|
||||
<DialogForm<CurrencyBindingModel>
|
||||
title="Форма валюты"
|
||||
description="Измените валюту"
|
||||
isOpen={isEditDialogOpen}
|
||||
onClose={() => setIsEditDialogOpen(false)}
|
||||
onSubmit={handleEdit}
|
||||
>
|
||||
<CurrencyFormEdit defaultValues={selectedItem} />
|
||||
</DialogForm>
|
||||
)}
|
||||
<div>
|
||||
<DataTable data={finalData} columns={columns} />
|
||||
<DataTable
|
||||
data={finalData}
|
||||
columns={columns}
|
||||
onRowSelected={(id) => handleSelectItem(id)}
|
||||
selectedRow={selectedItem?.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -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<PeriodTableData>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
@@ -25,10 +21,12 @@ const columns: ColumnDef<PeriodTableData>[] = [
|
||||
{
|
||||
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<PeriodTableData>[] = [
|
||||
];
|
||||
|
||||
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<boolean>(false);
|
||||
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
|
||||
const [isEditDialogOpen, setIsEditDialogOpen] =
|
||||
React.useState<boolean>(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 => {
|
||||
<main className="flex-1 flex relative">
|
||||
<AppSidebar
|
||||
onAddClick={() => {
|
||||
setIsDialogOpen(true);
|
||||
setIsAddDialogOpen(true);
|
||||
}}
|
||||
onEditClick={() => {
|
||||
openEditForm();
|
||||
}}
|
||||
onEditClick={() => {}}
|
||||
/>
|
||||
<div className="flex-1 p-4">
|
||||
<DialogForm<PeriodBindingModel>
|
||||
title="Форма"
|
||||
description="Описание"
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
title="Форма сроков"
|
||||
description="Добавить сроки"
|
||||
isOpen={isAddDialogOpen}
|
||||
onClose={() => setIsAddDialogOpen(false)}
|
||||
onSubmit={handleAdd}
|
||||
>
|
||||
<PeriodForm />
|
||||
<PeriodFormAdd onSubmit={handleAdd} />
|
||||
</DialogForm>
|
||||
{selectedItem && (
|
||||
<DialogForm<PeriodBindingModel>
|
||||
title="Форма сроков"
|
||||
description="Изменить сроки"
|
||||
isOpen={isEditDialogOpen}
|
||||
onClose={() => setIsEditDialogOpen(false)}
|
||||
onSubmit={handleEdit}
|
||||
>
|
||||
<PeriodFormEdit
|
||||
onSubmit={handleEdit}
|
||||
defaultValues={selectedItem}
|
||||
/>
|
||||
</DialogForm>
|
||||
)}
|
||||
<div>
|
||||
<DataTable data={finalData} columns={columns} />
|
||||
<DataTable
|
||||
data={finalData}
|
||||
columns={columns}
|
||||
onRowSelected={(id) => handleSelectItem(id)}
|
||||
selectedRow={selectedItem?.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
38
TheBank/bankui/src/components/pages/Profile.tsx
Normal file
38
TheBank/bankui/src/components/pages/Profile.tsx
Normal file
@@ -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 (
|
||||
<main className="container mx-auto py-10">
|
||||
Загрузка данных пользователя...
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const handleUpdate = (data: Partial<StorekeeperBindingModel>) => {
|
||||
console.log(data);
|
||||
updateUser(data);
|
||||
updateStorekeeper(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
<h1 className="text-2xl font-bold mb-6">Профиль пользователя</h1>
|
||||
<ProfileForm defaultValues={user} onSubmit={handleUpdate} />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
@@ -53,7 +53,11 @@ export const Storekeepers = (): React.JSX.Element => {
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
<h1 className="text-2xl font-bold mb-6">Кладовщики</h1>
|
||||
<DataTable data={storekeepers || []} columns={columns} />
|
||||
<DataTable
|
||||
data={storekeepers || []}
|
||||
columns={columns}
|
||||
onRowSelected={console.log}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
38
TheBank/bankui/src/hooks/useAuthCheck.ts
Normal file
38
TheBank/bankui/src/hooks/useAuthCheck.ts
Normal file
@@ -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<StorekeeperBindingModel, Error>({
|
||||
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 };
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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: <Periods />,
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
element: <Profile />,
|
||||
},
|
||||
],
|
||||
errorElement: <p>бля пизда рулям</p>,
|
||||
},
|
||||
@@ -38,6 +48,10 @@ const routes = createBrowserRouter([
|
||||
path: '/auth',
|
||||
element: <AuthStorekeeper />,
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
element: <Navigate to="/" replace />,
|
||||
},
|
||||
]);
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -46,6 +60,7 @@ createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={routes} />
|
||||
<Toaster />
|
||||
</QueryClientProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
||||
36
TheBank/bankui/src/store/workerStore.tsx
Normal file
36
TheBank/bankui/src/store/workerStore.tsx
Normal file
@@ -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<void>;
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthState>()(
|
||||
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',
|
||||
},
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user