631 lines
24 KiB
TypeScript
631 lines
24 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
||
import { type ColumnDef } from '@tanstack/react-table'
|
||
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip } from 'recharts'
|
||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||
import { useForm } from 'react-hook-form'
|
||
import { z } from 'zod'
|
||
|
||
import { DataTable } from '@/components/data-table/data-table'
|
||
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
||
import { DealStageBadge, dealStageLabels } from '@/components/crm/deal-stage-badge'
|
||
import { DealStatusBadge, dealStatusLabels } from '@/components/crm/deal-status-badge'
|
||
import { Button } from '@/components/ui/button'
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||
import { Form, FormControl, FormDescription, 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 { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||
import { useToast } from '@/components/ui/use-toast'
|
||
import { useContactsQuery } from '@/features/contacts/hooks'
|
||
import { useCreateDealMutation, useDealsQuery, useUpdateDealMutation } from '@/features/deals/hooks'
|
||
import { formatCurrency, formatDate } from '@/lib/utils'
|
||
import type { Contact, Deal, DealStage, DealStatus } from '@/types/crm'
|
||
|
||
const dealStatusList: DealStatus[] = ['new', 'in_progress', 'won', 'lost']
|
||
const dealStageList: DealStage[] = ['qualification', 'proposal', 'negotiation', 'closed']
|
||
|
||
const dealCreateSchema = z.object({
|
||
title: z.string().min(3, 'Минимум 3 символа'),
|
||
contactId: z.string().min(1, 'Введите ID контакта'),
|
||
amount: z.string().optional(),
|
||
currency: z.string().min(3).max(3).optional(),
|
||
ownerId: z.string().optional(),
|
||
})
|
||
|
||
type DealCreateFormValues = z.infer<typeof dealCreateSchema>
|
||
|
||
const dealUpdateSchema = z.object({
|
||
status: z.enum(dealStatusList),
|
||
stage: z.enum(dealStageList),
|
||
amount: z.string().optional(),
|
||
currency: z.string().min(3).max(3).optional(),
|
||
})
|
||
|
||
type DealUpdateFormValues = z.infer<typeof dealUpdateSchema>
|
||
|
||
const mapAmount = (value?: string | null) => {
|
||
if (!value) return 0
|
||
const number = Number(value)
|
||
return Number.isFinite(number) ? number : 0
|
||
}
|
||
|
||
const DealsPage = () => {
|
||
const [search, setSearch] = useState('')
|
||
const [statusFilter, setStatusFilter] = useState<'all' | DealStatus>('all')
|
||
const [stageFilter, setStageFilter] = useState<'all' | DealStage>('all')
|
||
const [ownerFilter, setOwnerFilter] = useState('')
|
||
const [minAmount, setMinAmount] = useState('')
|
||
const [maxAmount, setMaxAmount] = useState('')
|
||
const [createOpen, setCreateOpen] = useState(false)
|
||
const [dealToEdit, setDealToEdit] = useState<Deal | null>(null)
|
||
const { toast } = useToast()
|
||
|
||
const filters = useMemo(() => {
|
||
const ownerNumber = ownerFilter ? Number(ownerFilter) : undefined
|
||
const min = minAmount ? Number(minAmount) : undefined
|
||
const max = maxAmount ? Number(maxAmount) : undefined
|
||
return {
|
||
status: statusFilter === 'all' ? undefined : [statusFilter],
|
||
stage: stageFilter === 'all' ? undefined : stageFilter,
|
||
ownerId: Number.isFinite(ownerNumber) ? ownerNumber : undefined,
|
||
minAmount: Number.isFinite(min) ? min : undefined,
|
||
maxAmount: Number.isFinite(max) ? max : undefined,
|
||
}
|
||
}, [statusFilter, stageFilter, ownerFilter, minAmount, maxAmount])
|
||
|
||
const { data: deals = [], isLoading } = useDealsQuery(filters)
|
||
const { data: contacts = [], isLoading: contactsLoading } = useContactsQuery({ pageSize: 100 })
|
||
const filteredDeals = useMemo(() => {
|
||
const query = search.trim().toLowerCase()
|
||
if (!query) return deals
|
||
return deals.filter((deal) => deal.title.toLowerCase().includes(query))
|
||
}, [deals, search])
|
||
|
||
const createDeal = useCreateDealMutation()
|
||
const updateDeal = useUpdateDealMutation()
|
||
|
||
const ownerOptions = useMemo(
|
||
() =>
|
||
Array.from(
|
||
new Set(
|
||
[
|
||
...deals.map((deal) => deal.owner_id),
|
||
...contacts.map((contact) => contact.owner_id).filter((id): id is number => typeof id === 'number'),
|
||
],
|
||
),
|
||
)
|
||
.filter((id) => id !== undefined && id !== null)
|
||
.map((id) => ({ value: String(id), label: `Сотрудник #${id}` })),
|
||
[contacts, deals],
|
||
)
|
||
|
||
const stats = useMemo(() => {
|
||
const total = deals.length
|
||
const pipeline = deals.reduce((acc, deal) => acc + mapAmount(deal.amount), 0)
|
||
const won = deals.filter((deal) => deal.status === 'won')
|
||
const wonAmount = won.reduce((acc, deal) => acc + mapAmount(deal.amount), 0)
|
||
const conversion = total ? Math.round((won.length / total) * 100) : 0
|
||
return { total, pipeline, wonAmount, conversion }
|
||
}, [deals])
|
||
|
||
const stageChartData = useMemo(
|
||
() =>
|
||
dealStageList.map((stage) => {
|
||
const stageDeals = deals.filter((deal) => deal.stage === stage)
|
||
return {
|
||
stage: dealStageLabels[stage],
|
||
count: stageDeals.length,
|
||
value: stageDeals.reduce((acc, deal) => acc + mapAmount(deal.amount), 0),
|
||
}
|
||
}),
|
||
[deals],
|
||
)
|
||
|
||
const handleEditDeal = useCallback((deal: Deal) => setDealToEdit(deal), [])
|
||
|
||
const columns = useMemo<ColumnDef<Deal>[]>(
|
||
() => [
|
||
{
|
||
accessorKey: 'title',
|
||
header: 'Сделка',
|
||
cell: ({ row }) => (
|
||
<div>
|
||
<p className="font-medium">{row.original.title}</p>
|
||
<p className="text-xs text-muted-foreground">Контакт #{row.original.contact_id}</p>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
accessorKey: 'amount',
|
||
header: 'Сумма',
|
||
cell: ({ row }) => (
|
||
<div>
|
||
<p className="font-medium">{formatCurrency(row.original.amount, row.original.currency ?? 'USD')}</p>
|
||
<p className="text-xs text-muted-foreground">{row.original.currency ?? '—'}</p>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
accessorKey: 'status',
|
||
header: 'Статус',
|
||
cell: ({ row }) => <DealStatusBadge status={row.original.status} />,
|
||
},
|
||
{
|
||
accessorKey: 'stage',
|
||
header: 'Этап',
|
||
cell: ({ row }) => <DealStageBadge stage={row.original.stage} />,
|
||
},
|
||
{
|
||
accessorKey: 'owner_id',
|
||
header: 'Владелец',
|
||
cell: ({ row }) => <span className="text-sm text-muted-foreground">Сотрудник #{row.original.owner_id}</span>,
|
||
},
|
||
{
|
||
accessorKey: 'updated_at',
|
||
header: 'Обновлена',
|
||
cell: ({ row }) => <span className="text-sm text-muted-foreground">{formatDate(row.original.updated_at)}</span>,
|
||
},
|
||
{
|
||
id: 'actions',
|
||
header: '',
|
||
cell: ({ row }) => (
|
||
<Button variant="ghost" size="sm" onClick={() => handleEditDeal(row.original)}>
|
||
Обновить
|
||
</Button>
|
||
),
|
||
},
|
||
],
|
||
[handleEditDeal],
|
||
)
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<header>
|
||
<h1 className="text-2xl font-semibold text-foreground">Сделки</h1>
|
||
<p className="text-sm text-muted-foreground">Следите за воронкой продаж и обновляйте статусы в один клик.</p>
|
||
</header>
|
||
|
||
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||
<StatCard title="Всего сделок" value={stats.total} description="Количество в текущем списке" />
|
||
<StatCard title="Потенциал" value={formatCurrency(stats.pipeline)} description="Суммарный pipeline" />
|
||
<StatCard title="Выиграно" value={formatCurrency(stats.wonAmount)} description="Сумма выигранных сделок" />
|
||
<StatCard title="Конверсия" value={`${stats.conversion}%`} description="Выиграно от всех" />
|
||
</section>
|
||
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle>Воронка по этапам</CardTitle>
|
||
<CardDescription>Количество и сумма сделок на каждом этапе.</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="h-[280px]">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
<BarChart data={stageChartData}>
|
||
<CartesianGrid strokeDasharray="3 3" opacity={0.3} />
|
||
<XAxis dataKey="stage" tick={{ fill: 'currentColor', fontSize: 12 }} />
|
||
<YAxis tick={{ fill: 'currentColor', fontSize: 12 }} />
|
||
<Tooltip content={<StageTooltip />} cursor={{ fill: 'transparent' }} />
|
||
<Bar dataKey="count" name="Сделки" fill="hsl(var(--primary))" radius={[8, 8, 0, 0]} />
|
||
</BarChart>
|
||
</ResponsiveContainer>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
<DataTable
|
||
columns={columns}
|
||
data={filteredDeals}
|
||
isLoading={isLoading}
|
||
renderToolbar={
|
||
<DataTableToolbar
|
||
searchPlaceholder="Поиск по названию"
|
||
searchValue={search}
|
||
onSearchChange={setSearch}
|
||
actions={
|
||
<Button className="gap-2" onClick={() => setCreateOpen(true)}>
|
||
+ Новая сделка
|
||
</Button>
|
||
}
|
||
>
|
||
<Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as typeof statusFilter)}>
|
||
<SelectTrigger className="w-[180px] bg-background">
|
||
<SelectValue placeholder="Статус" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">Все статусы</SelectItem>
|
||
{dealStatusList.map((status) => (
|
||
<SelectItem key={status} value={status}>
|
||
{dealStatusLabels[status]}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<Select value={stageFilter} onValueChange={(value) => setStageFilter(value as typeof stageFilter)}>
|
||
<SelectTrigger className="w-[200px] bg-background">
|
||
<SelectValue placeholder="Этап" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">Все этапы</SelectItem>
|
||
{dealStageList.map((stage) => (
|
||
<SelectItem key={stage} value={stage}>
|
||
{dealStageLabels[stage]}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<Input
|
||
type="number"
|
||
placeholder="ID владельца"
|
||
value={ownerFilter}
|
||
onChange={(event) => setOwnerFilter(event.target.value)}
|
||
className="w-[140px]"
|
||
/>
|
||
<div className="flex items-center gap-2">
|
||
<Input
|
||
type="number"
|
||
placeholder="Мин сумма"
|
||
value={minAmount}
|
||
onChange={(event) => setMinAmount(event.target.value)}
|
||
className="w-[130px]"
|
||
/>
|
||
<Input
|
||
type="number"
|
||
placeholder="Макс сумма"
|
||
value={maxAmount}
|
||
onChange={(event) => setMaxAmount(event.target.value)}
|
||
className="w-[130px]"
|
||
/>
|
||
</div>
|
||
</DataTableToolbar>
|
||
}
|
||
/>
|
||
|
||
<DealCreateDrawer
|
||
open={createOpen}
|
||
onOpenChange={setCreateOpen}
|
||
isSubmitting={createDeal.isPending}
|
||
contacts={contacts}
|
||
contactsLoading={contactsLoading}
|
||
ownerOptions={ownerOptions}
|
||
onSubmit={async (values) => {
|
||
const payload = {
|
||
title: values.title,
|
||
contact_id: Number(values.contactId),
|
||
amount: values.amount ? Number(values.amount) : undefined,
|
||
currency: values.currency ? values.currency.toUpperCase() : undefined,
|
||
owner_id: values.ownerId && values.ownerId !== 'auto' ? Number(values.ownerId) : undefined,
|
||
}
|
||
try {
|
||
await createDeal.mutateAsync(payload)
|
||
toast({ title: 'Сделка создана', description: 'Запись появилась в воронке.' })
|
||
setCreateOpen(false)
|
||
} catch (error) {
|
||
toast({ title: 'Не удалось создать сделку', description: error instanceof Error ? error.message : 'Попробуйте снова', variant: 'destructive' })
|
||
}
|
||
}}
|
||
/>
|
||
|
||
<DealUpdateDrawer
|
||
deal={dealToEdit}
|
||
open={Boolean(dealToEdit)}
|
||
onOpenChange={(open) => {
|
||
if (!open) setDealToEdit(null)
|
||
}}
|
||
isSubmitting={updateDeal.isPending}
|
||
onSubmit={async (values) => {
|
||
if (!dealToEdit) return
|
||
const payload = {
|
||
status: values.status,
|
||
stage: values.stage,
|
||
amount: values.amount ? Number(values.amount) : undefined,
|
||
currency: values.currency ? values.currency.toUpperCase() : undefined,
|
||
}
|
||
try {
|
||
await updateDeal.mutateAsync({ dealId: dealToEdit.id, payload })
|
||
toast({ title: 'Сделка обновлена', description: 'Статус и этап сохранены.' })
|
||
setDealToEdit(null)
|
||
} catch (error) {
|
||
toast({ title: 'Ошибка обновления', description: error instanceof Error ? error.message : 'Попробуйте позже', variant: 'destructive' })
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface StatCardProps {
|
||
title: string
|
||
value: string | number
|
||
description: string
|
||
}
|
||
|
||
const StatCard = ({ title, value, description }: StatCardProps) => (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">{title}</CardTitle>
|
||
<CardDescription>{description}</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<p className="text-3xl font-semibold">{value}</p>
|
||
</CardContent>
|
||
</Card>
|
||
)
|
||
|
||
const StageTooltip = ({ active, payload }: { active?: boolean; payload?: Array<{ payload: { stage: string; count: number; value: number } }> }) => {
|
||
if (!active || !payload?.length) return null
|
||
const [{ payload: data }] = payload
|
||
return (
|
||
<div className="rounded-md border bg-background/90 px-4 py-2 text-sm shadow">
|
||
<p className="font-semibold">{data.stage}</p>
|
||
<p>Сделок: {data.count}</p>
|
||
<p>Сумма: {formatCurrency(data.value)}</p>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface DealCreateDrawerProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
onSubmit: (values: DealCreateFormValues) => Promise<void>
|
||
isSubmitting: boolean
|
||
contacts: Contact[]
|
||
contactsLoading: boolean
|
||
ownerOptions: Array<{ value: string; label: string }>
|
||
}
|
||
|
||
const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting, contacts, contactsLoading, ownerOptions }: DealCreateDrawerProps) => {
|
||
const form = useForm<DealCreateFormValues>({
|
||
resolver: zodResolver(dealCreateSchema),
|
||
defaultValues: { title: '', contactId: '', amount: '', currency: 'USD', ownerId: 'auto' },
|
||
})
|
||
|
||
const handleSubmit = async (values: DealCreateFormValues) => {
|
||
await onSubmit(values)
|
||
form.reset({ title: '', contactId: '', amount: '', currency: 'USD', ownerId: 'auto' })
|
||
}
|
||
|
||
return (
|
||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||
<SheetContent className="w-full max-w-lg">
|
||
<SheetHeader>
|
||
<SheetTitle>Новая сделка</SheetTitle>
|
||
<SheetDescription>Заполните данные для создания сделки.</SheetDescription>
|
||
</SheetHeader>
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(handleSubmit)} className="mt-6 space-y-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="title"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Название</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="Реставрация сайта" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="contactId"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Контакт</FormLabel>
|
||
<Select value={field.value} onValueChange={field.onChange} disabled={contactsLoading || !contacts.length}>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={contactsLoading ? 'Загружаем контакты…' : 'Выберите контакт'} />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
{contacts.map((contact) => (
|
||
<SelectItem key={contact.id} value={String(contact.id)}>
|
||
{contact.name} · #{contact.id}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FormDescription>
|
||
{contacts.length ? 'Контакт будет связан со сделкой.' : 'Сначала создайте контакт в разделе «Контакты».'}
|
||
</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="amount"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Сумма</FormLabel>
|
||
<FormControl>
|
||
<Input type="number" placeholder="10000" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="currency"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Валюта</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="USD" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="ownerId"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Владелец (необязательно)</FormLabel>
|
||
<Select value={field.value} onValueChange={field.onChange}>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder={ownerOptions.length ? 'Выберите владельца' : 'Только автоназначение'} />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
<SelectItem value="auto">Назначить автоматически</SelectItem>
|
||
{ownerOptions.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FormDescription>При отсутствии выбора сделка будет закреплена за вами.</FormDescription>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<div className="flex justify-end gap-2 pt-4">
|
||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||
Отмена
|
||
</Button>
|
||
<Button type="submit" disabled={isSubmitting}>
|
||
{isSubmitting ? 'Создаём…' : 'Создать'}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Form>
|
||
</SheetContent>
|
||
</Sheet>
|
||
)
|
||
}
|
||
|
||
interface DealUpdateDrawerProps {
|
||
deal: Deal | null
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
onSubmit: (values: DealUpdateFormValues) => Promise<void>
|
||
isSubmitting: boolean
|
||
}
|
||
|
||
const DealUpdateDrawer = ({ deal, open, onOpenChange, onSubmit, isSubmitting }: DealUpdateDrawerProps) => {
|
||
const form = useForm<DealUpdateFormValues>({
|
||
resolver: zodResolver(dealUpdateSchema),
|
||
defaultValues: { status: 'new', stage: 'qualification', amount: '', currency: 'USD' },
|
||
})
|
||
|
||
useEffect(() => {
|
||
if (deal) {
|
||
form.reset({
|
||
status: deal.status,
|
||
stage: deal.stage,
|
||
amount: deal.amount ?? '',
|
||
currency: deal.currency ?? 'USD',
|
||
})
|
||
}
|
||
}, [deal, form])
|
||
|
||
if (!deal) return null
|
||
|
||
const handleSubmit = async (values: DealUpdateFormValues) => {
|
||
await onSubmit(values)
|
||
}
|
||
|
||
return (
|
||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||
<SheetContent className="w-full max-w-lg">
|
||
<SheetHeader>
|
||
<SheetTitle>Обновление сделки</SheetTitle>
|
||
<SheetDescription>Измените статус, этап или сумму.</SheetDescription>
|
||
</SheetHeader>
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(handleSubmit)} className="mt-6 space-y-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="status"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Статус</FormLabel>
|
||
<Select value={field.value} onValueChange={field.onChange}>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Выберите статус" />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
{dealStatusList.map((status) => (
|
||
<SelectItem key={status} value={status}>
|
||
{dealStatusLabels[status]}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="stage"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Этап</FormLabel>
|
||
<Select value={field.value} onValueChange={field.onChange}>
|
||
<FormControl>
|
||
<SelectTrigger>
|
||
<SelectValue placeholder="Выберите этап" />
|
||
</SelectTrigger>
|
||
</FormControl>
|
||
<SelectContent>
|
||
{dealStageList.map((stage) => (
|
||
<SelectItem key={stage} value={stage}>
|
||
{dealStageLabels[stage]}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="amount"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Сумма</FormLabel>
|
||
<FormControl>
|
||
<Input type="number" placeholder="0" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="currency"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Валюта</FormLabel>
|
||
<FormControl>
|
||
<Input placeholder="USD" {...field} />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<div className="flex justify-end gap-2 pt-4">
|
||
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
|
||
Отмена
|
||
</Button>
|
||
<Button type="submit" disabled={isSubmitting}>
|
||
{isSubmitting ? 'Сохраняем…' : 'Сохранить'}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Form>
|
||
</SheetContent>
|
||
</Sheet>
|
||
)
|
||
}
|
||
|
||
export default DealsPage
|