Расширяемый CRUD: как строить кастомные интерфейсы на базе Admiral


С каким кейсом столкнулись мы
- Как предложить клиенту решение с разумным бюджетом и быстрым сроком запуска?
- Необходима ли разработка решения с нуля, если значительная часть функционала представляет собой CRUD’ы (справочники с клиентами, мастерами, местами обслуживания, прайс-листами), а для решения задач пользователя необходима буквально пара кастомных интерфейсов?
- Можно ли сделать кастомный интерфейс на базе Admiral?
Как Admiral обеспечивает кастомизацию без компромиссов
- Реализовать уникальные рабочие процессы, которые не вписываются в стандартную модель CRUD;
- Создавать интерактивные дашборды с графиками, статистикой и другими виджетами, которые отображают важные данные в удобном формате.
Пример API-клиента
Добавление кастомных страниц в Admiral
- Создавать и редактировать записи;
- Отслеживать загруженность мастеров через календарь.
Пример API-клиента
import React, { useCallback } from 'react'
import { Page, Form, Notification } from '@devfamily/admiral'
import { RecordSection, ClientSection, DiscountSection, PaymentSection } from './components'
import api from './api'
export enum RecordPageType {
CREATE = 'create',
EDIT = 'edit',
}
const RecordPage = ({ type }: { type: RecordPageType }) => {
// Function to get default data if this is an edit page
const fetchInitialData = useCallback(async () => {
if (type === RecordPageType.EDIT) {
let data = {}
try {
// Connect API request to get record data
} catch (err) {
// Displaying an error notification
Notification({
message: `Error getting record: ${err?.message}`,
type: 'error',
})
}
return Promise.resolve({
data,
values: {},
})
} else {
// Return empty data for the new record
return Promise.resolve({
data: {}, // Сan use default data
values: {},
})
}
}, [type, idRecord])
// Function for submitting form data
const onSubmit = useCallback(async (values) => {
// Connect API request to create or edit record
}, [])
return (
<Page title={type === RecordPageType.CREATE ? 'Add record' : `Edit record`}>
<Form submitData={onSubmit} fetchInitialData={fetchInitialData}>
{/* Include custom form sections as separate components */}
<RecordSection />
<ClientSection />
<DiscountSection />
<PaymentSection />
</Form>
</Page>
)
}
export default RecordPage

- create.tsx – страница для создания новой записи
import React from 'react'
import RecordPage, { RecordPageType } from '../RecordPage'
const CreateRecordPage = () => {
return <RecordPage type={RecordPageType.CREATE} />
}
export default CreateRecordPage
- [id].tsx – страница редактирования существующей записи (`[id]` в имени файла позволяет Admiral динамически определять маршрут)
import React from 'react'
import RecordPage, { RecordPageType } from '../RecordPage'
const EditRecordPage = () => {
return <RecordPage type={RecordPageType.EDIT} />
}
export default EditRecordPage
- index.tsx – базовая страница CRUD для отображения таблицы записей:
import { CRUD } from '@/src/crud/records'
export default CRUD.IndexPage

Создание календаря с записями
import React, { useEffect, useState } from 'react'
import { CalendarHeader, CalendarTable, CalendarRecordNote } from './ui'
import { TCalendarHeaderData, TGroupedServices, TCalendarHeaderData } from './ts'
import styles from './CalendarPage.module.scss'
const CelendarPage = () => {
const [isLoading, setIsLoading] = useState<boolean>(false)
const [timeSlots, setTimeSlots] = useState<string[]>([])
const [services, setServices] = useState<Array<TGroupedServices> | null>(null)
const [calendarHeaderData, setCalendarHeaderData] = useState<Array<TCalendarHeaderData>>([])
const [comment, setComment] = useState<string>('')
const fetchCalendarData = async ({date}: {date: string}) => {
// Connect API request to get data
}
useEffect(() => {
fetchCalendarData({
date: urlState.date || new Date()
})
}, [])
return <Page title="Главная">
<div className={styles.calendar_page}>
<CalendarHeader />
<CalendarRecordNote comment={comment} />
<CalendarTable
isLoading={isLoading}
timeSlots={timeSlots}
services={services}
calendarHeaderData={calendarHeaderData}
/>
</div>
</Page>
}
export default EditRecordPage

Интеграция в навигацию
import React from 'react'
import { Menu, MenuItemLink } from '@devfamily/admiral'
import { FiUsers, FiUser, FiSettings } from 'react-icons/fi'
const CustomMenu = () => {
return (
<Menu>
<MenuItemLink icon="FiUsers" name="Main" to="/main" /> // navigate to custom page
<MenuItemLink icon="FiUser" name="Records" to="/records" /> // navigate to page with custom components
<MenuItemLink icon="FiSettings" name="Services" to="/services" />
<MenuItemLink icon="FiSettings" name="Masters" to="/masters" />
</Menu>
)
}
export default CustomMenu
Заключение
- Подходит для проектов с нестандартной логикой – там, где типовые CRUD-генераторы быстро упираются в структурные ограничения, мы можем продолжать расширять интерфейс;
- Предоставляет гибкость без лишнего кода – фреймворк обладает открытой архитектурой, в которую можно встроить что угодно: кастомные компоненты, бизнес-логику, календарные интерфейсы.
- Обеспечивает скорость и масштабируемость – после быстрой сборки базового CRUD, вы сможете и дальше наращивать любые интерфейсы по мере роста проекта.