feat: первая простенькая версия ui для кладовщика

This commit is contained in:
2025-05-20 00:00:45 +04:00
parent 1c0bf1efd2
commit 9ed33690cf
26 changed files with 844 additions and 310 deletions

View File

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

View File

@@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace BankDatabase.Models;
namespace BankDatabase.Models;
public class CreditProgramCurrency
{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

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