feat: немного поменял маппинг, сделал отображение нормальное без View модели с фио, для кладовщика основные формы и создание готово, осталось редактирования
This commit is contained in:
@@ -4,14 +4,9 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace BankDatabase;
|
||||
|
||||
public class BankDbContext : DbContext
|
||||
internal class BankDbContext(IConfigurationDatabase configurationDatabase) : DbContext
|
||||
{
|
||||
private readonly IConfigurationDatabase? _configurationDatabase;
|
||||
|
||||
public BankDbContext(IConfigurationDatabase configurationDatabase)
|
||||
{
|
||||
_configurationDatabase = configurationDatabase;
|
||||
}
|
||||
private readonly IConfigurationDatabase? _configurationDatabase = configurationDatabase;
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
|
||||
@@ -21,9 +21,17 @@ public class PeriodAdapter : IPeriodAdapter
|
||||
{
|
||||
_periodBusinessLogicContract = periodBusinessLogicContract;
|
||||
_logger = logger;
|
||||
var config = new MapperConfiguration(cfg =>
|
||||
var config = new MapperConfiguration(cfg =>
|
||||
{
|
||||
cfg.CreateMap<PeriodBindingModel, PeriodDataModel>();
|
||||
cfg.CreateMap<PeriodBindingModel, PeriodDataModel>()
|
||||
.ConstructUsing(src => new PeriodDataModel(
|
||||
src.Id,
|
||||
src.StartTime,
|
||||
src.EndTime,
|
||||
src.StorekeeperId
|
||||
));
|
||||
|
||||
// Маппинг PeriodDataModel -> PeriodViewModel
|
||||
cfg.CreateMap<PeriodDataModel, PeriodViewModel>();
|
||||
});
|
||||
_mapper = new Mapper(config);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using BankContracts.AdapterContracts;
|
||||
using BankContracts.BindingModels;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace BankWebApi.Controllers;
|
||||
@@ -10,7 +9,7 @@ namespace BankWebApi.Controllers;
|
||||
[Route("api/[controller]/[action]")]
|
||||
[ApiController]
|
||||
[Produces("application/json")]
|
||||
public class PeriodController(IPeriodAdapter adapter) : ControllerBase
|
||||
public class PeriodsController(IPeriodAdapter adapter) : ControllerBase
|
||||
{
|
||||
private readonly IPeriodAdapter _adapter = adapter;
|
||||
|
||||
@@ -68,7 +68,13 @@ public class StorekeepersController(IStorekeeperAdapter adapter) : ControllerBas
|
||||
{
|
||||
var res = _adapter.Login(model, out string token);
|
||||
|
||||
Response.Cookies.Append(AuthOptions.CookieName, token);
|
||||
Response.Cookies.Append(AuthOptions.CookieName, token, new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
SameSite = SameSiteMode.None,
|
||||
Secure = true,
|
||||
Expires = DateTime.UtcNow.AddDays(2)
|
||||
});
|
||||
|
||||
return res.GetResponse(Request, Response);
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ app.UseHttpsRedirection();
|
||||
app.UseCookiePolicy(new CookiePolicyOptions
|
||||
{
|
||||
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
|
||||
Secure = CookieSecurePolicy.Always
|
||||
Secure = app.Environment.IsProduction() ? CookieSecurePolicy.Always : CookieSecurePolicy.None
|
||||
});
|
||||
|
||||
app.UseAuthentication();
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
VITE_API_URL=https://localhost:7204
|
||||
VITE_API_URL=https://localhost:7204
|
||||
# VITE_API_URL=http://localhost:5189
|
||||
Binary file not shown.
@@ -17,6 +17,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.14",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-menubar": "^1.1.14",
|
||||
"@radix-ui/react-popover": "^1.1.13",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-separator": "^1.1.6",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
@@ -26,9 +27,11 @@
|
||||
"@tanstack/react-query": "^5.76.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.4",
|
||||
"react-router-dom": "^7.6.0",
|
||||
|
||||
@@ -77,15 +77,15 @@ export const depositsApi = {
|
||||
|
||||
// Periods API
|
||||
export const periodsApi = {
|
||||
getAll: () => getData<PeriodBindingModel>('api/Period/GetAllRecords'),
|
||||
getAll: () => getData<PeriodBindingModel>('api/Periods/GetAllRecords'),
|
||||
getById: (id: string) =>
|
||||
getData<PeriodBindingModel>(`api/Period/GetRecord/${id}`),
|
||||
getData<PeriodBindingModel>(`api/Periods/GetRecord/${id}`),
|
||||
getByStorekeeper: (storekeeperId: string) =>
|
||||
getData<PeriodBindingModel>(
|
||||
`api/Period/GetRecordByStorekeeper/${storekeeperId}`,
|
||||
),
|
||||
create: (data: PeriodBindingModel) => postData('api/Period/Register', data),
|
||||
update: (data: PeriodBindingModel) => putData('api/Period/ChangeInfo', data),
|
||||
create: (data: PeriodBindingModel) => postData('api/Periods/Register', data),
|
||||
update: (data: PeriodBindingModel) => putData('api/Periods/ChangeInfo', data),
|
||||
};
|
||||
|
||||
// Replenishments API
|
||||
@@ -110,8 +110,7 @@ export const replenishmentsApi = {
|
||||
|
||||
// Storekeepers API
|
||||
export const storekeepersApi = {
|
||||
getAll: () =>
|
||||
getData<StorekeeperBindingModel>('api/Storekeepers/GetAllRecords'),
|
||||
getAll: () => getData<StorekeeperBindingModel>('api/storekeepers'),
|
||||
getById: (id: string) =>
|
||||
getData<StorekeeperBindingModel>(`api/Storekeepers/GetRecord/${id}`),
|
||||
create: (data: StorekeeperBindingModel) =>
|
||||
|
||||
@@ -4,9 +4,6 @@ const API_URL = ConfigManager.loadUrl();
|
||||
|
||||
export async function getData<T>(path: string): Promise<T[]> {
|
||||
const res = await fetch(`${API_URL}/${path}`, {
|
||||
headers: {
|
||||
mode: 'no-cors',
|
||||
},
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) {
|
||||
@@ -21,7 +18,7 @@ export async function postData<T>(path: string, data: T) {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
mode: 'no-cors',
|
||||
// mode: 'no-cors',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
@@ -36,7 +33,7 @@ export async function putData<T>(path: string, data: T) {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
mode: 'no-cors',
|
||||
// mode: 'no-cors',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(data),
|
||||
|
||||
165
TheBank/bankui/src/components/features/CurrencyForm.tsx
Normal file
165
TheBank/bankui/src/components/features/CurrencyForm.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React 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 {
|
||||
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'; // Импорт хука для кладовщиков
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
export const CurrencyForm = ({
|
||||
onSubmit,
|
||||
}: CurrencyFormProps): React.JSX.Element => {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
id: '',
|
||||
name: '',
|
||||
abbreviation: '',
|
||||
cost: 0,
|
||||
storekeeperId: '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
storekeepers,
|
||||
isLoading: isLoadingStorekeepers,
|
||||
error: storekeepersError,
|
||||
} = useStorekeepers(); // Получаем данные кладовщиков
|
||||
|
||||
const handleSubmit = (data: FormValues) => {
|
||||
const payload: CurrencyBindingModel = {
|
||||
...data,
|
||||
id: data.id || crypto.randomUUID(),
|
||||
};
|
||||
|
||||
onSubmit(payload);
|
||||
};
|
||||
|
||||
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="abbreviation"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Аббревиатура</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Например, USD" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="cost"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Стоимость</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="number" placeholder="Например, 1.0" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</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>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
213
TheBank/bankui/src/components/features/PeriodForm.tsx
Normal file
213
TheBank/bankui/src/components/features/PeriodForm.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import React 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 {
|
||||
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';
|
||||
import { format } from 'date-fns';
|
||||
import { CalendarIcon } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
||||
|
||||
const formSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
startTime: z.date({
|
||||
required_error: 'Укажите время начала',
|
||||
invalid_type_error: 'Неверный формат даты',
|
||||
}),
|
||||
endTime: z.date({
|
||||
required_error: 'Укажите время окончания',
|
||||
invalid_type_error: 'Неверный формат даты',
|
||||
}),
|
||||
storekeeperId: z.string().min(1, 'Выберите кладовщика'),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
type PeriodFormProps = {
|
||||
onSubmit: (data: PeriodBindingModel) => void;
|
||||
};
|
||||
|
||||
export const PeriodForm = ({
|
||||
onSubmit,
|
||||
}: PeriodFormProps): React.JSX.Element => {
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
id: '',
|
||||
storekeeperId: '',
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
storekeepers,
|
||||
isLoading: isLoadingStorekeepers,
|
||||
error: storekeepersError,
|
||||
} = useStorekeepers();
|
||||
|
||||
const handleSubmit = (data: FormValues) => {
|
||||
const payload: PeriodBindingModel = {
|
||||
...data,
|
||||
id: data.id || crypto.randomUUID(),
|
||||
startTime: data.startTime,
|
||||
endTime: data.endTime,
|
||||
};
|
||||
|
||||
onSubmit(payload);
|
||||
};
|
||||
|
||||
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="startTime"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Время начала</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className={cn(
|
||||
'w-full pl-3 text-left font-normal',
|
||||
!field.value && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, 'PPP')
|
||||
) : (
|
||||
<span>Выберите дату</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="endTime"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Время окончания</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className={cn(
|
||||
'w-full pl-3 text-left font-normal',
|
||||
!field.value && 'text-muted-foreground',
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, 'PPP')
|
||||
) : (
|
||||
<span>Выберите дату</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormMessage />
|
||||
</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>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -6,10 +6,17 @@ 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';
|
||||
|
||||
export const AuthStorekeeper = (): React.JSX.Element => {
|
||||
const { createStorekeeper, loginStorekeeper, isLoginError, loginError } =
|
||||
useStorekeepers();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
createStorekeeper,
|
||||
loginStorekeeper,
|
||||
isLoginError,
|
||||
loginError,
|
||||
isLoginSuccess,
|
||||
} = useStorekeepers();
|
||||
|
||||
const handleRegister = (data: StorekeeperBindingModel) => {
|
||||
console.log(data);
|
||||
@@ -26,6 +33,12 @@ export const AuthStorekeeper = (): React.JSX.Element => {
|
||||
}
|
||||
}, [isLoginError, loginError]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isLoginSuccess) {
|
||||
navigate('/storekeepers');
|
||||
}
|
||||
}, [isLoginSuccess]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex flex-col justify-center items-center">
|
||||
|
||||
@@ -5,6 +5,34 @@ import { DialogForm } from '../layout/DialogForm';
|
||||
import { DataTable } from '../layout/DataTable';
|
||||
import { CreditProgramForm } from '../features/CreditProgramForm';
|
||||
import type { CreditProgramBindingModel } from '@/types/types';
|
||||
import type { ColumnDef } from '../layout/DataTable';
|
||||
|
||||
const columns: ColumnDef<CreditProgramBindingModel>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Название',
|
||||
},
|
||||
{
|
||||
accessorKey: 'cost',
|
||||
header: 'Стоимость',
|
||||
},
|
||||
{
|
||||
accessorKey: 'maxCost',
|
||||
header: 'Макс. стоимость',
|
||||
},
|
||||
{
|
||||
accessorKey: 'storekeeperId',
|
||||
header: 'ID Кладовщика',
|
||||
},
|
||||
{
|
||||
accessorKey: 'periodId',
|
||||
header: 'ID Периода',
|
||||
},
|
||||
];
|
||||
|
||||
export const CreditPrograms = (): React.JSX.Element => {
|
||||
const {
|
||||
@@ -23,6 +51,18 @@ export const CreditPrograms = (): React.JSX.Element => {
|
||||
createCreditProgram(data);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
Ошибка загрузки: {error?.message}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex relative">
|
||||
<AppSidebar
|
||||
@@ -33,10 +73,6 @@ export const CreditPrograms = (): React.JSX.Element => {
|
||||
/>
|
||||
|
||||
<div className="flex-1 p-4">
|
||||
{isError && (
|
||||
<div className="text-red-500">Ошибка загрузки: {error?.message}</div>
|
||||
)}
|
||||
|
||||
<DialogForm<CreditProgramBindingModel>
|
||||
title="Форма"
|
||||
description="Описание"
|
||||
@@ -46,7 +82,7 @@ export const CreditPrograms = (): React.JSX.Element => {
|
||||
children={<CreditProgramForm />}
|
||||
/>
|
||||
<div className="">
|
||||
<DataTable data={[]} columns={[]} />
|
||||
<DataTable data={creditPrograms || []} columns={columns} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,5 +1,115 @@
|
||||
import React from 'react';
|
||||
import { AppSidebar } from '../layout/Sidebar';
|
||||
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 { ColumnDef } from '../layout/DataTable';
|
||||
import { CurrencyForm } from '../features/CurrencyForm';
|
||||
|
||||
interface CurrencyTableData extends CurrencyBindingModel {
|
||||
storekeeperName: string;
|
||||
}
|
||||
|
||||
const columns: ColumnDef<CurrencyTableData>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Название',
|
||||
},
|
||||
{
|
||||
accessorKey: 'abbreviation',
|
||||
header: 'Аббревиатура',
|
||||
},
|
||||
{
|
||||
accessorKey: 'cost',
|
||||
header: 'Стоимость',
|
||||
},
|
||||
{
|
||||
accessorKey: 'storekeeperName',
|
||||
header: 'Кладовщик',
|
||||
},
|
||||
];
|
||||
|
||||
export const Currencies = (): React.JSX.Element => {
|
||||
return <main className="">a</main>;
|
||||
const {
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
currencies,
|
||||
createCurrency,
|
||||
updateCurrency,
|
||||
} = useCurrencies();
|
||||
const { storekeepers } = useStorekeepers();
|
||||
|
||||
const finalData = React.useMemo(() => {
|
||||
if (!currencies || !storekeepers) return [];
|
||||
|
||||
return currencies.map((currency) => {
|
||||
const storekeeper = storekeepers.find(
|
||||
(s) => s.id === currency.storekeeperId,
|
||||
);
|
||||
const storekeeperName = storekeeper
|
||||
? [storekeeper.surname, storekeeper.name, storekeeper.middleName]
|
||||
.filter(Boolean)
|
||||
.join(' ') || 'Неизвестный кладовщик'
|
||||
: 'Неизвестный кладовщик';
|
||||
|
||||
return {
|
||||
...currency,
|
||||
storekeeperName,
|
||||
};
|
||||
});
|
||||
}, [currencies, storekeepers]);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(false);
|
||||
|
||||
const handleAdd = (data: CurrencyBindingModel) => {
|
||||
console.log(data);
|
||||
createCurrency(data);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
Ошибка загрузки: {error?.message}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex relative">
|
||||
<AppSidebar
|
||||
onAddClick={() => {
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
onEditClick={() => {}}
|
||||
/>
|
||||
<div className="flex-1 p-4">
|
||||
<DialogForm<CurrencyBindingModel>
|
||||
title="Форма валюты"
|
||||
description="Добавьте новую валюту"
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
onSubmit={handleAdd}
|
||||
>
|
||||
<CurrencyForm />
|
||||
</DialogForm>
|
||||
<div>
|
||||
<DataTable data={finalData} columns={columns} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
|
||||
106
TheBank/bankui/src/components/pages/Periods.tsx
Normal file
106
TheBank/bankui/src/components/pages/Periods.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { AppSidebar } from '../layout/Sidebar';
|
||||
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 { ColumnDef } from '../layout/DataTable';
|
||||
import { PeriodForm } from '../features/PeriodForm';
|
||||
|
||||
// Определяем расширенный тип для данных таблицы
|
||||
interface PeriodTableData extends PeriodBindingModel {
|
||||
storekeeperName: string;
|
||||
}
|
||||
|
||||
// Определяем столбцы
|
||||
const columns: ColumnDef<PeriodTableData>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'startTime',
|
||||
header: 'Время начала',
|
||||
},
|
||||
{
|
||||
accessorKey: 'endTime',
|
||||
header: 'Время окончания',
|
||||
},
|
||||
{
|
||||
accessorKey: 'storekeeperName',
|
||||
header: 'Кладовщик',
|
||||
},
|
||||
];
|
||||
|
||||
export const Periods = (): React.JSX.Element => {
|
||||
const { isLoading, isError, error, periods, createPeriod } = usePeriods();
|
||||
const { storekeepers } = useStorekeepers();
|
||||
|
||||
const finalData = React.useMemo(() => {
|
||||
if (!periods || !storekeepers) return [];
|
||||
|
||||
return periods.map((period) => {
|
||||
const storekeeper = storekeepers.find(
|
||||
(s) => s.id === period.storekeeperId,
|
||||
);
|
||||
const storekeeperName = storekeeper
|
||||
? [storekeeper.surname, storekeeper.name, storekeeper.middleName]
|
||||
.filter(Boolean)
|
||||
.join(' ') || 'Неизвестный кладовщик'
|
||||
: 'Неизвестный кладовщик';
|
||||
|
||||
return {
|
||||
...period,
|
||||
storekeeperName,
|
||||
};
|
||||
});
|
||||
}, [periods, storekeepers]);
|
||||
|
||||
const [isDialogOpen, setIsDialogOpen] = React.useState<boolean>(false);
|
||||
|
||||
const handleAdd = (data: PeriodBindingModel) => {
|
||||
console.log(data);
|
||||
createPeriod(data);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
Ошибка загрузки: {error?.message}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="flex-1 flex relative">
|
||||
<AppSidebar
|
||||
onAddClick={() => {
|
||||
setIsDialogOpen(true);
|
||||
}}
|
||||
onEditClick={() => {}}
|
||||
/>
|
||||
<div className="flex-1 p-4">
|
||||
<DialogForm<PeriodBindingModel>
|
||||
title="Форма"
|
||||
description="Описание"
|
||||
isOpen={isDialogOpen}
|
||||
onClose={() => setIsDialogOpen(false)}
|
||||
onSubmit={handleAdd}
|
||||
>
|
||||
<PeriodForm />
|
||||
</DialogForm>
|
||||
<div>
|
||||
<DataTable data={finalData} columns={columns} />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
};
|
||||
59
TheBank/bankui/src/components/pages/Storekeepers.tsx
Normal file
59
TheBank/bankui/src/components/pages/Storekeepers.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { DataTable } from '../layout/DataTable';
|
||||
import type { ColumnDef } from '../layout/DataTable';
|
||||
import { useStorekeepers } from '@/hooks/useStorekeepers';
|
||||
import type { StorekeeperBindingModel } from '@/types/types';
|
||||
|
||||
const columns: ColumnDef<StorekeeperBindingModel>[] = [
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Имя',
|
||||
},
|
||||
{
|
||||
accessorKey: 'surname',
|
||||
header: 'Фамилия',
|
||||
},
|
||||
{
|
||||
accessorKey: 'middleName',
|
||||
header: 'Отчество',
|
||||
},
|
||||
{
|
||||
accessorKey: 'login',
|
||||
header: 'Логин',
|
||||
},
|
||||
{
|
||||
accessorKey: 'email',
|
||||
header: 'Email',
|
||||
},
|
||||
{
|
||||
accessorKey: 'phoneNumber',
|
||||
header: 'Телефон',
|
||||
},
|
||||
];
|
||||
|
||||
export const Storekeepers = (): React.JSX.Element => {
|
||||
const { storekeepers, isLoading, error } = useStorekeepers();
|
||||
|
||||
if (isLoading) {
|
||||
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
Ошибка загрузки данных: {error.message}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
<h1 className="text-2xl font-bold mb-6">Кладовщики</h1>
|
||||
<DataTable data={storekeepers || []} columns={columns} />
|
||||
</main>
|
||||
);
|
||||
};
|
||||
73
TheBank/bankui/src/components/ui/calendar.tsx
Normal file
73
TheBank/bankui/src/components/ui/calendar.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import * as React from "react"
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker>) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row gap-2",
|
||||
month: "flex flex-col gap-4",
|
||||
caption: "flex justify-center pt-1 relative items-center w-full",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "flex items-center gap-1",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-x-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"size-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start:
|
||||
"day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_range_end:
|
||||
"day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ className, ...props }) => (
|
||||
<ChevronLeft className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
IconRight: ({ className, ...props }) => (
|
||||
<ChevronRight className={cn("size-4", className)} {...props} />
|
||||
),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Calendar }
|
||||
46
TheBank/bankui/src/components/ui/popover.tsx
Normal file
46
TheBank/bankui/src/components/ui/popover.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Popover({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
}
|
||||
|
||||
function PopoverTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverAnchor({
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
@@ -31,6 +31,7 @@ export const useStorekeepers = () => {
|
||||
const {
|
||||
mutate: loginStorekeeper,
|
||||
isError: isLoginError,
|
||||
isSuccess: isLoginSuccess,
|
||||
error: loginError,
|
||||
} = useMutation({
|
||||
mutationFn: storekeepersApi.login,
|
||||
@@ -45,6 +46,8 @@ export const useStorekeepers = () => {
|
||||
isGetAllError,
|
||||
isCreateError,
|
||||
isLoginError,
|
||||
isUpdateError,
|
||||
isLoginSuccess,
|
||||
loginError,
|
||||
error,
|
||||
createStorekeeper,
|
||||
|
||||
@@ -7,6 +7,8 @@ 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';
|
||||
|
||||
const routes = createBrowserRouter([
|
||||
{
|
||||
@@ -21,6 +23,14 @@ const routes = createBrowserRouter([
|
||||
path: '/credit-programs',
|
||||
element: <CreditPrograms />,
|
||||
},
|
||||
{
|
||||
path: '/storekeepers',
|
||||
element: <Storekeepers />,
|
||||
},
|
||||
{
|
||||
path: '/periods',
|
||||
element: <Periods />,
|
||||
},
|
||||
],
|
||||
errorElement: <p>бля пизда рулям</p>,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user