Skip to content

Commit 0fb64ff

Browse files
authored
feat: weekly mode for charts (#42)
* feat: weekly mode for charts * chore: avg check
1 parent ee80625 commit 0fb64ff

9 files changed

Lines changed: 406 additions & 71 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ const linkItems = computed(() => [
130130
active: route.path.startsWith('/head/task'),
131131
},
132132
{
133-
label: 'Показатели сети',
133+
label: t('app.network.metrics'),
134134
to: '/network',
135135
icon: 'i-lucide-square-kanban',
136136
active: route.path.startsWith('/network'),
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<template>
2+
<USelect
3+
v-model="model"
4+
:items="periods"
5+
variant="outline"
6+
size="lg"
7+
class="min-w-32 data-[state=open]:bg-elevated"
8+
:ui="{ trailingIcon: 'group-data-[state=open]:rotate-180 transition-transform duration-200' }"
9+
/>
10+
</template>
11+
12+
<script setup lang="ts">
13+
import type { Period, Range } from '#shared/types'
14+
import { eachDayOfInterval } from 'date-fns'
15+
16+
const props = defineProps<{
17+
range: Range
18+
}>()
19+
20+
const model = defineModel<Period>({ required: true })
21+
22+
const days = computed(() => eachDayOfInterval(props.range))
23+
24+
const periods = computed<{ label: string, value: Period }[]>(() => {
25+
if (days.value.length <= 14) {
26+
return [
27+
{
28+
label: 'По дням',
29+
value: 'daily',
30+
},
31+
]
32+
}
33+
34+
if (days.value.length <= 90) {
35+
return [
36+
{
37+
label: 'По дням',
38+
value: 'daily',
39+
},
40+
{
41+
label: 'По неделям',
42+
value: 'weekly',
43+
},
44+
]
45+
}
46+
47+
return [
48+
{
49+
label: 'По неделям',
50+
value: 'weekly',
51+
},
52+
// {
53+
// label: 'По месяцам',
54+
// value: 'monthly'
55+
// }
56+
]
57+
})
58+
59+
// Ensure the model value is always a valid period
60+
watch(periods, () => {
61+
if (!periods.value.some((p) => p.value === model.value) && periods.value[0]?.value) {
62+
model.value = periods.value[0]?.value
63+
}
64+
})
65+
</script>

apps/web-app/app/components/chart/KitchenChecks.client.vue

Lines changed: 84 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<template #header>
44
<div>
55
<p class="text-xs text-muted uppercase mb-1.5">
6-
Средний чек за {{ data.length }} {{ pluralizationRu(data.length, ['день', 'дня', 'дней']) }}
6+
{{ formatTotalLabel('Средний чек', data.length) }}
77
</p>
88
<p class="text-3xl text-highlighted font-semibold">
99
{{ formatNumber(total) }}
@@ -38,7 +38,7 @@
3838

3939
<VisCrosshair
4040
color="var(--ui-info)"
41-
:template="template"
41+
:template="formatTemplate"
4242
/>
4343

4444
<VisTooltip />
@@ -49,11 +49,12 @@
4949
<script setup lang="ts">
5050
import type { Period, Range } from '#shared/types'
5151
import { VisArea, VisAxis, VisCrosshair, VisLine, VisTooltip, VisXYContainer } from '@unovis/vue'
52-
import { eachDayOfInterval, eachMonthOfInterval, eachWeekOfInterval, format } from 'date-fns'
52+
import { addDays, eachDayOfInterval, eachMonthOfInterval, eachWeekOfInterval, format } from 'date-fns'
5353
import { ru } from 'date-fns/locale'
5454
5555
type DataRecord = {
56-
date: Date
56+
start: Date
57+
end: Date
5758
checks: number
5859
averageCheck: number
5960
commonAverageCheck: number
@@ -75,22 +76,72 @@ const data = ref<DataRecord[]>([])
7576
watch([() => period, () => range, () => values, () => metrics], () => {
7677
const dates = ({
7778
daily: eachDayOfInterval,
78-
weekly: eachWeekOfInterval,
79+
weekly: () => eachWeekOfInterval(range, { weekStartsOn: 1 }),
7980
monthly: eachMonthOfInterval,
8081
} as Record<Period, typeof eachDayOfInterval>)[period](range)
8182
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-
const metric = metrics.find((d) => d.date.startsWith(dateStr))
83+
if (period === 'daily') {
84+
data.value = dates.map((date) => {
85+
const dateStr = format(date, 'yyyy-MM-dd')
86+
const value = values.find((d) => d.date.startsWith(dateStr))
87+
const metric = metrics.find((d) => d.date.startsWith(dateStr))
88+
89+
return {
90+
start: date,
91+
end: date,
92+
checks: value?.checks ?? 0,
93+
averageCheck: value?.averageCheck ?? 0,
94+
commonAverageCheck: metric?.averageCheck ?? 0,
95+
}
96+
})
97+
}
8698
87-
return {
88-
date,
89-
checks: value?.checks ?? 0,
90-
averageCheck: value?.averageCheck ?? 0,
91-
commonAverageCheck: metric?.averageCheck ?? 0,
99+
if (period === 'weekly') {
100+
const points = []
101+
102+
for (const date of dates) {
103+
const dateTo = addDays(date, 6)
104+
const allDates = eachDayOfInterval({
105+
start: date,
106+
end: dateTo,
107+
})
108+
109+
let daysWithValues = 0
110+
let daysWithMetrics = 0
111+
112+
let checks = 0
113+
let averageCheck = 0
114+
let commonAverageCheck = 0
115+
116+
for (const d of allDates) {
117+
// All in one point
118+
const dateStr = format(d, 'yyyy-MM-dd')
119+
const value = values.find((d) => d.date.startsWith(dateStr))
120+
const metric = metrics.find((d) => d.date.startsWith(dateStr))
121+
122+
checks += value?.checks ?? 0
123+
124+
if (value?.averageCheck) {
125+
averageCheck += value.averageCheck
126+
daysWithValues++
127+
}
128+
if (metric?.averageCheck) {
129+
commonAverageCheck += metric.averageCheck
130+
daysWithMetrics++
131+
}
132+
}
133+
134+
points.push({
135+
start: date,
136+
end: dateTo,
137+
checks,
138+
averageCheck: daysWithValues > 0 ? averageCheck / daysWithValues : 0,
139+
commonAverageCheck: daysWithMetrics > 0 ? commonAverageCheck / daysWithMetrics : 0,
140+
})
92141
}
93-
})
142+
143+
data.value = points
144+
}
94145
}, { immediate: true })
95146
96147
const x = (_: DataRecord, i: number) => i
@@ -119,15 +170,31 @@ function formatDate(date: Date): string {
119170
})[period]
120171
}
121172
173+
function formatTotalLabel(label: string, value: number): string {
174+
return ({
175+
daily: `${label} за ${value} ${pluralizationRu(value, ['день', 'дня', 'дней'])}`,
176+
weekly: `${label} за ${value} ${pluralizationRu(value, ['неделю', 'недели', 'недель'])}`,
177+
monthly: `${label} за ${value} ${pluralizationRu(value, ['месяц', 'месяца', 'месяцев'])}`,
178+
})[period]
179+
}
180+
122181
function xTicks(i: number) {
123182
if (i === 0 || i === data.value.length - 1 || !data.value[i]) {
124183
return ''
125184
}
126185
127-
return formatDate(data.value[i].date)
186+
return formatDate(data.value[i].start)
128187
}
129188
130-
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.commonAverageCheck)}`
189+
function formatTemplate(d: DataRecord) {
190+
const title = ({
191+
daily: `${formatDate(d.start)}, ${format(d.start, 'eeee', { locale: ru })}`,
192+
weekly: `${formatDate(d.start)} - ${formatDate(d.end)}`,
193+
monthly: `${formatDate(d.start)} - ${formatDate(d.end)}`,
194+
})[period]
195+
196+
return `<strong>${title}</strong><br> ${d.checks} ${pluralizationRu(d.checks, ['чек', 'чека', 'чеков'])}, средний ${formatNumber(d.averageCheck)}<br> Средний по сети: ${formatNumber(d.commonAverageCheck)}`
197+
}
131198
</script>
132199

133200
<style scoped>

apps/web-app/app/components/chart/KitchenRevenue.client.vue

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<template #header>
44
<div>
55
<p class="text-xs text-muted uppercase mb-1.5">
6-
Выручка за {{ data.length }} {{ pluralizationRu(data.length, ['день', 'дня', 'дней']) }}
6+
{{ formatTotalLabel('Выручка', data.length) }}
77
</p>
88
<p class="text-3xl text-highlighted font-semibold">
99
{{ formatNumber(total) }}
@@ -38,7 +38,7 @@
3838

3939
<VisCrosshair
4040
color="var(--ui-secondary)"
41-
:template="template"
41+
:template="formatTemplate"
4242
/>
4343

4444
<VisTooltip />
@@ -49,11 +49,12 @@
4949
<script setup lang="ts">
5050
import type { Period, Range } from '#shared/types'
5151
import { VisArea, VisAxis, VisCrosshair, VisLine, VisTooltip, VisXYContainer } from '@unovis/vue'
52-
import { eachDayOfInterval, eachMonthOfInterval, eachWeekOfInterval, format } from 'date-fns'
52+
import { addDays, eachDayOfInterval, eachMonthOfInterval, eachWeekOfInterval, format } from 'date-fns'
5353
import { ru } from 'date-fns/locale'
5454
5555
type DataRecord = {
56-
date: Date
56+
start: Date
57+
end: Date
5758
total: number
5859
checks: number
5960
averageCheck: number
@@ -76,23 +77,72 @@ const data = ref<DataRecord[]>([])
7677
watch([() => period, () => range, () => values, () => metrics], () => {
7778
const dates = ({
7879
daily: eachDayOfInterval,
79-
weekly: eachWeekOfInterval,
80+
weekly: () => eachWeekOfInterval(range, { weekStartsOn: 1 }),
8081
monthly: eachMonthOfInterval,
8182
} as Record<Period, typeof eachDayOfInterval>)[period](range)
8283
83-
data.value = dates.map((date) => {
84-
const dateStr = format(date, 'yyyy-MM-dd')
85-
const value = values.find((d) => d.date.startsWith(dateStr))
86-
const metric = metrics.find((d) => d.date.startsWith(dateStr))
87-
88-
return {
89-
date,
90-
total: value?.total ?? 0,
91-
checks: value?.checks ?? 0,
92-
averageCheck: value?.averageCheck ?? 0,
93-
commonTotal: metric?.averageTotal ?? 0,
84+
if (period === 'daily') {
85+
data.value = dates.map((date) => {
86+
const dateStr = format(date, 'yyyy-MM-dd')
87+
const value = values.find((d) => d.date.startsWith(dateStr))
88+
const metric = metrics.find((d) => d.date.startsWith(dateStr))
89+
90+
return {
91+
start: date,
92+
end: date,
93+
total: value?.total ?? 0,
94+
checks: value?.checks ?? 0,
95+
averageCheck: value?.averageCheck ?? 0,
96+
commonTotal: metric?.averageTotal ?? 0,
97+
}
98+
})
99+
}
100+
101+
if (period === 'weekly') {
102+
const points = []
103+
104+
for (const date of dates) {
105+
const dateTo = addDays(date, 6)
106+
const allDates = eachDayOfInterval({
107+
start: date,
108+
end: dateTo,
109+
})
110+
111+
let daysWithValues = 0
112+
113+
let total = 0
114+
let checks = 0
115+
let averageCheck = 0
116+
let commonTotal = 0
117+
118+
for (const d of allDates) {
119+
// All in one point
120+
const dateStr = format(d, 'yyyy-MM-dd')
121+
const value = values.find((d) => d.date.startsWith(dateStr))
122+
const metric = metrics.find((d) => d.date.startsWith(dateStr))
123+
124+
total += value?.total ?? 0
125+
checks += value?.checks ?? 0
126+
commonTotal += metric?.averageTotal ?? 0
127+
128+
if (value?.averageCheck) {
129+
averageCheck += value.averageCheck
130+
daysWithValues++
131+
}
132+
}
133+
134+
points.push({
135+
start: date,
136+
end: dateTo,
137+
total,
138+
checks,
139+
averageCheck: daysWithValues > 0 ? averageCheck / daysWithValues : 0,
140+
commonTotal,
141+
})
94142
}
95-
})
143+
144+
data.value = points
145+
}
96146
}, { immediate: true })
97147
98148
const x = (_: DataRecord, i: number) => i
@@ -117,15 +167,31 @@ function formatDate(date: Date): string {
117167
})[period]
118168
}
119169
170+
function formatTotalLabel(label: string, value: number): string {
171+
return ({
172+
daily: `${label} за ${value} ${pluralizationRu(value, ['день', 'дня', 'дней'])}`,
173+
weekly: `${label} за ${value} ${pluralizationRu(value, ['неделю', 'недели', 'недель'])}`,
174+
monthly: `${label} за ${value} ${pluralizationRu(value, ['месяц', 'месяца', 'месяцев'])}`,
175+
})[period]
176+
}
177+
120178
function xTicks(i: number) {
121179
if (i === 0 || i === data.value.length - 1 || !data.value[i]) {
122180
return ''
123181
}
124182
125-
return formatDate(data.value[i].date)
183+
return formatDate(data.value[i].start)
126184
}
127185
128-
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.commonTotal)}`
186+
function formatTemplate(d: DataRecord) {
187+
const title = ({
188+
daily: `${formatDate(d.start)}, ${format(d.start, 'eeee', { locale: ru })}`,
189+
weekly: `${formatDate(d.start)} - ${formatDate(d.end)}`,
190+
monthly: `${formatDate(d.start)} - ${formatDate(d.end)}`,
191+
})[period]
192+
193+
return `<strong>${title}</strong><br> ${d.checks} ${pluralizationRu(d.checks, ['чек', 'чека', 'чеков'])}, средний ${formatNumber(d.averageCheck)}<br> Выручка: ${formatNumber(d.total)}<br> Средняя по сети: ${formatNumber(d.commonTotal)}`
194+
}
129195
</script>
130196

131197
<style scoped>

0 commit comments

Comments
 (0)