feat: clerk ui, частично не рабочий из-за маппингов

This commit is contained in:
2025-05-21 17:58:54 +04:00
parent 57f878a051
commit 8e930475a3
4 changed files with 392 additions and 14 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

View File

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

View File

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

View File

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