ERP System Web Interface Development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
ERP System Web Interface Development
Complex
from 2 weeks to 3 months
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

ERP System Development (Web Interface)

An ERP system for mid-market business is dozens of interconnected modules: warehouse, procurement, sales, production, finance, HR. The web interface is only part of it, but a critical one: this is where employees spend eight hours a day, and poor UX directly costs money in the form of errors and slow operation.

Architectural choice: SPA vs SSR vs hybrid

For ERP, the choice is clear — SPA (Single Page Application). Reasons:

Intensive interaction: forms with dozens of fields, modal windows, drag-and-drop tables, inline editing. Server-side rendering of each change is not an option.

Personalization: each user sees their own set of modules, their own workspace.

Offline mode: warehouse at production facility may have unstable internet — PWA with IndexedDB allows work and synchronization later.

Stack for serious ERP interface

Frontend:
- React 18+ (Concurrent Features for heavy tables)
- TypeScript (strict, no any in business logic)
- TanStack Table v8 (virtualization, 100k+ rows)
- TanStack Query (server state, cache, optimistic updates)
- React Hook Form + Zod (complex forms with nested objects)
- Zustand (global UI state: open panels, filters)

Backend (for web client):
- REST API or tRPC
- GraphQL justified if modules are independently developed by different teams

Component library:
- Radix UI + Tailwind (customization without CSS conflicts)
  or Ant Design / Mantine (quick start, rich components)

Key technical tasks

1. Working with large tables

A table with 50,000 rows is a typical task for inventory accounting or reporting. Without virtualization, the browser freezes.

// VirtualizedTable.tsx
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>
          {/* Empty space above */}
          {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.cell, cell.getContext())}
                  </td>
                ))}
              </tr>
            );
          })}
          {/* Empty space below */}
          {virtualItems.length > 0 && (
            <tr style={{ height: totalSize - virtualItems[virtualItems.length - 1].end }}>
              <td colSpan={columns.length} />
            </tr>
          )}
        </tbody>
      </table>
    </div>
  );
}

2. Complex forms with dependent fields

An ERP order creation form can include: contractor selection → loading their contracts → contract selection → auto-fill payment terms → add line items → recalculate amounts.

// OrderForm.tsx (fragment)
import { useForm, useFieldArray, useWatch } from 'react-hook-form';
import { useQuery } from '@tanstack/react-query';

function OrderForm() {
  const { control, register, setValue, watch } = useForm<OrderFormData>({
    resolver: zodResolver(orderSchema),
    defaultValues: {
      lines: [{ productId: '', qty: 1, price: 0, discount: 0 }],
    },
  });

  const { fields, append, remove } = useFieldArray({ control, name: 'lines' });
  const contractorId = watch('contractorId');

  // When contractor changes, load their contracts
  const { data: contracts } = useQuery({
    queryKey: ['contracts', contractorId],
    queryFn: () => fetchContracts(contractorId),
    enabled: !!contractorId,
  });

  // Auto-fill terms from contract
  function handleContractSelect(contractId: string) {
    const contract = contracts?.find(c => c.id === contractId);
    if (contract) {
      setValue('paymentTermsDays', contract.paymentTermsDays);
      setValue('currencyCode', contract.currencyCode);
      setValue('vatRate', contract.vatRate);
    }
  }

  // Recalculate totals when any line changes
  const lines = useWatch({ control, name: 'lines' });
  const totals = useMemo(() => {
    return lines.reduce((acc, line) => {
      const subtotal = line.qty * line.price * (1 - (line.discount ?? 0) / 100);
      return {
        subtotal: acc.subtotal + subtotal,
        vat: acc.vat + subtotal * (line.vatRate ?? 0.2),
      };
    }, { subtotal: 0, vat: 0 });
  }, [lines]);

  // ... JSX
}

3. Optimistic updates for response speed

User changes order status — the interface should respond immediately, not wait for server response:

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 }) => {
    // Cancel current requests for this order
    await queryClient.cancelQueries({ queryKey: ['orders', orderId] });

    // Save current state for rollback
    const prev = queryClient.getQueryData(['orders', orderId]);

    // Optimistically update
    queryClient.setQueryData(['orders', orderId], (old: Order) => ({
      ...old, status,
    }));

    return { prev };
  },

  onError: (_err, { orderId }, context) => {
    // Rollback on error
    queryClient.setQueryData(['orders', orderId], context?.prev);
    toast.error('Failed to change status');
  },

  onSettled: (_, __, { orderId }) => {
    queryClient.invalidateQueries({ queryKey: ['orders', orderId] });
  },
});

4. Access control to modules

// PermissionGuard.tsx
import { useAuth } from '@/stores/auth';

interface PermissionGuardProps {
  permission: string;     // 'orders:create', 'inventory:write'
  fallback?: ReactNode;
  children: ReactNode;
}

export function PermissionGuard({ permission, fallback, children }: PermissionGuardProps) {
  const { user } = useAuth();
  const hasPermission = user?.permissions.includes(permission)
    || user?.roles.some(role => ROLE_PERMISSIONS[role]?.includes(permission));

  if (!hasPermission) {
    return fallback ? <>{fallback}</> : null;
  }

  return <>{children}</>;
}

// Usage
<PermissionGuard permission="orders:create" fallback={<ReadOnlyBadge />}>
  <CreateOrderButton />
</PermissionGuard>

Performance of ERP interface

Several mandatory optimizations:

Code splitting by modules — warehouse user doesn't load HR module:

const routes = [
  {
    path: '/warehouse/*',
    element: React.lazy(() => import('@/modules/warehouse')),
    permission: 'warehouse:view',
  },
  {
    path: '/hr/*',
    element: React.lazy(() => import('@/modules/hr')),
    permission: 'hr:view',
  },
];

Debouncing for search and filters — don't send request after each keystroke.

Memoization of heavy computations — reports with aggregation in browser (not always possible on server) via useMemo.

Timeline

ERP interface is not developed "from scratch in three months". Realistic timeframes:

MVP with four-five key modules (orders, warehouse, references, reporting, users) — six-eight months for a team of three-four developers.

Full-fledged system with 15–20 modules — from one-and-a-half to two years with the same team.

Attempting to do everything at once without iterative approach is a guaranteed failure. The right strategy: launch with minimal working set of modules, constant feedback from real users, iterative expansion.