Разработка кастомной административной панели сайта
Кастомная административная панель — это решение, когда стандартные инструменты (Filament, AdminJS, Django Admin) не покрывают сложность бизнес-логики или требуют слишком много кастомизации. Подходит для проектов со специфическими рабочими процессами, нестандартными интерфейсами и высокими требованиями к производительности.
Когда нужна кастомная панель
- Сложные визуализации данных, которые сложно реализовать в готовых фреймворках
- Нестандартные права доступа с гранулярным контролем
- Интеграция с внешними системами прямо в интерфейс (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
Каждый эндпоинт защищён через 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 поддерживает server-side операции. Стейт таблицы синхронизируется с URL через query params:
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [sorting, setSorting] = useState<SortingState>([]);
const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 });
// Sync с 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-редактирование
Кастомные панели часто требуют редактирования прямо в таблице без открытия отдельной страницы:
// Ячейка с 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 />
);
};
Bulk-операции
Массовые действия — обязательный элемент для работы с большими объёмами данных:
// Выбор строк + действие
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 недель в зависимости от количества сущностей, сложности прав доступа и объёма кастомной логики.







