Skip to content

Repository files navigation

Форма создания организации

Репозиторий: https://github.com/sergr377/org-creation-form

React + TypeScript + Vite приложение с Ant Design: кнопка «Создать» → выбор типа аккаунта (продавец/покупатель) → трёхшаговый мастер (данные организации → контактные лица → предпросмотр и отправка) в модальном окне antd.

Стек

  • React 19, TypeScript, Vite
  • Ant Design 6 (antd, @ant-design/icons)
  • Состояние — только встроенный React (useState), без сторонних стейт-менеджеров

Как запустить

npm install
npm run dev

Откроется Vite dev-сервер (по умолчанию http://localhost:5173, если порт занят — Vite выберет следующий свободный).

Другие команды:

npm run build    # tsc -b && vite build — проверка типов + продакшн-сборка
npm run lint     # eslint .
npm run preview  # локальный просмотр собранного dist

Архитектура

Структура

src/
├── api/
│   └── organization.ts        # мок createOrganization(): Promise<{ id }>, задержка + случайная ошибка
├── components/
│   ├── OrganizationDataStep.tsx   # шаг 1 — данные организации
│   ├── ContactsStep.tsx           # шаг 2 — контактные лица (Form.List)
│   └── PreviewStep.tsx            # шаг 3 — предпросмотр + отправка
├── data/
│   └── organizationData.ts    # моковые справочники: countries, currencies, taxSystems
├── types/
│   └── organization.ts        # все общие типы: формы шагов, CreateOrganizationPayload/Result
├── utils/
│   ├── validation.ts          # маски телефона по стране, email, нормализация (email/phone)
│   └── payload.ts             # buildOrganizationPayload() — сборка payload из данных шагов 1–2
├── App.tsx                    # оркестрация: Dropdown, Modal, переключение шагов
└── main.tsx

Компоненты и управление состоянием

App.tsx — единственный «владелец» состояния мастера:

  • accountType: 'seller' | 'buyer' | null — выбран в Dropdown, определяет заголовок модалки и набор полей продавца;
  • currentStep: number (0 → 1 → 2) — какой шаг сейчас отрисован внутри Modal;
  • organizationData / contactsData — результаты валидации шагов 1 и 2, поднятые наверх (lifted state).

Каждый шаг — управляемый (controlled) компонент без собственного «глобального» состояния:

  • получает initialValues от родителя (чтобы восстановить ранее введённые данные при возврате назад);
  • ведёт свою форму через Form.useForm();
  • по кнопке «Далее» вызывает form.onFinish, который срабатывает только после успешной валидации antd-формы, и отдаёт готовые данные наверх через onNext(data);
  • по кнопке «Назад» отдаёт текущие (необязательно валидные) значения через onBack(data), чтобы ничего не терялось при возврате.

Такая схема — простой wizard на useState с явным подъёмом состояния — выбрана вместо Redux/Zustand/Context, потому что состояние локально для одной модалки, неглубокое (4 поля верхнего уровня) и не расшаривается между несвязанными частями приложения; на этом масштабе внешний стейт-менеджер добавил бы код без выгоды.

Валидация в каждом шаге — стандартные Form.Item rules antd (required, pattern, type: 'email') плюс кастомные async-валидаторы там, где нужна логика, недоступная декларативным правилам:

  • проверка «не только пробелы» (с trim() перед сравнением);
  • формат телефона, зависящий от выбранной страны;
  • в ContactsStep — пропуск полностью пустых строк, запрет дублей email/телефона между контактами, агрегирующее правило Form.List «нужен минимум один контакт».

Что выполнено / что нет

Выполнено:

  • кнопка «Создать» → Dropdown (Продавец/Покупатель) → Modal с заголовком «Создание организации: …»;
  • шаг 1: название, страна, юр. адрес (необязательное), региональные реквизиты по стране, поля продавца (система налогообложения — отфильтрована по стране, валюта, email, телефон) — только для seller;
  • шаг 2: динамический список контактов (ФИО/email/телефон), добавление/удаление, минимум один контакт, запрет дублей email/телефона (регистронезависимо/нормализованно), игнорирование полностью пустых строк, блокировка при частично заполненных;
  • шаг 3: предпросмотр всех данных, мок-отправка с задержкой 1.5с, индикатор загрузки, message.success/message.error, сохранение данных и модалки открытой при ошибке;
  • CreateOrganizationPayload собирается строго по типу из ТЗ, sellerDetails отсутствует в payload для покупателя (а не просто undefined);
  • отдельный проход ревью на 8 критических категорий (обход валидации, преждевременная отправка, утечка данных продавца, рассинхрон региона, отправка без валидного контакта, потеря данных при «Назад», сброс/закрытие при ошибке) — критических проблем не найдено.

Не выполнено / вне рамок:

  • RegionalDetails не определён в ТЗ (в примере CreateOrganizationPayload тип только упомянут по имени) — спроектирован самостоятельно как дискриминированный union по country, это решение кандидата, а не готовая спецификация;
  • реального бэкенда нет — только мок с setTimeout и случайной ошибкой (по заданию);
  • нет автотестов (unit/e2e) — не запрашивались;
  • нет персистентности между перезагрузками страницы (localStorage и т.п.) — не запрашивалась;
  • код-сплиттинг бандла не настроен — vite build выдаёт предупреждение о чанке >500 kB, это не блокирующая проблема для тестового проекта;
  • известная не критичная непоследовательность: при смене страны в шаге 1 сбрасываются региональные поля и система налогообложения, но не телефон продавца — устаревшее значение сразу же перевалидируется (dependencies={['countryId']}) и блокирует переход, но визуально остаётся в поле до исправления пользователем. Решение сознательно не доводилось до идеала в рамках этой сессии — зафиксировано при ревью, при необходимости чинится одной строкой (добавить phone: undefined в сброс).

Решение по переключению страны

При смене страны в шаге 1 (handleCountryChange в OrganizationDataStep) регионально-зависимые поля (inn, kpp, bin, pinfl, taxSystemId) явно сбрасываются в undefined, а не остаются заполненными старыми значениями.

Почему так:

  • набор и формат региональных реквизитов у стран разный (РФ — ИНН 10 цифр + КПП 9 цифр; Казахстан — БИН 12 цифр; Узбекистан — ПИНФЛ 14 + ИНН 9 цифр) — оставлять «старое» значение при смене страны означало бы либо показывать пользователю данные другой страны как будто они относятся к новой (визуальная путаница), либо полагаться только на last-moment-валидацию при сабмите;
  • система налогообложения (taxSystemId) тоже завязана на страну (справочник taxSystems фильтруется по countryId) — прежде выбранный ID может физически не существовать в списке новой страны;
  • поля рендерятся условно ({countryId === 'RF' && (...)}), т.е. при смене страны старые Form.Item реально размонтируются — если бы значение не сбрасывалось, оно осталось бы «висеть» в сторе формы невалидированным до следующего рендера того же поля;
  • валюта (currencyId) намеренно не сбрасывается — в этом приложении валюта не привязана к стране (список валют глобальный), поэтому сохранение выбора корректно и удобнее для пользователя.

Обработка состояний отправки

Состояние отправки — один useState<boolean> (submitting) в PreviewStep:

  • До отправки: кнопка «Подтвердить» активна, «Назад»/«Отмена» активны.
  • Во время отправки: submitting = true выставляется синхронно перед await createOrganization(...); кнопка «Подтвердить» получает loading (спиннер + автоматическая блокировка от повторных кликов), «Назад» и «Отмена» получают явный disabled — нельзя ни уйти назад, ни закрыть модалку, ни повторно кликнуть «Подтвердить» во время запроса.
  • Успех: message.success(...) + вызов onSuccess(result), который в App.tsx полностью сбрасывает состояние мастера и закрывает модалку.
  • Ошибка: message.error(...), submitting возвращается в falsefinally) — состояние App (шаг, данные организации, контакты) не трогается вообще, модалка остаётся открытой с введёнными данными, «Подтвердить» снова доступна для повтора.

Это было отдельно проверено вручную: временно выставлялась 100%-вероятность ошибки в моке, отправка нажималась дважды подряд — модалка не закрывалась и данные не терялись ни разу.

Как использовался ИИ (Claude Code)

Всё приложение (все три шага мастера, типы, утилиты валидации, сборка payload, мок API, оркестрация в App.tsx) написано в Claude Code итеративно, по одному блоку требований за раз, в рамках одной сессии:

  1. Базовый App с Dropdown + Modal.
  2. Шаг 1 — данные организации, условные поля по стране/типу аккаунта, валидация по правилам из ТЗ.
  3. Шаг 2 — динамический список контактов (Form.List), дедупликация, минимум один контакт.
  4. Шаг 3 — предпросмотр, мок-отправка, обработка состояний загрузки/успеха/ошибки, типизированный payload.
  5. Отдельный ревью-проход на 8 конкретных критических категорий проблем.

Что было найдено и исправлено по ходу:

  • deprecated-предупреждение antd 6 (Space directionSpace orientation) — замечено в консоли браузера при ручном тестировании, поправлено сразу;
  • дублирование логики паттерна телефона между шагом 1 и шагом 2 — вынесено в общий utils/validation.ts вместо копипаста;
  • при ревью найдена и задокументирована (но не исправлена автоматически, так как не запрашивалось) одна некритичная непоследовательность — телефон продавца не сбрасывается при смене страны (см. выше).

Отклонённых предложений ИИ не было: разработка велась пошагово, каждый шаг подтверждался проверкой (типы/линт/сборка/ручное тестирование) до перехода к следующему, поэтому спорных или отменённых решений не возникало.

Как проверялась корректность на каждом шаге:

  • tsc -b --noEmit — строгая проверка типов;
  • eslint src — линт;
  • npm run build — полная продакшн-сборка;
  • ручное тестирование в интерактивном браузере (Vite dev-сервер): заполнение форм, намеренный ввод невалидных/пустых/частично заполненных/задублированных/состоящих из пробелов значений, переключение страны для проверки условных полей и живой ревалидации, проверка disabled-состояния кнопки «Удалить» на последнем контакте, полный проход мастера туда-обратно («Назад») для проверки сохранности данных, принудительный форс ошибки мок-API (SIMULATED_FAILURE_RATE = 1) для проверки error-ветки с последующим откатом значения.

Фактическое затраченное время

~1 час

С 16:30 по 17:30

Ориентир по объёму работы с точки зрения сессии с Claude Code: 5 последовательных итераций (шаги 1–4 + ревью), каждая — с проверкой типов/линта/сборки и ручным тестированием в браузере перед переходом к следующей.

About

Тестовое задание: создание организации

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages