@htlkg/components 0.0.11 → 0.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js → AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js} +26 -29
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-BhnWQ-b0.js.map +1 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -1
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -1
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -1
- package/dist/components.css +4 -4
- package/dist/composables/index.js +23 -22
- package/dist/data/index.js +10 -10
- package/dist/{filterHelpers-DgRyoYSa.js → filterHelpers-DpHSlTuh.js} +11 -11
- package/dist/filterHelpers-DpHSlTuh.js.map +1 -0
- package/dist/index-QK97OdqQ.js.map +1 -1
- package/dist/index.js +34 -33
- package/dist/navigation/index.js +1 -1
- package/dist/{useAdminPage-GhgXp0x8.js → useAdminPage-AgWRvw6o.js} +150 -26
- package/dist/useAdminPage-AgWRvw6o.js.map +1 -0
- package/package.json +3 -3
- package/src/composables/index.ts +1 -0
- package/src/composables/useJsonForm.test.ts +272 -0
- package/src/composables/useJsonForm.ts +261 -0
- package/src/composables/useModal.test.ts +264 -0
- package/src/composables/useModal.ts +54 -8
- package/src/data/Chart/index.ts +2 -0
- package/src/data/DataList/index.ts +1 -0
- package/src/data/{DataTable.vue → DataTable/DataTable.vue} +2 -2
- package/src/data/DataTable/index.ts +8 -0
- package/src/data/SearchableSelect/index.ts +1 -0
- package/src/data/Table/index.ts +1 -0
- package/src/data/index.ts +5 -15
- package/src/domain/BrandCard/index.ts +1 -0
- package/src/domain/BrandSelector/index.ts +1 -0
- package/src/domain/ProductBadge/index.ts +1 -0
- package/src/domain/UserAvatar/index.ts +1 -0
- package/src/domain/index.ts +4 -4
- package/src/forms/DateRange/index.ts +2 -0
- package/src/forms/JsonSchemaForm/index.ts +1 -0
- package/src/forms/index.ts +2 -3
- package/src/navigation/{AdminWrapper.vue → AdminWrapper/AdminWrapper.vue} +41 -30
- package/src/navigation/AdminWrapper/index.ts +1 -0
- package/src/navigation/Breadcrumbs/index.ts +1 -0
- package/src/navigation/Stepper/index.ts +2 -0
- package/src/navigation/Tabs/index.ts +2 -0
- package/src/navigation/index.ts +4 -6
- package/src/overlays/Alert/index.ts +1 -0
- package/src/overlays/Drawer/index.ts +1 -0
- package/src/overlays/Modal/index.ts +1 -0
- package/src/overlays/Notification/index.ts +1 -0
- package/src/overlays/index.ts +4 -4
- package/src/patterns/DASHBOARD_PAGE.md +642 -0
- package/src/patterns/DETAIL_PAGE.md +446 -0
- package/src/patterns/FORM_PAGE.md +439 -0
- package/src/patterns/LIST_PAGE.md +340 -0
- package/src/patterns/PAGE_PATTERNS.md +110 -0
- package/src/patterns/WIZARD_PAGE.md +733 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +0 -1
- package/dist/filterHelpers-DgRyoYSa.js.map +0 -1
- package/dist/useAdminPage-GhgXp0x8.js.map +0 -1
- package/src/data/Table.vue +0 -295
- /package/src/data/{Chart.demo.vue → Chart/Chart.demo.vue} +0 -0
- /package/src/data/{Chart.md → Chart/Chart.md} +0 -0
- /package/src/data/{Chart.vue → Chart/Chart.vue} +0 -0
- /package/src/data/{DataList.md → DataList/DataList.md} +0 -0
- /package/src/data/{DataList.test.ts → DataList/DataList.test.ts} +0 -0
- /package/src/data/{DataList.vue → DataList/DataList.vue} +0 -0
- /package/src/data/{SearchableSelect.md → SearchableSelect/SearchableSelect.md} +0 -0
- /package/src/data/{SearchableSelect.vue → SearchableSelect/SearchableSelect.vue} +0 -0
- /package/src/data/{Table.demo.vue → Table/Table.demo.vue} +0 -0
- /package/src/data/{Table.md → Table/Table.md} +0 -0
- /package/src/data/{Table.property.test.ts → Table/Table.property.test.ts} +0 -0
- /package/src/data/{Table.test.ts → Table/Table.test.ts} +0 -0
- /package/src/data/{Table.unit.test.ts → Table/Table.unit.test.ts} +0 -0
- /package/src/domain/{BrandCard.md → BrandCard/BrandCard.md} +0 -0
- /package/src/domain/{BrandCard.vue → BrandCard/BrandCard.vue} +0 -0
- /package/src/domain/{BrandSelector.md → BrandSelector/BrandSelector.md} +0 -0
- /package/src/domain/{BrandSelector.vue → BrandSelector/BrandSelector.vue} +0 -0
- /package/src/domain/{ProductBadge.md → ProductBadge/ProductBadge.md} +0 -0
- /package/src/domain/{ProductBadge.vue → ProductBadge/ProductBadge.vue} +0 -0
- /package/src/domain/{UserAvatar.md → UserAvatar/UserAvatar.md} +0 -0
- /package/src/domain/{UserAvatar.vue → UserAvatar/UserAvatar.vue} +0 -0
- /package/src/forms/{DateRange.demo.vue → DateRange/DateRange.demo.vue} +0 -0
- /package/src/forms/{DateRange.md → DateRange/DateRange.md} +0 -0
- /package/src/forms/{DateRange.vue → DateRange/DateRange.vue} +0 -0
- /package/src/forms/{JsonSchemaForm.demo.vue → JsonSchemaForm/JsonSchemaForm.demo.vue} +0 -0
- /package/src/forms/{JsonSchemaForm.md → JsonSchemaForm/JsonSchemaForm.md} +0 -0
- /package/src/forms/{JsonSchemaForm.property.test.ts → JsonSchemaForm/JsonSchemaForm.property.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.test.ts → JsonSchemaForm/JsonSchemaForm.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.unit.test.ts → JsonSchemaForm/JsonSchemaForm.unit.test.ts} +0 -0
- /package/src/forms/{JsonSchemaForm.vue → JsonSchemaForm/JsonSchemaForm.vue} +0 -0
- /package/src/navigation/{Breadcrumbs.demo.vue → Breadcrumbs/Breadcrumbs.demo.vue} +0 -0
- /package/src/navigation/{Breadcrumbs.md → Breadcrumbs/Breadcrumbs.md} +0 -0
- /package/src/navigation/{Breadcrumbs.test.ts → Breadcrumbs/Breadcrumbs.test.ts} +0 -0
- /package/src/navigation/{Breadcrumbs.vue → Breadcrumbs/Breadcrumbs.vue} +0 -0
- /package/src/navigation/{Stepper.demo.vue → Stepper/Stepper.demo.vue} +0 -0
- /package/src/navigation/{Stepper.md → Stepper/Stepper.md} +0 -0
- /package/src/navigation/{Stepper.vue → Stepper/Stepper.vue} +0 -0
- /package/src/navigation/{Tabs.demo.vue → Tabs/Tabs.demo.vue} +0 -0
- /package/src/navigation/{Tabs.md → Tabs/Tabs.md} +0 -0
- /package/src/navigation/{Tabs.test.ts → Tabs/Tabs.test.ts} +0 -0
- /package/src/navigation/{Tabs.vue → Tabs/Tabs.vue} +0 -0
- /package/src/overlays/{Alert.demo.vue → Alert/Alert.demo.vue} +0 -0
- /package/src/overlays/{Alert.md → Alert/Alert.md} +0 -0
- /package/src/overlays/{Alert.test.ts → Alert/Alert.test.ts} +0 -0
- /package/src/overlays/{Alert.vue → Alert/Alert.vue} +0 -0
- /package/src/overlays/{Drawer.md → Drawer/Drawer.md} +0 -0
- /package/src/overlays/{Drawer.test.ts → Drawer/Drawer.test.ts} +0 -0
- /package/src/overlays/{Drawer.vue → Drawer/Drawer.vue} +0 -0
- /package/src/overlays/{Modal.demo.vue → Modal/Modal.demo.vue} +0 -0
- /package/src/overlays/{Modal.md → Modal/Modal.md} +0 -0
- /package/src/overlays/{Modal.test.ts → Modal/Modal.test.ts} +0 -0
- /package/src/overlays/{Modal.vue → Modal/Modal.vue} +0 -0
- /package/src/overlays/{Notification.md → Notification/Notification.md} +0 -0
- /package/src/overlays/{Notification.test.ts → Notification/Notification.test.ts} +0 -0
- /package/src/overlays/{Notification.vue → Notification/Notification.vue} +0 -0
|
@@ -0,0 +1,642 @@
|
|
|
1
|
+
# Dashboard Page Pattern
|
|
2
|
+
|
|
3
|
+
Display key metrics, statistics, and charts with date range filtering and tabbed navigation.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ AdminLayout │
|
|
10
|
+
├─────────────────────────────────────────────────────────────┤
|
|
11
|
+
│ Page Header (title + breadcrumbs) │
|
|
12
|
+
├─────────────────────────────────────────────────────────────┤
|
|
13
|
+
│ Date Range Filter (right-aligned) │
|
|
14
|
+
├─────────────────────────────────────────────────────────────┤
|
|
15
|
+
│ Tabs (Overview | Campaigns | Revenue | Contacts | ...) │
|
|
16
|
+
├─────────────────────────────────────────────────────────────┤
|
|
17
|
+
│ Stats Grid (1-4 columns) │
|
|
18
|
+
│ ├── Stat 1: Active Campaigns │ Stat 2: Total Contacts │
|
|
19
|
+
│ └── Stat 3: Reservations │ Stat 4: Revenue │
|
|
20
|
+
├─────────────────────────────────────────────────────────────┤
|
|
21
|
+
│ Section Title (e.g., "Email Performance") │
|
|
22
|
+
├─────────────────────────────────────────────────────────────┤
|
|
23
|
+
│ Stats Grid (Secondary metrics) │
|
|
24
|
+
│ ├── Email Delivered │ Open Rate │ CTR │ Deliverability │
|
|
25
|
+
├─────────────────────────────────────────────────────────────┤
|
|
26
|
+
│ Charts Grid (2 columns) │
|
|
27
|
+
│ ├── Chart 1: Database Growth │ Chart 2: Database Health │
|
|
28
|
+
│ └── Chart 3: Engagement │ Chart 4: Revenue │
|
|
29
|
+
└─────────────────────────────────────────────────────────────┘
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## CRM Template Reference
|
|
33
|
+
|
|
34
|
+
Based on: `ui/src/components/Templates/CRM/Dashboard.vue`
|
|
35
|
+
|
|
36
|
+
```vue
|
|
37
|
+
<!-- CRM Template Structure (raw @hotelinking/ui) -->
|
|
38
|
+
<uiWrapper :sidebar :topbar>
|
|
39
|
+
<uiViewHeader :pages="pages" title="Dashboard - Overview" :loading="loadingHeader" />
|
|
40
|
+
|
|
41
|
+
<!-- Date Range Filter -->
|
|
42
|
+
<div class="flex justify-end items-center mb-4 w-full">
|
|
43
|
+
<uiDateRange
|
|
44
|
+
id="charts-date-range"
|
|
45
|
+
:literals="{ from: 'From', to: 'To', search: 'Search' }"
|
|
46
|
+
:values="dateRangeValues"
|
|
47
|
+
:loading="loadingDateRange"
|
|
48
|
+
@uiDateRangeButtonClicked="handleDateRangeSearch"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- Tabs -->
|
|
53
|
+
<uiTabs :tabs="tabs" @tabClicked="handleTabClick" />
|
|
54
|
+
|
|
55
|
+
<!-- Primary Stats Grid -->
|
|
56
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 my-8">
|
|
57
|
+
<uiStats
|
|
58
|
+
v-for="(stat, index) in stats"
|
|
59
|
+
:key="stat.id"
|
|
60
|
+
:item="stat"
|
|
61
|
+
:loading="loadingStats[index]"
|
|
62
|
+
@statClick="handleStatClick"
|
|
63
|
+
/>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
<!-- Section Title -->
|
|
67
|
+
<uiSectionTitle title="Email Performance Overview" />
|
|
68
|
+
|
|
69
|
+
<!-- Secondary Stats Grid -->
|
|
70
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
71
|
+
<uiStats
|
|
72
|
+
v-for="(stat, index) in emailStats"
|
|
73
|
+
:key="stat.id"
|
|
74
|
+
:item="stat"
|
|
75
|
+
:loading="loadingEmailStats[index]"
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- Charts Grid -->
|
|
80
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
81
|
+
<uiChart
|
|
82
|
+
id="database-growth"
|
|
83
|
+
title="Database Growth"
|
|
84
|
+
type="area"
|
|
85
|
+
:series="series"
|
|
86
|
+
:options="options"
|
|
87
|
+
:loading="loadingChart"
|
|
88
|
+
/>
|
|
89
|
+
<!-- More charts... -->
|
|
90
|
+
</div>
|
|
91
|
+
</uiWrapper>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Enhanced Pattern (with @htlkg/*)
|
|
95
|
+
|
|
96
|
+
### Astro Page
|
|
97
|
+
|
|
98
|
+
```astro
|
|
99
|
+
---
|
|
100
|
+
// src/pages/[brandId]/admin/dashboard.astro
|
|
101
|
+
import Layout from '@/layouts/Layout.astro';
|
|
102
|
+
import DashboardView from '@/components/admin/DashboardView.vue';
|
|
103
|
+
import { getDashboardData } from '@/services/dashboard';
|
|
104
|
+
|
|
105
|
+
export const prerender = false;
|
|
106
|
+
|
|
107
|
+
const { brandId } = Astro.params;
|
|
108
|
+
const initialData = await getDashboardData(brandId);
|
|
109
|
+
|
|
110
|
+
const layoutProps = {
|
|
111
|
+
title: 'Dashboard',
|
|
112
|
+
breadcrumbs: [
|
|
113
|
+
{ name: 'Dashboard', current: true },
|
|
114
|
+
],
|
|
115
|
+
};
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
<Layout {...layoutProps}>
|
|
119
|
+
<DashboardView client:load brandId={brandId} initialData={initialData} />
|
|
120
|
+
</Layout>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Vue Component with useStats
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<!-- src/components/admin/DashboardView.vue -->
|
|
127
|
+
<script setup lang="ts">
|
|
128
|
+
import { ref, computed } from 'vue';
|
|
129
|
+
import { Chart } from '@htlkg/components/data';
|
|
130
|
+
import { DateRange } from '@htlkg/components/forms';
|
|
131
|
+
import { useStats, useTabs, countStat, sumStat, percentageStat } from '@htlkg/components/composables';
|
|
132
|
+
import { uiStats, uiSectionTitle } from '@hotelinking/ui';
|
|
133
|
+
import {
|
|
134
|
+
MegaphoneIcon,
|
|
135
|
+
UsersIcon,
|
|
136
|
+
CalendarIcon,
|
|
137
|
+
CurrencyDollarIcon,
|
|
138
|
+
EnvelopeIcon,
|
|
139
|
+
EyeIcon,
|
|
140
|
+
CursorArrowRaysIcon,
|
|
141
|
+
CheckCircleIcon,
|
|
142
|
+
} from '@heroicons/vue/24/outline';
|
|
143
|
+
|
|
144
|
+
interface DashboardData {
|
|
145
|
+
campaigns: Campaign[];
|
|
146
|
+
contacts: Contact[];
|
|
147
|
+
reservations: Reservation[];
|
|
148
|
+
emailStats: EmailStats;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface Props {
|
|
152
|
+
brandId: string;
|
|
153
|
+
initialData: DashboardData;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const props = defineProps<Props>();
|
|
157
|
+
|
|
158
|
+
// Date range state
|
|
159
|
+
const dateRange = ref({
|
|
160
|
+
from: '',
|
|
161
|
+
to: new Date().toISOString().slice(0, 16),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Tabs
|
|
165
|
+
const { tabs, activeTab, setActiveTab } = useTabs({
|
|
166
|
+
tabs: [
|
|
167
|
+
{ id: 'overview', label: 'Overview' },
|
|
168
|
+
{ id: 'campaigns', label: 'Campaigns' },
|
|
169
|
+
{ id: 'revenue', label: 'Revenue' },
|
|
170
|
+
{ id: 'contacts', label: 'Contacts' },
|
|
171
|
+
{ id: 'insights', label: 'Insights' },
|
|
172
|
+
],
|
|
173
|
+
initialTab: 'overview',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Data refs (would come from data hooks in real app)
|
|
177
|
+
const campaigns = ref(props.initialData.campaigns);
|
|
178
|
+
const contacts = ref(props.initialData.contacts);
|
|
179
|
+
const reservations = ref(props.initialData.reservations);
|
|
180
|
+
const loading = ref(false);
|
|
181
|
+
|
|
182
|
+
// Primary Stats with useStats
|
|
183
|
+
const { statsWithLoading: primaryStats } = useStats({
|
|
184
|
+
data: computed(() => ({
|
|
185
|
+
campaigns: campaigns.value,
|
|
186
|
+
contacts: contacts.value,
|
|
187
|
+
reservations: reservations.value,
|
|
188
|
+
})),
|
|
189
|
+
loading,
|
|
190
|
+
definitions: [
|
|
191
|
+
{
|
|
192
|
+
id: 'active-campaigns',
|
|
193
|
+
name: 'Active Campaigns',
|
|
194
|
+
icon: MegaphoneIcon,
|
|
195
|
+
color: 'green',
|
|
196
|
+
compute: (data) => data.campaigns.filter(c => c.status === 'active').length,
|
|
197
|
+
computeChange: () => '+12.5%',
|
|
198
|
+
computeChangeType: () => 'increase',
|
|
199
|
+
showFooter: true,
|
|
200
|
+
actionText: 'View all',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
id: 'total-contacts',
|
|
204
|
+
name: 'Total Contacts',
|
|
205
|
+
icon: UsersIcon,
|
|
206
|
+
color: 'green',
|
|
207
|
+
compute: (data) => data.contacts.length,
|
|
208
|
+
computeChange: () => '+8.2%',
|
|
209
|
+
computeChangeType: () => 'increase',
|
|
210
|
+
showFooter: true,
|
|
211
|
+
actionText: 'View all',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'active-reservations',
|
|
215
|
+
name: 'Active Reservations',
|
|
216
|
+
icon: CalendarIcon,
|
|
217
|
+
color: 'green',
|
|
218
|
+
compute: (data) => data.reservations.filter(r => r.status === 'active').length,
|
|
219
|
+
computeChange: () => '-3.1%',
|
|
220
|
+
computeChangeType: () => 'decrease',
|
|
221
|
+
showFooter: true,
|
|
222
|
+
actionText: 'View all',
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
id: 'revenue-month',
|
|
226
|
+
name: 'Revenue This Month',
|
|
227
|
+
icon: CurrencyDollarIcon,
|
|
228
|
+
color: 'green',
|
|
229
|
+
compute: (data) => {
|
|
230
|
+
const total = data.reservations.reduce((sum, r) => sum + r.amount, 0);
|
|
231
|
+
return `$${total.toLocaleString()}`;
|
|
232
|
+
},
|
|
233
|
+
computeChange: () => '+20.1%',
|
|
234
|
+
computeChangeType: () => 'increase',
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
staggerDelay: 100,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Email Stats
|
|
241
|
+
const emailStats = ref([
|
|
242
|
+
{
|
|
243
|
+
id: 'emails-delivered',
|
|
244
|
+
name: 'Emails Delivered',
|
|
245
|
+
stat: '2.4M',
|
|
246
|
+
icon: EnvelopeIcon,
|
|
247
|
+
color: 'green',
|
|
248
|
+
change: '+156K this month',
|
|
249
|
+
changeType: 'increase',
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
id: 'average-open-rate',
|
|
253
|
+
name: 'Average Open Rate',
|
|
254
|
+
stat: '24.8%',
|
|
255
|
+
icon: EyeIcon,
|
|
256
|
+
color: 'green',
|
|
257
|
+
change: '+2.1% vs last month',
|
|
258
|
+
changeType: 'increase',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'average-ctr',
|
|
262
|
+
name: 'Average CTR',
|
|
263
|
+
stat: '3.2%',
|
|
264
|
+
icon: CursorArrowRaysIcon,
|
|
265
|
+
color: 'green',
|
|
266
|
+
change: '+0.4% vs last month',
|
|
267
|
+
changeType: 'increase',
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'deliverability-rate',
|
|
271
|
+
name: 'Deliverability Rate',
|
|
272
|
+
stat: '98.1%',
|
|
273
|
+
icon: CheckCircleIcon,
|
|
274
|
+
color: 'green',
|
|
275
|
+
change: '+0.3% vs last month',
|
|
276
|
+
changeType: 'increase',
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
// Chart data
|
|
281
|
+
const databaseGrowthSeries = ref([
|
|
282
|
+
{
|
|
283
|
+
name: 'Total Contacts',
|
|
284
|
+
data: [1200, 1350, 1500, 1680, 1850, 2100, 2350, 2600, 2850, 3100, 3350, 3600],
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
name: 'Active Users',
|
|
288
|
+
data: [800, 920, 1050, 1180, 1320, 1500, 1680, 1850, 2020, 2200, 2380, 2550],
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
const chartOptions = {
|
|
293
|
+
xaxis: {
|
|
294
|
+
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
// Handlers
|
|
299
|
+
function handleDateRangeChange(dates: { from: string; to: string }) {
|
|
300
|
+
dateRange.value = dates;
|
|
301
|
+
// Refetch data with new date range
|
|
302
|
+
fetchDashboardData();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function handleStatClick(statId: string) {
|
|
306
|
+
if (statId === 'active-campaigns') {
|
|
307
|
+
window.location.href = `/${props.brandId}/admin/campaigns`;
|
|
308
|
+
} else if (statId === 'total-contacts') {
|
|
309
|
+
window.location.href = `/${props.brandId}/admin/contacts`;
|
|
310
|
+
} else if (statId === 'active-reservations') {
|
|
311
|
+
window.location.href = `/${props.brandId}/admin/reservations`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function fetchDashboardData() {
|
|
316
|
+
loading.value = true;
|
|
317
|
+
try {
|
|
318
|
+
// Fetch data with date range
|
|
319
|
+
const data = await getDashboardData(props.brandId, dateRange.value);
|
|
320
|
+
campaigns.value = data.campaigns;
|
|
321
|
+
contacts.value = data.contacts;
|
|
322
|
+
reservations.value = data.reservations;
|
|
323
|
+
} finally {
|
|
324
|
+
loading.value = false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
</script>
|
|
328
|
+
|
|
329
|
+
<template>
|
|
330
|
+
<!-- Date Range Filter -->
|
|
331
|
+
<div class="flex justify-end items-center mb-4 w-full">
|
|
332
|
+
<DateRange
|
|
333
|
+
id="dashboard-date-range"
|
|
334
|
+
:values="dateRange"
|
|
335
|
+
:loading="loading"
|
|
336
|
+
@search="handleDateRangeChange"
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
<!-- Tabs -->
|
|
341
|
+
<div class="border-b border-gray-200 mb-4">
|
|
342
|
+
<nav class="-mb-px flex space-x-8">
|
|
343
|
+
<button
|
|
344
|
+
v-for="tab in tabs"
|
|
345
|
+
:key="tab.id"
|
|
346
|
+
:class="[
|
|
347
|
+
'py-4 px-1 border-b-2 font-medium text-sm',
|
|
348
|
+
activeTab === tab.id
|
|
349
|
+
? 'border-indigo-500 text-indigo-600'
|
|
350
|
+
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
351
|
+
]"
|
|
352
|
+
@click="setActiveTab(tab.id)"
|
|
353
|
+
>
|
|
354
|
+
{{ tab.label }}
|
|
355
|
+
</button>
|
|
356
|
+
</nav>
|
|
357
|
+
</div>
|
|
358
|
+
|
|
359
|
+
<!-- Primary Stats Grid -->
|
|
360
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 my-8">
|
|
361
|
+
<uiStats
|
|
362
|
+
v-for="stat in primaryStats"
|
|
363
|
+
:key="stat.id"
|
|
364
|
+
:item="stat"
|
|
365
|
+
:loading="stat.loading"
|
|
366
|
+
@statClick="handleStatClick"
|
|
367
|
+
/>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<!-- Email Performance Section -->
|
|
371
|
+
<uiSectionTitle title="Email Performance Overview" />
|
|
372
|
+
|
|
373
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
|
374
|
+
<uiStats
|
|
375
|
+
v-for="stat in emailStats"
|
|
376
|
+
:key="stat.id"
|
|
377
|
+
:item="stat"
|
|
378
|
+
:loading="loading"
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<!-- Charts Grid -->
|
|
383
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
384
|
+
<Chart
|
|
385
|
+
id="database-growth"
|
|
386
|
+
title="Database Growth"
|
|
387
|
+
type="area"
|
|
388
|
+
:series="databaseGrowthSeries"
|
|
389
|
+
:options="chartOptions"
|
|
390
|
+
:loading="loading"
|
|
391
|
+
/>
|
|
392
|
+
<Chart
|
|
393
|
+
id="database-status"
|
|
394
|
+
title="Database Health"
|
|
395
|
+
type="bar"
|
|
396
|
+
:series="databaseStatusSeries"
|
|
397
|
+
:options="chartOptions"
|
|
398
|
+
:stacked="true"
|
|
399
|
+
:loading="loading"
|
|
400
|
+
/>
|
|
401
|
+
<Chart
|
|
402
|
+
id="monthly-engagement"
|
|
403
|
+
title="Monthly Engagement Metrics"
|
|
404
|
+
type="area"
|
|
405
|
+
:series="monthlyEngagementSeries"
|
|
406
|
+
:options="chartOptions"
|
|
407
|
+
:loading="loading"
|
|
408
|
+
/>
|
|
409
|
+
<Chart
|
|
410
|
+
id="revenue-booking"
|
|
411
|
+
title="Revenue and Booking Performance"
|
|
412
|
+
type="area"
|
|
413
|
+
:series="revenueBookingSeries"
|
|
414
|
+
:options="chartOptions"
|
|
415
|
+
:loading="loading"
|
|
416
|
+
/>
|
|
417
|
+
</div>
|
|
418
|
+
</template>
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Key Components
|
|
422
|
+
|
|
423
|
+
### useStats Composable
|
|
424
|
+
|
|
425
|
+
Derives statistics from reactive data with staggered loading animation.
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
import { useStats, countStat, sumStat, percentageStat } from '@htlkg/components/composables';
|
|
429
|
+
|
|
430
|
+
const { stats, loadingStates, statsWithLoading, allLoaded, refreshAnimation, getStat } = useStats({
|
|
431
|
+
// Reactive data source
|
|
432
|
+
data: campaigns,
|
|
433
|
+
// Loading state
|
|
434
|
+
loading: campaignsLoading,
|
|
435
|
+
// Stat definitions
|
|
436
|
+
definitions: [
|
|
437
|
+
{
|
|
438
|
+
id: 'active',
|
|
439
|
+
name: 'Active Campaigns',
|
|
440
|
+
icon: CheckCircleIcon,
|
|
441
|
+
color: 'green',
|
|
442
|
+
compute: (items) => items.filter(c => c.status === 'active').length,
|
|
443
|
+
computeChange: (items, prev) => `+${items.length - (prev?.length ?? 0)}`,
|
|
444
|
+
computeChangeType: () => 'increase',
|
|
445
|
+
showFooter: true,
|
|
446
|
+
actionText: 'View all',
|
|
447
|
+
},
|
|
448
|
+
],
|
|
449
|
+
// Animation settings
|
|
450
|
+
staggerDelay: 100, // Delay between each stat reveal
|
|
451
|
+
initialDelay: 0, // Delay before first stat reveals
|
|
452
|
+
// Optional: Previous data for computing changes
|
|
453
|
+
previousData: previousCampaigns,
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Stat Definition Helpers
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// Simple count stat
|
|
461
|
+
countStat<Campaign>(
|
|
462
|
+
'active',
|
|
463
|
+
'Active Campaigns',
|
|
464
|
+
CheckCircleIcon,
|
|
465
|
+
(c) => c.status === 'active',
|
|
466
|
+
{ color: 'green' }
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
// Sum stat
|
|
470
|
+
sumStat<Campaign>(
|
|
471
|
+
'total-sent',
|
|
472
|
+
'Total Emails Sent',
|
|
473
|
+
PaperAirplaneIcon,
|
|
474
|
+
(c) => c.sentCount,
|
|
475
|
+
{ color: 'blue', format: (val) => val.toLocaleString() }
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
// Percentage stat
|
|
479
|
+
percentageStat<Contact>(
|
|
480
|
+
'active-rate',
|
|
481
|
+
'Active Rate',
|
|
482
|
+
ChartBarIcon,
|
|
483
|
+
(c) => c.status === 'active',
|
|
484
|
+
{ color: 'green', decimals: 1 }
|
|
485
|
+
);
|
|
486
|
+
|
|
487
|
+
// Average stat
|
|
488
|
+
averageStat<Campaign>(
|
|
489
|
+
'avg-open-rate',
|
|
490
|
+
'Avg Open Rate',
|
|
491
|
+
EyeIcon,
|
|
492
|
+
(c) => c.openRate,
|
|
493
|
+
{ color: 'blue', decimals: 1, suffix: '%' }
|
|
494
|
+
);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Stat Item Interface
|
|
498
|
+
|
|
499
|
+
```typescript
|
|
500
|
+
interface StatItem {
|
|
501
|
+
id: string;
|
|
502
|
+
name: string;
|
|
503
|
+
stat: string | number;
|
|
504
|
+
icon: Component;
|
|
505
|
+
color: 'green' | 'red' | 'yellow' | 'blue' | 'gray' | 'purple' | 'orange' | 'pink';
|
|
506
|
+
change?: string; // e.g., "+12.5%", "+156K this month"
|
|
507
|
+
changeType?: 'increase' | 'decrease' | 'neutral';
|
|
508
|
+
showFooter?: boolean;
|
|
509
|
+
actionText?: string; // e.g., "View all"
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Chart Component
|
|
514
|
+
|
|
515
|
+
```vue
|
|
516
|
+
<Chart
|
|
517
|
+
id="unique-chart-id"
|
|
518
|
+
title="Chart Title"
|
|
519
|
+
type="area" // area, bar, line, donut
|
|
520
|
+
:series="chartSeries"
|
|
521
|
+
:options="chartOptions"
|
|
522
|
+
:stacked="false" // For bar charts
|
|
523
|
+
:loading="loading"
|
|
524
|
+
/>
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
### DateRange Component
|
|
528
|
+
|
|
529
|
+
```vue
|
|
530
|
+
<DateRange
|
|
531
|
+
id="date-range-id"
|
|
532
|
+
:values="{ from: '', to: '' }"
|
|
533
|
+
:loading="loading"
|
|
534
|
+
@search="handleDateRangeSearch"
|
|
535
|
+
/>
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Grid Layouts
|
|
539
|
+
|
|
540
|
+
### Stats Grid
|
|
541
|
+
|
|
542
|
+
```vue
|
|
543
|
+
<!-- 4 columns on large screens, 2 on medium, 1 on mobile -->
|
|
544
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
545
|
+
<uiStats v-for="stat in stats" :key="stat.id" :item="stat" />
|
|
546
|
+
</div>
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
### Charts Grid
|
|
550
|
+
|
|
551
|
+
```vue
|
|
552
|
+
<!-- 2 columns on large screens, 1 on smaller -->
|
|
553
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
554
|
+
<Chart ... />
|
|
555
|
+
<Chart ... />
|
|
556
|
+
</div>
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
## Staggered Loading Pattern
|
|
560
|
+
|
|
561
|
+
The CRM template shows staggered loading where each stat reveals after a delay:
|
|
562
|
+
|
|
563
|
+
```typescript
|
|
564
|
+
// Manual approach (from CRM template)
|
|
565
|
+
const loadingStats = ref([true, true, true, true]);
|
|
566
|
+
setTimeout(() => loadingStats.value[0] = false, 300);
|
|
567
|
+
setTimeout(() => loadingStats.value[1] = false, 400);
|
|
568
|
+
setTimeout(() => loadingStats.value[2] = false, 500);
|
|
569
|
+
setTimeout(() => loadingStats.value[3] = false, 600);
|
|
570
|
+
|
|
571
|
+
// Enhanced approach with useStats (automatic)
|
|
572
|
+
const { statsWithLoading } = useStats({
|
|
573
|
+
data: campaigns,
|
|
574
|
+
loading: campaignsLoading,
|
|
575
|
+
definitions: [...],
|
|
576
|
+
staggerDelay: 100, // 100ms between each stat
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// statsWithLoading includes `loading` property for each stat
|
|
580
|
+
<uiStats
|
|
581
|
+
v-for="stat in statsWithLoading"
|
|
582
|
+
:key="stat.id"
|
|
583
|
+
:item="stat"
|
|
584
|
+
:loading="stat.loading"
|
|
585
|
+
/>
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
## Tab-Based Views
|
|
589
|
+
|
|
590
|
+
For dashboards with multiple views (Overview, Campaigns, Revenue, etc.):
|
|
591
|
+
|
|
592
|
+
```vue
|
|
593
|
+
<script setup>
|
|
594
|
+
import { useTabs } from '@htlkg/components/composables';
|
|
595
|
+
|
|
596
|
+
const { tabs, activeTab, setActiveTab } = useTabs({
|
|
597
|
+
tabs: [
|
|
598
|
+
{ id: 'overview', label: 'Overview' },
|
|
599
|
+
{ id: 'campaigns', label: 'Campaigns' },
|
|
600
|
+
{ id: 'revenue', label: 'Revenue' },
|
|
601
|
+
],
|
|
602
|
+
initialTab: 'overview',
|
|
603
|
+
onTabChange: (tabId) => {
|
|
604
|
+
// Load tab-specific data
|
|
605
|
+
fetchTabData(tabId);
|
|
606
|
+
},
|
|
607
|
+
});
|
|
608
|
+
</script>
|
|
609
|
+
|
|
610
|
+
<template>
|
|
611
|
+
<!-- Overview Tab Content -->
|
|
612
|
+
<div v-if="activeTab === 'overview'">
|
|
613
|
+
<!-- Stats and charts -->
|
|
614
|
+
</div>
|
|
615
|
+
|
|
616
|
+
<!-- Campaigns Tab Content -->
|
|
617
|
+
<div v-else-if="activeTab === 'campaigns'">
|
|
618
|
+
<!-- Campaign-specific metrics -->
|
|
619
|
+
</div>
|
|
620
|
+
|
|
621
|
+
<!-- Revenue Tab Content -->
|
|
622
|
+
<div v-else-if="activeTab === 'revenue'">
|
|
623
|
+
<!-- Revenue-specific charts -->
|
|
624
|
+
</div>
|
|
625
|
+
</template>
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
## Checklist
|
|
629
|
+
|
|
630
|
+
- [ ] Astro page with layout
|
|
631
|
+
- [ ] Vue component with stats and charts
|
|
632
|
+
- [ ] `useStats` composable for derived statistics
|
|
633
|
+
- [ ] Staggered loading animation
|
|
634
|
+
- [ ] Date range filter
|
|
635
|
+
- [ ] Tab navigation (if multiple views)
|
|
636
|
+
- [ ] Stats grid (4 columns)
|
|
637
|
+
- [ ] Section titles for grouping
|
|
638
|
+
- [ ] Charts grid (2 columns)
|
|
639
|
+
- [ ] Chart types: area, bar, line, donut
|
|
640
|
+
- [ ] Stat click handlers for navigation
|
|
641
|
+
- [ ] Loading states for all components
|
|
642
|
+
- [ ] Responsive grid layouts
|