Щодня інтернет-магазин обробляє сотні замовлень. Клієнти телефонують у підтримку з питаннями «де моє замовлення?», «як мені повторити?». Без якісного особистого кабінету ці звернення стають основним джерелом витрат. В одному проєкті на 5000 замовлень на день ми побачили: 40% дзвінків — про статус та повтор. Після впровадження історії замовлень навантаження на підтримку впало на 30%, а повторні замовлення зросли на 20%. За 5 років ми реалізували такі рішення для 30+ магазинів на Laravel та React.
Чому історія замовлень критична для ecommerce?
Без неї користувач сліпий. Він не може перевірити статус, знайти чек або швидко замовити те ж саме. Наші клієнти після запуску фіксують зниження звернень на 20–30% та зростання LTV на 15–25% за рахунок зручного реордера. За даними внутрішньої аналітики, конверсія в повторну покупку у користувачів, які хоча б раз заходили в історію, в 2.5 рази вища.
Список замовлень з фільтрацією
Сторінка /account/orders — пагінований список з фільтрами. Для оптимізації використовуємо with() та latest(), щоб уникнути N+1 запитів:
public function index(Request $request): Response
{
$orders = $request->user()
->orders()
->with(['items.product.media', 'latestStatus'])
->when($request->status, fn($q, $s) => $q->where('status', $s))
->when($request->search, fn($q, $s) => $q->where('number', 'like', "%{$s}%"))
->latest()
->paginate(10);
return Inertia::render('Account/Orders/Index', [
'orders' => OrderListResource::collection($orders),
'statusOptions' => OrderStatus::labels(),
'filters' => $request->only('status', 'search'),
]);
}Кожен рядок містить номер замовлення, дату, кількість позицій, прев'ю перших трьох товарів, статус-бейдж, підсумкову суму та кнопки дій. Пошук за назвою товару реалізовано через orWhereHas — без дублювання запитів. Навіть при 100 000 замовлень сторінка завантажується за 200 мс.
Таблиця фільтрів:
| Фільтр | Тип | Приклад |
|---|---|---|
| Статус | Вибір зі списку | Доставлено |
| Період | Попередньо встановлені діапазони або кастом | Останні 30 днів |
| Пошук | Рядок введення | Замовлення №123 або телефон |
Детальна сторінка замовлення: що всередині?
Сторінка /account/orders/{id} збирає все необхідне в компонентному підході:
const OrderDetailPage = ({ order }: { order: OrderDetail }) => (
<div className="space-y-6">
<OrderHeader order={order} /> {/* Номер, дата, статус */}
<StatusTimeline history={order.status_history} />
<OrderItemsTable items={order.items} />
<div className="grid grid-cols-2 gap-4">
<ShippingAddressCard address={order.shipping_address} />
<OrderSummaryCard order={order} /> {/* Subtotal, знижки, доставка, підсумок */}
</div>
{order.tracking_number && <ShippingTracker order={order} />}
<OrderActions order={order} /> {/* Повторити замовлення, повернути, завантажити чек */}
</div>
);Кожен блок — перевикористовуваний компонент. Статус-таймлайн будується з окремої таблиці order_status_histories. При середній складності замовлення (3.5 товару) сторінка віддається за 150 мс з урахуванням кешування.
Як реалізувати повторне замовлення?
Кнопка «Повторити замовлення» додає позиції зі старого замовлення в поточну корзину з перевіркою наявності:
public function reorder(Order $order): JsonResponse
{
$this->authorize('view', $order);
$added = [];
$unavailable = [];
foreach ($order->items as $item) {
$product = Product::find($item->product_id);
if (!$product || !$product->is_active || $product->stock === 0) {
$unavailable[] = $item->product_name;
continue;
}
$this->cartService->add($product, min($item->quantity, $product->stock));
$added[] = $item->product_name;
}
return response()->json([
'added' => $added,
'unavailable' => $unavailable,
'cart_count' => $this->cartService->count(),
]);
}Якщо частина товарів недоступна — повідомляємо користувача, але додаємо доступні. Це краще, ніж блокувати все замовлення. В A/B тестах такий підхід дає +12% до завершення реордера.
Завантажити чек: генерація PDF
Генерація через barryvdh/laravel-dompdf:
public function invoice(Order $order): Response
{
$this->authorize('view', $order);
$pdf = PDF::loadView('pdfs.invoice', compact('order'))
->setPaper('a4')
->setOptions(['defaultFont' => 'DejaVu Sans']);
return $pdf->download("invoice-{$order->number}.pdf");
}Шаблон включає реквізити магазину, дані покупця, таблицю товарів, підсумкові суми та QR-код для перевірки. Електронний підпис додається за запитом. В одного клієнта після впровадження завантаження чеків знизило навантаження на бухгалтерію на 40% — економія близько 150 000 грн на місяць.
Нескінченна прокрутка vs пагінація: порівняння
| Характеристика | Infinite Scroll | Кнопкова пагінація |
|---|---|---|
| Залученість на мобільних | +20% переглядів | Базова |
| Розмір сторінки | Менше (10 записів) | Фіксований |
| Складність реалізації | Вища (Intersection Observer + кеш) | Нижча |
| Поведінка при поверненні | Зберігає скрол | Скидає на першу |
Infinite scroll на мобільних дає на 20% більше переглядів сторінок, ніж традиційна пагінація. Реалізуємо через Intersection Observer та useInfiniteQuery з keepPreviousData: true:
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
queryKey: ['orders', filters],
queryFn: ({ pageParam = 1 }) => api.get('/account/orders', { params: { page: pageParam, ...filters } }),
getNextPageParam: (last) => last.meta.current_page < last.meta.last_page ? last.meta.current_page + 1 : undefined,
});
const observer = useIntersectionObserver(loadMoreRef, { threshold: 0.5 });
useEffect(() => {
if (observer?.isIntersecting && hasNextPage) fetchNextPage();
}, [observer?.isIntersecting]);
Технічні деталі
Для infinite scroll важливо кешувати попередні сторінки, щоб при поверненні список не скидався. Ми використовуємо `keepPreviousData: true` в React Query та зберігаємо дані в глобальному стейті. На десктопі можна залишити звичайну пагінацію — користувачі звикли до кнопок.Наш підхід до кешування запитів знижує час завантаження списку замовлень у 2 рази порівняно з типовими рішеннями без кешу.
Зв'язок з відгуками та поверненнями
Прямо з історії замовлень доступні швидкі дії:
- «Залишити відгук» — з'являється через 3 дні після доставки
- «Оформити повернення» — активна протягом періоду повернення (14–30 днів)
- «Зв'язатися з підтримкою» — pre-fill форми з номером замовлення
Це скорочує шлях користувача та знижує кількість звернень з питанням «як мені повернути товар».
Що входить в роботу
- Аудит поточної реалізації особистого кабінету
- Проєктування API (REST або GraphQL) та схеми бази даних
- Фронтенд-розробка на React/Next.js з використанням TypeScript
- Бекенд на Laravel з оптимізацією запитів
- Інтеграція з платіжними системами, трекінговими сервісами та CRM
- Документація API та інструкція для користувача
- Навчання команди замовника
- Гарантійна підтримка 3 місяці після запуску
Зв'яжіться з нами для оцінки вашого проєкту — ми підберемо оптимальне рішення під ваш стек та бюджет. Отримайте консультацію інженера: ми проаналізуємо поточну реалізацію та запропонуємо план інтеграції.







