+
-
-
-
-
- {{ t('projectPage.tree') }}
-
+
+
{{ row.term }}
- {{ row.value }}
+ {{ row.value }}
+ {{ row.value }}
@@ -388,7 +422,12 @@
icon="ph-activity"
:options="relevanceOptions"
/>
-
+
@@ -479,32 +518,34 @@
color: var(--text-muted, #888);
font-size: 0.9em;
}
+.view-tabs {
+ margin-bottom: 1rem;
+}
+/* фильтры: подписанные селекты в ряд, кнопка сброса прижата к низу */
.filters {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
+ align-items: flex-end;
margin-bottom: 1rem;
}
.filters :deep(.form-group) {
- width: auto;
+ width: 14rem;
margin-bottom: 0;
}
.filters :deep(.select-wrap) {
- width: 14rem;
margin-top: 0;
}
+.filters-reset {
+ margin-bottom: 0.25rem;
+}
.task-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.tree-section {
- margin-top: 2rem;
-}
-.tree-title {
- margin: 0 0 0.75rem;
- font-size: 1.1rem;
-}
-.tree-title i {
- color: var(--accent, #7aa2f7);
- margin-right: 0.35rem;
+ margin-top: 0.5rem;
}
.edit-form {
display: flex;
diff --git a/frontend/src/views/ProjectsView.vue b/frontend/src/views/ProjectsView.vue
index 9e49966..76a2d56 100644
--- a/frontend/src/views/ProjectsView.vue
+++ b/frontend/src/views/ProjectsView.vue
@@ -3,7 +3,15 @@
import { useI18n } from 'vue-i18n'
import { useToast } from 'gnexus-ui-kit/vue'
import { api, type Project, type Task } from '../api'
-import { relevanceLabel, renderMarkdown } from '../taskui'
+import {
+ gradeToPriority,
+ priorityOptions,
+ priorityToGrade,
+ priorityLabel,
+ priorityVariant,
+ relevanceLabel,
+ renderMarkdown,
+} from '../taskui'
import QuickPrompt from '../components/QuickPrompt.vue'
// Проекты (ТЗ 3.7): актуальность, приоритет, Markdown-заметка с ресурсами.
@@ -36,7 +44,7 @@
const editForm = ref({
name: '',
relevance_status: 'active',
- priority: null as number | null,
+ priorityGrade: '' as string,
note: '',
})
@@ -63,7 +71,7 @@
editForm.value = {
name: project.name,
relevance_status: project.relevance_status,
- priority: project.priority,
+ priorityGrade: project.priority === null ? '' : priorityToGrade(project.priority),
note: project.note,
}
drawerOpen.value = true
@@ -80,7 +88,8 @@
await api.updateProject(editing.value.id, {
name: editForm.value.name,
relevance_status: editForm.value.relevance_status,
- priority: editForm.value.priority,
+ priority:
+ editForm.value.priorityGrade === '' ? null : gradeToPriority(editForm.value.priorityGrade),
note: editForm.value.note,
})
toast.success({ title: t('common.saved') })
@@ -129,6 +138,11 @@
const relevanceOptions = computed(() =>
['active', 'paused', 'archived'].map((value) => ({ value, label: relevanceLabel(value) })),
)
+
+const priorityOptionsWithNone = computed(() => [
+ { value: '', label: t('common.none') },
+ ...priorityOptions(),
+])
@@ -169,8 +183,8 @@
{{ relevanceLabel(project.relevance_status) }}
-
- P{{ project.priority }}
+
+ {{ priorityLabel(project.priority) }}
{{ t('projectPage.taskCount', { n: taskCounts.get(project.id)?.total ?? 0 }) }}
@@ -195,7 +209,12 @@
icon="ph-activity"
:options="relevanceOptions"
/>
-
+
diff --git a/frontend/src/views/TaskView.vue b/frontend/src/views/TaskView.vue
index 1d2f8f1..c2f36c4 100644
--- a/frontend/src/views/TaskView.vue
+++ b/frontend/src/views/TaskView.vue
@@ -2,17 +2,20 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
-import { api, ApiError, type Attachment, type Task } from '../api'
+import { api, ApiError, type Attachment, type Tag, type Task } from '../api'
import {
deadlineLabel,
deadlineOverdue,
formatDate,
formatMinutes,
formatMoney,
+ priorityLabel,
+ priorityVariant,
recurrenceLabel,
renderMarkdown,
statusOptions,
statusLabel,
+ statusVariant,
} from '../taskui'
import { useToast } from 'gnexus-ui-kit/vue'
import QuickPrompt from '../components/QuickPrompt.vue'
@@ -52,7 +55,10 @@
try {
task.value = await api.getTask(taskId.value)
attachments.value = await api.listAttachments(taskId.value)
- tasks.value = await api.listTasks({ detail_state: 'approved' })
+ // все задачи (не только утверждённые) — подзадача видна сразу после создания
+ tasks.value = await api.listTasks()
+ tagNames.value = task.value.tags.map((t0) => t0.name)
+ if (!tagsCatalog.value.length) tagsCatalog.value = await api.listTags()
} catch (e) {
if (e instanceof ApiError && e.status === 404) {
notFound.value = true
@@ -70,7 +76,7 @@
const paramRows = computed(() => {
const task0 = task.value
if (!task0) return []
- const rows: { key: string; icon: string; term: string; value: string; warn?: boolean }[] = []
+ const rows: { key: string; icon: string; term: string; value: string; warn?: boolean; badge?: string }[] = []
rows.push({
key: 'taskType',
icon: task0.task_type === 'recurring' ? 'ph-repeat' : 'ph-arrow-bend-down-right',
@@ -81,7 +87,13 @@
: t('stack.recur.oneTime'),
})
if (task0.priority !== null)
- rows.push({ key: 'priority', icon: 'ph-flag', term: t('task.field.priority'), value: `P${task0.priority}` })
+ rows.push({
+ key: 'priority',
+ icon: 'ph-flag',
+ term: t('task.field.priority'),
+ value: priorityLabel(task0.priority),
+ badge: priorityVariant(task0.priority),
+ })
if (task0.estimated_minutes)
rows.push({
key: 'estimate',
@@ -155,7 +167,7 @@
rows.push({
icon: 'ph-flag',
term: t('task.field.priority'),
- value: `P${p.priority}`,
+ value: priorityLabel(p.priority),
})
if (p.estimated_minutes !== null && p.estimated_minutes !== undefined)
rows.push({
@@ -175,6 +187,17 @@
}))
}
+// Действия страницы — меню в шапке (статус вынесен в сайдбар)
+function pageActions(): { label: string; icon: string; danger?: boolean; onSelect: () => void }[] {
+ const task0 = task.value
+ if (!task0) return []
+ return [
+ { label: t('common.edit'), icon: 'ph-pencil-simple', onSelect: () => (editing.value = true) },
+ { label: t('stack.redetail'), icon: 'ph-arrows-counter-clockwise', onSelect: redetail },
+ { label: t('common.delete'), icon: 'ph-trash', danger: true, onSelect: () => askDelete(task0) },
+ ]
+}
+
async function setStatus(_task: Task, status: string) {
error.value = ''
try {
@@ -207,6 +230,7 @@
function onSaved(fresh: Task) {
editing.value = false
task.value = fresh
+ tagNames.value = fresh.tags.map((t0) => t0.name)
}
// Подзадачи: дерево строится из утверждённых задач (подзадача попадает сюда
@@ -217,12 +241,47 @@
subtaskOpen.value = false
try {
await api.createTask(title, '', taskId.value)
+ toast.info({ title: t('task.subtaskCreated') })
await load()
} catch (e) {
toast.error({ title: t('common.error'), text: String(e) })
}
}
+// Теги в сайдбаре: быстрое добавление/удаление прямо на странице.
+// Новое имя создаёт тег в каталоге; каждое изменение — PATCH tag_ids.
+const tagNames = ref([])
+const tagsCatalog = ref([])
+
+async function onTagAdd(name: string) {
+ const task0 = task.value
+ if (!task0 || task0.tags.some((t0) => t0.name === name)) return
+ try {
+ let tag = tagsCatalog.value.find((t0) => t0.name === name)
+ if (!tag) {
+ tag = await api.createTag(name)
+ tagsCatalog.value.push(tag)
+ }
+ task.value = await api.updateTask(task0.id, {
+ tag_ids: [...task0.tags.map((t0) => t0.id), tag.id],
+ })
+ } catch (e) {
+ toast.error({ title: t('common.error'), text: String(e) })
+ }
+}
+
+async function onTagRemove(name: string) {
+ const task0 = task.value
+ if (!task0) return
+ try {
+ task.value = await api.updateTask(task0.id, {
+ tag_ids: task0.tags.filter((t0) => t0.name !== name).map((t0) => t0.id),
+ })
+ } catch (e) {
+ toast.error({ title: t('common.error'), text: String(e) })
+ }
+}
+
// Удаление (страницы и подзадач в дереве) — через подтверждение
const deleteTarget = ref(null)
const confirmOpen = ref(false)
@@ -269,6 +328,9 @@
+
+
+
{{ error }}
@@ -284,20 +346,6 @@
/>
-
-
-
-
- {{ t('common.edit') }}
-
-
- {{ t('stack.redetail') }}
-
-
- {{ t('common.delete') }}
-
-
-
-
+
-