feat: clerk ui, частично не рабочий из-за маппингов
This commit is contained in:
BIN
TheBank/bankuiclerk/public/clerk.jpg
Normal file
BIN
TheBank/bankuiclerk/public/clerk.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
@@ -1,9 +1,10 @@
|
|||||||
import { useAuthCheck } from '@/hooks/useAuthCheck';
|
import { useAuthCheck } from '@/hooks/useAuthCheck';
|
||||||
import { useAuthStore } from '@/store/workerStore';
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
import { Link, Navigate, Outlet, useLocation } from 'react-router-dom';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { Footer } from '@/components/layout/Footer';
|
import { Footer } from '@/components/layout/Footer';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
|
import { Button } from './components/ui/button';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const user = useAuthStore((store) => store.user);
|
const user = useAuthStore((store) => store.user);
|
||||||
@@ -24,7 +25,21 @@ function App() {
|
|||||||
<Header />
|
<Header />
|
||||||
<Suspense fallback={<p>Loading...</p>}>
|
<Suspense fallback={<p>Loading...</p>}>
|
||||||
{location.pathname === '/' && (
|
{location.pathname === '/' && (
|
||||||
<main>Удобный сервис для работы клерков</main>
|
<main>
|
||||||
|
<div>Удобный сервис для работы клерков</div>
|
||||||
|
<div className="w-full h-full flex">
|
||||||
|
<div className="">
|
||||||
|
<img
|
||||||
|
className="max-w-[65%]"
|
||||||
|
src="/clerk.jpg"
|
||||||
|
alt="Клерк улыбается"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Link className="block my-auto" to="/clerks">
|
||||||
|
<Button>Работать!</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
)}
|
)}
|
||||||
{location.pathname !== '/' && <Outlet />}
|
{location.pathname !== '/' && <Outlet />}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -0,0 +1,248 @@
|
|||||||
|
import React, { useEffect, useMemo } 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 { ReplenishmentBindingModel } from '@/types/types';
|
||||||
|
import { useAuthStore } from '@/store/workerStore';
|
||||||
|
import { useDeposits } from '@/hooks/useDeposits';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@/components/ui/select';
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { CalendarIcon } from 'lucide-react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from '@/components/ui/popover';
|
||||||
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
|
||||||
|
type BaseFormValues = {
|
||||||
|
id?: string;
|
||||||
|
amount: number;
|
||||||
|
date: Date;
|
||||||
|
depositId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EditFormValues = {
|
||||||
|
id?: string;
|
||||||
|
amount?: number;
|
||||||
|
date?: Date;
|
||||||
|
depositId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
amount: z.coerce.number().min(0.01, 'Сумма пополнения должна быть больше 0'),
|
||||||
|
date: z.date({
|
||||||
|
required_error: 'Укажите дату пополнения',
|
||||||
|
invalid_type_error: 'Неверный формат даты',
|
||||||
|
}),
|
||||||
|
depositId: z.string().min(1, 'Выберите вклад'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const addSchema = baseSchema;
|
||||||
|
|
||||||
|
const editSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
amount: z.coerce
|
||||||
|
.number()
|
||||||
|
.min(0.01, 'Сумма пополнения должна быть больше 0')
|
||||||
|
.optional(),
|
||||||
|
date: z
|
||||||
|
.date({
|
||||||
|
required_error: 'Укажите дату пополнения',
|
||||||
|
invalid_type_error: 'Неверный формат даты',
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
depositId: z.string().min(1, 'Выберите вклад').optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
interface BaseReplenishmentFormProps {
|
||||||
|
onSubmit: (data: ReplenishmentBindingModel) => void;
|
||||||
|
schema: z.ZodType<BaseFormValues | EditFormValues>;
|
||||||
|
defaultValues?: Partial<ReplenishmentBindingModel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseReplenishmentForm = ({
|
||||||
|
onSubmit,
|
||||||
|
schema,
|
||||||
|
defaultValues,
|
||||||
|
}: BaseReplenishmentFormProps): React.JSX.Element => {
|
||||||
|
const { deposits } = useDeposits();
|
||||||
|
|
||||||
|
const form = useForm<BaseFormValues | EditFormValues>({
|
||||||
|
resolver: zodResolver(schema),
|
||||||
|
defaultValues: {
|
||||||
|
id: defaultValues?.id || '',
|
||||||
|
amount: defaultValues?.amount || 0,
|
||||||
|
date: defaultValues?.date ? new Date(defaultValues.date) : new Date(), // Ensure Date object
|
||||||
|
depositId: defaultValues?.depositId || '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (defaultValues) {
|
||||||
|
form.reset({
|
||||||
|
id: defaultValues.id || '',
|
||||||
|
amount: defaultValues.amount || 0,
|
||||||
|
date: defaultValues.date ? new Date(defaultValues.date) : new Date(), // Ensure Date object
|
||||||
|
depositId: defaultValues.depositId || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [defaultValues, form]);
|
||||||
|
|
||||||
|
const clerk = useAuthStore((store) => store.user);
|
||||||
|
|
||||||
|
const handleSubmit = (data: BaseFormValues | EditFormValues) => {
|
||||||
|
const payload: ReplenishmentBindingModel = {
|
||||||
|
id: data.id || crypto.randomUUID(),
|
||||||
|
clerkId: clerk?.id,
|
||||||
|
amount: 'amount' in data && data.amount !== undefined ? data.amount : 0,
|
||||||
|
date: 'date' in data && data.date !== undefined ? data.date : new Date(),
|
||||||
|
depositId:
|
||||||
|
'depositId' in data && data.depositId !== undefined
|
||||||
|
? data.depositId
|
||||||
|
: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
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="amount"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Сумма</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Введите сумму пополнения"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="date"
|
||||||
|
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="depositId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Вклад</FormLabel>
|
||||||
|
<Select onValueChange={field.onChange} value={field.value || ''}>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Выберите вклад" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{deposits?.map((deposit) => (
|
||||||
|
<SelectItem key={deposit.id} value={deposit.id || ''}>
|
||||||
|
{`Вклад ${deposit.interestRate}% - ${deposit.cost}₽`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="w-full">
|
||||||
|
Сохранить
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReplenishmentFormAdd = ({
|
||||||
|
onSubmit,
|
||||||
|
}: {
|
||||||
|
onSubmit: (data: ReplenishmentBindingModel) => void;
|
||||||
|
}): React.JSX.Element => {
|
||||||
|
return <BaseReplenishmentForm onSubmit={onSubmit} schema={addSchema} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ReplenishmentFormEdit = ({
|
||||||
|
onSubmit,
|
||||||
|
defaultValues,
|
||||||
|
}: {
|
||||||
|
onSubmit: (data: ReplenishmentBindingModel) => void;
|
||||||
|
defaultValues: Partial<ReplenishmentBindingModel>;
|
||||||
|
}): React.JSX.Element => {
|
||||||
|
return (
|
||||||
|
<BaseReplenishmentForm
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
schema={editSchema}
|
||||||
|
defaultValues={defaultValues}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,25 +1,38 @@
|
|||||||
import { useReplenishments } from '@/hooks/useReplenishments';
|
import { useReplenishments } from '@/hooks/useReplenishments';
|
||||||
|
import { useClerks } from '@/hooks/useClerks';
|
||||||
|
import { useDeposits } from '@/hooks/useDeposits';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { AppSidebar } from '../layout/Sidebar';
|
import { AppSidebar } from '../layout/Sidebar';
|
||||||
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
||||||
import type { ReplenishmentBindingModel } from '@/types/types';
|
import type { ReplenishmentBindingModel } from '@/types/types';
|
||||||
|
import { DialogForm } from '../layout/DialogForm';
|
||||||
|
import {
|
||||||
|
ReplenishmentFormAdd,
|
||||||
|
ReplenishmentFormEdit,
|
||||||
|
} from '../features/ReplenishmentForm';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
const columns: ColumnDef<ReplenishmentBindingModel>[] = [
|
type ReplenishmentRowData = ReplenishmentBindingModel & {
|
||||||
|
clerkName: string;
|
||||||
|
depositDisplay: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns: ColumnDef<ReplenishmentRowData>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'id',
|
accessorKey: 'id',
|
||||||
header: 'ID',
|
header: 'ID',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'amout',
|
accessorKey: 'amount',
|
||||||
header: 'Сумма',
|
header: 'Сумма',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'date',
|
accessorKey: 'date',
|
||||||
header: 'Стоимость',
|
header: 'Дата',
|
||||||
renderCell: (item) => new Date(item.date).toLocaleDateString(),
|
renderCell: (item) => new Date(item.date).toLocaleDateString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: 'depositName',
|
accessorKey: 'depositDisplay',
|
||||||
header: 'Вклад',
|
header: 'Вклад',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -29,16 +42,95 @@ const columns: ColumnDef<ReplenishmentBindingModel>[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export const Replenishments = (): React.JSX.Element => {
|
export const Replenishments = (): React.JSX.Element => {
|
||||||
const { replenishments, isLoading, error } = useReplenishments();
|
const {
|
||||||
|
replenishments,
|
||||||
|
isLoading: isReplenishmentsLoading,
|
||||||
|
error: replenishmentsError,
|
||||||
|
createReplenishment,
|
||||||
|
updateReplenishment,
|
||||||
|
} = useReplenishments();
|
||||||
|
const {
|
||||||
|
clerks,
|
||||||
|
isLoading: isClerksLoading,
|
||||||
|
error: clerksError,
|
||||||
|
} = useClerks();
|
||||||
|
const {
|
||||||
|
deposits,
|
||||||
|
isLoading: isDepositsLoading,
|
||||||
|
error: depositsError,
|
||||||
|
} = useDeposits();
|
||||||
|
|
||||||
if (isLoading) {
|
const [isAddDialogOpen, setIsAddDialogOpen] = React.useState<boolean>(false);
|
||||||
|
const [isEditDialogOpen, setIsEditDialogOpen] =
|
||||||
|
React.useState<boolean>(false);
|
||||||
|
const [selectedItem, setSelectedItem] = React.useState<
|
||||||
|
ReplenishmentBindingModel | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const finalData = React.useMemo(() => {
|
||||||
|
if (!replenishments || !clerks || !deposits) return [];
|
||||||
|
|
||||||
|
return replenishments.map((replenishment) => {
|
||||||
|
const clerk = clerks.find((c) => c.id === replenishment.clerkId);
|
||||||
|
const deposit = deposits.find((d) => d.id === replenishment.depositId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...replenishment,
|
||||||
|
clerkName: clerk ? `${clerk.name} ${clerk.surname}` : 'Неизвестно',
|
||||||
|
depositDisplay: deposit
|
||||||
|
? `Вклад ${deposit.interestRate}% - ${deposit.cost}₽`
|
||||||
|
: 'Неизвестно',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, [replenishments, clerks, deposits]);
|
||||||
|
|
||||||
|
const handleAdd = (data: ReplenishmentBindingModel) => {
|
||||||
|
createReplenishment(data);
|
||||||
|
setIsAddDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (data: ReplenishmentBindingModel) => {
|
||||||
|
if (selectedItem) {
|
||||||
|
updateReplenishment({
|
||||||
|
...selectedItem,
|
||||||
|
...data,
|
||||||
|
});
|
||||||
|
setIsEditDialogOpen(false);
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectItem = (id: string | undefined) => {
|
||||||
|
const item = replenishments?.find((p) => p.id === id);
|
||||||
|
if (item) {
|
||||||
|
setSelectedItem({
|
||||||
|
...item,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditForm = () => {
|
||||||
|
if (!selectedItem) {
|
||||||
|
toast('Выберите элемент для редактирования');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isReplenishmentsLoading || isClerksLoading || isDepositsLoading) {
|
||||||
return <main className="container mx-auto py-10">Загрузка...</main>;
|
return <main className="container mx-auto py-10">Загрузка...</main>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (replenishmentsError || clerksError || depositsError) {
|
||||||
return (
|
return (
|
||||||
<main className="container mx-auto py-10">
|
<main className="container mx-auto py-10">
|
||||||
Ошибка загрузки данных: {error.message}
|
Ошибка загрузки данных:{' '}
|
||||||
|
{replenishmentsError?.message ||
|
||||||
|
clerksError?.message ||
|
||||||
|
depositsError?.message}
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -47,18 +139,41 @@ export const Replenishments = (): React.JSX.Element => {
|
|||||||
<main className="flex-1 flex relative">
|
<main className="flex-1 flex relative">
|
||||||
<AppSidebar
|
<AppSidebar
|
||||||
onAddClick={() => {
|
onAddClick={() => {
|
||||||
// setIsAddDialogOpen(true);
|
setIsAddDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
onEditClick={() => {
|
onEditClick={() => {
|
||||||
// openEditForm();
|
openEditForm();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 p-4">
|
<div className="flex-1 p-4">
|
||||||
|
{!selectedItem && (
|
||||||
|
<DialogForm<ReplenishmentBindingModel>
|
||||||
|
title="Форма пополнений"
|
||||||
|
description="Добавить пополнение"
|
||||||
|
isOpen={isAddDialogOpen}
|
||||||
|
onClose={() => setIsAddDialogOpen(false)}
|
||||||
|
onSubmit={handleAdd}
|
||||||
|
>
|
||||||
|
<ReplenishmentFormAdd />
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
|
{selectedItem && (
|
||||||
|
<DialogForm<ReplenishmentBindingModel>
|
||||||
|
title="Форма пополнений"
|
||||||
|
description="Изменить данные"
|
||||||
|
isOpen={isEditDialogOpen}
|
||||||
|
onClose={() => setIsEditDialogOpen(false)}
|
||||||
|
onSubmit={handleEdit}
|
||||||
|
>
|
||||||
|
<ReplenishmentFormEdit defaultValues={selectedItem} />
|
||||||
|
</DialogForm>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<DataTable
|
<DataTable
|
||||||
data={replenishments || []}
|
data={finalData}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
onRowSelected={console.log}
|
onRowSelected={(id) => handleSelectItem(id)}
|
||||||
|
selectedRow={selectedItem?.id}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user