Аудит маркетплейса PlanForge
Актуально на: 2026-05-26
Дата анализа: 15 апреля 2026
Проанализированы все файлы: страницы, API, компоненты, модели данных, уведомления, навигация
1. Текущая архитектура (что есть)
Модель данных
| Модель | Назначение |
|---|---|
PlanListing | Листинг (projectId, sellerId, purchaseType, price, status, previewFields) |
Order | Заказ/покупка (listingId, buyerId, purchaseType, price, resultProjectId) |
ListingReview | Решение ревьюера (decision: APPROVE/REJECT, comment) |
PlanAccess | Контроль доступа к проекту (userId, projectId, accessLevel) |
Notification | Уведомления (type, title, message, link, isRead) |
Страницы и навигация
| Страница | Навигация | Назначение |
|---|---|---|
/marketplace | Header (все пользователи) | Каталог одобренных листингов |
/marketplace/[id] | Не ссылается из UI | Отдельная страница листинга (мёртвый код) |
/projects/[id]/publish | Иконка на карточке проекта | Форма публикации плана |
/my-listings | Header (все пользователи) | Управление своими листингами |
/my-purchases | Header (все пользователи) | История покупок |
/reviews | Header (REVIEWER/ADMIN) | Единая очередь ревью (публикации + планы) |
/projects/[id]?back=/reviews | Ссылка из очереди | Ревью прямо в плане (панель ревью) |
/admin/marketplace | Таб в админке (ADMIN) | Статистика |
API endpoints (13 шт.)
- Публичные: listings (GET), filters (GET), listing detail (GET)
- Покупка: purchase (POST)
- Продавец: my-listings (GET), listing details (GET), archive (POST), resubmit (POST), reviews history (GET)
- Ревьюер: очередь
/api/reviews/plans(GET), заявка/api/plans/[id]/review(GET/PATCH — решение по публикации), пометки/api/plans/[id]/review/notes - Покупатель: my-purchases (GET)
2. Детальный разбор механик: что работает и что нет
2.1. Публикация листинга
Поток: Страница проектов → иконка «Опубликовать» → /projects/[id]/publish → выбор типа/цены/превью-полей → POST /api/plans/[id]/listings → статус PENDING_REVIEW
Что работает:
- Выбор типа продажи (Копия / Эксклюзив)
- Ввод цены (валидация > 0 и <= 10 000 000)
- Выбор превью-полей (checkbox-и: название, компания, отрасль, регион, сценарий, срок)
- Проверка что у проекта нет активного листинга (race-safe через транзакцию)
- Проверка что пользователь — владелец проекта
Что НЕ работает / отсутствует:
- Нет описания листинга — продавец не может добавить свободный текст, рассказывающий о плане
- Нет предпросмотра — продавец не видит как его листинг будет выглядеть до отправки
Нет проверки «пустого» плана✅ — серверная + клиентская валидация: нельзя опубликовать план без продуктов, расходов и сотрудников- Нет предупреждения для EXCLUSIVE_TRANSFER — при эксклюзиве продавец потеряет план, но на странице публикации нет яркого предупреждения о необратимости
После публикации редирект на✅ — заменён на/projects/my-listings- Нет валидации минимальной цены на клиенте — можно ввести 0.01 RUB
2.2. Ревью листинга ✅ ПЕРЕВЕДЕНО НА ДВИЖОК РЕВЬЮ ПЛАНА
Поток: публикация (или правка описания/обложки/галереи одобренного листинга, или resubmit после отказа) открывает заявку PlanReviewRequest с kind = LISTING и ссылкой на листинг → ревьюер видит её в общей очереди /reviews → открывает сам проект в воркспейсе (read-only через canReviewProject) → оставляет пометки с привязкой к месту в плане → автор закрывает их → ревьюер завершает заявку решением APPROVE / REJECT, что и публикует листинг или возвращает его на доработку.
Что работает:
- Единая очередь: заявки на публикацию и планы «просто на разбор» в одном списке с фильтром по поводу
- Ревьюер работает в реальном плане: расчёты, графики, все разделы — а не readonly-выгрузка
- Пометки по разделам с якорем на элемент, снимком среза и диффом «что изменил автор»
- Коммерческая карточка листинга (цена, тип, тираж, обложка, галерея, previewFields) — в панели ревью
- Одобрение с незакрытыми пометками требует подтверждения (
force) - Отклонение требует причину — она уходит в
rejectionCommentи в уведомление автору - История решений остаётся в
ListingReview, её читают «Мои листинги» - Отзыв заявки автором снимает листинг с публикации (архив); архивация листинга закрывает заявку
Что осталось:
-
Нет чеклиста / критериев ревью — ревьюер не имеет гайдлайнов, на что обращать внимание
-
PlanAccess REVIEW по-прежнему не используется — доступ ревьюера даёт
canReviewProject(активная заявка + глобальная роль), а уровеньREVIEWвPlanAccessи API/api/plans/[id]/reviewersживут отдельно и не привязаны к UI -
Нет промежуточного статуса «нужны правки» — вместо него пометки: заявка остаётся
IN_PROGRESS, пока автор их не закроет
2.3. Копирование проекта при покупке (COPY)
Поток: Покупатель нажимает «Купить» → подтверждение → POST /api/purchase/[listingId] → loadWorkspaceStateFromDb → создание нового проекта → saveWorkspaceStateToDb → ответ с projectId
Что работает:
- Атомарная транзакция для создания проекта, PlanAccess и Order
- Дубль-проверка внутри транзакции (перепроверка статуса)
- Защита от повторной покупки одной и той же копии
- Создание workspace для покупателя если его нет
- Имя проекта с суффиксом «(копия)»
- Откат при неудачном сохранении state
Что потенциально проблемно:
-
saveWorkspaceStateToDb выполняется ВНЕ транзакции — это осознанный выбор (save слишком тяжёлый для транзакции), но откат неатомарен:
// Откатываем в единой транзакции, чтобы не оставлять сирот await prisma.$transaction([...]).catch(() => {});Если откат тоже упадёт — останутся Order и Project-сироты.
-
Не копируются настройки проекта— проверено, claim снят.loadWorkspaceStateFromDbзагружает все данные (ProjectInfo, ProjectSettings, Products, Personnel, Expenses, Calendar),saveWorkspaceStateToDbих полностью сохраняет. Копирование данных — полное. -
Нет уведомления покупателю✅ — добавлено уведомление типаPLAN_PURCHASEDдля покупателя при COPY и EXCLUSIVE_TRANSFER. -
Скопированный проект получает статус DRAFT — это ок, но нет объяснения для покупателя что это значит.
-
Нет «происхождения» — в скопированном проекте нигде не указано, что он был куплен на маркетплейсе, от какого продавца.
2.4. Эксклюзивная передача (EXCLUSIVE_TRANSFER)
Что работает:
- FOR UPDATE lock на строке листинга (race condition protection)
- Перенос проекта в workspace покупателя
- Seller получает NO_ACCESS
- Buyer получает OWNER
- Листинг переходит в статус SOLD
- Уведомление продавцу
Что проблемно:
Необратимость без предупреждения✅ — добавлен заметный amber-баннер с иконкой ⚠️ на странице публикации при выборе EXCLUSIVE_TRANSFER- Продавец видит «Продан» но не знает кому — в
/my-listingsнет информации о покупателе Нет проверки workspace переноса— ложный claim, удалён. Вся операция (PlanAccess, workspace transfer, статус SOLD, Order) выполняется внутри одной$transaction. Если любой шаг упадёт — откатится всё. Это сделано правильно.
2.5. Маркетплейс (каталог)
Что работает:
- Поиск (debounced)
- 6 фильтров: тип покупки, отрасль, регион, сценарий, мин/макс цена
- 4 варианта сортировки
- Серверная пагинация (12 на страницу)
- Индикатор «Ваш план» / «Куплено» на карточках
- Модалка с деталями по клику
- Сайдбар фильтров с мобильным toggle
Что НЕ работает / отсутствует:
-
previewFields игнорируются при показе — в API
listings/[id]/route.ts(строка 55-70) превью формируется из всех полей ProjectInfo (title, company, industry, region, variant, scenario, durationMonths, startDate), а не из тех полей, которые продавец выбрал вpreviewFields. Продавец выбирает что показывать, но API это полностью игнорирует и показывает всё.previewFieldsиспользуется только как fallback если ProjectInfo пуст:const previewData = Object.keys(richPreview).length > 0 ? richPreview : listing.previewFields; -
Счётчик «Состав плана» виден только владельцу/покупателю — другие пользователи не видят даже количество продуктов/сотрудников/расходов. Это снижает привлекательность карточки.
-
Нет информации о дате публикации на карточке — createdAt не отображается.
-
Модалка vs отдельная страница рассинхронизированы:
ListingModalиспользует поляisOwner,isPurchased,purchasedProjectIdListingDetailPage(/marketplace/[id]) используетaccessLevel: "full" | "preview"— поле которого нет в API ответе- Отдельная страница листинга не показывает кнопки для владельца/покупателя (нет isOwner/isPurchased в типе)
- Это мёртвый код: страница существует, но никуда не ведёт из основного UI
-
Нет пустого состояния с CTA — «Опубликуйте свой план или зайдите позже» но нет кнопки «Опубликовать»
2.6. Мои листинги (продавец)
Что работает:
- Таблица с колонками: название, статус, тип, цена, продажи, доход
- Статусные бейджи (PENDING_REVIEW, APPROVED, REJECTED, SOLD, ARCHIVED)
- Причина отклонения при REJECTED
- Архивирование (PENDING/APPROVED → ARCHIVED)
- Повторная отправка (REJECTED → PENDING_REVIEW) с изменением цены/типа/превью
- Модалка детальной информации (продукты, персонал, расходы, инвестиции)
- История ревью (inline)
- Пагинация
Что НЕ работает / отсутствует:
Нет редактирования активного листинга✅ — добавлено изменение цены APPROVED листинга (без повторного ревью, с настраиваемым кулдауном)- Нет ссылки «Посмотреть в маркетплейсе» — для APPROVED листингов нет быстрого перехода на его страницу/модалку в маркетплейсе
- Нет информации о покупателях — продавец видит «3 продажи», но не знает кто купил
- Нет фильтрации по статусу — если листингов много, нельзя отфильтровать только активные/отклонённые
- Модалка деталей не закрывается при навигации — query-параметр
listingостаётся в URL
2.7. Мои покупки (покупатель)
Что работает:
- Таблица: название, продавец, тип, цена, дата, ссылка на проект
- Пагинация
- ✅ Поиск по названию плана и продавцу (debounced, серверный)
- ✅ Сортировка: по дате (новые/старые), по цене (↑/↓)
- ✅ Суммарная статистика: количество покупок и общая потраченная сумма
- ✅ Filter-aware empty state: «Ничего не найдено» + сброс фильтров
Что НЕ работает / отсутствует:
Скудный UI — это самая простая из всех страниц, просто таблица без фильтров, поиска, сортировки✅- Нет статуса проекта — покупатель не видит состояние купленного проекта
- Нет возможности оставить отзыв — нет CTA для обратной связи
- Нет группировки — все покупки в одном плоском списке
Нет суммарной статистики — общая потраченная сумма, количество покупок✅
2.8. Админ-панель маркетплейса
Что работает:
- 7 счётчиков: всего листингов, на ревью, активных, продано, всего продаж, сумма продаж, пользователей
- Auto-refresh каждые 5 секунд
Что НЕ работает / отсутствует:
- Polling каждые 5 секунд — слишком агрессивно, лишняя нагрузка на БД
- Нет списка листингов — админ видит только числа, не может просмотреть конкретные листинги, модерировать, удалять
- Нет графиков/трендов — сколько продаж за неделю/месяц, динамика
- Нет управления пользователями — нельзя заблокировать продавца, изменить роль
- Нет логов действий — кто когда что одобрил/отклонил/купил
2.9. Уведомления
Что работает:
- Колокольчик в header
- Polling каждые 30 сек
- «Прочитать все»
- Относительное время (только что, N мин назад, N ч назад)
- Ссылки в уведомлениях
Что НЕ работает / отсутствует:
-
4 типа уведомлений реализованы:
LISTING_APPROVED→ продавцу (ссылка на/my-listings) ✅ ссылка исправленаLISTING_REJECTED→ продавцу (ссылка на/my-listings)PLAN_SOLD→ продавцу (ссылка на/my-listings)PLAN_PURCHASED→ покупателю (ссылка на/my-purchases) ✅ добавлено
-
Не реализованы уведомления:
Покупателю при покупке✅- Ревьюерам о новых листингах
- Продавцу о просмотрах
- При изменении цены
-
Нет удаления отдельных уведомлений — только массовое «прочитать все»
-
Нет push-уведомлений — только polling
-
Ссылка в уведомлении LISTING_APPROVED ведёт на мёртвую страницу
/marketplace/${listingId}— страница существует но не интегрирована в UI
3. Критические баги и рассинхронизации
3.1. previewFields полностью сломаны ✅ ИСПРАВЛЕНО
Продавец на странице Теперь richPreview и project.info фильтруются по ключам из previewFields./projects/[id]/publish выбирает checkbox-ами какие поля показывать (title, company, industry…). Эти настройки сохраняются в PlanListing.previewFields как JSON { title: true, industry: true }. Но API в /api/marketplace/listings/[id]/route.ts полностью игнорирует previewFields и всегда показывает ВСЕ заполненные поля из ProjectInfo.
3.2. Отдельная страница листинга /marketplace/[id] рассинхронизирована ✅ ИСПРАВЛЕНО
/marketplace/[id] рассинхронизированаСтраница удалена как мёртвый код. Ссылка в уведомлении LISTING_APPROVED заменена на /my-listings.
3.3. Покупка COPY: дубль-проверка слишком строгая
const existingOrder = await tx.order.findFirst({
where: { listingId: listing.id, buyerId, purchaseType: "COPY" },
});
if (existingOrder) throw new Error("DUPLICATE_COPY_PURCHASE");Это значит что покупатель не может купить копию одного плана дважды. Для типа COPY это может быть нежелательно — зачем ограничивать? Если план обновился, покупатель может захотеть новую копию.
4. Создание листинга: нет нормальной точки входа
Единственный способ создать листинг — найти маленькую иконку IconUpload на карточке проекта в /projects. Всё. Больше нигде нет CTA «Продать» или «Создать листинг»:
✅ — добавлена кнопка «Опубликовать план» →/marketplace(пустое состояние)/projects✅ — добавлена кнопка «Опубликовать план» →/my-listings(пустое состояние)/projects- Header — нет кнопки «Продать» / «Создать листинг»
- Проект (workspace) — нет кнопки «Опубликовать» внутри самого редактора проекта
Пользователь, который хочет продать план, должен сам догадаться вернуться на страницу со списком проектов и найти мелкую иконку. Это критическая проблема discoverability — основной revenue-путь маркетплейса спрятан.
Что нужно:
- CTA «Продать план» на
/marketplaceи/my-listings(хотя бы в пустых состояниях) - Кнопка «Опубликовать в маркетплейсе» внутри редактора проекта
- Возможно, кнопка «Продать» в header рядом с навигацией
5. Что отсутствует в UI полностью
| Фича | Статус | Комментарий |
|---|---|---|
| Описание листинга | ❌ Нет | Нигде нет textarea для описания |
| Превью содержимого для покупателя | ❌ Нет | Покупатель видит только мета-информацию |
| Редактирование листинга | ✅ Частично | Изменение цены APPROVED листинга (с кулдауном), тип/описание — только через resubmit |
| Профиль продавца | ❌ Нет | Только имя/email |
| Отзывы покупателей | ❌ Нет | Нет модели, нет UI |
| Избранное | ❌ Нет | Нет wishlist |
| Сравнение | ✅ Есть | Модалка сравнения 2-3 планов (карточки/таблица), подсветка лучших значений |
| Категории/теги | ❌ Нет | Только поля из ProjectInfo |
| Скелетоны при загрузке | ✅ Есть | animate-pulse skeleton карточки и строки таблиц |
| Ревьюер → проект (read-only) | ❌ Нет | Только статические таблицы |
| Анимации переходов | ✅ Есть | CSS transitions на модалках и stagger на карточках |
| Mobile-оптимизация таблиц | ❌ Нет | Таблицы на /my-listings и /my-purchases без responsive |
Sorting в /my-listings | ✅ Есть | Сортировка по дате/цене |
Фильтры в /my-listings | ✅ Есть | Фильтрация по статусу |
Поиск в /my-purchases | ✅ Есть | Поиск по названию/продавцу + сортировка |
| Платёжная система | ❌ Нет | Демо-режим |
6. Безопасность и надёжность
5.1. Email продавца виден всем ✅ ИСПРАВЛЕНО
Убрано из публичных API, fallback на «Автор».seller.email отдаётся в API и отображается если нет имени.
5.2. Нет rate limiting
Нет ограничения на API-запросы. Можно спамить покупками и т.д.
5.3. Нет кеширования фильтров
/api/marketplace/filters пересчитывает distinct-значения на каждый запрос.
5.4. Неатомарный откат копирования
saveWorkspaceStateToDb вне транзакции, откат может не сработать.
5.5. Нет audit log
Кто и когда что сделал — не логируется (кроме ListingReview).
7. Задачи по категориям сложности
Задачи из GitHub issues (#82-#90) помечены тегом
GH#XX. Issue #82 (сайдбар с фильтрами) — уже реализован (marketplace-sidebar.tsx), можно закрыть.
🟢 Быстрые победы (1-2 часа каждая)
Минимальные изменения, максимальный эффект. Баги, однострочные фиксы, мелкие UI-доработки.
| # | Задача | Что делать | Файлы |
|---|---|---|---|
| 1 | richPreview фильтруется по ключам из listing.previewFields; каталог также фильтрует project.info | api/marketplace/listings/[id]/route.ts, api/marketplace/listings/route.ts | |
| 2 | Ссылка заменена на /my-listings | api/reviews/[listingId]/approve/route.ts | |
| 3 | /marketplace/[id] рассинхронизирована | Страница удалена (мёртвый код) | — |
| 4 | Добавлены кнопки «Опубликовать план» (ссылка на /projects) в empty state на /marketplace и /my-listings | marketplace-client.tsx, my-listings-client.tsx | |
| 5 | Добавлены createNotification типа PLAN_PURCHASED для buyer в handleCopyPurchase и handleExclusiveTransfer. Добавлено значение PLAN_PURCHASED в enum NotificationType | api/purchase/[listingId]/route.ts, schema.prisma | |
| 6 | seller.email убран из публичных API, fallback на «Автор» | api/marketplace/listings/route.ts, api/marketplace/listings/[id]/route.ts, listing-modal.tsx, marketplace-client.tsx | |
| 7 | /my-listings | Заменён router.push("/projects") на router.push("/my-listings") | projects/[projectId]/publish/page.tsx |
| 8 | Добавлен заметный amber-баннер с предупреждением о необратимости при выборе «Эксклюзивная передача» | projects/[projectId]/publish/page.tsx | |
| 9 | GH#90 Галочка пройденного ревью | Добавлен бейдж «Проверено» с иконкой IconRosetteDiscountCheck на каждой карточке листинга в маркетплейсе | marketplace-client.tsx (ListingCard) |
🟡 Средние задачи (3-6 часов каждая)
Новые UI-компоненты, расширение API, изменения в нескольких файлах.
| # | Задача | Что делать | Файлы |
|---|---|---|---|
| 10 | Поле description в PlanListing, textarea на publish/resubmit, отображение в модалке и карточке, поиск по описанию | schema, publish, resubmit, listing-modal, marketplace-client | |
| 11 | Фильтр по статусу (5 значений), сортировка по дате/цене. Select-компоненты в UI | my-listings-client.tsx, api/my-listings/route.ts | |
| 12 | Поиск по названию/продавцу (debounced), сортировка (дата/цена), суммарная статистика (потрачено, кол-во покупок), filter-aware empty state | my-purchases-client.tsx, api/my-purchases/route.ts | |
| 13 | Тип LISTING_SUBMITTED, уведомление всем REVIEWER/ADMIN при publish и resubmit | api/plans/[id]/listings/route.ts, api/.../resubmit/route.ts | |
| 14 | Кнопка «В маркетплейсе» для APPROVED листингов открывает ListingModal прямо из /my-listings | my-listings-client.tsx | |
| 15 | PATCH endpoint для изменения цены без повторного ревью, кулдаун (настраивается в админке), модалка в «Мои листинги» | api/marketplace/listings/[id]/price/route.ts, api/admin/settings/route.ts, my-listings-client.tsx, admin/marketplace/page.tsx | |
| 16 | Skeleton-карточки (animate-pulse) на маркетплейсе, skeleton-строки в таблицах «Мои листинги» и «Мои покупки» | marketplace-client.tsx, my-listings-client.tsx, my-purchases-client.tsx | |
| 17 | Серверная проверка (products + expenses + employees = 0 → 400), клиентский warning-баннер с блокировкой кнопки | api/plans/[id]/listings/route.ts, api/plans/[id]/route.ts, publish/page.tsx | |
| 18 | CSS transitions: fade+slide модалок (listing-modal, project-detail-modal), stagger fadeSlideUp карточек в каталоге | listing-modal.tsx, project-detail-modal.tsx, marketplace-client.tsx | |
| 19 | GH#85 Пины карточек от проекта | Поле isPinned в PlanListing, toggle в админке, закреплённые отображаются вверху каталога с бейджем | schema, API listings, marketplace-client, admin |
| 20 | GH#86 Маркетинговые цифры магазина (как на VC) | Счётчики на главной: «Планов», «Авторов», «Покупок», «Средняя цена». API endpoint /api/marketplace/stats с агрегатами | api/marketplace/stats/route.ts, marketplace-client.tsx |
| 21 | GH#87 Картинки для карточек листингов | Загрузка обложки (cover image) при публикации, отображение на карточке и в модалке. Хранение в S3/Supabase Storage | schema (imageUrl), upload API, publish page, ListingCard, listing-modal |
| 22 | GH#88 Метки авторов и инфлюенсеров | Бейджи на карточке/профиле: «Эксперт», «Топ-автор», «Инфлюенсер». Поле badges/tags в User или отдельная модель | schema, API, ListingCard, seller info |
| 23 | GH#83 Сравнение бизнес-планов | Модалка сравнения 2-3 планов (карточки рядом / таблица), подсветка лучших значений, плавающая панель выбора, API /api/marketplace/compare | compare-modal.tsx, marketplace-client.tsx, api/marketplace/compare/route.ts |
🔴 Сложные задачи (1-3 дня каждая)
Новые модели данных, полноценные фичи, архитектурные изменения.
| # | Задача | Что делать | Объём |
|---|---|---|---|
| 24 | Ревьюер → read-only доступ к проекту | При поступлении листинга на ревью: создавать PlanAccess REVIEW, пускать ревьюера в workspace в режиме чтения. Нужен read-only mode в редакторе проекта | Миграция логики, UI read-only mode, permissions |
| 25 | Система отзывов и рейтингов | Новая модель BuyerReview (rating, comment), API CRUD, средний рейтинг на карточке, страница отзывов, рейтинг продавца | Schema, API (3 routes), UI (2-3 компонента) |
| 26 | Профиль продавца | Страница /sellers/[id] с аватаром, описанием, статистикой, списком листингов, рейтингом | Новая страница, API route, компоненты |
| 27 | Интеграция платёжной системы | ЮKassa/Stripe: создание платежа, webhook, статусы (pending/paid/failed/refunded), Order только после оплаты | API routes, webhook handler, UI checkout flow, env config |
| 28 | Избранное / Wishlist | Новая модель Favorite, API toggle, иконка сердечка на карточках, страница «Избранное» | Schema, API, UI компоненты, новая страница |
| 29 | Превью содержимого для покупателя | Показать частичные данные плана (например, названия продуктов без цен, категории расходов без сумм) до покупки | API логика маскирования, UI компоненты |
| 30 | Возвраты и диспуты | Модель Dispute, поток запроса возврата, UI для покупателя и админа, статусы | Schema, API (4-5 routes), UI (2 страницы) |
| 31 | GH#84 Перенести с VC формат карточек | Полный редизайн карточек листингов: обложка, описание, теги, автор с аватаром, социальные метрики — стиль карточек как на vc.ru | ListingCard, listing-modal, marketplace-client, возможно новые компоненты |
| 32 | GH#89 MVP партнерской программы | Реферальные ссылки, модель Referral (referrerId, referredId, orderId, commission), дашборд партнёра, начисление комиссии при продаже | Schema, API (3-4 routes), UI (новая страница дашборда), бизнес-логика в purchase |
8. Что сделано хорошо
- Атомарные транзакции — покупка и передача прав через
$transaction - FOR UPDATE lock — защита от двойной покупки эксклюзива
- Debounce цены — фильтры по цене не спамят API
- Ролевая модель — USER / REVIEWER / ADMIN в навигации и API
- Уведомления с polling — рабочий notification bell с 30-сек обновлением
- Аналитика — трекинг всех ключевых событий воронки
- Модалка деталей для продавца — полная информация по проекту с аккордеонами
- Серверная пагинация — с настраиваемым pageSize и подсчётом total
- Мобильный toggle фильтров — сайдбар скрывается на маленьких экранах
- Повторная отправка — отклонённый листинг можно переподать с новыми параметрами