feat(ux): улучшение работы с датами validFrom/validTo

Backend (defensive):
- GlobalExceptionHandler ловит HttpMessageNotReadableException → 400 c понятным
  сообщением вместо 500 'Internal server error'.
- DateTimeParseException → code='invalid_date_format', сообщение с подсказкой
  ISO формата (например 2026-05-08T14:30:00+03:00).
- InvalidFormatException → code='invalid_field_format' с именем сломанного поля.
- Раньше падало 500 если фронт отправлял 'YYYY-MM-DD' вместо OffsetDateTime
  (из-за browser cache на старом bundle).

Frontend:
- Submit-валидация validFrom < validTo с inline error на validTo поле,
  переключение на таб 'Идентификация' с фокусом на ошибке.
- DateTimeField теперь принимает hint и error props и показывает их под
  парой DatePicker+TextInput[time].
- Подсказки i18n под полями: 'Когда запись становится активной. Пусто = с момента
  создания.' / 'Когда перестаёт действовать. Пусто = бессрочно.'
- Новая i18n ключ form.error.validToBeforeFrom (RU/EN).
This commit is contained in:
Zimin A.N.
2026-05-04 04:15:58 +03:00
parent b5c2378ab7
commit c7b651011a
3 changed files with 65 additions and 13 deletions
@@ -1,15 +1,19 @@
package cloud.nstart.terravault.ordinis.restapi.error;
import cloud.nstart.terravault.ordinis.validation.ValidationException;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import io.micrometer.tracing.Tracer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.time.format.DateTimeParseException;
import java.util.List;
@RestControllerAdvice
@@ -45,6 +49,30 @@ public class GlobalExceptionHandler {
return ResponseEntity.badRequest().body(body);
}
/**
* Невалидный JSON или несовместимый формат поля (напр. дата без времени для OffsetDateTime).
* Возвращаем 400 с понятным сообщением вместо 500.
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> onMessageNotReadable(HttpMessageNotReadableException e) {
String message = "Невалидный JSON в теле запроса";
String code = "invalid_request_body";
Throwable root = e.getRootCause() != null ? e.getRootCause() : e.getCause();
if (root instanceof DateTimeParseException dtpe) {
message = "Поле даты не распарсилось: '" + dtpe.getParsedString()
+ "'. Ожидается ISO-8601 datetime, например 2026-05-08T14:30:00+03:00.";
code = "invalid_date_format";
} else if (e.getCause() instanceof InvalidFormatException ife) {
String field = ife.getPath().isEmpty()
? "?"
: ife.getPath().get(ife.getPath().size() - 1).getFieldName();
message = "Поле '" + field + "' не распарсилось. Ожидается " + ife.getTargetType().getSimpleName() + ".";
code = "invalid_field_format";
}
var body = ApiErrorResponse.of(400, code, message, currentTraceId());
return ResponseEntity.badRequest().body(body);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorResponse> onGeneric(Exception e) {
log.error("Unhandled exception in REST API", e);