Розробка кастомної административної панелі
Кастомна адміністративна панель — рішення, коли стандартні інструменти (Filament, AdminJS, Django Admin) не покривають складність бізнес-логіки або вимагають занадто багато кастомізації. Підходить для проектів зі специфічними рабочими процесами, нестандартними інтерфейсами та високими вимогами до брендування та UX для внутрішніх користувачів.
Коли потрібна кастомна панель
- Складні візуалізації даних, які складно реалізувати в готових фреймворках
- Нестандартні права доступу з гранульованим контролем
- Інтеграція з зовнішніми системами прямо в інтерфейсі (1С, CRM, телефонія)
- Специфічні рабочі процеси (багатоетапні workflow, узгодження)
- Високі вимоги до брендування та UX для внутрішніх користувачів
Технологічний стек
Backend: Laravel + REST API (Resource Controllers, API Resources, Policies) Frontend: React + TanStack Query + React Hook Form + Shadcn/ui Таблиці: TanStack Table v8 (віртуалізація, сортування, фільтрація на клієнті/сервері) Стан: Zustand або Jotai для глобального стану Авторизація: Spatie Laravel Permission для ролей та дозволів
Архітектура доступу до даних
React SPA → Laravel API → Eloquent → PostgreSQL
↓
Gates & Policies
↓
Response Resources
Кожен endpoint захищен через Sanctum (token-based auth) та перевіряє дозволи через Gate:
// AdminOrderController
public function index(Request $request): JsonResponse
{
$this->authorize('viewAny', Order::class);
$orders = Order::query()
->with(['customer', 'items.product'])
->when($request->status, fn($q, $s) => $q->where('status', $s))
->when($request->search, fn($q, $s) => $q->where(function($q) use ($s) {
$q->where('id', $s)
->orWhereHas('customer', fn($q) => $q->where('email', 'like', "%{$s}%"));
}))
->orderBy($request->sort_by ?? 'created_at', $request->sort_dir ?? 'desc')
->paginate($request->per_page ?? 25);
return OrderResource::collection($orders)->response();
}
Серверна пагінація та фільтрація
TanStack Table підтримує серверні операції. Стейт таблиці синхронізується з URL через query params:
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [sorting, setSorting] = useState<SortingState>([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
// Синхронізація з URL
useEffect(() => {
const params = new URLSearchParams();
params.set('page', String(pagination.pageIndex + 1));
params.set('per_page', String(pagination.pageSize));
sorting.forEach(s => {
params.set('sort_by', s.id);
params.set('sort_dir', s.desc ? 'desc' : 'asc');
});
router.replace(`?${params.toString()}`);
}, [columnFilters, sorting, pagination]);
Inline-редактування
Кастомні панелі часто вимагають редагування прямо в таблиці без відкриття окремої сторінки:
const EditableCell = ({ row, column, table }) => {
const [isEditing, setIsEditing] = useState(false);
const [value, setValue] = useState(row.original[column.id]);
const save = async () => {
await updateMutation.mutateAsync({
id: row.original.id,
[column.id]: value
});
setIsEditing(false);
};
if (!isEditing) {
return <span onDoubleClick={() => setIsEditing(true)}>{value}</span>;
}
return (
<input value={value} onChange={e => setValue(e.target.value)}
onBlur={save} onKeyDown={e => e.key === 'Enter' && save()} autoFocus />
);
};
Масові операції
Обов'язковий елемент для роботи з великими обсягами даних:
const selectedIds = table.getSelectedRowModel().rows.map(r => r.original.id);
const handleBulkAction = async (action: string) => {
await bulkMutation.mutateAsync({ ids: selectedIds, action });
table.resetRowSelection();
};
Права доступу на рівні UI
Кнопки та розділи відображаються тільки користувачам з відповідними правами:
const { can } = usePermissions();
return (
<DropdownMenu>
{can('orders.update') && <DropdownMenuItem onClick={editOrder}>Редактувати</DropdownMenuItem>}
{can('orders.delete') && <DropdownMenuItem onClick={deleteOrder} className="text-red-500">Видалити</DropdownMenuItem>}
</DropdownMenu>
);
Аудит-лог
Усі дії в admin-панелі логуються:
OrderAuditLog::create([
'admin_id' => auth()->id(),
'order_id' => $order->id,
'action' => 'status_changed',
'old_value' => $order->getOriginal('status'),
'new_value' => $order->status,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent()
]);
Реалтайм-оновлення
При оновленні даних іншим користувачем — уведомлення через WebSocket:
useEffect(() => {
const channel = Echo.private('admin').listen('OrderUpdated', (event) => {
queryClient.invalidateQueries(['orders']);
toast.info(`Замовлення #${event.orderId} оновлено`);
});
return () => channel.stopListening('OrderUpdated');
}, []);
Срок розробки: 8–16 тижнів залежно від кількості сущностей, складності прав доступу та обсягу кастомної логіки.







