Розробка веб-інтерфейсу ERP: стек, віртуалізація, продуктивність

Коли складський звіт в ERP зависає на десятки секунд, співробітники йдуть в Excel. Ми розробляємо веб-інтерфейси ERP, які не гальмують навіть на великих обсягах даних: використовуємо SPA, віртуалізацію таблиць та оптимістичні оновлення. Наша команда реалізує проєкт під ключ — від архітектури до підтримки, забезпечуючи стабільну роботу та захист від втрати даних при одночасному редагуванні.

Розробка та обслуговування будь-яких видів сайтів:

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

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

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Розробка веб-інтерфейсу ERP: стек, віртуалізація, продуктивність
Складний
від 2 тижнів до 3 місяців
Часті запитання

Зауважимо: коли в ERP-системі складський звіт зависає на 10 секунд при завантаженні 50 000 рядків, користувачі починають використовувати Excel. Розробка веб-інтерфейсу ERP — це не про React з коробки, а про тонке налаштування віртуалізації, керування станом та вирішення конфліктів доступу. У багатокористувацькому середовищі критична блокування записів при одночасному редагуванні. Без proper optimistic concurrency control користувачі перезаписують дані один одного. Типові болі: повільне завантаження таблиць, плутанина з правами доступу, втрата даних при offline. Ми вирішуємо їх за допомогою SPA, віртуалізації, оптимістичних оновлень та Permission Guard для ERP. Розробляємо інтерфейси від MVP з чотирма модулями до повноцінної системи на 15+ модулів. У цьому матеріалі розберемо ключові архітектурні рішення та практичні прийоми, які допоможуть уникнути типових помилок і побудувати масштабований інтерфейс.

Вибір стеку для ERP-інтерфейсу

Вибір архітектури та стеку — фундамент, від якого залежить продуктивність і вартість підтримки. Розглянемо два поширених підходи: SPA та SSR.

Критерій SPA SSR
Продуктивність при інтенсивному UI Висока Низька (кожен клік — запит)
Персоналізація робочих місць Гнучка Обмежена
Офлайн-режим (PWA) Підтримується Неможливий
SEO (не критично для ERP) Погане Добре
Початкове завантаження Повільніше (bundle) Швидше (HTML)

Для ERP вибір однозначний — SPA. Виняток — якщо потрібно SEO для публічних частин (наприклад, каталог продукції).

Frontend стек: React 18+ з Concurrent Features, TypeScript (strict), TanStack Table для таблиць, TanStack Query для даних, React Hook Form + Zod для форм, Zustand для UI-стану. Компонентна бібліотека — Radix UI + Tailwind (гнучкість) або Ant Design (швидкість).

Як працювати з великими таблицями?

Таблиця на 50 000 рядків — типове завдання для складського обліку або звітності. Без віртуалізації браузер зависає. Завдяки віртуалізації час рендерингу падає до 100 мс навіть при 100 000 рядків.

import { useReactTable, getCoreRowModel, flexRender, type ColumnDef, } from '@tanstack/react-table';
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

interface VirtualizedTableProps<T> {
  data: T[];
  columns: ColumnDef<T>[];
  rowHeight?: number;
}

export function VirtualizedTable<T>({
  data,
  columns,
  rowHeight = 40,
}: VirtualizedTableProps<T>) {
  const parentRef = useRef<HTMLDivElement>(null);

  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  const { rows } = table.getRowModel();

  const virtualizer = useVirtualizer({
    count: rows.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => rowHeight,
    overscan: 20,
  });

  const virtualItems = virtualizer.getVirtualItems();
  const totalSize = virtualizer.getTotalSize();

  return (
    <div ref={parentRef} className="overflow-auto h-full">
      <table className="w-full border-collapse">
        <thead className="sticky top-0 bg-white z-10 shadow-sm">
          {table.getHeaderGroups().map(headerGroup => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <th
                  key={header.id}
                  style={{ width: header.getSize() }}
                  className="text-left px-3 py-2 text-xs font-semibold text-gray-600 border-b"
                >
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {virtualItems.length > 0 && (
            <tr style={{ height: virtualItems[0].start }}>
              <td colSpan={columns.length} />
            </tr>
          )}
          {virtualItems.map(virtualRow => {
            const row = rows[virtualRow.index];
            return (
              <tr
                key={row.id}
                className="hover:bg-gray-50 border-b border-gray-100"
                style={{ height: rowHeight }}
              >
                {row.getVisibleCells().map(cell => (
                  <td key={cell.id} className="px-3 py-2 text-sm">
                    {flexRender(cell.column.columnDef.header, cell.getContext())}
                  </td>
                ))}
              </tr>
            );
          })}
          {virtualItems.length > 0 && (
            <tr style={{ height: totalSize - virtualItems[virtualItems.length - 1].end }}>
              <td colSpan={columns.length} />
            </tr>
          )}
        </tbody>
      </table>
    </div>
  );
}

Як прискорити відгук інтерфейсу?

Оптимістичні оновлення — користувач змінює статус замовлення, інтерфейс реагує негайно, не чекаючи відповіді сервера. При помилці відбувається відкат. Це скорочує сприйнятий час відгуку на 200–500 мс.

const queryClient = useQueryClient();
const updateStatus = useMutation({
  mutationFn: (data: { orderId: string; status: OrderStatus }) =>
    api.patch(`/orders/${data.orderId}/status`, { status: data.status }),
  onMutate: async ({ orderId, status }) => {
    await queryClient.cancelQueries({ queryKey: ['orders', orderId] });
    const prev = queryClient.getQueryData(['orders', orderId]);
    queryClient.setQueryData(['orders', orderId], (old: Order) => ({
      ...old,
      status,
    }));
    return { prev };
  },
  onError: (_err, { orderId }, context) => {
    queryClient.setQueryData(['orders', orderId], context?.prev);
    toast.error('Не вдалося змінити статус');
  },
  onSettled: (_, __, { orderId }) => {
    queryClient.invalidateQueries({ queryKey: ['orders', orderId] });
  },
});

Вирішення конфліктів даних при багатокористувацькому доступі

Використовуємо стратегію last-write-wins з версіонуванням сутностей. Кожен запис містить поле version (integer), яке збільшується при зміні. Клієнт передає поточну версію, сервер перевіряє збіг. Якщо версія застаріла — повертає 409 Conflict, клієнт отримує актуальні дані та пропонує користувачеві вирішити конфлікт. Це реалізовано поверх TanStack Query через механізм optimistic updates + rollback.

Розмежування доступу до модулів з Permission Guard

Створюємо компонент Permission Guard, який перевіряє наявність дозволу у користувача. Якщо дозволу немає — можна показати заглушку або нічого. Ролі та дозволи зберігаються на сервері, клієнт отримує їх при аутентифікації.

type Permission = 'orders:read' | 'orders:write' | 'stock:read';

function PermissionGuard({
  permission,
  children,
  fallback = null,
}: {
  permission: Permission;
  children: ReactNode;
  fallback?: ReactNode;
}) {
  const user = useUser();

  if (user.permissions.includes(permission)) {
    return <>{children}</>;
  }

  return <>{fallback}</>;
}

Продуктивність: додаткові оптимізації

  • Code splitting по модулях — користувач складу не завантажує модуль HR, прискорюючи завантаження на 40%.
  • Дебаунс для пошуку та фільтрів — відправляємо запит не частіше одного разу на 300 мс, знижуючи навантаження на сервер у 3 рази.
  • Мемоізація важких обчислень — звіти з агрегацією в браузері через useMemo.

Стежимо за показниками Core Web Vitals — ключовими метриками продуктивності, які безпосередньо впливають на UX. MDN Web Docs рекомендує орієнтуватися на LCP < 2.5 с, FID < 100 мс, CLS < 0.1. Використовуємо TanStack Query для керування серверним станом.

Процес роботи

Етапи розробки ERP-інтерфейсу:

Етап Тривалість Результат
Аналітика 2–4 тижні Опис бізнес-процесів, специфікація модулів
Проєктування 2–4 тижні Архітектура, прототипи ключових екранів
Реалізація 4–6 тижнів на модуль Працюючий функціонал з тестами
Тестування 2–3 тижні Звіт про навантажувальне тестування, виправлення багів
Деплой 1–2 тижні CI/CD, документація, навчання
  1. Аналітика — вивчаємо бізнес-процеси, визначаємо модулі та пріоритети.
  2. Проектування — створюємо архітектуру, обираємо стек, прототипуємо ключові екрани.
  3. Реалізація — ітеративна розробка з демо кожні два тижні.
  4. Тестування — unit, integration, e2e тести, навантажувальне тестування.
  5. Деплой — налаштування CI/CD, міграція даних, навчання користувачів.

На кожному етапі готуємо документацію: технічне завдання, архітектурні рішення, API docs, інструкції користувача.

Строки

  • MVP з чотирма-п'ятьма модулями: 6–8 місяців для команди з трьох-чотирьох розробників.
  • Повноцінна система (15+ модулів): 1,5–2 роки при тій же команді.

Ітеративний підхід — запуск мінімального набору, зворотний зв'язок від користувачів, поступове розширення. Спроба зробити все одразу веде до провалу.

Що входить в роботу

  • Технічне завдання та архітектурна документація.
  • Розробка та налаштування CI/CD.
  • Покриття коду тестами (unit, integration).
  • Документація API та документація користувача.
  • Навчання адміністраторів та користувачів.
  • Гарантійна підтримка 6 місяців після запуску.

Зв'яжіться з нами для оцінки вашого проекту. Отримайте консультацію з архітектури та строків.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1393
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1368
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1139
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060