<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { api, type Task, type XpSummary } from '../api'
import { formatDate } from '../taskui'
defineOptions({ name: 'GardenView' })
// Сад (ТЗ 3.13): каждая закрытая задача — растение на грядке. Только позитив:
// сад никогда не уменьшается, стриков нет, уровень не падает.
const { t } = useI18n()
const tasks = ref<Task[]>([])
const xp = ref<XpSummary | null>(null)
const error = ref('')
const loading = ref(false)
// Растение детерминировано по id задачи — при каждом визите то же самое
const PLANTS = ['ph-flower', 'ph-flower-lotus', 'ph-tree-evergreen', 'ph-tree', 'ph-leaf', 'ph-cactus']
const doneTasks = computed(() =>
tasks.value
.filter((task) => task.status === 'done' && task.done_at)
.sort((a, b) => (a.done_at! < b.done_at! ? 1 : -1)),
)
function plantIcon(task: Task): string {
if (task.task_type === 'recurring') return 'ph-tree'
return PLANTS[task.id % PLANTS.length]
}
// --- Ачивки-вехи: только достижения, без «провала» ---
interface Achievement {
key: string
icon: string
progress: number
goal: number
}
const achievements = computed<Achievement[]>(() => {
const done = doneTasks.value
const recurringDone = done.some((task) => task.task_type === 'recurring')
// проект закрыт: есть проект (от 3 задач), все задачи которого завершены
const projects = new Map<number, { total: number; done: number }>()
tasks.value.forEach((task) => {
if (!task.project) return
const bucket = projects.get(task.project.id) ?? { total: 0, done: 0 }
bucket.total += 1
if (task.status === 'done') bucket.done += 1
projects.set(task.project.id, bucket)
})
const projectDone = [...projects.values()].some((p) => p.total >= 3 && p.done === p.total)
return [
{ key: 'firstDone', icon: 'ph-flower-lotus', progress: Math.min(done.length, 1), goal: 1 },
{ key: 'done10', icon: 'ph-leaf', progress: Math.min(done.length, 10), goal: 10 },
{ key: 'done50', icon: 'ph-tree', progress: Math.min(done.length, 50), goal: 50 },
{ key: 'level5', icon: 'ph-crown-simple', progress: Math.min(xp.value?.level ?? 1, 5), goal: 5 },
{ key: 'level10', icon: 'ph-mountains', progress: Math.min(xp.value?.level ?? 1, 10), goal: 10 },
{ key: 'recurring', icon: 'ph-repeat', progress: recurringDone ? 1 : 0, goal: 1 },
{ key: 'projectDone', icon: 'ph-flag-checkered', progress: projectDone ? 1 : 0, goal: 1 },
]
})
async function load() {
loading.value = true
error.value = ''
try {
const [taskList, xpSummary] = await Promise.all([api.listTasks(), api.getXp()])
tasks.value = taskList
xp.value = xpSummary
} catch (e) {
error.value = String(e)
} finally {
loading.value = false
}
}
onMounted(load)
</script>
<template>
<section>
<GnSkeleton v-if="loading && !xp" type="block" stack :count="2" class="page-skeleton" />
<template v-else>
<GnPageHeader kicker="GNexus Tasks" :title="t('garden.title')">
<template #meta>
<GnBadge variant="primary"><i class="ph ph-trophy" aria-hidden="true" />
{{ t('garden.level', { n: xp?.level ?? 1 }) }}
</GnBadge>
<GnBadge variant="neutral"><i class="ph ph-lightning" aria-hidden="true" />
{{ t('garden.xpTotal', { n: xp?.total_xp ?? 0 }) }}
</GnBadge>
<GnBadge variant="success"><i class="ph ph-leaf" aria-hidden="true" />
{{ t('garden.plants', { n: doneTasks.length }) }}
</GnBadge>
</template>
</GnPageHeader>
<GnAlert v-if="error" variant="error">{{ error }}</GnAlert>
<!-- Прогресс уровня: XP только растёт, уровень не падает -->
<GnProgress
v-if="xp"
class="level-progress"
:value="xp.level_xp"
:max="xp.level_span"
:label="t('garden.levelProgress', { xp: xp.level_xp, span: xp.level_span })"
/>
<!-- Грядка: растение за каждую закрытую задачу -->
<div v-if="doneTasks.length" class="garden-grid">
<RouterLink
v-for="task in doneTasks"
:key="task.id"
class="plant"
:to="`/tasks/${task.id}`"
>
<i :class="`ph ${plantIcon(task)}`" aria-hidden="true" />
<span class="plant-title">{{ task.title }}</span>
<span class="plant-date">{{ formatDate(task.done_at!) }}</span>
</RouterLink>
</div>
<GnEmptyState
v-else
icon="ph-flower-lotus"
:title="t('garden.emptyTitle')"
:text="t('garden.emptyText')"
/>
<!-- Ачивки-вехи: прогресс к следующей вехе виден всегда -->
<div class="achievements-title">
<i class="ph ph-trophy" aria-hidden="true" />
<span>{{ t('garden.achievements') }}</span>
</div>
<div class="achievements-grid">
<div
v-for="a in achievements"
:key="a.key"
class="achievement"
:class="{ unlocked: a.progress >= a.goal }"
>
<i :class="`ph ${a.icon}`" aria-hidden="true" />
<span class="achievement-name">{{ t(`garden.ach.${a.key}`) }}</span>
<GnBadge :variant="a.progress >= a.goal ? 'success' : 'neutral'">
{{ a.progress }}/{{ a.goal }}
</GnBadge>
</div>
</div>
</template>
</section>
</template>
<style scoped>
.page-skeleton {
margin-bottom: 1rem;
}
.level-progress {
margin-bottom: 1.5rem;
}
/* грядка: карточки-растения ровной сеткой, на узком экране — уже */
.garden-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
gap: 0.75rem;
margin-bottom: 2rem;
}
.plant {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.3rem;
padding: 1rem 0.75rem;
border: 1px solid var(--border, #2a2f45);
border-radius: 10px;
text-decoration: none;
color: inherit;
background: color-mix(in srgb, var(--success, #9ece6a) 6%, transparent);
}
.plant > i {
font-size: 1.9rem;
color: var(--success, #9ece6a);
}
.plant-title {
font-size: 0.85em;
font-weight: 600;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.plant-date {
font-size: 0.75em;
color: var(--text-muted, #888);
}
.plant:hover {
border-color: var(--success, #9ece6a);
}
/* ачивки */
.achievements-title {
display: flex;
gap: 0.6rem;
align-items: center;
margin: 0 0 0.75rem;
font-weight: 600;
font-size: 1.05rem;
}
.achievements-title > i {
color: var(--accent, #7aa2f7);
}
.achievements-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 0.75rem;
}
.achievement {
display: flex;
gap: 0.6rem;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--border, #2a2f45);
border-radius: 10px;
opacity: 0.55;
}
.achievement.unlocked {
opacity: 1;
background: color-mix(in srgb, var(--success, #9ece6a) 8%, transparent);
}
.achievement > i {
font-size: 1.3rem;
color: var(--success, #9ece6a);
}
.achievement-name {
flex: 1 1 auto;
font-size: 0.92em;
font-weight: 600;
}
</style>