feat: форма обращения сотрудника в HR департамент

Переносит вопросы из Microsoft Forms в собственный интерфейс: компания,
анонимность, ФИО и должность, вид обращения, описание ситуации и предложение.

Поля автора скрываются при анонимном обращении, а не блокируются: требовать ФИО
у анонима — противоречие, а серое неактивное поле выглядит как ошибка формы.

Состояние целиком в useFeedbackForm, правила заполнения — в отдельной чистой
функции validateFeedback: её можно покрыть тестами без React.

Отправки пока нет — по договорённости делаем только UI, submit пишет данные в
консоль (TODO в useFeedbackForm).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 18:15:10 +05:00
parent e921062e3b
commit 010f78b138
18 changed files with 739 additions and 0 deletions
+71
View File
@@ -0,0 +1,71 @@
import { useCallback, useState } from 'react';
import { EMPTY_FEEDBACK, type FeedbackErrors, type IFeedbackData } from './model';
import { TEXTS } from './texts';
import { validateFeedback } from './validateFeedback';
export type SubmitStatus = 'idle' | 'submitting' | 'done';
export interface IFeedbackForm {
data: IFeedbackData;
errors: FeedbackErrors;
status: SubmitStatus;
/** Общая ошибка формы (показывается над полями). */
formError: string;
set: <K extends keyof IFeedbackData>(key: K, value: IFeedbackData[K]) => void;
submit: () => void;
reset: () => void;
}
/**
* Состояние формы обращения: данные, валидация, отправка.
* Бэкенда пока нет — submit только имитирует отправку (см. TODO ниже),
* поэтому вся логика синхронная и живёт здесь, без сервисного слоя.
*/
export function useFeedbackForm(): IFeedbackForm {
const [data, setData] = useState<IFeedbackData>(EMPTY_FEEDBACK);
const [errors, setErrors] = useState<FeedbackErrors>({});
const [formError, setFormError] = useState('');
const [status, setStatus] = useState<SubmitStatus>('idle');
const set = useCallback(
<K extends keyof IFeedbackData>(key: K, value: IFeedbackData[K]): void => {
setData((prev) => ({ ...prev, [key]: value }));
// Ошибку поля снимаем сразу при правке — иначе она висит до повторной отправки.
setErrors((prev) => {
if (!prev[key]) return prev;
const next = { ...prev };
delete next[key];
return next;
});
},
[]
);
const submit = useCallback((): void => {
const found = validateFeedback(data);
setErrors(found);
if (Object.keys(found).length > 0) {
setFormError(TEXTS.errors.hasErrors);
return;
}
setFormError('');
setStatus('submitting');
// TODO: заменить на реальную отправку, когда определимся с бэкендом.
// Сейчас это заглушка по договорённости: делаем только UI.
// eslint-disable-next-line no-console
console.info('Обращение (заглушка отправки):', data);
setStatus('done');
}, [data]);
const reset = useCallback((): void => {
setData(EMPTY_FEEDBACK);
setErrors({});
setFormError('');
setStatus('idle');
}, []);
return { data, errors, status, formError, set, submit, reset };
}