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 { 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 { Footer } from '@/components/layout/Footer';
|
||||
import { Suspense } from 'react';
|
||||
import { Button } from './components/ui/button';
|
||||
|
||||
function App() {
|
||||
const user = useAuthStore((store) => store.user);
|
||||
@@ -24,7 +25,21 @@ function App() {
|
||||
<Header />
|
||||
<Suspense fallback={<p>Loading...</p>}>
|
||||
{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 />}
|
||||
</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 { useClerks } from '@/hooks/useClerks';
|
||||
import { useDeposits } from '@/hooks/useDeposits';
|
||||
import React from 'react';
|
||||
import { AppSidebar } from '../layout/Sidebar';
|
||||
import { DataTable, type ColumnDef } from '../layout/DataTable';
|
||||
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',
|
||||
header: 'ID',
|
||||
},
|
||||
{
|
||||
accessorKey: 'amout',
|
||||
accessorKey: 'amount',
|
||||
header: 'Сумма',
|
||||
},
|
||||
{
|
||||
accessorKey: 'date',
|
||||
header: 'Стоимость',
|
||||
header: 'Дата',
|
||||
renderCell: (item) => new Date(item.date).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
accessorKey: 'depositName',
|
||||
accessorKey: 'depositDisplay',
|
||||
header: 'Вклад',
|
||||
},
|
||||
{
|
||||
@@ -29,16 +42,95 @@ const columns: ColumnDef<ReplenishmentBindingModel>[] = [
|
||||
];
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (replenishmentsError || clerksError || depositsError) {
|
||||
return (
|
||||
<main className="container mx-auto py-10">
|
||||
Ошибка загрузки данных: {error.message}
|
||||
Ошибка загрузки данных:{' '}
|
||||
{replenishmentsError?.message ||
|
||||
clerksError?.message ||
|
||||
depositsError?.message}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -47,18 +139,41 @@ export const Replenishments = (): React.JSX.Element => {
|
||||
<main className="flex-1 flex relative">
|
||||
<AppSidebar
|
||||
onAddClick={() => {
|
||||
// setIsAddDialogOpen(true);
|
||||
setIsAddDialogOpen(true);
|
||||
}}
|
||||
onEditClick={() => {
|
||||
// openEditForm();
|
||||
openEditForm();
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
<DataTable
|
||||
data={replenishments || []}
|
||||
data={finalData}
|
||||
columns={columns}
|
||||
onRowSelected={console.log}
|
||||
onRowSelected={(id) => handleSelectItem(id)}
|
||||
selectedRow={selectedItem?.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user