diff --git a/docs/TZ.md b/docs/TZ.md index 81d2717..dddc4ee 100644 --- a/docs/TZ.md +++ b/docs/TZ.md @@ -4,7 +4,7 @@ | | | |---|---| -| Версия ТЗ | 0.9 | +| Версия ТЗ | 0.10 | | Дата | 2026-09-20 | | Статус | На обсуждении | @@ -185,6 +185,14 @@ - **Связка с «3 вариантами»** (3.9): закрытие задачи из режима помечается (`earned_via`) и празднуется меткой «выбрал и сделал» + счётчик-ачивкой; XP при этом не меняется. +- **Страница «Статистика»**: закрываемость за выбранный **день / неделю / месяц** + (период листается стрелками назад/вперёд). Метрики — закрытые задачи, XP, + созданные задачи, часы работы, «дни с результатом» — каждая со сравнением + с **предыдущим аналогичным периодом** (дельта и проценты). Анимированный + график закрытых задач по бакетам периода (часы / дни недели / дни месяца). + Данные считаются из `xp_events` и дат задач, без новых эндпоинтов. +- **Справка «О саде»**: кнопка на странице сада открывает модалку с объяснением + механик (XP, уровни, растения и редкости, ачивки, принцип «только позитив»). ## 4. Нефункциональные требования diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 577f3a4..1161449 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -15,6 +15,7 @@ const navItems = computed(() => [ { id: 'home', label: t('nav.home'), icon: 'ph-house', to: '/' }, { id: 'garden', label: t('nav.garden'), icon: 'ph-leaf', to: '/garden' }, + { id: 'stats', label: t('nav.stats'), icon: 'ph-chart-bar', to: '/stats' }, { id: 'list', label: t('nav.list'), icon: 'ph-list-bullets', to: '/list' }, { id: 'projects', label: t('nav.projects'), icon: 'ph-folders', to: '/projects' }, { id: 'options', label: t('nav.options'), icon: 'ph-shuffle', to: '/options' }, diff --git a/frontend/src/locales/en.ts b/frontend/src/locales/en.ts index ce9312b..9eedcc0 100644 --- a/frontend/src/locales/en.ts +++ b/frontend/src/locales/en.ts @@ -38,6 +38,7 @@ stack: 'Inbox stack', list: 'Task list', garden: 'Garden', + stats: 'Statistics', projects: 'Projects', options: '3 options', settings: 'Settings', @@ -48,6 +49,16 @@ xpTotal: '{n} XP', plants: 'plants: {n}', levelProgress: 'To next level: {xp} / {span} XP', + aboutOpen: 'How it works', + aboutTitle: 'How the garden works', + about: [ + 'Every completed task leaves a plant on the bed — the garden only grows and never shrinks.', + 'Completing a task earns XP: base 10 + a priority bonus (up to 15) + a size bonus from the time estimate (up to 15). Bigger and more urgent work is worth more.', + 'Earned XP raises your level — from Sprout to Garden Legend. Levels never drop: XP does not expire and is not taken away, even if a task is deleted.', + 'Sometimes a task yields a rare plant — the chance grows with the task weight. Epic plants only come from the biggest completions.', + 'Achievements mark milestones: tasks completed, levels, different projects, hours of work, strict deadlines. Each has steps I, II, III with always-visible progress.', + 'A task completed from the “3 options” mode earns the “chose and did” mark. Nothing expires, no streaks or penalties — only pleasant rewards.', + ], lastPlant: 'Latest sprout: {title} — {date}', history: 'History: tasks completed by month', emptyTitle: 'The garden is empty', @@ -91,6 +102,27 @@ epicPlant: 'A legendary plant in your garden!', chosen: 'chose and did', }, + stats: { + title: 'Statistics', + modeDay: 'Day', + modeWeek: 'Week', + modeMonth: 'Month', + today: 'Today, {date}', + prevPeriod: 'Previous period', + nextPeriod: 'Next period', + ofDays: 'of {n} days', + totalVs: 'Completed this period: {n} · previous period: {m}', + chartTitle: 'Completed tasks', + emptyTitle: 'No data yet', + emptyText: 'Complete a couple of tasks — dynamics with comparisons will appear here.', + metric: { + closed: 'Tasks completed', + xp: 'XP earned', + created: 'Tasks created', + hours: 'Hours worked', + activeDays: 'Productive days', + }, + }, home: { title: 'Home', stackCount: 'in stack: {n}', diff --git a/frontend/src/locales/ru.ts b/frontend/src/locales/ru.ts index 790a7e4..a32d919 100644 --- a/frontend/src/locales/ru.ts +++ b/frontend/src/locales/ru.ts @@ -39,6 +39,7 @@ stack: 'Стек входящих', list: 'Список задач', garden: 'Сад', + stats: 'Статистика', projects: 'Проекты', options: '3 варианта', settings: 'Настройки', @@ -49,6 +50,16 @@ xpTotal: '{n} XP', plants: 'растений: {n}', levelProgress: 'До уровня: {xp} / {span} XP', + aboutOpen: 'Как это работает', + aboutTitle: 'Как устроен сад', + about: [ + 'Каждая закрытая задача оставляет на грядке растение — сад только растёт и никогда не уменьшается.', + 'За закрытие начисляется XP: база 10 + бонус за приоритет задачи (до 15) + бонус за объём работы по оценке времени (до 15). Крупные и срочные дела ценнее.', + 'Накопленный XP повышает уровень — от «Ростка» до «Легенды сада». Уровень не падает: XP не сгорает и не отнимается, даже если задачу удалить.', + 'Иногда задача приносит редкое растение — шанс тем выше, чем «весомее» задача. Эпические растения достаются только крупным закрытиям.', + 'Достижения отмечают вехи: закрытые задачи, уровни, разные проекты, часы работы, строгие дедлайны. У каждой ступени I, II, III — прогресс виден всегда.', + 'Задача, закрытая из режима «3 вариантов», отмечается меткой «выбрал и сделал». Ничего не сгорает, серий и наказаний нет — только приятные награды.', + ], lastPlant: 'Последний росток: {title} — {date}', history: 'История: закрытых задач по месяцам', emptyTitle: 'Сад пока пуст', @@ -92,6 +103,27 @@ epicPlant: 'Легендарное растение в саду!', chosen: 'выбрал и сделал', }, + stats: { + title: 'Статистика', + modeDay: 'День', + modeWeek: 'Неделя', + modeMonth: 'Месяц', + today: 'Сегодня, {date}', + prevPeriod: 'Предыдущий период', + nextPeriod: 'Следующий период', + ofDays: 'из {n} дн.', + totalVs: 'За период закрыто: {n} · в предыдущий раз: {m}', + chartTitle: 'Закрытые задачи', + emptyTitle: 'Пока нет данных', + emptyText: 'Закройте пару задач — и здесь появится динамика со сравнением.', + metric: { + closed: 'Закрыто задач', + xp: 'XP получено', + created: 'Создано задач', + hours: 'Часы работы', + activeDays: 'Дни с результатом', + }, + }, home: { title: 'Главное', stackCount: 'в стеке: {n}', diff --git a/frontend/src/locales/uk.ts b/frontend/src/locales/uk.ts index 5cd0062..47b2ac1 100644 --- a/frontend/src/locales/uk.ts +++ b/frontend/src/locales/uk.ts @@ -38,6 +38,7 @@ stack: 'Вхідний стек', list: 'Список задач', garden: 'Сад', + stats: 'Статистика', projects: 'Проєкти', options: '3 варіанти', settings: 'Налаштування', @@ -48,6 +49,16 @@ xpTotal: '{n} XP', plants: 'рослин: {n}', levelProgress: 'До наступного рівня: {xp} / {span} XP', + aboutOpen: 'Як це працює', + aboutTitle: 'Як влаштований сад', + about: [ + 'Кожне виконане завдання залишає на грядці рослину — сад лише зростає і ніколи не зменшується.', + 'За виконання нараховується XP: база 10 + бонус за пріоритет (до 15) + бонус за обсяг за оцінкою часу (до 15). Великі й термінові справи цінніші.', + 'Накопичений XP підвищує рівень — від «Паростка» до «Легенди саду». Рівень не падає: XP не згорає і не віднімається, навіть якщо завдання видалити.', + 'Іноді завдання приносить рідкісну рослину — шанс тим вищий, що «важче» завдання. Епічні рослини дістаються лише великим завершенням.', + 'Досягнення відзначають віхи: виконані завдання, рівні, різні проєкти, години роботи, строгі дедлайни. У кожного ступені I, II, III — прогрес видно завжди.', + 'Завдання, виконане з режиму «3 варіанти», відзначається меткою «обрав і зробив». Нічого не згорає, серій і покарань немає — лише приємні нагороди.', + ], lastPlant: 'Останній паросток: {title} — {date}', history: 'Історія: виконаних завдань за місяцями', emptyTitle: 'Сад поки порожній', @@ -91,6 +102,27 @@ epicPlant: 'Легендарна рослина в саду!', chosen: 'обрав і зробив', }, + stats: { + title: 'Статистика', + modeDay: 'День', + modeWeek: 'Тиждень', + modeMonth: 'Місяць', + today: 'Сьогодні, {date}', + prevPeriod: 'Попередній період', + nextPeriod: 'Наступний період', + ofDays: 'з {n} дн.', + totalVs: 'За період виконано: {n} · минулого разу: {m}', + chartTitle: 'Виконані завдання', + emptyTitle: 'Поки немає даних', + emptyText: 'Закрийте пару завдань — і тут з’явиться динаміка з порівнянням.', + metric: { + closed: 'Виконано завдань', + xp: 'XP отримано', + created: 'Створено завдань', + hours: 'Години роботи', + activeDays: 'Дні з результатом', + }, + }, home: { title: 'Головне', stackCount: 'у стеці: {n}', diff --git a/frontend/src/main.ts b/frontend/src/main.ts index f97bd44..677aac5 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -10,6 +10,7 @@ import { setPageTitle } from './taskui' import ListView from './views/ListView.vue' import GardenView from './views/GardenView.vue' +import StatsView from './views/StatsView.vue' import OptionsView from './views/OptionsView.vue' import ProjectView from './views/ProjectView.vue' import ProjectsView from './views/ProjectsView.vue' @@ -25,6 +26,7 @@ { path: '/stack', redirect: '/' }, { path: '/list', component: ListView, meta: { title: 'nav.list' } }, { path: '/garden', component: GardenView, meta: { title: 'nav.garden' } }, + { path: '/stats', component: StatsView, meta: { title: 'nav.stats' } }, { path: '/projects', component: ProjectsView, meta: { title: 'nav.projects' } }, // название проекта/задачи в тайтл подставляет вид после загрузки данных { path: '/projects/:id', component: ProjectView, meta: { title: 'nav.projects' } }, diff --git a/frontend/src/views/GardenView.vue b/frontend/src/views/GardenView.vue index 2320505..614ecf8 100644 --- a/frontend/src/views/GardenView.vue +++ b/frontend/src/views/GardenView.vue @@ -10,7 +10,7 @@ // сад никогда не уменьшается, стриков нет, уровень не падает. // Редкость растения и признак «из 3 вариантов» приходят из событий XP. -const { t, locale } = useI18n() +const { t, locale, tm } = useI18n() const tasks = ref([]) const events = ref([]) @@ -18,6 +18,15 @@ const error = ref('') const loading = ref(false) +// Справка «как это работает»: модалка с абзацами из локали +const aboutOpen = ref(false) +const aboutParagraphs = computed(() => { + const messages = tm('garden.about') + return Array.isArray(messages) + ? (messages as unknown[]).filter((p): p is string => typeof p === 'string') + : [] +}) + // Растение детерминировано по id задачи — при каждом визите то же самое const PLANTS = ['ph-flower', 'ph-flower-lotus', 'ph-tree-evergreen', 'ph-tree', 'ph-leaf', 'ph-cactus'] @@ -184,8 +193,18 @@ {{ t('garden.plants', { n: doneTasks.length }) }} + + + +
+

{{ p }}

+
+
+ {{ error }} @@ -244,7 +263,11 @@ icon="ph-flower-lotus" :title="t('garden.emptyTitle')" :text="t('garden.emptyText')" - /> + > + + {{ t('garden.aboutOpen') }} + +
@@ -276,6 +299,14 @@ .level-progress { margin-bottom: 1rem; } +/* справка о саде */ +.about-body { + display: flex; + flex-direction: column; + gap: 0.75rem; + font-size: 0.95em; + line-height: 1.5; +} /* последний росток */ .last-plant { display: flex; diff --git a/frontend/src/views/StatsView.vue b/frontend/src/views/StatsView.vue new file mode 100644 index 0000000..b33125c --- /dev/null +++ b/frontend/src/views/StatsView.vue @@ -0,0 +1,460 @@ + + + + + \ No newline at end of file