feat: enhance forms with improved select components and data handling for contacts and deals
Test / test (push) Successful in 17s
Details
Test / test (push) Successful in 17s
Details
This commit is contained in:
parent
8718df9686
commit
ecb6daad1b
|
|
@ -82,7 +82,7 @@ define(['./workbox-c5fd805d'], (function (workbox) { 'use strict';
|
||||||
"revision": "d41d8cd98f00b204e9800998ecf8427e"
|
"revision": "d41d8cd98f00b204e9800998ecf8427e"
|
||||||
}, {
|
}, {
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.6kfctj76vbg"
|
"revision": "0.b5rg9utgn3"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef<
|
||||||
<SelectPrimitive.Trigger
|
<SelectPrimitive.Trigger
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
|
'flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring data-[state=open]:bg-muted disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|
@ -38,7 +38,7 @@ const SelectContent = React.forwardRef<
|
||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80',
|
'relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-background text-foreground shadow-lg animate-in fade-in-80',
|
||||||
position === 'popper' && 'data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1',
|
position === 'popper' && 'data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1',
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { z } from 'zod'
|
||||||
import { DataTable } from '@/components/data-table/data-table'
|
import { DataTable } from '@/components/data-table/data-table'
|
||||||
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||||
|
|
@ -50,7 +50,7 @@ const ContactsPage = () => {
|
||||||
const deleteContact = useDeleteContactMutation()
|
const deleteContact = useDeleteContactMutation()
|
||||||
const { toast } = useToast()
|
const { toast } = useToast()
|
||||||
|
|
||||||
const ownerOptions = useMemo(() => {
|
const ownerIds = useMemo(() => {
|
||||||
const ids = new Set<number>()
|
const ids = new Set<number>()
|
||||||
contacts.forEach((contact) => {
|
contacts.forEach((contact) => {
|
||||||
if (contact.owner_id) ids.add(contact.owner_id)
|
if (contact.owner_id) ids.add(contact.owner_id)
|
||||||
|
|
@ -58,6 +58,8 @@ const ContactsPage = () => {
|
||||||
return Array.from(ids).sort((a, b) => a - b)
|
return Array.from(ids).sort((a, b) => a - b)
|
||||||
}, [contacts])
|
}, [contacts])
|
||||||
|
|
||||||
|
const ownerSelectOptions = useMemo(() => ownerIds.map((ownerId) => ({ value: String(ownerId), label: `Сотрудник #${ownerId}` })), [ownerIds])
|
||||||
|
|
||||||
const openCreateDrawer = () => {
|
const openCreateDrawer = () => {
|
||||||
setEditingContact(null)
|
setEditingContact(null)
|
||||||
setDrawerOpen(true)
|
setDrawerOpen(true)
|
||||||
|
|
@ -151,14 +153,14 @@ const ContactsPage = () => {
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{ownerOptions.length ? (
|
{ownerIds.length ? (
|
||||||
<Select value={ownerFilter} onValueChange={setOwnerFilter}>
|
<Select value={ownerFilter} onValueChange={setOwnerFilter}>
|
||||||
<SelectTrigger className="w-[200px] bg-background">
|
<SelectTrigger className="w-[200px] bg-background">
|
||||||
<SelectValue placeholder="Все владельцы" />
|
<SelectValue placeholder="Все владельцы" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">Все владельцы</SelectItem>
|
<SelectItem value="all">Все владельцы</SelectItem>
|
||||||
{ownerOptions.map((ownerId) => (
|
{ownerIds.map((ownerId) => (
|
||||||
<SelectItem key={ownerId} value={String(ownerId)}>
|
<SelectItem key={ownerId} value={String(ownerId)}>
|
||||||
Сотрудник #{ownerId}
|
Сотрудник #{ownerId}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
|
|
@ -174,6 +176,7 @@ const ContactsPage = () => {
|
||||||
onOpenChange={setDrawerOpen}
|
onOpenChange={setDrawerOpen}
|
||||||
contact={editingContact}
|
contact={editingContact}
|
||||||
isSubmitting={createContact.isPending || updateContact.isPending}
|
isSubmitting={createContact.isPending || updateContact.isPending}
|
||||||
|
ownerOptions={ownerSelectOptions}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: values.name,
|
name: values.name,
|
||||||
|
|
@ -206,9 +209,10 @@ interface ContactFormDrawerProps {
|
||||||
contact: Contact | null
|
contact: Contact | null
|
||||||
onSubmit: (values: ContactFormValues) => Promise<void>
|
onSubmit: (values: ContactFormValues) => Promise<void>
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
|
ownerOptions: Array<{ value: string; label: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ContactFormDrawer = ({ open, onOpenChange, contact, onSubmit, isSubmitting }: ContactFormDrawerProps) => {
|
const ContactFormDrawer = ({ open, onOpenChange, contact, onSubmit, isSubmitting, ownerOptions }: ContactFormDrawerProps) => {
|
||||||
const form = useForm<ContactFormValues>({
|
const form = useForm<ContactFormValues>({
|
||||||
resolver: zodResolver(contactFormSchema),
|
resolver: zodResolver(contactFormSchema),
|
||||||
defaultValues,
|
defaultValues,
|
||||||
|
|
@ -285,10 +289,23 @@ const ContactFormDrawer = ({ open, onOpenChange, contact, onSubmit, isSubmitting
|
||||||
name="ownerId"
|
name="ownerId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>ID владельца (необязательно)</FormLabel>
|
<FormLabel>Владелец</FormLabel>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange} disabled={!ownerOptions.length}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" min={1} placeholder="Например, 42" {...field} />
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={ownerOptions.length ? 'Выберите владельца' : 'Назначение недоступно'} />
|
||||||
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="">Назначить меня</SelectItem>
|
||||||
|
{ownerOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>Выберите руководителя из списка или оставьте поле пустым, чтобы назначить себя.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -11,14 +11,15 @@ import { DealStageBadge, dealStageLabels } from '@/components/crm/deal-stage-bad
|
||||||
import { DealStatusBadge, dealStatusLabels } from '@/components/crm/deal-status-badge'
|
import { DealStatusBadge, dealStatusLabels } from '@/components/crm/deal-status-badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||||
import { useToast } from '@/components/ui/use-toast'
|
import { useToast } from '@/components/ui/use-toast'
|
||||||
|
import { useContactsQuery } from '@/features/contacts/hooks'
|
||||||
import { useCreateDealMutation, useDealsQuery, useUpdateDealMutation } from '@/features/deals/hooks'
|
import { useCreateDealMutation, useDealsQuery, useUpdateDealMutation } from '@/features/deals/hooks'
|
||||||
import { formatCurrency, formatDate } from '@/lib/utils'
|
import { formatCurrency, formatDate } from '@/lib/utils'
|
||||||
import type { Deal, DealStage, DealStatus } from '@/types/crm'
|
import type { Contact, Deal, DealStage, DealStatus } from '@/types/crm'
|
||||||
|
|
||||||
const dealStatusList: DealStatus[] = ['new', 'in_progress', 'won', 'lost']
|
const dealStatusList: DealStatus[] = ['new', 'in_progress', 'won', 'lost']
|
||||||
const dealStageList: DealStage[] = ['qualification', 'proposal', 'negotiation', 'closed']
|
const dealStageList: DealStage[] = ['qualification', 'proposal', 'negotiation', 'closed']
|
||||||
|
|
@ -73,6 +74,7 @@ const DealsPage = () => {
|
||||||
}, [statusFilter, stageFilter, ownerFilter, minAmount, maxAmount])
|
}, [statusFilter, stageFilter, ownerFilter, minAmount, maxAmount])
|
||||||
|
|
||||||
const { data: deals = [], isLoading } = useDealsQuery(filters)
|
const { data: deals = [], isLoading } = useDealsQuery(filters)
|
||||||
|
const { data: contacts = [], isLoading: contactsLoading } = useContactsQuery({ pageSize: 100 })
|
||||||
const filteredDeals = useMemo(() => {
|
const filteredDeals = useMemo(() => {
|
||||||
const query = search.trim().toLowerCase()
|
const query = search.trim().toLowerCase()
|
||||||
if (!query) return deals
|
if (!query) return deals
|
||||||
|
|
@ -82,6 +84,21 @@ const DealsPage = () => {
|
||||||
const createDeal = useCreateDealMutation()
|
const createDeal = useCreateDealMutation()
|
||||||
const updateDeal = useUpdateDealMutation()
|
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 stats = useMemo(() => {
|
||||||
const total = deals.length
|
const total = deals.length
|
||||||
const pipeline = deals.reduce((acc, deal) => acc + mapAmount(deal.amount), 0)
|
const pipeline = deals.reduce((acc, deal) => acc + mapAmount(deal.amount), 0)
|
||||||
|
|
@ -265,13 +282,16 @@ const DealsPage = () => {
|
||||||
open={createOpen}
|
open={createOpen}
|
||||||
onOpenChange={setCreateOpen}
|
onOpenChange={setCreateOpen}
|
||||||
isSubmitting={createDeal.isPending}
|
isSubmitting={createDeal.isPending}
|
||||||
|
contacts={contacts}
|
||||||
|
contactsLoading={contactsLoading}
|
||||||
|
ownerOptions={ownerOptions}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
title: values.title,
|
title: values.title,
|
||||||
contact_id: Number(values.contactId),
|
contact_id: Number(values.contactId),
|
||||||
amount: values.amount ? Number(values.amount) : undefined,
|
amount: values.amount ? Number(values.amount) : undefined,
|
||||||
currency: values.currency ? values.currency.toUpperCase() : undefined,
|
currency: values.currency ? values.currency.toUpperCase() : undefined,
|
||||||
owner_id: values.ownerId ? Number(values.ownerId) : undefined,
|
owner_id: values.ownerId && values.ownerId !== 'auto' ? Number(values.ownerId) : undefined,
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await createDeal.mutateAsync(payload)
|
await createDeal.mutateAsync(payload)
|
||||||
|
|
@ -346,17 +366,20 @@ interface DealCreateDrawerProps {
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onSubmit: (values: DealCreateFormValues) => Promise<void>
|
onSubmit: (values: DealCreateFormValues) => Promise<void>
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
|
contacts: Contact[]
|
||||||
|
contactsLoading: boolean
|
||||||
|
ownerOptions: Array<{ value: string; label: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: DealCreateDrawerProps) => {
|
const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting, contacts, contactsLoading, ownerOptions }: DealCreateDrawerProps) => {
|
||||||
const form = useForm<DealCreateFormValues>({
|
const form = useForm<DealCreateFormValues>({
|
||||||
resolver: zodResolver(dealCreateSchema),
|
resolver: zodResolver(dealCreateSchema),
|
||||||
defaultValues: { title: '', contactId: '', amount: '', currency: 'USD', ownerId: '' },
|
defaultValues: { title: '', contactId: '', amount: '', currency: 'USD', ownerId: 'auto' },
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = async (values: DealCreateFormValues) => {
|
const handleSubmit = async (values: DealCreateFormValues) => {
|
||||||
await onSubmit(values)
|
await onSubmit(values)
|
||||||
form.reset({ title: '', contactId: '', amount: '', currency: 'USD', ownerId: '' })
|
form.reset({ title: '', contactId: '', amount: '', currency: 'USD', ownerId: 'auto' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -386,10 +409,24 @@ const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: DealCr
|
||||||
name="contactId"
|
name="contactId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>ID контакта</FormLabel>
|
<FormLabel>Контакт</FormLabel>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange} disabled={contactsLoading || !contacts.length}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="101" {...field} />
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={contactsLoading ? 'Загружаем контакты…' : 'Выберите контакт'} />
|
||||||
|
</SelectTrigger>
|
||||||
</FormControl>
|
</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 />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
@ -425,10 +462,23 @@ const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: DealCr
|
||||||
name="ownerId"
|
name="ownerId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>ID владельца (необязательно)</FormLabel>
|
<FormLabel>Владелец (необязательно)</FormLabel>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="42" {...field} />
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={ownerOptions.length ? 'Выберите владельца' : 'Только автоназначение'} />
|
||||||
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="auto">Назначить автоматически</SelectItem>
|
||||||
|
{ownerOptions.map((option) => (
|
||||||
|
<SelectItem key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>При отсутствии выбора сделка будет закреплена за вами.</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,18 @@ import { DataTable } from '@/components/data-table/data-table'
|
||||||
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
import { DataTableToolbar } from '@/components/data-table/data-table-toolbar'
|
||||||
import { TaskStatusPill } from '@/components/crm/task-status-pill'
|
import { TaskStatusPill } from '@/components/crm/task-status-pill'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||||
import { Input } from '@/components/ui/input'
|
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 { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { useToast } from '@/components/ui/use-toast'
|
import { useToast } from '@/components/ui/use-toast'
|
||||||
import { useCreateTaskMutation, useTasksQuery } from '@/features/tasks/hooks'
|
import { useCreateTaskMutation, useTasksQuery } from '@/features/tasks/hooks'
|
||||||
|
import { useDealsQuery } from '@/features/deals/hooks'
|
||||||
import { useDebounce } from '@/hooks/use-debounce'
|
import { useDebounce } from '@/hooks/use-debounce'
|
||||||
import { formatDate, formatRelativeDate } from '@/lib/utils'
|
import { formatDate, formatRelativeDate } from '@/lib/utils'
|
||||||
import type { Task } from '@/types/crm'
|
import type { Deal, Task } from '@/types/crm'
|
||||||
|
|
||||||
const taskFormSchema = z
|
const taskFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|
@ -63,6 +65,7 @@ const TasksPage = () => {
|
||||||
}, [tasks, search])
|
}, [tasks, search])
|
||||||
|
|
||||||
const createTask = useCreateTaskMutation()
|
const createTask = useCreateTaskMutation()
|
||||||
|
const { data: deals = [], isLoading: dealsLoading } = useDealsQuery({ pageSize: 100, orderBy: 'updated_at', order: 'desc' })
|
||||||
|
|
||||||
const columns = useMemo<ColumnDef<Task>[]>(
|
const columns = useMemo<ColumnDef<Task>[]>(
|
||||||
() => [
|
() => [
|
||||||
|
|
@ -129,7 +132,7 @@ const TasksPage = () => {
|
||||||
className="w-[140px]"
|
className="w-[140px]"
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center gap-2 rounded-lg border bg-background px-3 py-1.5 text-sm">
|
<div className="flex items-center gap-2 rounded-lg border bg-background px-3 py-1.5 text-sm">
|
||||||
<Switch checked={onlyOpen} onCheckedChange={setOnlyOpen} id="only-open" />
|
<Switch checked={onlyOpen} onCheckedChange={(value) => setOnlyOpen(value === true)} id="only-open" />
|
||||||
<label htmlFor="only-open" className="cursor-pointer">
|
<label htmlFor="only-open" className="cursor-pointer">
|
||||||
Только открытые
|
Только открытые
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -143,6 +146,8 @@ const TasksPage = () => {
|
||||||
open={drawerOpen}
|
open={drawerOpen}
|
||||||
onOpenChange={setDrawerOpen}
|
onOpenChange={setDrawerOpen}
|
||||||
isSubmitting={createTask.isPending}
|
isSubmitting={createTask.isPending}
|
||||||
|
deals={deals}
|
||||||
|
dealsLoading={dealsLoading}
|
||||||
onSubmit={async (values) => {
|
onSubmit={async (values) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
deal_id: Number(values.dealId),
|
deal_id: Number(values.dealId),
|
||||||
|
|
@ -168,9 +173,11 @@ interface TaskDrawerProps {
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onSubmit: (values: TaskFormValues) => Promise<void>
|
onSubmit: (values: TaskFormValues) => Promise<void>
|
||||||
isSubmitting: boolean
|
isSubmitting: boolean
|
||||||
|
deals: Deal[]
|
||||||
|
dealsLoading: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: TaskDrawerProps) => {
|
const TaskDrawer = ({ open, onOpenChange, onSubmit, isSubmitting, deals, dealsLoading }: TaskDrawerProps) => {
|
||||||
const form = useForm<TaskFormValues>({
|
const form = useForm<TaskFormValues>({
|
||||||
resolver: zodResolver(taskFormSchema),
|
resolver: zodResolver(taskFormSchema),
|
||||||
defaultValues: defaultTaskValues,
|
defaultValues: defaultTaskValues,
|
||||||
|
|
@ -208,10 +215,24 @@ const TaskDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: TaskDrawerPr
|
||||||
name="dealId"
|
name="dealId"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>ID сделки</FormLabel>
|
<FormLabel>Сделка</FormLabel>
|
||||||
|
<Select value={field.value} onValueChange={field.onChange} disabled={dealsLoading || !deals.length}>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input type="number" placeholder="201" {...field} />
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder={dealsLoading ? 'Загружаем сделки…' : 'Выберите сделку'} />
|
||||||
|
</SelectTrigger>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
{deals.map((deal) => (
|
||||||
|
<SelectItem key={deal.id} value={String(deal.id)}>
|
||||||
|
{deal.title} · #{deal.id}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
{deals.length ? 'Задача появится в таймлайне выбранной сделки.' : 'Сначала создайте сделку в разделе «Сделки».'}
|
||||||
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue