Newer
Older
gnexus-tasks / frontend / src / components / DurationInput.vue
<script setup lang="ts">
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

// Ввод длительности: два поля — часы и минуты (минуты с шагом 5). Заставлять
// пользователя переводить время в минуты не нужно. Значение компонента —
// минуты (число|null), как в API; часы/минуты раскладываются при вводе и
// собираются обратно при изменении. Оба поля пустые = null («не указано»).

const props = defineProps<{
  modelValue: number | null
  /** Общая подпись блока; в инлайн-редакторах подпись даёт строка параметров */
  label?: string
  /** Минимальное суммарное значение в минутах (0 = можно пусто) */
  min?: number
}>()

const emit = defineEmits<{ 'update:modelValue': [value: number | null] }>()

const { t } = useI18n()

const hoursText = ref('')
const minutesText = ref('')

function toNumber(text: string): number | null {
  const trimmed = text.trim()
  if (trimmed === '') return null
  const n = Number(trimmed)
  return Number.isFinite(n) ? n : null
}

// внешнее значение → поля (загрузка задачи, сброс формы)
watch(
  () => props.modelValue,
  (v) => {
    if (v === null || v === undefined) {
      hoursText.value = ''
      minutesText.value = ''
      return
    }
    hoursText.value = String(Math.floor(v / 60))
    minutesText.value = String(v % 60)
  },
  { immediate: true },
)

watch([hoursText, minutesText], () => {
  const h = toNumber(hoursText.value)
  const m = toNumber(minutesText.value)
  if (h === null && m === null) {
    emit('update:modelValue', null)
    return
  }
  let total = Math.max(0, h ?? 0) * 60 + Math.max(0, m ?? 0)
  const min = props.min ?? 0
  if (total < min) total = min
  emit('update:modelValue', total)
})
</script>

<template>
  <span class="duration-input">
    <span v-if="label" class="duration-label">{{ label }}</span>
    <span class="duration-fields">
      <GnInput
        v-model="hoursText"
        type="number"
        min="0"
        class="hours"
        :aria-label="t('duration.hours')"
      />
      <span class="unit">{{ t('duration.h') }}</span>
      <GnInput
        v-model="minutesText"
        type="number"
        min="0"
        max="55"
        step="5"
        class="minutes"
        :aria-label="t('duration.minutes')"
      />
      <span class="unit">{{ t('duration.min') }}</span>
    </span>
  </span>
</template>

<style scoped>
.duration-input {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.25rem;
  min-width: 0;
}
.duration-label {
  font-size: 0.85em;
  color: var(--text-muted, #888);
}
.duration-fields {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}
/* GnInput рендерит div.form-group > label > input: убираем вертикальные
   отступы формы и задаём компактную ширину полей (переопределение размеров,
   стили кита остаются) */
.duration-fields :deep(.form-group) {
  margin: 0;
  width: auto;
}
.duration-fields :deep(input) {
  width: 4rem;
  padding: 4px 10px;
}
.unit {
  color: var(--text-muted, #888);
  font-size: 0.9em;
}
</style>