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
135 changes: 135 additions & 0 deletions apps/web-app/app/components/DateRangePicker.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<template>
<UPopover :content="{ align: 'start' }" :modal="true">
<UButton
color="neutral"
variant="outline"
icon="i-lucide-calendar"
size="lg"
class="data-[state=open]:bg-elevated group"
>
<span class="truncate">
<template v-if="selected.start">
<template v-if="selected.end">
{{ df.format(selected.start) }} - {{ df.format(selected.end) }}
</template>
<template v-else>
{{ df.format(selected.start) }}
</template>
</template>
<template v-else>
Выберите даты
</template>
</span>

<template #trailing>
<UIcon name="i-lucide-chevron-down" class="shrink-0 text-dimmed size-5 group-data-[state=open]:rotate-180 transition-transform duration-200" />
</template>
</UButton>

<template #content>
<div class="flex items-stretch sm:divide-x divide-default">
<div class="hidden sm:flex flex-col justify-center">
<UButton
v-for="(range, index) in ranges"
:key="index"
:label="range.label"
color="neutral"
variant="ghost"
class="rounded-none px-4"
:class="[isRangeSelected(range) ? 'bg-elevated' : 'hover:bg-elevated/50']"
truncate
@click="selectRange(range)"
/>
</div>

<UCalendar
v-model="calendarRange"
class="p-2"
:number-of-months="2"
range
/>
</div>
</template>
</UPopover>
</template>

<script setup lang="ts">
import type { Range } from '#shared/types'
import { CalendarDate, DateFormatter, getLocalTimeZone, today } from '@internationalized/date'

const df = new DateFormatter('ru-RU', {
dateStyle: 'full',
})

const selected = defineModel<Range>({ required: true })

const ranges = [
{ label: 'Последние 7 дней', days: 7 - 1 },
{ label: 'Последние 14 дней', days: 14 - 1 },
{ label: 'Последние 30 дней', days: 30 - 1 },
{ label: 'Последние 3 месяца', days: 90 - 1 },
{ label: 'Последние 6 месяцев', days: 180 - 1 },
{ label: 'Последний год', days: 365 - 1 },
]

function toCalendarDate(date: Date) {
return new CalendarDate(
date.getFullYear(),
date.getMonth() + 1,
date.getDate(),
)
}

const calendarRange = computed({
get: () => ({
start: selected.value.start ? toCalendarDate(selected.value.start) : undefined,
end: selected.value.end ? toCalendarDate(selected.value.end) : undefined,
}),
set: (newValue: { start: CalendarDate | null, end: CalendarDate | null }) => {
selected.value = {
start: newValue.start ? newValue.start.toDate(getLocalTimeZone()) : new Date(),
end: newValue.end ? newValue.end.toDate(getLocalTimeZone()) : new Date(),
}
},
})
Comment on lines +83 to +94
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix null value handling in calendarRange setter.

The setter logic has an issue: when newValue.start or newValue.end is null, it defaults to new Date(). This means clearing a date selection unexpectedly sets it to the current date instead of keeping it unselected.

Consider this fix to handle null values properly:

  set: (newValue: { start: CalendarDate | null, end: CalendarDate | null }) => {
    selected.value = {
-     start: newValue.start ? newValue.start.toDate(getLocalTimeZone()) : new Date(),
-     end: newValue.end ? newValue.end.toDate(getLocalTimeZone()) : new Date(),
+     start: newValue.start ? newValue.start.toDate(getLocalTimeZone()) : null as any,
+     end: newValue.end ? newValue.end.toDate(getLocalTimeZone()) : null as any,
    }
  },

Or update the Range interface to allow null values if that's the intended behavior.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const calendarRange = computed({
get: () => ({
start: selected.value.start ? toCalendarDate(selected.value.start) : undefined,
end: selected.value.end ? toCalendarDate(selected.value.end) : undefined,
}),
set: (newValue: { start: CalendarDate | null, end: CalendarDate | null }) => {
selected.value = {
start: newValue.start ? newValue.start.toDate(getLocalTimeZone()) : new Date(),
end: newValue.end ? newValue.end.toDate(getLocalTimeZone()) : new Date(),
}
},
})
const calendarRange = computed({
get: () => ({
start: selected.value.start ? toCalendarDate(selected.value.start) : undefined,
end: selected.value.end ? toCalendarDate(selected.value.end) : undefined,
}),
set: (newValue: { start: CalendarDate | null, end: CalendarDate | null }) => {
selected.value = {
start: newValue.start ? newValue.start.toDate(getLocalTimeZone()) : null as any,
end: newValue.end ? newValue.end.toDate(getLocalTimeZone()) : null as any,
}
},
})
🤖 Prompt for AI Agents
In apps/web-app/app/components/DateRangePicker.vue around lines 83 to 94, the
setter for calendarRange incorrectly assigns new Date() when newValue.start or
newValue.end is null, causing cleared dates to reset to the current date. To fix
this, update the setter to assign undefined or null instead of new Date() when
the values are null, preserving the cleared state properly without defaulting to
the current date.


function isRangeSelected(range: { days?: number, months?: number, years?: number }) {
if (!selected.value.start || !selected.value.end) {
return false
}

const currentDate = today(getLocalTimeZone())
let startDate = currentDate.copy()

if (range.days) {
startDate = startDate.subtract({ days: range.days })
} else if (range.months) {
startDate = startDate.subtract({ months: range.months })
} else if (range.years) {
startDate = startDate.subtract({ years: range.years })
}

const selectedStart = toCalendarDate(selected.value.start)
const selectedEnd = toCalendarDate(selected.value.end)

return selectedStart.compare(startDate) === 0 && selectedEnd.compare(currentDate) === 0
}

function selectRange(range: { days?: number, months?: number, years?: number }) {
const endDate = today(getLocalTimeZone())
let startDate = endDate.copy()

if (range.days) {
startDate = startDate.subtract({ days: range.days })
} else if (range.months) {
startDate = startDate.subtract({ months: range.months })
} else if (range.years) {
startDate = startDate.subtract({ years: range.years })
}

selected.value = {
start: startDate.toDate(getLocalTimeZone()),
end: endDate.toDate(getLocalTimeZone()),
}
}
</script>
7 changes: 4 additions & 3 deletions apps/web-app/app/components/chart/KitchenChecks.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
:data="data"
:padding="{ top: 40 }"
:width="width"
class="h-80"
class="h-72"
>
<VisLine
:x="x"
Expand Down Expand Up @@ -103,7 +103,8 @@ const lineDashArray = (_: DataRecord, i: number) => [i === 0 ? undefined : 3]

const total = computed(() => {
const count = data.value.filter((d) => d.averageCheck).length
return Math.floor(data.value.reduce((acc: number, { averageCheck }) => acc + averageCheck, 0) / count)
const avg = data.value.reduce((acc: number, { averageCheck }) => acc + averageCheck, 0)
return avg > 0 && count > 0 ? Math.floor(avg / count) : 0
})

const formatNumber = new Intl.NumberFormat('ru', { style: 'currency', currency: 'RUB', maximumFractionDigits: 0 }).format
Expand All @@ -124,7 +125,7 @@ function xTicks(i: number) {
return formatDate(data.value[i].date)
}

const template = (d: DataRecord) => `<strong>${formatDate(d.date)}, ${format(d.date, 'eeee', { locale: ru })}</strong><br> Средний чек: ${formatNumber(d.averageCheck)}<br> Среднее по сети: ${formatNumber(d.commonAverageCheck)}`
const template = (d: DataRecord) => `<strong>${formatDate(d.date)}, ${format(d.date, 'eeee', { locale: ru })}</strong><br> Средний чек: ${formatNumber(d.averageCheck)}<br> Средний по сети: ${formatNumber(d.commonAverageCheck)}`
</script>

<style scoped>
Expand Down
8 changes: 5 additions & 3 deletions apps/web-app/app/components/chart/KitchenRevenue.client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
:data="data"
:padding="{ top: 40 }"
:width="width"
class="h-80"
class="h-72"
>
<VisLine
:x="x"
Expand Down Expand Up @@ -56,13 +56,14 @@ type DataRecord = {
date: Date
total: number
checks: number
averageCheck: number
commonTotal: number
}

const { period, range, values } = defineProps<{
period: Period
range: Range
values: { date: string, total: number, checks: number, commonTotal: number }[]
values: { date: string, total: number, checks: number, averageCheck: number, commonTotal: number }[]
}>()

const cardRef = useTemplateRef<HTMLElement | null>('cardRef')
Expand All @@ -86,6 +87,7 @@ watch([() => period, () => range, () => values], () => {
date,
total: value?.total ?? 0,
checks: value?.checks ?? 0,
averageCheck: value?.averageCheck ?? 0,
commonTotal: value?.commonTotal ?? 0,
}
})
Expand Down Expand Up @@ -121,7 +123,7 @@ function xTicks(i: number) {
return formatDate(data.value[i].date)
}

const template = (d: DataRecord) => `<strong>${formatDate(d.date)}, ${format(d.date, 'eeee', { locale: ru })}</strong><br> ${d.checks} ${pluralizationRu(d.checks, ['чек', 'чека', 'чеков'])}<br> Выручка: ${formatNumber(d.total)}<br> Среднее по сети: ${formatNumber(d.commonTotal)}`
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)}`
</script>

<style scoped>
Expand Down
9 changes: 7 additions & 2 deletions apps/web-app/app/pages/kitchen/[id]/finance.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<template>
<Content>
<div>
<DateRangePicker v-model="range" />
</div>

<ChartKitchenRevenue
:period="period"
:range="range"
Expand All @@ -22,9 +26,10 @@ const { params } = useRoute('kitchen-id')

const { data } = useFetch(`/api/kitchen/id/${params.id}/revenue`)

const today = new Date()
const range = shallowRef<Range>({
start: sub(new Date(), { days: 89 }),
end: new Date(),
start: sub(today, { days: 14 - 1 }),
end: today,
})
const period = ref<Period>('daily')
</script>