Разработка CRM-системы (веб-интерфейс)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.
Разработка и обслуживание любых видов сайтов:
Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка CRM-системы (веб-интерфейс)
Сложная
от 2 недель до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

Разработка CRM-системы (веб-интерфейс)

CRM-система в большинстве случаев — это не про «управление отношениями с клиентами» в маркетинговом смысле, а про конкретный инструмент: очередь задач, карточка клиента, воронка сделок, история коммуникаций. Задача разработки веб-интерфейса — реализовать именно тот набор функций, который нужен конкретному бизнесу, без лишних модулей, нагромождения настроек и лицензионных ограничений.

Что входит в типовой интерфейс CRM

Минимальный набор сущностей для большинства B2B-компаний:

  • Контакты — физические лица с историей взаимодействий
  • Компании — юридические лица, к которым привязаны контакты
  • Сделки — потенциальные и активные продажи со статусами
  • Задачи — поручения с дедлайнами, привязанные к сделкам/контактам
  • Активности — звонки, письма, встречи (лог коммуникаций)
  • Воронка — визуальный Kanban или Pipeline с колонками-статусами

Дополнительно в зависимости от специфики: прайс-листы и коммерческие предложения, интеграция с телефонией, модуль рассылок, отчёты по менеджерам.

Технологический стек

Для веб-интерфейса CRM оптимально работает стек SPA или SSR-приложения с реактивным UI:

Backend:

  • Laravel / Node.js (NestJS) в роли API
  • PostgreSQL — основная база данных
  • Redis — кеш и очереди событий (звонки, уведомления)
  • WebSocket (Laravel Echo + Pusher / Socket.io) — realtime-обновления

Frontend:

  • React + TypeScript
  • React Query для серверного состояния
  • Zustand или Redux Toolkit для глобального UI-состояния
  • React Hook Form + Zod для форм
  • TanStack Table для таблиц с фильтрацией и сортировкой
  • @dnd-kit для drag-and-drop воронки

Структура базы данных

CREATE TABLE contacts (
    id          BIGSERIAL PRIMARY KEY,
    company_id  BIGINT REFERENCES companies(id),
    name        VARCHAR(255) NOT NULL,
    email       VARCHAR(255),
    phone       VARCHAR(50),
    source      VARCHAR(64),      -- откуда пришёл
    responsible_id BIGINT REFERENCES users(id),
    created_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE deals (
    id           BIGSERIAL PRIMARY KEY,
    contact_id   BIGINT REFERENCES contacts(id),
    company_id   BIGINT REFERENCES companies(id),
    title        VARCHAR(255) NOT NULL,
    amount       DECIMAL(14,2),
    currency     CHAR(3) DEFAULT 'RUB',
    stage_id     BIGINT REFERENCES pipeline_stages(id),
    responsible_id BIGINT REFERENCES users(id),
    closed_at    DATE,
    created_at   TIMESTAMPTZ DEFAULT NOW(),
    updated_at   TIMESTAMPTZ DEFAULT NOW()
);

CREATE TABLE pipeline_stages (
    id           BIGSERIAL PRIMARY KEY,
    pipeline_id  BIGINT REFERENCES pipelines(id),
    name         VARCHAR(128) NOT NULL,
    sort_order   INT DEFAULT 0,
    is_won       BOOLEAN DEFAULT FALSE,
    is_lost      BOOLEAN DEFAULT FALSE
);

CREATE TABLE activities (
    id           BIGSERIAL PRIMARY KEY,
    entity_type  VARCHAR(32) NOT NULL, -- 'contact', 'deal', 'company'
    entity_id    BIGINT NOT NULL,
    type         VARCHAR(32) NOT NULL, -- 'call', 'email', 'meeting', 'note'
    body         TEXT,
    user_id      BIGINT REFERENCES users(id),
    happened_at  TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX ON activities(entity_type, entity_id);

API-дизайн

RESTful JSON API с ресурсо-ориентированной структурой:

GET    /api/deals?stage_id=2&responsible_id=5&page=1&per_page=50
POST   /api/deals
PATCH  /api/deals/{id}
DELETE /api/deals/{id}

POST   /api/deals/{id}/move   # смена стадии
POST   /api/activities        # добавить активность к любой сущности
GET    /api/contacts/{id}/timeline  # хронология взаимодействий

Пример PATCH-ответа при смене стадии:

{
  "id": 1042,
  "stage_id": 4,
  "stage": { "id": 4, "name": "Переговоры" },
  "updated_at": "2025-03-15T14:22:00Z",
  "activity": {
    "id": 3891,
    "type": "stage_change",
    "body": "Стадия изменена: Квалификация → Переговоры",
    "user_id": 12
  }
}

Воронка: drag-and-drop Kanban

import { DndContext, DragEndEvent, closestCenter } from '@dnd-kit/core';
import { SortableContext } from '@dnd-kit/sortable';

const Pipeline: React.FC<{ stages: Stage[]; deals: Deal[] }> = ({ stages, deals }) => {
    const moveDeal = useMutation({
        mutationFn: ({ dealId, stageId }: { dealId: number; stageId: number }) =>
            api.patch(`/deals/${dealId}/move`, { stage_id: stageId }),
        onMutate: async ({ dealId, stageId }) => {
            // Optimistic update
            await queryClient.cancelQueries({ queryKey: ['deals'] });
            const prev = queryClient.getQueryData(['deals']);
            queryClient.setQueryData(['deals'], (old: Deal[]) =>
                old.map(d => d.id === dealId ? { ...d, stage_id: stageId } : d)
            );
            return { prev };
        },
        onError: (_, __, context) => {
            queryClient.setQueryData(['deals'], context?.prev);
        },
    });

    const onDragEnd = (event: DragEndEvent) => {
        const { active, over } = event;
        if (!over || active.id === over.id) return;
        moveDeal.mutate({ dealId: Number(active.id), stageId: Number(over.id) });
    };

    return (
        <DndContext collisionDetection={closestCenter} onDragEnd={onDragEnd}>
            <div className="flex gap-4 overflow-x-auto p-4">
                {stages.map(stage => (
                    <KanbanColumn
                        key={stage.id}
                        stage={stage}
                        deals={deals.filter(d => d.stage_id === stage.id)}
                    />
                ))}
            </div>
        </DndContext>
    );
};

Optimistic update важен — пользователь видит карточку на новой позиции мгновенно, без ожидания ответа сервера.

Timeline активностей

Хронология взаимодействий — одна из ключевых частей CRM. Реализуется как полиморфная лента:

type ActivityItem =
    | { type: 'call'; duration: number; result: string }
    | { type: 'email'; subject: string; direction: 'in' | 'out' }
    | { type: 'note'; body: string }
    | { type: 'stage_change'; from: string; to: string };

const ActivityFeed: React.FC<{ entityType: string; entityId: number }> = (props) => {
    const { data } = useQuery({
        queryKey: ['timeline', props.entityType, props.entityId],
        queryFn: () => api.get(`/${props.entityType}s/${props.entityId}/timeline`),
    });

    return (
        <div className="space-y-3">
            <AddActivityForm entityType={props.entityType} entityId={props.entityId} />
            {data?.items.map(item => (
                <ActivityCard key={item.id} item={item} />
            ))}
        </div>
    );
};

Права доступа

CRM требует гранулярных прав: менеджер видит только своих клиентов, руководитель — всех в своём отделе. Реализуется через Policy-классы:

// Laravel Policy
class DealPolicy {
    public function viewAny(User $user): bool {
        return $user->hasPermission('deals.view');
    }

    public function view(User $user, Deal $deal): bool {
        if ($user->hasRole('admin')) return true;
        if ($user->hasRole('team_lead')) {
            return $deal->responsible->team_id === $user->team_id;
        }
        return $deal->responsible_id === $user->id;
    }

    public function update(User $user, Deal $deal): bool {
        return $user->hasRole('admin') || $deal->responsible_id === $user->id;
    }
}

Scope на уровне Eloquent:

class Deal extends Model {
    public function scopeVisibleTo(Builder $query, User $user): Builder {
        if ($user->hasRole('admin')) return $query;
        if ($user->hasRole('team_lead')) {
            return $query->whereHas('responsible', fn($q) =>
                $q->where('team_id', $user->team_id)
            );
        }
        return $query->where('responsible_id', $user->id);
    }
}

Realtime-уведомления

Когда сделка назначается другому менеджеру или по задаче наступает дедлайн — уведомление приходит без обновления страницы:

// Frontend: подключение к каналу пользователя
import Echo from 'laravel-echo';

const echo = new Echo({ broadcaster: 'pusher', ... });

echo.private(`user.${currentUser.id}`).listen('DealAssigned', (event) => {
    toast.info(`Вам назначена сделка: ${event.deal.title}`);
    queryClient.invalidateQueries({ queryKey: ['deals'] });
});
// Backend: событие
class DealAssigned implements ShouldBroadcast {
    public function broadcastOn(): PrivateChannel {
        return new PrivateChannel('user.' . $this->deal->responsible_id);
    }
}

Сроки реализации

MVP с воронкой, карточками контактов/сделок, задачами и базовой аналитикой: 4–6 недель. Добавление интеграций (телефония, email, мессенджеры), расширенных отчётов, ролей и мультиязычности: плюс 3–4 недели. Мобильная версия (PWA или React Native): плюс 3–4 недели.

Основная часть времени уходит не на код, а на проработку бизнес-логики: какие поля обязательны, какие переходы между стадиями разрешены, кто видит чьи данные, какие уведомления критичны.