Skip to content

Commit 0801f09

Browse files
authored
feat: activity schedules (#59)
* feat: activity schedules * chore: update
1 parent fecd2b0 commit 0801f09

13 files changed

Lines changed: 336 additions & 1 deletion

File tree

apps/web-app/app/app.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ const ticket = useTicketStore()
5050
const notification = useNotificationStore()
5151
const post = usePostStore()
5252
const print = usePrintStore()
53+
const activity = useActivityStore()
5354
5455
await Promise.all([
5556
client.update(),
@@ -61,6 +62,7 @@ await Promise.all([
6162
notification.update(),
6263
post.update(),
6364
print.update(),
65+
activity.update(),
6466
])
6567
6668
// Auto Update Online
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<template>
2+
<ActiveCard>
3+
<div class="shrink-0 w-full h-full flex flex-col gap-3 items-start justify-start">
4+
<UIcon name="i-lucide-calendar-fold" class="size-14 text-primary" />
5+
6+
<h3 class="text-xl md:text-xl/6 font-semibold">
7+
{{ schedule.title }}
8+
</h3>
9+
10+
<p v-if="schedule.description" class="text-base/5">
11+
{{ schedule.description }}
12+
</p>
13+
</div>
14+
</ActiveCard>
15+
</template>
16+
17+
<script setup lang="ts">
18+
import type { ActivitySchedule } from '@roll-stack/database'
19+
20+
defineProps<{ schedule: ActivitySchedule }>()
21+
</script>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
<template>
2+
<UCard>
3+
<div class="shrink-0 w-full flex flex-col gap-3.5">
4+
<UIcon name="i-lucide-calendar-range" class="size-14 text-primary" />
5+
6+
<h3 class="text-xl md:text-xl/6 font-semibold">
7+
{{ item.title }}
8+
</h3>
9+
10+
<h4 class="flex flex-row gap-1.5 items-start md:text-base/5 font-semibold text-secondary">
11+
<UIcon name="i-lucide-clock" class="shrink-0 size-5" /> {{ item.period }}
12+
</h4>
13+
14+
<p class="text-base/5 whitespace-pre-wrap">
15+
{{ item.description }}
16+
</p>
17+
18+
<UAccordion
19+
:items="accordionItems"
20+
:ui="{
21+
trigger: 'text-base/5',
22+
body: 'text-base/5',
23+
}"
24+
/>
25+
26+
<div class="flex flex-row flex-wrap gap-1.5">
27+
<UBadge
28+
v-for="channel in item.communicationChannels"
29+
:key="channel"
30+
:label="getCommunicationChannelData(channel).label"
31+
:icon="getCommunicationChannelData(channel).icon"
32+
color="neutral"
33+
variant="outline"
34+
:ui="{
35+
leadingIcon: 'text-dimmed/75',
36+
}"
37+
/>
38+
39+
<UBadge
40+
v-for="tag in tags"
41+
:key="tag"
42+
:label="tag"
43+
color="neutral"
44+
variant="subtle"
45+
/>
46+
</div>
47+
</div>
48+
</UCard>
49+
</template>
50+
51+
<script setup lang="ts">
52+
import type { AccordionItem } from '@nuxt/ui'
53+
import type { ActivityScheduleItem } from '@roll-stack/database'
54+
55+
type CommunicationChannel = ActivityScheduleItem['communicationChannels'][number]
56+
57+
const { item } = defineProps<{ item: ActivityScheduleItem }>()
58+
59+
const accordionItems = ref<AccordionItem[]>([
60+
{
61+
label: 'Условия',
62+
icon: 'i-lucide-trending-up-down',
63+
content: item.terms ?? '',
64+
},
65+
])
66+
67+
const tags = [
68+
'Постоянная акция',
69+
'Опционально',
70+
]
71+
72+
function getCommunicationChannelData(type: CommunicationChannel) {
73+
switch (type) {
74+
case 'vk':
75+
return {
76+
icon: 'simple-icons:vk',
77+
label: 'Вконтакте',
78+
}
79+
case 'telegram':
80+
return {
81+
icon: 'simple-icons:telegram',
82+
label: 'Telegram',
83+
}
84+
case 'website':
85+
return {
86+
icon: 'i-lucide-app-window',
87+
label: 'Вебсайт',
88+
}
89+
case 'uds_feed':
90+
return {
91+
icon: 'i-lucide-newspaper',
92+
label: 'UDS рассылка',
93+
}
94+
case 'table_tent':
95+
return {
96+
icon: 'i-lucide-layout-dashboard',
97+
label: 'Тейбл тент',
98+
}
99+
case 'mobile_app':
100+
return {
101+
icon: 'i-lucide-smartphone',
102+
label: 'Мобильное приложение',
103+
}
104+
case 'store_administrator':
105+
return {
106+
icon: 'i-lucide-circle-user-round',
107+
label: 'Администратор магазина',
108+
}
109+
case 'contextual_advertising':
110+
return {
111+
icon: 'i-lucide-megaphone',
112+
label: 'Контекстная реклама',
113+
}
114+
default:
115+
return {
116+
icon: 'i-lucide-info',
117+
label: type,
118+
}
119+
}
120+
}
121+
</script>

apps/web-app/app/components/Navigation.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ const menuItems = computed(() => [
8787
icon: 'i-lucide-scroll',
8888
active: route.path.startsWith('/agreement'),
8989
},
90+
{
91+
label: t('app.menu.activity-schedules'),
92+
to: '/activity',
93+
icon: 'i-lucide-calendar-fold',
94+
active: route.path.startsWith('/activity'),
95+
},
9096
{
9197
label: 'Отзывы клиентов',
9298
icon: 'i-lucide-star',
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<template>
2+
<Header :title="schedule?.title ?? t('app.menu.activity-schedule')" />
3+
4+
<Content>
5+
<div class="grid grid-cols-1 gap-4 md:gap-6 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-3">
6+
<ActivityScheduleItemCard
7+
v-for="item in schedule?.items"
8+
:key="item.id"
9+
:item="item"
10+
/>
11+
</div>
12+
</Content>
13+
</template>
14+
15+
<script setup lang="ts">
16+
const { t } = useI18n()
17+
const { params } = useRoute('activity-id')
18+
19+
const activityStore = useActivityStore()
20+
const schedule = computed(() => activityStore.schedules.find((s) => s.id === params.id))
21+
22+
useHead({
23+
title: t('app.menu.activity-schedule'),
24+
})
25+
</script>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<template>
2+
<Header :title="t('app.menu.activity-schedules')" />
3+
4+
<Content>
5+
<div class="mb-20 grid grid-cols-1 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-2 gap-4.5">
6+
<NuxtLink
7+
v-for="schedule in activityStore.schedules"
8+
:key="schedule.id"
9+
:to="`/activity/${schedule.id}`"
10+
>
11+
<ActivityScheduleCard :schedule="schedule" />
12+
</NuxtLink>
13+
</div>
14+
</Content>
15+
</template>
16+
17+
<script setup lang="ts">
18+
const { t } = useI18n()
19+
20+
const activityStore = useActivityStore()
21+
22+
useHead({
23+
title: t('app.menu.activity-schedules'),
24+
})
25+
</script>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { ActivitySchedule, ActivityScheduleItem } from '@roll-stack/database'
2+
3+
type ActivityScheduleWithData = ActivitySchedule & {
4+
items: ActivityScheduleItem[]
5+
}
6+
7+
export const useActivityStore = defineStore('activity', () => {
8+
const schedules = ref<ActivityScheduleWithData[]>([])
9+
10+
async function update() {
11+
try {
12+
const data = await $fetch('/api/activity/schedule/list')
13+
if (!data) {
14+
return
15+
}
16+
17+
schedules.value = data
18+
} catch (error) {
19+
if (error instanceof Error) {
20+
if (error.message.includes('404')) {
21+
// Not found
22+
}
23+
}
24+
}
25+
}
26+
27+
return {
28+
schedules,
29+
30+
update,
31+
}
32+
})

apps/web-app/i18n/locales/ru-RU.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,9 @@
129129
"tickets": "Тикеты",
130130
"staff": "Сотрудники",
131131
"agreements": "Договоры",
132-
"prestige": "Престиж"
132+
"prestige": "Престиж",
133+
"activity-schedule": "Регламент активностей",
134+
"activity-schedules": "Регламенты активностей"
133135
},
134136
"product": {
135137
"available-for-purchase": "Доступен для покупки",
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { repository } from '@roll-stack/database'
2+
3+
export default defineEventHandler(async () => {
4+
return repository.activity.listSchedules()
5+
})
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type { ActivityScheduleDraft } from '../types'
2+
import { eq, sql } from 'drizzle-orm'
3+
import { useDatabase } from '../database'
4+
import { activitySchedules } from '../tables'
5+
6+
export class Activity {
7+
static async findSchedule(id: string) {
8+
return useDatabase().query.activitySchedules.findFirst({
9+
where: (activity, { eq }) => eq(activity.id, id),
10+
with: {
11+
items: true,
12+
},
13+
})
14+
}
15+
16+
static async listSchedules() {
17+
return useDatabase().query.activitySchedules.findMany({
18+
with: {
19+
items: true,
20+
},
21+
})
22+
}
23+
24+
static async createSchedule(data: ActivityScheduleDraft) {
25+
const [schedule] = await useDatabase().insert(activitySchedules).values(data).returning()
26+
return schedule
27+
}
28+
29+
static async updateSchedule(id: string, data: ActivityScheduleDraft) {
30+
const [schedule] = await useDatabase()
31+
.update(activitySchedules)
32+
.set({
33+
...data,
34+
updatedAt: sql`now()`,
35+
})
36+
.where(eq(activitySchedules.id, id))
37+
.returning()
38+
return schedule
39+
}
40+
41+
static async deleteSchedule(id: string) {
42+
return useDatabase().delete(activitySchedules).where(eq(activitySchedules.id, id))
43+
}
44+
}

0 commit comments

Comments
 (0)