feat: первая простенькая версия ui для кладовщика
This commit is contained in:
@@ -3,13 +3,13 @@ using Microsoft.EntityFrameworkCore.Design;
|
|||||||
|
|
||||||
namespace BankDatabase;
|
namespace BankDatabase;
|
||||||
|
|
||||||
public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<BankDbContext>
|
//public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<BankDbContext>
|
||||||
{
|
//{
|
||||||
public BankDbContext CreateDbContext(string[] args)
|
// //public BankDbContext CreateDbContext(string[] args)
|
||||||
{
|
// //{
|
||||||
return new BankDbContext(new ConfigurationDatabase());
|
// // return new BankDbContext(new ConfigurationDatabase());
|
||||||
}
|
// //}
|
||||||
}
|
//}
|
||||||
|
|
||||||
internal class ConfigurationDatabase : IConfigurationDatabase
|
internal class ConfigurationDatabase : IConfigurationDatabase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations.Schema;
|
namespace BankDatabase.Models;
|
||||||
|
|
||||||
namespace BankDatabase.Models;
|
|
||||||
|
|
||||||
public class CreditProgramCurrency
|
public class CreditProgramCurrency
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public class StorekeeperAdapter : IStorekeeperAdapter
|
|||||||
|
|
||||||
token = _jwtProvider.GenerateToken(storekeeper);
|
token = _jwtProvider.GenerateToken(storekeeper);
|
||||||
|
|
||||||
return StorekeeperOperationResponse.OK(token);
|
return StorekeeperOperationResponse.OK(_mapper.Map<StorekeeperViewModel>(storekeeper));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ public class CreditProgramsController(ICreditProgramAdapter adapter) : Controlle
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>список кредитных программ</returns>
|
/// <returns>список кредитных программ</returns>
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[AllowAnonymous]
|
|
||||||
public IActionResult GetAllRecords()
|
public IActionResult GetAllRecords()
|
||||||
{
|
{
|
||||||
return _adapter.GetList().GetResponse(Request, Response);
|
return _adapter.GetList().GetResponse(Request, Response);
|
||||||
@@ -63,7 +62,6 @@ public class CreditProgramsController(ICreditProgramAdapter adapter) : Controlle
|
|||||||
/// <param name="model">модель от пользователя</param>
|
/// <param name="model">модель от пользователя</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[AllowAnonymous]
|
|
||||||
public IActionResult Register([FromBody] CreditProgramBindingModel model)
|
public IActionResult Register([FromBody] CreditProgramBindingModel model)
|
||||||
{
|
{
|
||||||
return _adapter.RegisterCreditProgram(model).GetResponse(Request, Response);
|
return _adapter.RegisterCreditProgram(model).GetResponse(Request, Response);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using BankContracts.BindingModels;
|
using BankContracts.BindingModels;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
namespace BankWebApi.Controllers;
|
namespace BankWebApi.Controllers;
|
||||||
|
|
||||||
@@ -67,6 +68,10 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
|
|||||||
public IActionResult Login([FromBody] StorekeeperAuthBindingModel model)
|
public IActionResult Login([FromBody] StorekeeperAuthBindingModel model)
|
||||||
{
|
{
|
||||||
var res = _adapter.Login(model, out string token);
|
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
|
Response.Cookies.Append(AuthOptions.CookieName, token, new CookieOptions
|
||||||
{
|
{
|
||||||
@@ -78,4 +83,32 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
|
|||||||
|
|
||||||
return res.GetResponse(Request, Response);
|
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(
|
var token = new JwtSecurityToken(
|
||||||
issuer: AuthOptions.ISSUER,
|
issuer: AuthOptions.ISSUER,
|
||||||
audience: AuthOptions.AUDIENCE,
|
audience: AuthOptions.AUDIENCE,
|
||||||
claims: [new("id", dataModel.Id)],
|
claims: [new(ClaimTypes.NameIdentifier, dataModel.Id)],
|
||||||
expires: DateTime.UtcNow.Add(TimeSpan.FromDays(2)),
|
expires: DateTime.UtcNow.Add(TimeSpan.FromDays(2)),
|
||||||
signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
|
signingCredentials: new SigningCredentials(AuthOptions.GetSymmetricSecurityKey(), SecurityAlgorithms.HmacSha256));
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@@ -39,7 +39,8 @@
|
|||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.0",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
"tw-animate-css": "^1.3.0",
|
"tw-animate-css": "^1.3.0",
|
||||||
"zod": "^3.24.4"
|
"zod": "^3.24.4",
|
||||||
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.0",
|
"@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 { Suspense } from 'react';
|
||||||
import { Footer } from './components/layout/Footer';
|
|
||||||
import { Header } from './components/layout/Header';
|
|
||||||
import { Outlet } from 'react-router-dom';
|
|
||||||
|
|
||||||
function App() {
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
<Suspense fallback={<p>loading</p>}>
|
<Suspense fallback={<p>Loading...</p>}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { getData, postData, putData } from './client';
|
import {
|
||||||
|
getData,
|
||||||
|
getSingleData,
|
||||||
|
postData,
|
||||||
|
postLoginData,
|
||||||
|
putData,
|
||||||
|
} from './client';
|
||||||
import type {
|
import type {
|
||||||
ClientBindingModel,
|
ClientBindingModel,
|
||||||
ClerkBindingModel,
|
ClerkBindingModel,
|
||||||
@@ -115,7 +121,11 @@ export const storekeepersApi = {
|
|||||||
getData<StorekeeperBindingModel>(`api/Storekeepers/GetRecord/${id}`),
|
getData<StorekeeperBindingModel>(`api/Storekeepers/GetRecord/${id}`),
|
||||||
create: (data: StorekeeperBindingModel) =>
|
create: (data: StorekeeperBindingModel) =>
|
||||||
postData('api/Storekeepers/Register', data),
|
postData('api/Storekeepers/Register', data),
|
||||||
update: (data: StorekeeperBindingModel) =>
|
update: (data: StorekeeperBindingModel) => putData('api/Storekeepers', data),
|
||||||
putData('api/Storekeepers/ChangeInfo', data),
|
// auth
|
||||||
login: (data: LoginBindingModel) => postData('api/Storekeepers/login', data),
|
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) {
|
export async function putData<T>(path: string, data: T) {
|
||||||
const res = await fetch(`${API_URL}/${path}`, {
|
const res = await fetch(`${API_URL}/${path}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
// mode: 'no-cors',
|
|
||||||
},
|
},
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify(data),
|
||||||
|
|||||||
@@ -19,26 +19,10 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select';
|
} from '@/components/ui/select';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type {
|
import type { CreditProgramBindingModel } from '@/types/types';
|
||||||
CreditProgramBindingModel,
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
CurrencyBindingModel,
|
import { usePeriods } from '@/hooks/usePeriods';
|
||||||
} from '@/types/types';
|
import { useCurrencies } from '@/hooks/useCurrencies';
|
||||||
|
|
||||||
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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
@@ -47,7 +31,6 @@ const formSchema = z.object({
|
|||||||
maxCost: z.coerce
|
maxCost: z.coerce
|
||||||
.number()
|
.number()
|
||||||
.min(0, 'Максимальная стоимость не может быть отрицательной'),
|
.min(0, 'Максимальная стоимость не может быть отрицательной'),
|
||||||
storekeeperId: z.string().min(1, 'Выберите кладовщика'),
|
|
||||||
periodId: z.string().min(1, 'Выберите период'),
|
periodId: z.string().min(1, 'Выберите период'),
|
||||||
currencyCreditPrograms: z
|
currencyCreditPrograms: z
|
||||||
.array(z.string())
|
.array(z.string())
|
||||||
@@ -70,12 +53,16 @@ export const CreditProgramForm = ({
|
|||||||
name: '',
|
name: '',
|
||||||
cost: 0,
|
cost: 0,
|
||||||
maxCost: 0,
|
maxCost: 0,
|
||||||
storekeeperId: '',
|
|
||||||
periodId: '',
|
periodId: '',
|
||||||
currencyCreditPrograms: [],
|
currencyCreditPrograms: [],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { periods } = usePeriods();
|
||||||
|
const { currencies } = useCurrencies();
|
||||||
|
|
||||||
|
const storekeeper = useAuthStore((store) => store.user);
|
||||||
|
|
||||||
const handleSubmit = (data: FormValues) => {
|
const handleSubmit = (data: FormValues) => {
|
||||||
const dataWithId = {
|
const dataWithId = {
|
||||||
...data,
|
...data,
|
||||||
@@ -86,6 +73,7 @@ export const CreditProgramForm = ({
|
|||||||
currencyCreditPrograms: data.currencyCreditPrograms.map((currencyId) => ({
|
currencyCreditPrograms: data.currencyCreditPrograms.map((currencyId) => ({
|
||||||
currencyId,
|
currencyId,
|
||||||
})),
|
})),
|
||||||
|
storekeeperId: storekeeper?.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
@@ -145,30 +133,6 @@ export const CreditProgramForm = ({
|
|||||||
</FormItem>
|
</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
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="periodId"
|
name="periodId"
|
||||||
@@ -182,11 +146,16 @@ export const CreditProgramForm = ({
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{periods.map((period) => (
|
{periods &&
|
||||||
<SelectItem key={period.id} value={period.id}>
|
periods.map((period) => (
|
||||||
{period.name}
|
<SelectItem key={period.id} value={period.id}>
|
||||||
</SelectItem>
|
{`${new Date(
|
||||||
))}
|
period.startTime,
|
||||||
|
).toLocaleDateString()} - ${new Date(
|
||||||
|
period.endTime,
|
||||||
|
).toLocaleDateString()}`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
@@ -212,11 +181,12 @@ export const CreditProgramForm = ({
|
|||||||
}}
|
}}
|
||||||
className="w-full border rounded-md p-2 h-24"
|
className="w-full border rounded-md p-2 h-24"
|
||||||
>
|
>
|
||||||
{currencies.map((currency) => (
|
{currencies &&
|
||||||
<option key={currency.id} value={currency.id}>
|
currencies.map((currency) => (
|
||||||
{currency.name}
|
<option key={currency.id} value={currency.id}>
|
||||||
</option>
|
{currency.name}
|
||||||
))}
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -11,55 +11,88 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { CurrencyBindingModel } from '@/types/types';
|
import type { CurrencyBindingModel } from '@/types/types';
|
||||||
import { useStorekeepers } from '@/hooks/useStorekeepers'; // Импорт хука для кладовщиков
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
|
|
||||||
const formSchema = z.object({
|
type BaseFormValues = {
|
||||||
id: z.string().optional(),
|
id?: string;
|
||||||
name: z.string().min(1, 'Укажите название валюты'),
|
name: string;
|
||||||
abbreviation: z.string().min(1, 'Укажите аббревиатуру'),
|
abbreviation: string;
|
||||||
cost: z.coerce.number().min(0, 'Стоимость не может быть отрицательной'),
|
cost: number;
|
||||||
storekeeperId: z.string().min(1, 'Выберите кладовщика'),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
|
||||||
|
|
||||||
type CurrencyFormProps = {
|
|
||||||
onSubmit: (data: CurrencyBindingModel) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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,
|
onSubmit,
|
||||||
}: CurrencyFormProps): React.JSX.Element => {
|
schema,
|
||||||
const form = useForm<FormValues>({
|
defaultValues,
|
||||||
resolver: zodResolver(formSchema),
|
}: BaseCurrencyFormProps): React.JSX.Element => {
|
||||||
|
const form = useForm<BaseFormValues | EditFormValues>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: '',
|
id: defaultValues?.id || '',
|
||||||
name: '',
|
name: defaultValues?.name || '',
|
||||||
abbreviation: '',
|
abbreviation: defaultValues?.abbreviation || '',
|
||||||
cost: 0,
|
cost: defaultValues?.cost || 0,
|
||||||
storekeeperId: '',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
useEffect(() => {
|
||||||
storekeepers,
|
if (defaultValues) {
|
||||||
isLoading: isLoadingStorekeepers,
|
form.reset({
|
||||||
error: storekeepersError,
|
id: defaultValues.id || '',
|
||||||
} = useStorekeepers(); // Получаем данные кладовщиков
|
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 = {
|
const payload: CurrencyBindingModel = {
|
||||||
...data,
|
|
||||||
id: data.id || crypto.randomUUID(),
|
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);
|
onSubmit(payload);
|
||||||
@@ -115,47 +148,6 @@ export const CurrencyForm = ({
|
|||||||
</FormItem>
|
</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 type="submit" className="w-full">
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
@@ -163,3 +155,27 @@ export const CurrencyForm = ({
|
|||||||
</Form>
|
</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 { useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
@@ -10,13 +10,6 @@ import {
|
|||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form';
|
} from '@/components/ui/form';
|
||||||
import {
|
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectValue,
|
|
||||||
} from '@/components/ui/select';
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import type { PeriodBindingModel } from '@/types/types';
|
import type { PeriodBindingModel } from '@/types/types';
|
||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
@@ -28,9 +21,21 @@ import {
|
|||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover';
|
} 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(),
|
id: z.string().optional(),
|
||||||
startTime: z.date({
|
startTime: z.date({
|
||||||
required_error: 'Укажите время начала',
|
required_error: 'Укажите время начала',
|
||||||
@@ -40,38 +45,70 @@ const formSchema = z.object({
|
|||||||
required_error: 'Укажите время окончания',
|
required_error: 'Укажите время окончания',
|
||||||
invalid_type_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;
|
onSubmit: (data: PeriodBindingModel) => void;
|
||||||
};
|
schema: z.ZodType<BaseFormValues | EditFormValues>;
|
||||||
|
defaultValues?: Partial<BaseFormValues | EditFormValues>;
|
||||||
|
}
|
||||||
|
|
||||||
export const PeriodForm = ({
|
const BasePeriodForm = ({
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: PeriodFormProps): React.JSX.Element => {
|
schema,
|
||||||
const form = useForm<FormValues>({
|
defaultValues,
|
||||||
resolver: zodResolver(formSchema),
|
}: BasePeriodFormProps): React.JSX.Element => {
|
||||||
|
const form = useForm<BaseFormValues | EditFormValues>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
id: '',
|
id: defaultValues?.id || '',
|
||||||
storekeeperId: '',
|
startTime: defaultValues?.startTime || new Date(),
|
||||||
|
endTime: defaultValues?.endTime || new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
useEffect(() => {
|
||||||
storekeepers,
|
if (defaultValues) {
|
||||||
isLoading: isLoadingStorekeepers,
|
form.reset({
|
||||||
error: storekeepersError,
|
id: defaultValues.id || '',
|
||||||
} = useStorekeepers();
|
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 = {
|
const payload: PeriodBindingModel = {
|
||||||
...data,
|
|
||||||
id: data.id || crypto.randomUUID(),
|
id: data.id || crypto.randomUUID(),
|
||||||
startTime: data.startTime,
|
storekeeperId: storekeeper?.id,
|
||||||
endTime: data.endTime,
|
startTime:
|
||||||
|
'startTime' in data && data.startTime !== undefined
|
||||||
|
? data.startTime
|
||||||
|
: new Date(),
|
||||||
|
endTime:
|
||||||
|
'endTime' in data && data.endTime !== undefined
|
||||||
|
? data.endTime
|
||||||
|
: new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
onSubmit(payload);
|
onSubmit(payload);
|
||||||
@@ -164,46 +201,7 @@ export const PeriodForm = ({
|
|||||||
</FormItem>
|
</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 type="submit" className="w-full">
|
||||||
Сохранить
|
Сохранить
|
||||||
</Button>
|
</Button>
|
||||||
@@ -211,3 +209,27 @@ export const PeriodForm = ({
|
|||||||
</Form>
|
</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> = {
|
type DataTableProps<T> = {
|
||||||
data: T[];
|
data: T[];
|
||||||
columns: ColumnDef<T>[];
|
columns: ColumnDef<T>[];
|
||||||
|
selectedRow?: string;
|
||||||
|
onRowSelected: (id: string | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ColumnDef<T> = {
|
export type ColumnDef<T> = {
|
||||||
@@ -23,13 +25,11 @@ export type ColumnDef<T> = {
|
|||||||
export const DataTable = <T extends {}>({
|
export const DataTable = <T extends {}>({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
|
selectedRow,
|
||||||
|
onRowSelected,
|
||||||
}: DataTableProps<T>): React.JSX.Element => {
|
}: DataTableProps<T>): React.JSX.Element => {
|
||||||
const [selectedRowId, setSelectedRowId] = React.useState<
|
|
||||||
string | undefined
|
|
||||||
>();
|
|
||||||
|
|
||||||
const handleRowSelect = (id: string) => {
|
const handleRowSelect = (id: string) => {
|
||||||
setSelectedRowId((prev) => (prev === id ? undefined : id));
|
onRowSelected(selectedRow === id ? undefined : id);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md border">
|
<div className="rounded-md border">
|
||||||
@@ -59,12 +59,12 @@ export const DataTable = <T extends {}>({
|
|||||||
<TableRow
|
<TableRow
|
||||||
key={(item as any).id || index}
|
key={(item as any).id || index}
|
||||||
data-state={
|
data-state={
|
||||||
selectedRowId === (item as any).id ? 'selected' : undefined
|
selectedRow === (item as any).id ? 'selected' : undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={selectedRowId === (item as any).id}
|
checked={selectedRow === (item as any).id}
|
||||||
onCheckedChange={() => handleRowSelect((item as any).id)}
|
onCheckedChange={() => handleRowSelect((item as any).id)}
|
||||||
aria-label="Select row"
|
aria-label="Select row"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import {
|
|||||||
} from '../ui/dropdown-menu';
|
} from '../ui/dropdown-menu';
|
||||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
|
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
||||||
|
|
||||||
type NavOptionValue = {
|
type NavOptionValue = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -38,11 +40,6 @@ const navOptions = [
|
|||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/currencies',
|
link: '/currencies',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Создать',
|
|
||||||
link: '',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -53,11 +50,6 @@ const navOptions = [
|
|||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/credit-programs',
|
link: '/credit-programs',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Создать',
|
|
||||||
link: '',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -68,11 +60,6 @@ const navOptions = [
|
|||||||
name: 'Просмотреть',
|
name: 'Просмотреть',
|
||||||
link: '/periods',
|
link: '/periods',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
name: 'Создать',
|
|
||||||
link: '',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -88,6 +75,14 @@ const navOptions = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const Header = (): React.JSX.Element => {
|
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 (
|
return (
|
||||||
<header className="flex w-full p-2 justify-between">
|
<header className="flex w-full p-2 justify-between">
|
||||||
<nav className="text-black">
|
<nav className="text-black">
|
||||||
@@ -98,7 +93,7 @@ export const Header = (): React.JSX.Element => {
|
|||||||
</Menubar>
|
</Menubar>
|
||||||
</nav>
|
</nav>
|
||||||
<div>
|
<div>
|
||||||
<ProfileIcon name={'Евгений Эгов'} />
|
<ProfileIcon name={fullName || 'Евгений Эгов'} logout={loggedOut} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
@@ -127,9 +122,13 @@ const MenuOption = ({ item }: { item: NavOption }) => {
|
|||||||
|
|
||||||
type ProfileIconProps = {
|
type ProfileIconProps = {
|
||||||
name: string;
|
name: string;
|
||||||
|
logout: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileIcon = ({ name }: ProfileIconProps): React.JSX.Element => {
|
export const ProfileIcon = ({
|
||||||
|
name,
|
||||||
|
logout,
|
||||||
|
}: ProfileIconProps): React.JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -144,13 +143,17 @@ export const ProfileIcon = ({ name }: ProfileIconProps): React.JSX.Element => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link to="/" className="block w-full text-left">
|
<Link to="/profile" className="block w-full text-left">
|
||||||
Профиль
|
Профиль
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Button variant="outline" className="block w-full text-left">
|
<Button
|
||||||
|
onClick={logout}
|
||||||
|
variant="outline"
|
||||||
|
className="block w-full text-left"
|
||||||
|
>
|
||||||
Выйти
|
Выйти
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|||||||
@@ -1,43 +1,47 @@
|
|||||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
||||||
import React from 'react';
|
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 { RegisterForm } from '../features/RegisterForm';
|
||||||
import type { LoginBindingModel, StorekeeperBindingModel } from '@/types/types';
|
|
||||||
import { LoginForm } from '../features/LoginForm';
|
import { LoginForm } from '../features/LoginForm';
|
||||||
import { Toaster } from '../ui/sonner';
|
|
||||||
import { toast } from '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 => {
|
export const AuthStorekeeper = (): React.JSX.Element => {
|
||||||
const navigate = useNavigate();
|
|
||||||
const {
|
const {
|
||||||
createStorekeeper,
|
createStorekeeper,
|
||||||
loginStorekeeper,
|
loginStorekeeper,
|
||||||
isLoginError,
|
isLoginError,
|
||||||
loginError,
|
loginError,
|
||||||
isLoginSuccess,
|
isCreateError,
|
||||||
} = useStorekeepers();
|
} = useStorekeepers();
|
||||||
|
|
||||||
|
const [currentForm, setCurrentForm] = React.useState<Forms>('login');
|
||||||
|
|
||||||
const handleRegister = (data: StorekeeperBindingModel) => {
|
const handleRegister = (data: StorekeeperBindingModel) => {
|
||||||
console.log(data);
|
createStorekeeper(data, {
|
||||||
createStorekeeper(data);
|
onSuccess: () => {
|
||||||
|
toast('Регистрация успешна! Войдите в систему.');
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast(`Ошибка регистрации: ${error.message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLogin = (data: LoginBindingModel) => {
|
const handleLogin = (data: LoginBindingModel) => {
|
||||||
console.log(data);
|
|
||||||
loginStorekeeper(data);
|
loginStorekeeper(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (isLoginError) {
|
if (isLoginError) {
|
||||||
toast(`Ошибка ${loginError?.message}`);
|
toast(`Ошибка входа: ${loginError?.message}`);
|
||||||
}
|
}
|
||||||
}, [isLoginError, loginError]);
|
if (isCreateError) {
|
||||||
|
toast('Ошибка при регистрации');
|
||||||
React.useEffect(() => {
|
|
||||||
if (isLoginSuccess) {
|
|
||||||
navigate('/storekeepers');
|
|
||||||
}
|
}
|
||||||
}, [isLoginSuccess]);
|
}, [isLoginError, loginError, isCreateError]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -45,10 +49,20 @@ export const AuthStorekeeper = (): React.JSX.Element => {
|
|||||||
<div>
|
<div>
|
||||||
<Tabs defaultValue="login" className="w-[400px]">
|
<Tabs defaultValue="login" className="w-[400px]">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="login">Вход</TabsTrigger>
|
<TabsTrigger
|
||||||
<TabsTrigger value="register">Регистрация</TabsTrigger>
|
onClick={() => setCurrentForm('login')}
|
||||||
|
value="login"
|
||||||
|
>
|
||||||
|
Вход
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
onClick={() => setCurrentForm('register')}
|
||||||
|
value="register"
|
||||||
|
>
|
||||||
|
Регистрация
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="login">
|
<TabsContent value={currentForm}>
|
||||||
<LoginForm onSubmit={handleLogin} />
|
<LoginForm onSubmit={handleLogin} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="register">
|
<TabsContent value="register">
|
||||||
@@ -57,7 +71,6 @@ export const AuthStorekeeper = (): React.JSX.Element => {
|
|||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Toaster />
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ import { DialogForm } from '../layout/DialogForm';
|
|||||||
import { DataTable } from '../layout/DataTable';
|
import { DataTable } from '../layout/DataTable';
|
||||||
import { useCurrencies } from '@/hooks/useCurrencies';
|
import { useCurrencies } from '@/hooks/useCurrencies';
|
||||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
||||||
import type {
|
import type { CurrencyBindingModel } from '@/types/types';
|
||||||
CurrencyBindingModel,
|
|
||||||
StorekeeperBindingModel,
|
|
||||||
} from '@/types/types';
|
|
||||||
import type { ColumnDef } from '../layout/DataTable';
|
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 {
|
interface CurrencyTableData extends CurrencyBindingModel {
|
||||||
storekeeperName: string;
|
storekeeperName: string;
|
||||||
@@ -69,11 +67,41 @@ export const Currencies = (): React.JSX.Element => {
|
|||||||
});
|
});
|
||||||
}, [currencies, storekeepers]);
|
}, [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) => {
|
const handleAdd = (data: CurrencyBindingModel) => {
|
||||||
console.log(data);
|
|
||||||
createCurrency(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) {
|
if (isLoading) {
|
||||||
@@ -92,22 +120,39 @@ export const Currencies = (): React.JSX.Element => {
|
|||||||
<main className="flex-1 flex relative">
|
<main className="flex-1 flex relative">
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
onAddClick={() => {
|
onAddClick={() => {
|
||||||
setIsDialogOpen(true);
|
setIsAddDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onEditClick={() => {
|
||||||
|
openEditForm();
|
||||||
}}
|
}}
|
||||||
onEditClick={() => {}}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
<DialogForm<CurrencyBindingModel>
|
<DialogForm<CurrencyBindingModel>
|
||||||
title="Форма валюты"
|
title="Форма валюты"
|
||||||
description="Добавьте новую валюту"
|
description="Добавьте новую валюту"
|
||||||
isOpen={isDialogOpen}
|
isOpen={isAddDialogOpen}
|
||||||
onClose={() => setIsDialogOpen(false)}
|
onClose={() => setIsAddDialogOpen(false)}
|
||||||
onSubmit={handleAdd}
|
|
||||||
>
|
>
|
||||||
<CurrencyForm />
|
<CurrencyFormAdd onSubmit={handleAdd} />
|
||||||
</DialogForm>
|
</DialogForm>
|
||||||
|
{selectedItem && (
|
||||||
|
<DialogForm<CurrencyBindingModel>
|
||||||
|
title="Форма валюты"
|
||||||
|
description="Измените валюту"
|
||||||
|
isOpen={isEditDialogOpen}
|
||||||
|
onClose={() => setIsEditDialogOpen(false)}
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
>
|
||||||
|
<CurrencyFormEdit defaultValues={selectedItem} />
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<DataTable data={finalData} columns={columns} />
|
<DataTable
|
||||||
|
data={finalData}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={(id) => handleSelectItem(id)}
|
||||||
|
selectedRow={selectedItem?.id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -4,19 +4,15 @@ import { DialogForm } from '../layout/DialogForm';
|
|||||||
import { DataTable } from '../layout/DataTable';
|
import { DataTable } from '../layout/DataTable';
|
||||||
import { usePeriods } from '@/hooks/usePeriods';
|
import { usePeriods } from '@/hooks/usePeriods';
|
||||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
||||||
import type {
|
import type { PeriodBindingModel } from '@/types/types';
|
||||||
PeriodBindingModel,
|
|
||||||
StorekeeperBindingModel,
|
|
||||||
} from '@/types/types';
|
|
||||||
import type { ColumnDef } from '../layout/DataTable';
|
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 {
|
interface PeriodTableData extends PeriodBindingModel {
|
||||||
storekeeperName: string;
|
storekeeperName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяем столбцы
|
|
||||||
const columns: ColumnDef<PeriodTableData>[] = [
|
const columns: ColumnDef<PeriodTableData>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'id',
|
accessorKey: 'id',
|
||||||
@@ -25,10 +21,12 @@ const columns: ColumnDef<PeriodTableData>[] = [
|
|||||||
{
|
{
|
||||||
accessorKey: 'startTime',
|
accessorKey: 'startTime',
|
||||||
header: 'Время начала',
|
header: 'Время начала',
|
||||||
|
renderCell: (item) => new Date(item.startTime).toLocaleDateString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'endTime',
|
accessorKey: 'endTime',
|
||||||
header: 'Время окончания',
|
header: 'Время окончания',
|
||||||
|
renderCell: (item) => new Date(item.endTime).toLocaleDateString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'storekeeperName',
|
accessorKey: 'storekeeperName',
|
||||||
@@ -37,7 +35,8 @@ const columns: ColumnDef<PeriodTableData>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const Periods = (): React.JSX.Element => {
|
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 { storekeepers } = useStorekeepers();
|
||||||
|
|
||||||
const finalData = React.useMemo(() => {
|
const finalData = React.useMemo(() => {
|
||||||
@@ -60,11 +59,49 @@ export const Periods = (): React.JSX.Element => {
|
|||||||
});
|
});
|
||||||
}, [periods, storekeepers]);
|
}, [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) => {
|
const handleAdd = (data: PeriodBindingModel) => {
|
||||||
console.log(data);
|
|
||||||
createPeriod(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) {
|
if (isLoading) {
|
||||||
@@ -83,22 +120,43 @@ export const Periods = (): React.JSX.Element => {
|
|||||||
<main className="flex-1 flex relative">
|
<main className="flex-1 flex relative">
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
onAddClick={() => {
|
onAddClick={() => {
|
||||||
setIsDialogOpen(true);
|
setIsAddDialogOpen(true);
|
||||||
|
}}
|
||||||
|
onEditClick={() => {
|
||||||
|
openEditForm();
|
||||||
}}
|
}}
|
||||||
onEditClick={() => {}}
|
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
<DialogForm<PeriodBindingModel>
|
<DialogForm<PeriodBindingModel>
|
||||||
title="Форма"
|
title="Форма сроков"
|
||||||
description="Описание"
|
description="Добавить сроки"
|
||||||
isOpen={isDialogOpen}
|
isOpen={isAddDialogOpen}
|
||||||
onClose={() => setIsDialogOpen(false)}
|
onClose={() => setIsAddDialogOpen(false)}
|
||||||
onSubmit={handleAdd}
|
onSubmit={handleAdd}
|
||||||
>
|
>
|
||||||
<PeriodForm />
|
<PeriodFormAdd onSubmit={handleAdd} />
|
||||||
</DialogForm>
|
</DialogForm>
|
||||||
|
{selectedItem && (
|
||||||
|
<DialogForm<PeriodBindingModel>
|
||||||
|
title="Форма сроков"
|
||||||
|
description="Изменить сроки"
|
||||||
|
isOpen={isEditDialogOpen}
|
||||||
|
onClose={() => setIsEditDialogOpen(false)}
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
>
|
||||||
|
<PeriodFormEdit
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
defaultValues={selectedItem}
|
||||||
|
/>
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<DataTable data={finalData} columns={columns} />
|
<DataTable
|
||||||
|
data={finalData}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={(id) => handleSelectItem(id)}
|
||||||
|
selectedRow={selectedItem?.id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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 (
|
return (
|
||||||
<main className="container mx-auto py-10">
|
<main className="container mx-auto py-10">
|
||||||
<h1 className="text-2xl font-bold mb-6">Кладовщики</h1>
|
<h1 className="text-2xl font-bold mb-6">Кладовщики</h1>
|
||||||
<DataTable data={storekeepers || []} columns={columns} />
|
<DataTable
|
||||||
|
data={storekeepers || []}
|
||||||
|
columns={columns}
|
||||||
|
onRowSelected={console.log}
|
||||||
|
/>
|
||||||
</main>
|
</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 { storekeepersApi } from '@/api/api';
|
||||||
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export const useStorekeepers = () => {
|
export const useStorekeepers = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const setAuth = useAuthStore((store) => store.setAuth);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: storekeepers,
|
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,
|
mutationFn: storekeepersApi.update,
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['storekeepers'] });
|
queryClient.invalidateQueries({ queryKey: ['storekeepers'] });
|
||||||
@@ -35,8 +44,12 @@ export const useStorekeepers = () => {
|
|||||||
error: loginError,
|
error: loginError,
|
||||||
} = useMutation({
|
} = useMutation({
|
||||||
mutationFn: storekeepersApi.login,
|
mutationFn: storekeepersApi.login,
|
||||||
onSuccess: () => {
|
onSuccess: (userData) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['storekeepers'] });
|
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,
|
isUpdateError,
|
||||||
isLoginSuccess,
|
isLoginSuccess,
|
||||||
loginError,
|
loginError,
|
||||||
|
updateError,
|
||||||
error,
|
error,
|
||||||
createStorekeeper,
|
createStorekeeper,
|
||||||
loginStorekeeper,
|
loginStorekeeper,
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ import { StrictMode } from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App.tsx';
|
import App from './App.tsx';
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
import {
|
||||||
|
createBrowserRouter,
|
||||||
|
Navigate,
|
||||||
|
RouterProvider,
|
||||||
|
} from 'react-router-dom';
|
||||||
import { Currencies } from './components/pages/Currencies.tsx';
|
import { Currencies } from './components/pages/Currencies.tsx';
|
||||||
import { CreditPrograms } from './components/pages/CreditPrograms.tsx';
|
import { CreditPrograms } from './components/pages/CreditPrograms.tsx';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { AuthStorekeeper } from './components/pages/AuthStorekeeper.tsx';
|
import { AuthStorekeeper } from './components/pages/AuthStorekeeper.tsx';
|
||||||
import { Storekeepers } from './components/pages/Storekeepers.tsx';
|
import { Storekeepers } from './components/pages/Storekeepers.tsx';
|
||||||
import { Periods } from './components/pages/Periods.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([
|
const routes = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -31,6 +37,10 @@ const routes = createBrowserRouter([
|
|||||||
path: '/periods',
|
path: '/periods',
|
||||||
element: <Periods />,
|
element: <Periods />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
element: <Profile />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
errorElement: <p>бля пизда рулям</p>,
|
errorElement: <p>бля пизда рулям</p>,
|
||||||
},
|
},
|
||||||
@@ -38,6 +48,10 @@ const routes = createBrowserRouter([
|
|||||||
path: '/auth',
|
path: '/auth',
|
||||||
element: <AuthStorekeeper />,
|
element: <AuthStorekeeper />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '*',
|
||||||
|
element: <Navigate to="/" replace />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@@ -46,6 +60,7 @@ createRoot(document.getElementById('root')!).render(
|
|||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<RouterProvider router={routes} />
|
<RouterProvider router={routes} />
|
||||||
|
<Toaster />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</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