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
@@ -163,6 +163,15 @@ export const SchemaDrivenForm = ({
const idSource = (schema as { 'x-id-source'?: string })['x-id-source']
const submit: SubmitHandler<FormValues> = (values) => {
if (values.validFrom && values.validTo) {
const from = new Date(values.validFrom)
const to = new Date(values.validTo)
if (!isNaN(from.getTime()) && !isNaN(to.getTime()) && from >= to) {
setError('validTo', { type: 'manual', message: t('form.error.validToBeforeFrom') })
setActiveTab('identity')
return
}
}
if (validator) {
const ok = validator(values.data)
if (!ok && validator.errors && validator.errors.length > 0) {
@@ -227,6 +236,7 @@ export const SchemaDrivenForm = ({
render={({ field }) => (
<DateTimeField
label={t('form.validFrom')}
hint={t('form.validFromHint')}
value={field.value as string | undefined}
defaultTime="00:00"
onChange={field.onChange}
@@ -239,6 +249,8 @@ export const SchemaDrivenForm = ({
render={({ field }) => (
<DateTimeField
label={t('form.validTo')}
hint={t('form.validToHint')}
error={(formState.errors.validTo as { message?: string } | undefined)?.message}
value={field.value as string | undefined}
defaultTime="23:59"
onChange={field.onChange}
@@ -594,26 +606,32 @@ type DateTimeFieldProps = {
value: string | undefined
defaultTime: string
onChange: (iso: string) => void
hint?: string
error?: string
}
const DateTimeField = ({ label, value, defaultTime, onChange }: DateTimeFieldProps) => {
const DateTimeField = ({ label, value, defaultTime, onChange, hint, error }: DateTimeFieldProps) => {
const dateValue = parseFormDate(value)
const timeValue = extractTime(value, defaultTime)
return (
<div className="grid grid-cols-[1fr_auto] gap-2 items-end">
<DatePicker
label={label}
value={dateValue}
onChange={(d) => onChange(combineDateTime(d, timeValue, value ?? ''))}
/>
<div className="w-28">
<TextInput
label="HH:mm"
type="time"
value={timeValue}
onChange={(e) => onChange(combineDateTime(dateValue, e.target.value, value ?? ''))}
<div>
<div className="grid grid-cols-[1fr_auto] gap-2 items-end">
<DatePicker
label={label}
value={dateValue}
onChange={(d) => onChange(combineDateTime(d, timeValue, value ?? ''))}
/>
<div className="w-28">
<TextInput
label="HH:mm"
type="time"
value={timeValue}
onChange={(e) => onChange(combineDateTime(dateValue, e.target.value, value ?? ''))}
/>
</div>
</div>
{hint && !error && <p className="text-2xs text-carbon/60 mt-1">{hint}</p>}
{error && <p className="text-2xs text-mars mt-1">{error}</p>}
</div>
)
}