// Общие помощники 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(' / ')
}
// Подписи чернового предложения LLM (стек, страница задачи)
export function proposalItems(task: {
ai_proposal: import('./api').AiProposal | null
}): string[] {
const p = task.ai_proposal
if (!p) return []
const items: string[] = []
p.tags.forEach((tag) => items.push(tag))
if (p.project)
items.push(p.new_project ? t('stack.proposalNewProject', { name: p.project }) : p.project)
if (p.priority !== null) items.push(t('stack.proposalPriority', { n: p.priority }))
if (p.estimated_minutes !== null && p.estimated_minutes !== undefined)
items.push(`≈ ${formatMinutes(p.estimated_minutes)}`)
return items
}