feat: немного поменял маппинг, сделал отображение нормальное без View модели с фио, для кладовщика основные формы и создание готово, осталось редактирования

This commit is contained in:
2025-05-19 02:21:13 +04:00
parent e8f493691f
commit 1c0bf1efd2
21 changed files with 875 additions and 33 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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