Skip to content

Commit 6b3da32

Browse files
authored
feat: tasks page on telegram (#226)
1 parent 07200ca commit 6b3da32

5 files changed

Lines changed: 267 additions & 3 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<template>
2+
<ActiveCard>
3+
<Section>
4+
<div class="flex flex-row gap-2 items-center">
5+
<UAvatar :src="performer?.avatarUrl ?? undefined" class="size-8" />
6+
7+
<UIcon
8+
:name="isCompleted ? 'i-lucide-check' : 'i-lucide-loader-circle'"
9+
class="shrink-0 size-8"
10+
:class="[
11+
isCompleted ? 'text-primary' : 'text-muted/50',
12+
!isCompleted && 'motion-preset-spin motion-duration-4000',
13+
]"
14+
/>
15+
</div>
16+
17+
<h3 class="text-xl/6 font-bold">
18+
{{ task.name }}
19+
</h3>
20+
21+
<div v-if="task.description" class="w-full text-base/5 font-normal whitespace-pre-wrap break-words line-clamp-8">
22+
{{ task.description }}
23+
</div>
24+
25+
<div v-if="task.report" class="flex flex-row gap-2 items-start w-full">
26+
<UIcon name="i-lucide-clipboard-pen" class="shrink-0 size-5 text-primary" />
27+
<p class="text-base/5 font-semibold whitespace-pre-wrap break-words line-clamp-12">
28+
{{ task.report }}
29+
</p>
30+
</div>
31+
32+
<div v-if="task?.completedAt" class="flex flex-row gap-2 items-start w-full">
33+
<UIcon name="i-lucide-calendar" class="shrink-0 size-5 text-primary" />
34+
<p class="text-base/5 font-semibold">
35+
{{ format(new Date(task.completedAt), 'd MMMM yyyy в HH:mm', { locale: ru }) }}
36+
</p>
37+
</div>
38+
</Section>
39+
</ActiveCard>
40+
</template>
41+
42+
<script setup lang="ts">
43+
import type { Task } from '@roll-stack/database'
44+
import { format } from 'date-fns'
45+
import { ru } from 'date-fns/locale/ru'
46+
47+
const { task } = defineProps<{
48+
task: Task
49+
}>()
50+
51+
const userStore = useUserStore()
52+
53+
const isCompleted = computed(() => !!task.completedAt)
54+
const performer = computed(() => userStore.staff.find((staff) => staff.id === task.performerId))
55+
</script>
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
<template>
2+
<PageContainer>
3+
<div class="flex flex-row gap-2.5 items-center">
4+
<SectionTitle title="Задачи" />
5+
<CounterBadge :value="filteredTasks.all.length" />
6+
</div>
7+
8+
<div class="grid grid-cols-1 gap-2.5 items-center">
9+
<USelect
10+
v-model="sortedBy"
11+
size="xl"
12+
trailing-icon="i-lucide-arrow-down-wide-narrow"
13+
:ui="{
14+
base: 'rounded-lg text-lg/5 font-bold ring-0',
15+
}"
16+
:items="[
17+
{ label: 'По дате создания (убывание)', value: 'updatedAtDesc' },
18+
{ label: 'По дате создания (возрастание)', value: 'updatedAtAsc' },
19+
]"
20+
class="motion-preset-slide-down"
21+
/>
22+
23+
<USelect
24+
v-model="filteredBy"
25+
size="xl"
26+
trailing-icon="i-lucide-funnel"
27+
:ui="{
28+
base: 'rounded-lg text-lg/5 font-bold ring-0',
29+
}"
30+
:items="[
31+
{ label: 'Все задачи', value: 'all' },
32+
{ label: 'Только выполненные', value: 'completed' },
33+
]"
34+
class="motion-preset-slide-up"
35+
/>
36+
37+
<USelectMenu
38+
v-model="selectedPerformer"
39+
:items="availablePerformers"
40+
size="xl"
41+
:ui="{
42+
base: 'rounded-lg text-lg/5 font-bold ring-0',
43+
}"
44+
placeholder="Все исполнители"
45+
class="motion-preset-slide-up"
46+
>
47+
<template v-if="selectedPerformer?.avatar.src" #trailing>
48+
<UAvatar
49+
v-if="selectedPerformer?.avatar"
50+
:src="selectedPerformer.avatar.src"
51+
size="xs"
52+
/>
53+
</template>
54+
</USelectMenu>
55+
</div>
56+
57+
<div class="flex flex-col gap-2.5">
58+
<div class="flex flex-col gap-4">
59+
<TaskInfoCard
60+
v-for="task in filteredTasks.show"
61+
:key="task.id"
62+
:task="task"
63+
/>
64+
65+
<UButton
66+
v-if="filteredTasks.canShowMore"
67+
variant="solid"
68+
color="secondary"
69+
size="xl"
70+
class="mt-6 mx-auto px-8 w-fit items-center justify-center"
71+
icon="i-lucide-arrow-down"
72+
:label="$t('common.show-more')"
73+
@click="handleClickShowMore()"
74+
/>
75+
</div>
76+
</div>
77+
</PageContainer>
78+
</template>
79+
80+
<script setup lang="ts">
81+
import type { Task } from '@roll-stack/database'
82+
83+
const { vibrate } = useFeedback()
84+
const userStore = useUserStore()
85+
const taskStore = useTaskStore()
86+
87+
// On load show last 50 tasks. On button click = show more
88+
const shownTasks = ref(50)
89+
90+
function handleClickShowMore() {
91+
vibrate('success')
92+
shownTasks.value += 50
93+
}
94+
95+
const sortedBy = ref<'updatedAtDesc' | 'updatedAtAsc'>('updatedAtDesc')
96+
97+
function sortByUpdatedAtDesc(a: Task, b: Task) {
98+
const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0
99+
const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0
100+
return bTime - aTime
101+
}
102+
103+
function sortByUpdatedAtAsc(a: Task, b: Task) {
104+
const aTime = a.updatedAt ? new Date(a.updatedAt).getTime() : 0
105+
const bTime = b.updatedAt ? new Date(b.updatedAt).getTime() : 0
106+
return aTime - bTime
107+
}
108+
109+
function chooseSortFunction() {
110+
switch (sortedBy.value) {
111+
case 'updatedAtDesc':
112+
return sortByUpdatedAtDesc
113+
case 'updatedAtAsc':
114+
return sortByUpdatedAtAsc
115+
}
116+
}
117+
118+
const filteredBy = ref<'all' | 'completed'>('all')
119+
120+
function filterByAll() {
121+
return true
122+
}
123+
124+
function filterByCompleted(task: Task) {
125+
return task.completedAt
126+
}
127+
128+
function chooseFilterFunction() {
129+
switch (filteredBy.value) {
130+
case 'all':
131+
return filterByAll
132+
case 'completed':
133+
return filterByCompleted
134+
}
135+
}
136+
137+
const availablePerformers = computed(() => [{
138+
label: 'Все исполнители',
139+
value: '',
140+
avatar: {
141+
src: undefined,
142+
alt: '',
143+
},
144+
onSelect: () => {
145+
selectedPerformer.value = undefined
146+
},
147+
}, ...userStore.staff.map((staff) => ({
148+
label: `${staff.name} ${staff.surname}`,
149+
value: staff.id,
150+
avatar: {
151+
src: staff.avatarUrl ?? undefined,
152+
alt: '',
153+
},
154+
}))])
155+
156+
const selectedPerformer = ref<{ label: string, value: string, avatar: { src: string | undefined, alt: string } } | undefined>()
157+
158+
function filterByPerformer(task: Task) {
159+
return selectedPerformer.value?.value ? task.performerId === selectedPerformer.value.value : true
160+
}
161+
162+
const filteredTasks = computed(() => {
163+
const sorted = taskStore.tasks.toSorted(chooseSortFunction())
164+
const filtered = sorted.filter(chooseFilterFunction()).filter(filterByPerformer)
165+
166+
const show = filtered.slice(0, shownTasks.value)
167+
168+
return {
169+
show,
170+
all: filtered,
171+
canShowMore: filtered.length > show.length,
172+
}
173+
})
174+
</script>

apps/atrium-telegram/app/pages/navigation.vue

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
>
1010
<ActiveCard>
1111
<Section>
12-
<div class="flex flex-row gap-2 items-center">
13-
<UIcon :name="item.icon" />
12+
<div class="flex flex-row gap-2.5 items-center">
13+
<UIcon :name="item.icon" class="size-5 shrink-0" />
1414
<h3 class="text-2xl/6 font-bold">
1515
{{ item.label }}
1616
</h3>
@@ -35,7 +35,7 @@ const items = ref([
3535
{
3636
label: 'Договоры',
3737
to: '/agreement',
38-
icon: 'i-lucide-list-checks',
38+
icon: 'i-lucide-scroll',
3939
onClick: () => vibrate(),
4040
},
4141
{
@@ -44,5 +44,11 @@ const items = ref([
4444
icon: 'i-lucide-store',
4545
onClick: () => vibrate(),
4646
},
47+
{
48+
label: 'Задачи',
49+
to: '/all-tasks',
50+
icon: 'i-lucide-list-checks',
51+
onClick: () => vibrate(),
52+
},
4753
])
4854
</script>

apps/atrium-telegram/app/stores/task.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ type TaskListWithData = TaskList & {
1313

1414
export const useTaskStore = defineStore('task', () => {
1515
const lists = ref<TaskListWithData[]>([])
16+
const tasks = ref<Task[]>([])
1617
const isTodayOnly = ref(false)
1718
const isInitialized = ref(false)
1819

@@ -41,6 +42,32 @@ export const useTaskStore = defineStore('task', () => {
4142
lists.value = data
4243

4344
isInitialized.value = true
45+
46+
await updateCompleted()
47+
} catch (error) {
48+
if (error instanceof Error) {
49+
if (error.message.includes('401')) {
50+
// No session
51+
}
52+
if (error.message.includes('404')) {
53+
// Not found
54+
}
55+
}
56+
}
57+
}
58+
59+
async function updateCompleted() {
60+
try {
61+
const data = await $fetch('/api/task/list/completed', {
62+
headers: {
63+
Authorization: `tma ${initDataRaw.value}`,
64+
},
65+
})
66+
if (!data) {
67+
return
68+
}
69+
70+
tasks.value = data
4471
} catch (error) {
4572
if (error instanceof Error) {
4673
if (error.message.includes('401')) {
@@ -89,6 +116,7 @@ export const useTaskStore = defineStore('task', () => {
89116

90117
return {
91118
lists,
119+
tasks,
92120
isTodayOnly,
93121
isInitialized,
94122

packages/database/src/repository/task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export class Task {
1313
static async findAll() {
1414
return useDatabase().query.tasks.findMany({
1515
orderBy: (tasks, { desc }) => desc(tasks.updatedAt),
16+
limit: 1500,
1617
})
1718
}
1819

0 commit comments

Comments
 (0)