Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions alfy-bot-frontend/src/api/goals.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { FrequencyType, Goal, GoalStatus, Question, QuestionType, Schedule } from '../types'
import type { Task } from '@/features/tasks/model/types'
import { api } from './client'

export interface UpdateScheduleDto {
Expand Down Expand Up @@ -117,3 +118,18 @@ export async function fetchQuestionAnswerCount(questionId: number): Promise<numb
const { data } = await api.get<{ count: number }>(`/questions/${questionId}/answer-count`)
return data.count
}

export async function fetchGoalTasks(goalId: number): Promise<Task[]> {
const { data } = await api.get<Record<string, unknown>[]>(`/goals/${goalId}/tasks`)
return data.map(raw => ({
...raw,
dueDate: raw.dueDate ? new Date(raw.dueDate as string) : undefined,
deadline: raw.deadline ? new Date(raw.deadline as string) : undefined,
goalIds: Array.isArray(raw.goalIds) ? raw.goalIds as number[] : [],
}) as Task)
}

export async function setGoalTasks(goalId: number, taskIds: string[]): Promise<string[]> {
const { data } = await api.put<{ taskIds: string[] }>(`/goals/${goalId}/tasks`, { taskIds })
return data.taskIds
}
2 changes: 1 addition & 1 deletion alfy-bot-frontend/src/components/UserSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function handleLogout() {
<ChevronDown class="h-4 w-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" :side-offset="8" class="w-48">
<DropdownMenuContent :side-offset="8">
<DropdownMenuItem
v-for="link in props.links"
:key="link.to"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const props = withDefaults(
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes["class"] }>(),
{
sideOffset: 4,
align: "start",
},
)
const emits = defineEmits<DropdownMenuContentEmits>()
Expand All @@ -27,7 +28,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DropdownMenuContent
data-slot="dropdown-menu-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--reka-dropdown-menu-content-available-height) min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border p-1 shadow-lg dark:shadow-xl dark:shadow-black/30', props.class)"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-56 max-h-(--reka-dropdown-menu-content-available-height) origin-(--reka-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border border-border p-1 shadow-lg dark:shadow-xl dark:shadow-black/30', props.class)"
>
<slot />
</DropdownMenuContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<DropdownMenuSubContent
data-slot="dropdown-menu-sub-content"
v-bind="forwarded"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg', props.class)"
:class="cn('bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-56 origin-(--reka-dropdown-menu-content-transform-origin) overflow-hidden rounded-xl border border-border p-1 shadow-lg dark:shadow-xl dark:shadow-black/30', props.class)"
>
<slot />
</DropdownMenuSubContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
@update:model-value="emit('toggle', event.taskId)"
/>
<Repeat v-if="event.isRecurring" :size="10" class="shrink-0" />
<Target
v-if="event.task.goalIds && event.task.goalIds.length > 0"
:size="10"
class="shrink-0"
data-testid="goal-link-icon"
/>
<Flag
v-if="showPriorityMarks"
:size="10"
Expand Down Expand Up @@ -72,7 +78,7 @@

<script setup lang="ts">
import { computed } from 'vue'
import { CalendarPlus, Flag, Repeat, Timer } from 'lucide-vue-next'
import { CalendarPlus, Flag, Repeat, Target, Timer } from 'lucide-vue-next'
import RoundCheckbox from '@/components/ui/roundCheckbox/RoundCheckbox.vue'
import type { CalendarEvent } from '../model/types'
import { PRIORITY_LABELS } from '@/features/tasks/model/constants'
Expand Down
91 changes: 91 additions & 0 deletions alfy-bot-frontend/src/features/goals/ui/GoalPicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<template>
<DropdownMenu v-model:open="open">
<DropdownMenuTrigger as-child>
<button
type="button"
:class="[
'w-full px-4 py-2.5 transition-colors border-b border-border/40 text-left',
!disabled && 'cursor-pointer hover:bg-muted/50',
]"
:disabled="disabled"
>
<div class="flex items-center gap-2 mb-1">
<Target :size="13" class="text-muted-foreground/60" />
<span class="text-[11px] text-muted-foreground/60 font-medium">Цель</span>
</div>
<div class="text-[13px] truncate" :class="modelValue.length === 0 && 'text-muted-foreground/40'">
{{ triggerLabel }}
</div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent @open-auto-focus.prevent>
<GoalPickerContent
ref="contentRef"
:model-value="modelValue"
:goals="goals"
:loading="loading"
@update:model-value="$emit('update:modelValue', $event)"
/>
</DropdownMenuContent>
</DropdownMenu>
</template>

<script setup lang="ts">
import type { Goal } from '@/types'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { Target } from 'lucide-vue-next'
import { fetchGoals } from '@/api/goals'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import GoalPickerContent from './GoalPickerContent.vue'

const props = defineProps<{
modelValue: number[]
disabled?: boolean
}>()

defineEmits<{
'update:modelValue': [value: number[]]
}>()

const open = ref(false)
const goals = ref<Goal[]>([])
const loading = ref(true)
const contentRef = ref<{ focusSearch: () => void, resetQuery: () => void } | null>(null)

onMounted(async () => {
try {
goals.value = (await fetchGoals()).filter(g => g.status !== 'deleted')
}
finally {
loading.value = false
}
})

watch(open, async (isOpen) => {
if (!isOpen)
return
await nextTick()
contentRef.value?.resetQuery()
contentRef.value?.focusSearch()
})

const triggerLabel = computed(() => {
const ids = props.modelValue
if (ids.length === 0)
return 'Не привязано'
const names = ids
.map(id => goals.value.find(g => g.id === id)?.goal_name)
.filter((n): n is string => !!n)
if (names.length === 0)
return ids.length === 1 ? 'Цель' : `${ids.length} цели`
if (names.length === 1)
return names[0]!
if (names.length === 2)
return `${names[0]}, ${names[1]}`
return `${names[0]}, ${names[1]} +${names.length - 2}`
})
</script>
Loading
Loading