1<template>
2 <div
3 class="transition-all animate-fade-in duration-300 relative"
4 :class="{ 'animate-pulse': loading }"
5 >
6 <slot name="number-cards">
7 <div v-if="showVariant('number-cards')" :class="numberCardsGridClass">
8 <!-- Vertical: title → value → subtitle stacked top→bottom -->
9 <div
10 v-if="orientation === 'vertical'"
11 v-for="i in numberCardsCount"
12 :key="`card-${i}`"
13 class="border rounded-5 p-4 space-y-3 max-h-[140px] min-h-[114px]"
14 >
15 <div class="h-3 w-1/2 bg-surface-gray-3 rounded-4" />
16 <div class="h-7 w-2/3 bg-surface-gray-1 rounded-4" />
17 <div class="h-3 w-1/2 bg-surface-gray-1 rounded-4" />
18 </div>
19 <!-- Horizontal: title on top, value + mini chart side-by-side, percentage row -->
20 <div
21 v-else
22 v-for="i in numberCardsCount"
23 :key="`card-h-${i}`"
24 class="border rounded-5 flex flex-col p-4 pt-3 gap-2"
25 :style="{ minHeight: height }"
26 >
27 <div class="h-4 w-1/2 bg-surface-gray-3 rounded-4" />
28 <div class="flex items-end w-full gap-2 justify-between flex-1">
29 <div class="h-7 w-10 bg-surface-gray-2 rounded-4" />
30 <div
31 class="h-full max-w-[50%] w-[50%] flex items-end justify-end gap-1"
32 >
33 <div
34 v-for="(h, idx) in barHeights"
35 :key="idx"
36 :style="{ height: `${h}%` }"
37 class="w-2 bg-surface-gray-3 rounded-t-1"
38 />
39 </div>
40 </div>
41 <div class="h-3 w-1/3 bg-surface-gray-2 rounded-4" />
42 </div>
43 </div>
44 </slot>
45 <slot name="bar-chart">
46 <div
47 v-if="showVariant('bar-chart')"
48 :class="
49 barChartCount !== 1 ? 'grid grid-cols-1 md:grid-cols-2 gap-4' : ''
50 "
51 >
52 <div
53 v-for="i in barChartCount"
54 :key="`chart-${i}`"
55 class="border rounded-5 min-h-[300px] p-4 space-y-3"
56 >
57 <template v-if="getEmptyState(i).chartTitle">
58 <div class="text-ink-gray-8 text-p-base-medium">
59 {{ __(getEmptyState(i).chartTitle) }}
60 </div>
61 <div
62 v-if="getEmptyState(i).chartSubtitle"
63 class="text-ink-gray-6 text-p-sm"
64 >
65 {{ __(getEmptyState(i).chartSubtitle) }}
66 </div>
67 </template>
68 <template v-else>
69 <div class="h-4 w-1/3 bg-surface-gray-3 rounded-4" />
70 <div class="h-4 w-1/4 bg-surface-gray-1 rounded-4" />
71 </template>
72
73 <div
74 class="h-64 w-full rounded-4 flex items-center justify-center relative overflow-hidden"
75 >
76 <div
77 class="absolute inset-0 flex items-end justify-center px-[33px] py-[23px] gap-10"
78 >
79 <div
80 class="absolute inset-x-[33px] inset-y-[23px] left-[50px] flex flex-col justify-between pointer-events-none"
81 >
82 <div
83 v-for="j in 5"
84 :key="j"
85 class="border-t border-dotted border-[var(--ink-gray-3)]"
86 />
87 </div>
88 <div
89 v-for="(height, idx) in barHeights"
90 :key="idx"
91 :style="{ height: `${height}%` }"
92 class="w-[30px] rounded-1 bg-[var(--surface-gray-3)] shrink-0 z-10"
93 />
94 <!-- x-y labels -->
95
96 <div
97 class="absolute left-0 hidden md:flex flex-col justify-between inset-x-[33px] inset-y-[23px]"
98 >
99 <div
100 v-for="j in 5"
101 :key="j"
102 class="h-[10px] w-[28px] rounded-1 bg-[var(--surface-gray-3)]"
103 />
104 </div>
105 <div class="absolute flex justify-center bottom-0 gap-10">
106 <div
107 v-for="j in barHeights.length"
108 :key="j"
109 class="h-[10px] w-[30px] rounded-1 bg-[var(--surface-gray-3)]"
110 />
111 </div>
112 </div>
113
114 <!-- empty state -->
115 <div
116 v-if="showVariant('empty-state')"
117 :style="{
118 backgroundImage:
119 'radial-gradient(ellipse at center, var(--surface-base) 10%, color-mix(in srgb, var(--surface-base) 90%, transparent) 25%, transparent 70%)',
120 }"
121 class="rounded-7 p-6 w-2/3 text-center pointer-events-auto space-y-0.5 relative z-10 bottom-4.5"
122 >
123 <div
124 class="relative z-10 text-ink-gray-7 text-center text-p-base-medium leading-[1.15]"
125 >
126 {{ __(getEmptyState(i).title) }}
127 </div>
128 <div
129 class="relative z-10 text-ink-gray-6 text-center text-p-base"
130 >
131 {{
132 hasAppliedFilter
133 ? __(getEmptyState(i).filterMessage)
134 : __(getEmptyState(i).message)
135 }}
136 </div>
137 </div>
138 </div>
139 </div>
140 </div>
141 </slot>
142 </div>
143</template>
144
145<script setup>
146import { useSlots } from "vue";
147
148const props = defineProps({
149 variants: {
150 type: Array,
151 default: () => ["number-cards", "bar-chart"],
152 },
153 numberCardsCount: { type: Number, default: 5 },
154 // "vertical": title → value → subtitle stacked (default, KPI tiles).
155 // "horizontal": title on top, value + mini chart side-by-side, percentage row (compact chart cards).
156 orientation: {
157 type: String,
158 default: "vertical",
159 validator: (v) => ["vertical", "horizontal"].includes(v),
160 },
161 numberCardsGridClass: {
162 type: String,
163 default: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4",
164 },
165 // Min-height of each card in the horizontal number-cards layout (any CSS length).
166 height: {
167 type: String,
168 default: "160px",
169 },
170 barChartCount: { type: Number, default: 4 },
171 barHeights: { type: Array, default: () => [63, 95, 44, 37, 70] },
172 hasAppliedFilter: { type: Boolean, default: false },
173 loading: { type: Boolean, default: false },
174 emptyStates: {
175 type: Array,
176 default: () => [],
177 },
178});
179
180const slots = useSlots();
181
182const defaultEmptyStateMsg = {
183 title: "No tickets found",
184 message:
185 "Dashboard charts will appear here once you start receiving or creating tickets.",
186 filterMessage:
187 "Based on the selected filters no tickets found. Try adjusting the date range or filters applied.",
188};
189
190function getEmptyState(index) {
191 const entry = props.emptyStates[index - 1] ?? {};
192 return { ...defaultEmptyStateMsg, ...entry };
193}
194function showVariant(slotName) {
195 return !slots[slotName] && props.variants.includes(slotName);
196}
197</script>
198