Newer
Older
gnexus-tasks / frontend / src / taskui.ts
// Общие помощники UI задач: подписи статусов, приоритеты, Markdown.
// Все строки — через vue-i18n (i18n.global), локаль переключается реактивно.

import DOMPurify from 'dompurify'
import { marked } from 'marked'
import i18n from './i18n'
import { intlLocale } from './i18n/locale'

function t(key: string, params?: Record<string, unknown>): string {
  return i18n.global.t(key, params ?? {})
}

// Ключ отсутствует в словаре (неизвестный код статуса) → отдаём код как есть
function tOr(key: string, fallback: string): string {
  return i18n.global.te(key) ? i18n.global.t(key) : fallback
}

export function statusLabel(status: string): string {
  return tOr(`taskui.status.${status}`, status)
}

export const STATUS_VARIANTS: Record<string, string> = {
  to_do: 'info',
  in_progress: 'accent',
  done: 'success',
  cancelled: 'error',
  deferred: 'warning',
}

export function statusVariant(status: string): string {
  return STATUS_VARIANTS[status] ?? 'neutral'
}

export function relevanceLabel(status: string): string {
  return tOr(`taskui.relevance.${status}`, status)
}

// Опции статусов для GnSelect (в видах оборачивать в computed)
export function statusOptions(): { value: string; label: string }[] {
  return ['to_do', 'in_progress', 'done', 'cancelled', 'deferred'].map((s) => ({
    value: s,
    label: t(`taskui.status.${s}`),
  }))
}

export function renderMarkdown(text: string): string {
  return DOMPurify.sanitize(marked.parse(text, { async: false }))
}

// «90» → «1 ч 30 м»; для чипов оценок времени
export function formatMinutes(minutes: number | null): string {
  if (minutes === null) return ''
  const h = Math.floor(minutes / 60)
  const m = minutes % 60
  if (h && m) return t('taskui.time.hm', { h, m })
  if (h) return t('taskui.time.h', { h })
  return t('taskui.time.m', { m })
}

// Валюта бюджетов — глобальная настройка, выбирается один раз и применяется всюду
export function currencyOptions(): { value: string; label: string }[] {
  return ['UAH', 'USD', 'EUR', 'GBP', 'PLN'].map((c) => ({
    value: c,
    label: t(`taskui.currency.${c}`),
  }))
}

const CURRENCY_SYMBOLS: Record<string, string> = {
  UAH: '₴',
  USD: '$',
  EUR: '€',
  GBP: '£',
  PLN: 'zł',
}

let activeCurrency = 'UAH'

export function setCurrency(currency: string): void {
  activeCurrency = currency
}

export function currencySymbol(): string {
  return CURRENCY_SYMBOLS[activeCurrency] ?? activeCurrency
}

// «5 000 ₴» — суммы задач; валюта берётся из глобальной настройки
export function formatMoney(amount: number | null): string {
  if (amount === null) return ''
  return `${amount.toLocaleString(intlLocale())} ${currencySymbol()}`
}

// --- Дедлайны и регулярность (ТЗ 3.4, 3.5) ---

export const DEADLINE_PERIOD_VALUES = ['day', 'week', 'month', 'year'] as const

export function deadlinePeriodOptions(): { value: string; label: string }[] {
  return DEADLINE_PERIOD_VALUES.map((p) => ({ value: p, label: t(`taskui.deadline.${p}`) }))
}

export const RECUR_KIND_VALUES = ['interval', 'weekdays', 'monthly'] as const

export function recurKindOptions(): { value: string; label: string }[] {
  return RECUR_KIND_VALUES.map((k) => ({
    value: k,
    label: t(`taskui.recur.kind.${k}`),
  }))
}

// Короткое имя дня недели через Intl: 2024-01-01 — понедельник (ISO: пн=1..вс=7)
export function weekdayShort(isoWeekday: number): string {
  const d = new Date(2024, 0, 7 + isoWeekday)
  return new Intl.DateTimeFormat(intlLocale(), { weekday: 'short' }).format(d)
}

// Дата дедлайна в локали: ru → «19.09.2026», en → «09/19/2026»
export function formatDate(isoDate: string): string {
  return new Intl.DateTimeFormat(intlLocale(), {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  }).format(new Date(isoDate))
}

export function deadlineLabel(task: { deadline_date: string | null; deadline_period: string | null }): string {
  if (task.deadline_date) return t('taskui.deadline.until', { date: formatDate(task.deadline_date) })
  const period = task.deadline_period ?? ''
  return i18n.global.te(`taskui.deadline.${period}`)
    ? t(`taskui.deadline.${period}`)
    : ''
}

export function deadlineOverdue(task: { deadline_date: string | null }): boolean {
  if (!task.deadline_date) return false
  const today = new Date().toISOString().slice(0, 10)
  return task.deadline_date < today
}

export function recurrenceLabel(task: {
  recur_kind: string | null
  recur_interval_days: number | null
  recur_weekdays: string | null
  recur_day_of_month: number | null
}): string {
  if (task.recur_kind === 'interval') {
    const step = task.recur_interval_days ?? 1
    if (step === 1) return t('taskui.recur.daily')
    if (step === 7) return t('taskui.recur.weekly')
    return t('taskui.recur.everyN', { n: step })
  }
  if (task.recur_kind === 'weekdays') {
    if (!task.recur_weekdays) return t('taskui.recur.byWeekdays')
    return task.recur_weekdays
      .split(',')
      .map((d) => weekdayShort(Number(d)))
      .join(' ')
  }
  if (task.recur_kind === 'monthly') {
    return t('taskui.recur.monthlyDay', { day: task.recur_day_of_month ?? '' })
  }
  return ''
}

// «бюджет 5 000 ₴ / оценка 2 000 ₴» — бейджи в видах
export function budgetBadge(task: { budget_money: number | null; cost_estimate_money: number | null }): string {
  if (task.budget_money === null) return ''
  const parts = [t('taskui.money.budget', { amount: formatMoney(task.budget_money) })]
  if (task.cost_estimate_money !== null) {
    parts.push(t('taskui.money.estimate', { amount: formatMoney(task.cost_estimate_money) }))
  }
  return parts.join(' / ')
}