Skip to content

Commit 7c68c86

Browse files
committed
feat: network metrics page
1 parent 8e1deaf commit 7c68c86

4 files changed

Lines changed: 320 additions & 0 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,12 @@ const linkItems = computed(() => [
129129
icon: 'i-lucide-list-checks',
130130
active: route.path.startsWith('/head/task'),
131131
},
132+
{
133+
label: 'Показатели сети',
134+
to: '/network',
135+
icon: 'i-lucide-square-kanban',
136+
active: route.path.startsWith('/network'),
137+
},
132138
{
133139
label: 'SMM',
134140
to: '/post',
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<template>
2+
<UCard ref="cardRef" :ui="{ root: 'overflow-visible', body: '!px-0 !pt-0 !pb-3' }">
3+
<template #header>
4+
<div>
5+
<p class="text-xs text-muted uppercase mb-1.5">
6+
Средний чек за {{ data.length }} {{ pluralizationRu(data.length, ['день', 'дня', 'дней']) }}
7+
</p>
8+
<p class="text-3xl text-highlighted font-semibold">
9+
{{ formatNumber(total) }}
10+
</p>
11+
</div>
12+
</template>
13+
14+
<VisXYContainer
15+
:data="data"
16+
:padding="{ top: 40 }"
17+
:width="width"
18+
class="h-72"
19+
>
20+
<VisLine
21+
:x="x"
22+
:y="y"
23+
:color="color"
24+
:line-dash-array="lineDashArray"
25+
/>
26+
<VisArea
27+
:x="x"
28+
:y="yArea"
29+
color="var(--ui-info)"
30+
:opacity="0.1"
31+
/>
32+
33+
<VisAxis
34+
type="x"
35+
:x="x"
36+
:tick-format="xTicks"
37+
/>
38+
39+
<VisCrosshair
40+
color="var(--ui-info)"
41+
:template="template"
42+
/>
43+
44+
<VisTooltip />
45+
</VisXYContainer>
46+
</UCard>
47+
</template>
48+
49+
<script setup lang="ts">
50+
import type { Period, Range } from '#shared/types'
51+
import { VisArea, VisAxis, VisCrosshair, VisLine, VisTooltip, VisXYContainer } from '@unovis/vue'
52+
import { eachDayOfInterval, eachMonthOfInterval, eachWeekOfInterval, format } from 'date-fns'
53+
import { ru } from 'date-fns/locale'
54+
55+
type DataRecord = {
56+
date: Date
57+
checks: number
58+
averageCheck: number
59+
}
60+
61+
const { period, range, values } = defineProps<{
62+
period: Period
63+
range: Range
64+
values: { date: string, checks: number, averageCheck: number }[]
65+
}>()
66+
67+
const cardRef = useTemplateRef<HTMLElement | null>('cardRef')
68+
69+
const { width } = useElementSize(cardRef)
70+
71+
const data = ref<DataRecord[]>([])
72+
73+
watch([() => period, () => range, () => values], () => {
74+
const dates = ({
75+
daily: eachDayOfInterval,
76+
weekly: eachWeekOfInterval,
77+
monthly: eachMonthOfInterval,
78+
} as Record<Period, typeof eachDayOfInterval>)[period](range)
79+
80+
data.value = dates.map((date) => {
81+
const dateStr = format(date, 'yyyy-MM-dd')
82+
const value = values.find((d) => d.date.startsWith(dateStr))
83+
84+
return {
85+
date,
86+
checks: value?.checks ?? 0,
87+
averageCheck: value?.averageCheck ?? 0,
88+
}
89+
})
90+
}, { immediate: true })
91+
92+
const x = (_: DataRecord, i: number) => i
93+
const y = (d: DataRecord) => d.averageCheck
94+
const yArea = (d: DataRecord) => d.averageCheck
95+
96+
const color = (_: DataRecord, i: number) => ['var(--ui-info)', 'var(--ui-info)'][i]
97+
const lineDashArray = (_: DataRecord, i: number) => [i === 0 ? undefined : 3]
98+
99+
const total = computed(() => {
100+
const count = data.value.filter((d) => d.averageCheck).length
101+
const avg = data.value.reduce((acc: number, { averageCheck }) => acc + averageCheck, 0)
102+
return avg > 0 && count > 0 ? Math.floor(avg / count) : 0
103+
})
104+
105+
const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
106+
107+
function formatDate(date: Date): string {
108+
return ({
109+
daily: format(date, 'd MMMM', { locale: ru }),
110+
weekly: format(date, 'd MMMM', { locale: ru }),
111+
monthly: format(date, 'MMMM yyy', { locale: ru }),
112+
})[period]
113+
}
114+
115+
function xTicks(i: number) {
116+
if (i === 0 || i === data.value.length - 1 || !data.value[i]) {
117+
return ''
118+
}
119+
120+
return formatDate(data.value[i].date)
121+
}
122+
123+
const template = (d: DataRecord) => `<strong>${formatDate(d.date)}, ${format(d.date, 'eeee', { locale: ru })}</strong><br> ${d.checks} ${pluralizationRu(d.checks, ['чек', 'чека', 'чеков'])}, средний ${formatNumber(d.averageCheck)}`
124+
</script>
125+
126+
<style scoped>
127+
.unovis-xy-container {
128+
--vis-crosshair-line-stroke-color: var(--ui-info);
129+
--vis-crosshair-circle-stroke-color: var(--ui-bg);
130+
131+
--vis-axis-grid-color: var(--ui-border);
132+
--vis-axis-tick-color: var(--ui-border);
133+
--vis-axis-tick-label-color: var(--ui-text-dimmed);
134+
135+
--vis-tooltip-background-color: var(--ui-bg);
136+
--vis-tooltip-border-color: var(--ui-border);
137+
--vis-tooltip-text-color: var(--ui-text-highlighted);
138+
}
139+
</style>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<template>
2+
<UCard ref="cardRef" :ui="{ root: 'overflow-visible', body: '!px-0 !pt-0 !pb-3' }">
3+
<template #header>
4+
<div>
5+
<p class="text-xs text-muted uppercase mb-1.5">
6+
Выручка за {{ data.length }} {{ pluralizationRu(data.length, ['день', 'дня', 'дней']) }}
7+
</p>
8+
<p class="text-3xl text-highlighted font-semibold">
9+
{{ formatNumber(total) }}
10+
</p>
11+
</div>
12+
</template>
13+
14+
<VisXYContainer
15+
:data="data"
16+
:padding="{ top: 40 }"
17+
:width="width"
18+
class="h-72"
19+
>
20+
<VisLine
21+
:x="x"
22+
:y="y"
23+
:color="color"
24+
:line-dash-array="lineDashArray"
25+
/>
26+
<VisArea
27+
:x="x"
28+
:y="yArea"
29+
color="var(--ui-secondary)"
30+
:opacity="0.1"
31+
/>
32+
33+
<VisAxis
34+
type="x"
35+
:x="x"
36+
:tick-format="xTicks"
37+
/>
38+
39+
<VisCrosshair
40+
color="var(--ui-secondary)"
41+
:template="template"
42+
/>
43+
44+
<VisTooltip />
45+
</VisXYContainer>
46+
</UCard>
47+
</template>
48+
49+
<script setup lang="ts">
50+
import type { Period, Range } from '#shared/types'
51+
import { VisArea, VisAxis, VisCrosshair, VisLine, VisTooltip, VisXYContainer } from '@unovis/vue'
52+
import { eachDayOfInterval, eachMonthOfInterval, eachWeekOfInterval, format } from 'date-fns'
53+
import { ru } from 'date-fns/locale'
54+
55+
type DataRecord = {
56+
date: Date
57+
total: number
58+
checks: number
59+
averageTotal: number
60+
averageCheck: number
61+
}
62+
63+
const { period, range, values } = defineProps<{
64+
period: Period
65+
range: Range
66+
values: { date: string, total: number, checks: number, averageTotal: number, averageCheck: number }[]
67+
}>()
68+
69+
const cardRef = useTemplateRef<HTMLElement | null>('cardRef')
70+
71+
const { width } = useElementSize(cardRef)
72+
73+
const data = ref<DataRecord[]>([])
74+
75+
watch([() => period, () => range, () => values], () => {
76+
const dates = ({
77+
daily: eachDayOfInterval,
78+
weekly: eachWeekOfInterval,
79+
monthly: eachMonthOfInterval,
80+
} as Record<Period, typeof eachDayOfInterval>)[period](range)
81+
82+
data.value = dates.map((date) => {
83+
const dateStr = format(date, 'yyyy-MM-dd')
84+
const value = values.find((d) => d.date.startsWith(dateStr))
85+
86+
return {
87+
date,
88+
total: value?.total ?? 0,
89+
checks: value?.checks ?? 0,
90+
averageCheck: value?.averageCheck ?? 0,
91+
averageTotal: value?.averageTotal ?? 0,
92+
}
93+
})
94+
}, { immediate: true })
95+
96+
const x = (_: DataRecord, i: number) => i
97+
const y = [
98+
(d: DataRecord) => d.total,
99+
(d: DataRecord) => d.averageTotal,
100+
]
101+
const yArea = (d: DataRecord) => d.total
102+
103+
const color = (_: DataRecord, i: number) => ['var(--ui-secondary)', 'var(--ui-secondary)'][i]
104+
const lineDashArray = (_: DataRecord, i: number) => [i === 0 ? undefined : 3]
105+
106+
const total = computed(() => data.value.reduce((acc: number, { total }) => acc + total, 0))
107+
108+
const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
109+
110+
function formatDate(date: Date): string {
111+
return ({
112+
daily: format(date, 'd MMMM', { locale: ru }),
113+
weekly: format(date, 'd MMMM', { locale: ru }),
114+
monthly: format(date, 'MMMM yyy', { locale: ru }),
115+
})[period]
116+
}
117+
118+
function xTicks(i: number) {
119+
if (i === 0 || i === data.value.length - 1 || !data.value[i]) {
120+
return ''
121+
}
122+
123+
return formatDate(data.value[i].date)
124+
}
125+
126+
const template = (d: DataRecord) => `<strong>${formatDate(d.date)}, ${format(d.date, 'eeee', { locale: ru })}</strong><br> ${d.checks} ${pluralizationRu(d.checks, ['чек', 'чека', 'чеков'])}, средний ${formatNumber(d.averageCheck)}<br> Выручка: ${formatNumber(d.total)}<br> Средняя у кухни: ${formatNumber(d.averageTotal)}`
127+
</script>
128+
129+
<style scoped>
130+
.unovis-xy-container {
131+
--vis-crosshair-line-stroke-color: var(--ui-secondary);
132+
--vis-crosshair-circle-stroke-color: var(--ui-bg);
133+
134+
--vis-axis-grid-color: var(--ui-border);
135+
--vis-axis-tick-color: var(--ui-border);
136+
--vis-axis-tick-label-color: var(--ui-text-dimmed);
137+
138+
--vis-tooltip-background-color: var(--ui-bg);
139+
--vis-tooltip-border-color: var(--ui-border);
140+
--vis-tooltip-text-color: var(--ui-text-highlighted);
141+
}
142+
</style>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<template>
2+
<Content>
3+
<div>
4+
<DateRangePicker v-model="range" />
5+
</div>
6+
7+
<ChartNetworkRevenue
8+
:period="period"
9+
:range="range"
10+
:values="metrics ?? []"
11+
/>
12+
13+
<ChartNetworkChecks
14+
:period="period"
15+
:range="range"
16+
:values="metrics ?? []"
17+
/>
18+
</Content>
19+
</template>
20+
21+
<script setup lang="ts">
22+
import type { Period, Range } from '#shared/types'
23+
import { sub } from 'date-fns'
24+
25+
const { data: metrics } = useFetch('/api/network/metrics')
26+
27+
const today = new Date()
28+
const range = shallowRef<Range>({
29+
start: sub(today, { days: 30 - 1 }),
30+
end: today,
31+
})
32+
const period = ref<Period>('daily')
33+
</script>

0 commit comments

Comments
 (0)