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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user