1<template>
2 <div
3 class="flex flex-col w-full h-full rounded-5 p-4"
4 :class="[orientation == 'horizontal' && 'pt-3']"
5 >
6 <slot name="title">
7 <div class="text-ink-gray-5 text-base mb-2">
8 {{ title }}
9 </div>
10 </slot>
11 <div
12 class="flex flex-col gap-2 h-full w-full"
13 v-if="orientation === 'vertical'"
14 >
15 <div class="flex items-end w-full gap-2">
16 <slot name="text">
17 <div
18 class="text-2xl-medium text-center text-ink-gray-8 whitespace-nowrap"
19 >
20 {{ text }}
21 </div>
22 </slot>
23 <div v-if="timelineFilter" class="flex items-center text-sm gap-1">
24 <div class="flex items-center gap-1" :class="percentageChange.color">
25 <span
26 v-if="percentageChange.icon"
27 :class="percentageChange.icon"
28 class="size-4"
29 />
30 <div>{{ percentageChange.value }}%</div>
31 </div>
32 <Dropdown :options="durationOptions">
33 <div
34 class="flex items-center gap-0.5 text-ink-gray-5 hover:text-ink-gray-6 cursor-pointer shrink-0"
35 >
36 <div class="flex gap-1">
37 <span>vs</span>
38 <span>{{ __(currentDuration).toLowerCase() }}</span>
39 </div>
40 <LucideChevronDown class="size-4" />
41 </div>
42 <template #item-label="{ item }">
43 <div
44 class="data-[disabled]:cursor-not-allowed group flex w-full items-center rounded-4 px-2 text-base focus:outline-none focus:bg-surface-gray-3 data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3 whitespace-nowrap text-ink-gray-7 cursor-pointer justify-between"
45 >
46 <span>
47 {{ item.label }}
48 </span>
49 </div>
50 </template>
51 <template #item-suffix="{ item }">
52 <LucideCheck
53 v-if="item.label == __(currentDuration)"
54 class="size-4"
55 />
56 </template>
57 </Dropdown>
58 </div>
59 </div>
60 <slot name="chart">
61 <div v-if="chartConfig" class="w-full h-full">
62 <ECharts :options="chartConfig" class="w-full h-full" />
63 </div>
64 </slot>
65 </div>
66 <div class="flex flex-col gap-2 h-full w-full" v-else>
67 <div class="flex items-end w-full gap-2 justify-between flex-1">
68 <slot name="text">
69 <span
70 class="text-2xl-medium text-center text-ink-gray-8 whitespace-nowrap"
71 >
72 {{ text }}
73 </span>
74 </slot>
75 <slot name="chart">
76 <div v-if="chartConfig" class="h-full max-w-[50%] w-[50%]">
77 <ECharts :options="chartConfig" class="w-full h-full" />
78 </div>
79 </slot>
80 </div>
81 <div v-if="timelineFilter" class="flex items-center text-sm gap-1">
82 <div class="flex items-center gap-1" :class="percentageChange.color">
83 <span
84 v-if="percentageChange.icon"
85 :class="percentageChange.icon"
86 class="size-4"
87 />
88 <div>{{ percentageChange.value }}%</div>
89 </div>
90 <Dropdown :options="durationOptions">
91 <div
92 class="flex items-center gap-0.5 text-ink-gray-5 hover:text-ink-gray-6 cursor-pointer shrink-0"
93 >
94 vs {{ __(currentDuration).toLowerCase() }}
95 <LucideChevronDown class="size-4" />
96 </div>
97 <template #item-label="{ item }">
98 <div
99 class="data-[disabled]:cursor-not-allowed group flex w-full items-center rounded-4 px-2 text-base focus:outline-none focus:bg-surface-gray-3 data-[highlighted]:bg-surface-gray-3 data-[state=open]:bg-surface-gray-3 whitespace-nowrap text-ink-gray-7 cursor-pointer justify-between"
100 >
101 <span>
102 {{ item.label }}
103 </span>
104 </div>
105 </template>
106 <template #item-suffix="{ item }">
107 <LucideCheck
108 v-if="item.label == __(currentDuration)"
109 class="size-4"
110 />
111 </template>
112 </Dropdown>
113 </div>
114 </div>
115 </div>
116</template>
117
118<script setup lang="ts">
119import LucideCheck from "~icons/lucide/check";
120import LucideChevronDown from "~icons/lucide/chevron-down";
121import { __ } from "@/translation";
122import { EChartsOption } from "echarts";
123import { Dropdown } from "frappe-ui";
124import { ECharts } from "frappe-ui/experimental";
125import { computed, type PropType } from "vue";
126
127const props = defineProps({
128 title: {
129 type: String,
130 },
131 text: {
132 type: [Number, String] as PropType<number | string>,
133 default: "",
134 },
135 percentageChange: {
136 type: Object,
137 required: true,
138 },
139 chartConfig: {
140 type: Object as PropType<EChartsOption>,
141 required: true,
142 },
143 currentDuration: {
144 type: String,
145 default: __("Last month"),
146 },
147 orientation: {
148 type: String as PropType<"vertical" | "horizontal">,
149 default: "vertical",
150 },
151 timelineFilter: {
152 type: Boolean,
153 default: true,
154 },
155});
156
157const chartConfig = computed<EChartsOption>(() => props.chartConfig);
158
159const currentDuration = computed(() => props.currentDuration);
160
161const emit = defineEmits(["changeDuration"]);
162
163// the emitted value is the untranslated key: it travels to the API as the
164// period, so only the label goes through __()
165const DURATIONS = ["Last week", "Last month", "Last 3 months"];
166
167const durationOptions = DURATIONS.map((duration) => ({
168 label: __(duration),
169 onClick: () => {
170 if (currentDuration.value !== duration) emit("changeDuration", duration);
171 },
172}));
173</script>
174