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

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка CRM-системи (веб-інтерфейс)
Складна
від 2 тижнів до 3 місяців
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка 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 важливий — користувач бачить карточку на новій позиції миттєво, без очікування відповіді сервера.

Графік активностей

Хронологія взаємодій — одна з ключових частин 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 тижні.

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