import { DatasetColor } from '@pi/ui'; import { IssueArea, IssueLinkType, IssuePriority, IssueStatus, IssueType, ProjectRole, ReleaseStatus, } from '@task-stream/web/tracker/domain'; type ChipColor = | 'accent' | 'grey' | 'highlighted' | 'primary' | 'success' | 'warn'; type IconColor = | 'primary' | 'accent' | 'warn' | 'success' | 'grey' | 'highlighted'; /* UI copy (es) and the visual language of the tracker. */ /** `tone`: CSS colour token of the status/type (see `--ts-s-*`, `--ts-t-*` in styles.scss). */ export const STATUS: Record< IssueStatus, { label: string; color: ChipColor; chart: DatasetColor; tone: string } > = { [IssueStatus.BACKLOG]: { label: 'Backlog', color: 'grey', chart: DatasetColor.grey, tone: 'var(--ts-s-backlog)', }, [IssueStatus.TODO]: { label: 'Por hacer', color: 'grey', chart: DatasetColor.grey300, tone: 'var(--ts-s-todo)', }, [IssueStatus.IN_PROGRESS]: { label: 'En curso', color: 'primary', chart: DatasetColor.primary, tone: 'var(--ts-s-progress)', }, [IssueStatus.IN_REVIEW]: { label: 'En revisión', color: 'accent', chart: DatasetColor.accent, tone: 'var(--ts-s-review)', }, [IssueStatus.READY_FOR_QA]: { label: 'Listo para QA', color: 'highlighted', chart: DatasetColor.highlighted, tone: 'var(--ts-s-ready)', }, [IssueStatus.IN_QA]: { label: 'En QA', color: 'highlighted', chart: DatasetColor.highlighted50, tone: 'var(--ts-s-qa)', }, [IssueStatus.QA_FAILED]: { label: 'QA fallido', color: 'warn', chart: DatasetColor.warn, tone: 'var(--ts-s-failed)', }, [IssueStatus.DONE]: { label: 'Hecho', color: 'success', chart: DatasetColor.success, tone: 'var(--ts-s-done)', }, [IssueStatus.CANCELLED]: { label: 'Cancelado', color: 'grey', chart: DatasetColor.grey50, tone: 'var(--ts-s-cancelled)', }, }; export const TYPE: Record< IssueType, { label: string; icon: string; color: IconColor; tone: string } > = { [IssueType.BUG]: { label: 'Bug', icon: 'bug', color: 'warn', tone: 'var(--ts-t-bug)', }, [IssueType.FEATURE]: { label: 'Feature', icon: 'feature', color: 'primary', tone: 'var(--ts-t-feature)', }, [IssueType.UI_CHANGE]: { label: 'Cambio de UI', icon: 'palette', color: 'accent', tone: 'var(--ts-t-ui)', }, [IssueType.TASK]: { label: 'Tarea', icon: 'task', color: 'grey', tone: 'var(--ts-t-task)', }, [IssueType.QA]: { label: 'QA', icon: 'qa', color: 'highlighted', tone: 'var(--ts-t-qa)', }, }; /** Signal strength of a priority, drawn as bars (1–4) like a phone signal. */ export const PRIORITY_LEVEL: Record = { [IssuePriority.CRITICAL]: 4, [IssuePriority.HIGH]: 3, [IssuePriority.MEDIUM]: 2, [IssuePriority.LOW]: 1, [IssuePriority.LOWEST]: 0, }; export const PRIORITY: Record< IssuePriority, { label: string; color: ChipColor } > = { [IssuePriority.CRITICAL]: { label: 'Crítica', color: 'warn' }, [IssuePriority.HIGH]: { label: 'Alta', color: 'accent' }, [IssuePriority.MEDIUM]: { label: 'Media', color: 'primary' }, [IssuePriority.LOW]: { label: 'Baja', color: 'grey' }, [IssuePriority.LOWEST]: { label: 'Mínima', color: 'grey' }, }; export const AREA: Record = { [IssueArea.FRONTEND]: 'Frontend', [IssueArea.BACKEND]: 'Backend', [IssueArea.MOBILE]: 'Mobile', [IssueArea.QA]: 'QA', [IssueArea.DESIGN]: 'Diseño', [IssueArea.DEVOPS]: 'DevOps', }; export const LINK: Record = { [IssueLinkType.BLOCKS]: { outward: 'bloquea a', inward: 'está bloqueado por', }, [IssueLinkType.DEPENDS_ON]: { outward: 'depende de', inward: 'es dependencia de', }, [IssueLinkType.TESTS]: { outward: 'prueba', inward: 'es probado por' }, [IssueLinkType.DUPLICATES]: { outward: 'duplica a', inward: 'está duplicado por', }, [IssueLinkType.RELATES_TO]: { outward: 'relacionado con', inward: 'relacionado con', }, }; export const ROLE: Record = { [ProjectRole.OWNER]: 'Propietario', [ProjectRole.ADMIN]: 'Admin', [ProjectRole.MEMBER]: 'Miembro', [ProjectRole.VIEWER]: 'Lector', }; export const RELEASE_STATUS: Record< ReleaseStatus, { label: string; color: ChipColor } > = { [ReleaseStatus.PLANNED]: { label: 'Planificada', color: 'primary' }, [ReleaseStatus.RELEASED]: { label: 'Publicada', color: 'success' }, [ReleaseStatus.ARCHIVED]: { label: 'Archivada', color: 'grey' }, }; export const options = ( record: Record, ) => (Object.keys(record) as T[]).map((value) => { const entry = record[value]; return { value, label: typeof entry === 'string' ? entry : entry.label }; }); /** "hace 5 min", "ayer"… */ export function timeAgo(iso: string, now = Date.now()): string { const seconds = Math.max(0, Math.round((now - Date.parse(iso)) / 1000)); const rtf = new Intl.RelativeTimeFormat('es', { numeric: 'auto' }); if (seconds < 60) return rtf.format(-seconds, 'second'); if (seconds < 3600) return rtf.format(-Math.round(seconds / 60), 'minute'); if (seconds < 86400) return rtf.format(-Math.round(seconds / 3600), 'hour'); return rtf.format(-Math.round(seconds / 86400), 'day'); } const PROJECT_TONES = [ 'var(--ts-t-feature)', 'var(--ts-s-review)', 'var(--ts-t-ui)', 'var(--ts-t-qa)', 'var(--ts-s-done)', 'var(--ts-s-qa)', ]; /** A stable colour per project (from its key), so it is recognisable everywhere. */ export function projectTone(key: string): string { let hash = 0; for (const char of key) hash = (hash * 31 + char.charCodeAt(0)) >>> 0; return PROJECT_TONES[hash % PROJECT_TONES.length]; } /** "Ana García" → "AG". */ export function initials(name: string): string { return name .split(/\s+/) .filter(Boolean) .map((part) => part[0]) .join('') .slice(0, 2) .toUpperCase(); } /** "14:05" today, "3 oct" otherwise (logs and histories). */ export function clock(iso: string, now = new Date()): string { const date = new Date(iso); return date.toDateString() === now.toDateString() ? date.toLocaleTimeString('es', { hour: '2-digit', minute: '2-digit' }) : date.toLocaleDateString('es', { day: 'numeric', month: 'short' }); }