diff --git a/frontend/src/locales/en.ts b/frontend/src/locales/en.ts index 32e903d..8415b0e 100644 --- a/frontend/src/locales/en.ts +++ b/frontend/src/locales/en.ts @@ -106,16 +106,24 @@ notFoundTitle: 'Task not found', notFoundText: 'The task may have been deleted or the link is stale.', proposalTitle: 'AI proposal', + proposalHint: 'Review the details and approve — the task joins the list.', description: 'Description', descriptionEmpty: 'No description yet — detail the task.', + attachments: 'Attachments', fields: 'Details', + dates: 'Dates', field: { project: 'Project', + taskType: 'Task type', priority: 'Priority', estimate: 'Time estimate', + actual: 'Actual', money: 'Money', deadline: 'Deadline', recurrence: 'Recurrence', + created: 'Created', + approved: 'Approved', + done: 'Done', }, subtasks: 'Subtasks', noSubtasks: 'No subtasks', diff --git a/frontend/src/locales/ru.ts b/frontend/src/locales/ru.ts index 144fa5d..be0965d 100644 --- a/frontend/src/locales/ru.ts +++ b/frontend/src/locales/ru.ts @@ -107,16 +107,24 @@ notFoundTitle: 'Задача не найдена', notFoundText: 'Возможно, задача удалена или ссылка устарела.', proposalTitle: 'Предложение ИИ', + proposalHint: 'Проверьте детали и утвердите — задача попадёт в список.', description: 'Описание', descriptionEmpty: 'Описание пусто — детализируйте задачу.', + attachments: 'Вложения', fields: 'Параметры', + dates: 'Даты', field: { project: 'Проект', + taskType: 'Тип задачи', priority: 'Приоритет', estimate: 'Оценка времени', + actual: 'Фактически', money: 'Деньги', deadline: 'Дедлайн', recurrence: 'Повторение', + created: 'Создана', + approved: 'Утверждена', + done: 'Завершена', }, subtasks: 'Подзадачи', noSubtasks: 'Подзадач нет', diff --git a/frontend/src/locales/uk.ts b/frontend/src/locales/uk.ts index 3b3aba9..9c7ba38 100644 --- a/frontend/src/locales/uk.ts +++ b/frontend/src/locales/uk.ts @@ -106,16 +106,24 @@ notFoundTitle: 'Задачу не знайдено', notFoundText: 'Можливо, задачу видалено або посилання застаріло.', proposalTitle: 'Пропозиція ШІ', + proposalHint: 'Перевірте деталі та затвердіть — задача потрапить до списку.', description: 'Опис', descriptionEmpty: 'Опис порожній — деталізуйте задачу.', + attachments: 'Вкладення', fields: 'Параметри', + dates: 'Дати', field: { project: 'Проєкт', + taskType: 'Тип задачі', priority: 'Пріоритет', estimate: 'Оцінка часу', + actual: 'Фактично', money: 'Гроші', deadline: 'Дедлайн', recurrence: 'Повторення', + created: 'Створено', + approved: 'Затверджено', + done: 'Завершено', }, subtasks: 'Підзадачі', noSubtasks: 'Підзадач немає', diff --git a/frontend/src/views/TaskView.vue b/frontend/src/views/TaskView.vue index 78f1bf1..1b2fe86 100644 --- a/frontend/src/views/TaskView.vue +++ b/frontend/src/views/TaskView.vue @@ -5,9 +5,10 @@ import { api, ApiError, type Attachment, type Task } from '../api' import { deadlineLabel, + deadlineOverdue, + formatDate, formatMinutes, formatMoney, - proposalItems, recurrenceLabel, renderMarkdown, statusOptions, @@ -21,8 +22,9 @@ defineOptions({ name: 'TaskView' }) -// Страница задачи: просмотр всей детализации (описание md, вложения, параметры, -// предложение ИИ) и редактирование через TaskForm. Подзадачи — TaskTree. +// Страница задачи: двухколоночная раскладка (содержимое + сайдбар параметров). +// Вся детализация открыта целиком: предложение ИИ построчно, описание md, +// крупные вложения, параметры и даты с иконками. Правка — TaskForm. const route = useRoute() const router = useRouter() @@ -64,24 +66,40 @@ onMounted(load) -// Параметры — список «термин: значение» (GnDefinitionList) -const fields = computed(() => { +// Сайдбар «Параметры» — строка = иконка + термин + значение +const paramRows = computed(() => { const task0 = task.value if (!task0) return [] - const rows: { key: string; term: string; value: string }[] = [] - if (task0.project) - rows.push({ key: 'project', term: t('task.field.project'), value: task0.project.name }) + const rows: { key: string; icon: string; term: string; value: string; warn?: boolean }[] = [] + rows.push({ + key: 'taskType', + icon: task0.task_type === 'recurring' ? 'ph-repeat' : 'ph-arrow-bend-down-right', + term: t('task.field.taskType'), + value: + task0.task_type === 'recurring' + ? recurrenceLabel(task0) || t('stack.recur.recurring') + : t('stack.recur.oneTime'), + }) if (task0.priority !== null) - rows.push({ key: 'priority', term: t('task.field.priority'), value: `P${task0.priority}` }) + rows.push({ key: 'priority', icon: 'ph-flag', term: t('task.field.priority'), value: `P${task0.priority}` }) if (task0.estimated_minutes) rows.push({ key: 'estimate', + icon: 'ph-clock', term: t('task.field.estimate'), - value: formatMinutes(task0.estimated_minutes), + value: `≈ ${formatMinutes(task0.estimated_minutes)}`, + }) + if (task0.actual_minutes) + rows.push({ + key: 'actual', + icon: 'ph-timer', + term: t('task.field.actual'), + value: formatMinutes(task0.actual_minutes), }) if (task0.budget_money !== null || task0.cost_estimate_money !== null) rows.push({ key: 'money', + icon: 'ph-coins', term: t('task.field.money'), value: [ task0.budget_money !== null @@ -95,9 +113,56 @@ .join(' · '), }) const deadline = deadlineLabel(task0) - if (deadline) rows.push({ key: 'deadline', term: t('task.field.deadline'), value: deadline }) - const recur = recurrenceLabel(task0) - if (recur) rows.push({ key: 'recurrence', term: t('task.field.recurrence'), value: recur }) + if (deadline) + rows.push({ + key: 'deadline', + icon: 'ph-alarm', + term: t('task.field.deadline'), + value: deadline, + warn: deadlineOverdue(task0) && task0.status !== 'done' && task0.status !== 'cancelled', + }) + return rows +}) + +// Сайдбар «Даты» — что с задачей происходило и когда +const dateRows = computed(() => { + const task0 = task.value + if (!task0) return [] + const rows: { key: string; icon: string; term: string; value: string }[] = [ + { key: 'created', icon: 'ph-calendar-plus', term: t('task.field.created'), value: formatDate(task0.created_at) }, + ] + if (task0.approved_at) + rows.push({ key: 'approved', icon: 'ph-circle-wavy-check', term: t('task.field.approved'), value: formatDate(task0.approved_at) }) + if (task0.done_at) + rows.push({ key: 'done', icon: 'ph-check-circle', term: t('task.field.done'), value: formatDate(task0.done_at) }) + return rows +}) + +// Предложение ИИ — построчно с иконками (не сжатые бейджи) +const proposalRows = computed(() => { + const p = task.value?.ai_proposal + if (!p) return [] + const rows: { icon: string; term: string; value: string }[] = [] + if (p.tags.length) + rows.push({ icon: 'ph-tag', term: t('stack.form.tags'), value: p.tags.join(', ') }) + if (p.project) + rows.push({ + icon: p.new_project ? 'ph-folder-plus' : 'ph-folder', + term: t('task.field.project'), + value: p.new_project ? t('stack.proposalNewProject', { name: p.project }) : p.project, + }) + if (p.priority !== null) + rows.push({ + icon: 'ph-flag', + term: t('task.field.priority'), + value: `P${p.priority}`, + }) + if (p.estimated_minutes !== null && p.estimated_minutes !== undefined) + rows.push({ + icon: 'ph-clock', + term: t('task.field.estimate'), + value: `≈ ${formatMinutes(p.estimated_minutes)}`, + }) return rows }) @@ -219,7 +284,7 @@ /> @@ -321,19 +458,74 @@ .task-actions { margin-bottom: 1.5rem; } +.btn-delete { + margin-left: auto; +} +/* Две колонки на широком экране: содержимое + сайдбар параметров */ +.task-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 340px; + gap: 1.5rem; + align-items: start; +} +@media (max-width: 960px) { + .task-grid { + grid-template-columns: 1fr; + } +} +.task-main > * + *, +.task-side > * + * { + margin-top: 1.5rem; +} .proposal-card, .desc-card, -.fields-card { - margin-bottom: 1.5rem; +.att-card { + margin-bottom: 0; } -.proposal { +.card-title-icon { + margin-right: 0.4rem; + color: var(--accent, #7aa2f7); +} +.proposal-hint { + margin-top: -0.25rem; +} +.proposal-rows, +.p-rows { display: flex; - flex-wrap: wrap; - gap: 0.4rem; - align-items: center; + flex-direction: column; + gap: 0.55rem; +} +.p-row { + display: flex; + gap: 0.6rem; + align-items: baseline; + font-size: 0.95rem; +} +.p-row > i { + flex: 0 0 1.25rem; + text-align: center; + color: var(--accent, #7aa2f7); + font-size: 1.05em; + align-self: center; +} +.p-term { + color: var(--text-muted, #888); + flex: 0 0 auto; + min-width: 7.5rem; +} +.p-value { + flex: 1 1 auto; + overflow-wrap: anywhere; +} +.p-row.warn > i, +.p-row.warn .p-value { + color: var(--danger, #f7768e); } .proposal-actions { - margin-top: 0.75rem; + margin-top: 1rem; + display: flex; + flex-wrap: wrap; + gap: 0.75rem; } .muted { color: var(--text-muted, #888); @@ -341,20 +533,47 @@ } .md-view { overflow-x: auto; + font-size: 1.02rem; + line-height: 1.65; } -.thumbs { +.side-card { + margin-bottom: 0; +} +.side-tags { display: flex; flex-wrap: wrap; - gap: 0.75rem; - margin-top: 1rem; + gap: 0.4rem; } -.thumbs img { - max-width: 160px; - max-height: 120px; - border-radius: 6px; +/* Вложения: сетка крупных превью с подписями */ +.att-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); + gap: 1rem; +} +.att-item { + display: flex; + flex-direction: column; + gap: 0.4rem; + text-decoration: none; +} +.att-item img { + width: 100%; + aspect-ratio: 4 / 3; + object-fit: cover; + border-radius: 8px; border: 1px solid var(--border, #333); display: block; } +.att-name { + font-size: 0.85em; + color: var(--text-muted, #888); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.att-item:hover .att-name { + color: var(--accent, #7aa2f7); +} .subtasks-header { display: flex; gap: 0.75rem; @@ -365,4 +584,8 @@ margin: 0; font-size: 1.1rem; } +.subtasks-title i { + color: var(--accent, #7aa2f7); + margin-right: 0.35rem; +} \ No newline at end of file