From ecb6daad1b87c8f8a68f98d181804bfcc500e3f1 Mon Sep 17 00:00:00 2001 From: Artem Kashaev Date: Mon, 1 Dec 2025 14:16:24 +0500 Subject: [PATCH] feat: enhance forms with improved select components and data handling for contacts and deals --- frontend/dev-dist/sw.js | 2 +- frontend/src/components/ui/select.tsx | 4 +- frontend/src/pages/contacts/contacts-page.tsx | 35 ++++++--- frontend/src/pages/deals/deals-page.tsx | 78 +++++++++++++++---- frontend/src/pages/tasks/tasks-page.tsx | 37 +++++++-- 5 files changed, 122 insertions(+), 34 deletions(-) diff --git a/frontend/dev-dist/sw.js b/frontend/dev-dist/sw.js index 948273e..e8eed88 100644 --- a/frontend/dev-dist/sw.js +++ b/frontend/dev-dist/sw.js @@ -82,7 +82,7 @@ define(['./workbox-c5fd805d'], (function (workbox) { 'use strict'; "revision": "d41d8cd98f00b204e9800998ecf8427e" }, { "url": "index.html", - "revision": "0.6kfctj76vbg" + "revision": "0.b5rg9utgn3" }], {}); workbox.cleanupOutdatedCaches(); workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), { diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index a26d69a..f2a213b 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -17,7 +17,7 @@ const SelectTrigger = React.forwardRef< { const deleteContact = useDeleteContactMutation() const { toast } = useToast() - const ownerOptions = useMemo(() => { + const ownerIds = useMemo(() => { const ids = new Set() contacts.forEach((contact) => { if (contact.owner_id) ids.add(contact.owner_id) @@ -58,6 +58,8 @@ const ContactsPage = () => { return Array.from(ids).sort((a, b) => a - b) }, [contacts]) + const ownerSelectOptions = useMemo(() => ownerIds.map((ownerId) => ({ value: String(ownerId), label: `Сотрудник #${ownerId}` })), [ownerIds]) + const openCreateDrawer = () => { setEditingContact(null) setDrawerOpen(true) @@ -151,14 +153,14 @@ const ContactsPage = () => { } > - {ownerOptions.length ? ( + {ownerIds.length ? ( - + Владелец + + Выберите руководителя из списка или оставьте поле пустым, чтобы назначить себя. )} diff --git a/frontend/src/pages/deals/deals-page.tsx b/frontend/src/pages/deals/deals-page.tsx index 5f96f3a..92664b5 100644 --- a/frontend/src/pages/deals/deals-page.tsx +++ b/frontend/src/pages/deals/deals-page.tsx @@ -11,14 +11,15 @@ import { DealStageBadge, dealStageLabels } from '@/components/crm/deal-stage-bad 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, 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 { 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 { Deal, DealStage, DealStatus } from '@/types/crm' +import type { Contact, Deal, DealStage, DealStatus } from '@/types/crm' const dealStatusList: DealStatus[] = ['new', 'in_progress', 'won', 'lost'] const dealStageList: DealStage[] = ['qualification', 'proposal', 'negotiation', 'closed'] @@ -73,6 +74,7 @@ const DealsPage = () => { }, [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 @@ -82,6 +84,21 @@ const DealsPage = () => { 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) @@ -265,13 +282,16 @@ const DealsPage = () => { 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 ? Number(values.ownerId) : undefined, + owner_id: values.ownerId && values.ownerId !== 'auto' ? Number(values.ownerId) : undefined, } try { await createDeal.mutateAsync(payload) @@ -346,17 +366,20 @@ interface DealCreateDrawerProps { onOpenChange: (open: boolean) => void onSubmit: (values: DealCreateFormValues) => Promise 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({ resolver: zodResolver(dealCreateSchema), - defaultValues: { title: '', contactId: '', amount: '', currency: 'USD', ownerId: '' }, + defaultValues: { title: '', contactId: '', amount: '', currency: 'USD', ownerId: 'auto' }, }) const handleSubmit = async (values: DealCreateFormValues) => { await onSubmit(values) - form.reset({ title: '', contactId: '', amount: '', currency: 'USD', ownerId: '' }) + form.reset({ title: '', contactId: '', amount: '', currency: 'USD', ownerId: 'auto' }) } return ( @@ -386,10 +409,24 @@ const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: DealCr name="contactId" render={({ field }) => ( - ID контакта - - - + Контакт + + + {contacts.length ? 'Контакт будет связан со сделкой.' : 'Сначала создайте контакт в разделе «Контакты».'} + )} @@ -425,10 +462,23 @@ const DealCreateDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: DealCr name="ownerId" render={({ field }) => ( - ID владельца (необязательно) - - - + Владелец (необязательно) + + При отсутствии выбора сделка будет закреплена за вами. )} diff --git a/frontend/src/pages/tasks/tasks-page.tsx b/frontend/src/pages/tasks/tasks-page.tsx index 83b41e1..1cd32c5 100644 --- a/frontend/src/pages/tasks/tasks-page.tsx +++ b/frontend/src/pages/tasks/tasks-page.tsx @@ -8,16 +8,18 @@ import { DataTable } from '@/components/data-table/data-table' import { DataTableToolbar } from '@/components/data-table/data-table-toolbar' import { TaskStatusPill } from '@/components/crm/task-status-pill' 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from '@/components/ui/sheet' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { useToast } from '@/components/ui/use-toast' import { useCreateTaskMutation, useTasksQuery } from '@/features/tasks/hooks' +import { useDealsQuery } from '@/features/deals/hooks' import { useDebounce } from '@/hooks/use-debounce' import { formatDate, formatRelativeDate } from '@/lib/utils' -import type { Task } from '@/types/crm' +import type { Deal, Task } from '@/types/crm' const taskFormSchema = z .object({ @@ -63,6 +65,7 @@ const TasksPage = () => { }, [tasks, search]) const createTask = useCreateTaskMutation() + const { data: deals = [], isLoading: dealsLoading } = useDealsQuery({ pageSize: 100, orderBy: 'updated_at', order: 'desc' }) const columns = useMemo[]>( () => [ @@ -129,7 +132,7 @@ const TasksPage = () => { className="w-[140px]" />
- + setOnlyOpen(value === true)} id="only-open" /> @@ -143,6 +146,8 @@ const TasksPage = () => { open={drawerOpen} onOpenChange={setDrawerOpen} isSubmitting={createTask.isPending} + deals={deals} + dealsLoading={dealsLoading} onSubmit={async (values) => { const payload = { deal_id: Number(values.dealId), @@ -168,9 +173,11 @@ interface TaskDrawerProps { onOpenChange: (open: boolean) => void onSubmit: (values: TaskFormValues) => Promise 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({ resolver: zodResolver(taskFormSchema), defaultValues: defaultTaskValues, @@ -208,10 +215,24 @@ const TaskDrawer = ({ open, onOpenChange, onSubmit, isSubmitting }: TaskDrawerPr name="dealId" render={({ field }) => ( - ID сделки - - - + Сделка + + + {deals.length ? 'Задача появится в таймлайне выбранной сделки.' : 'Сначала создайте сделку в разделе «Сделки».'} + )}