@htlkg/components 0.0.2 → 0.0.4
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/README.md +52 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js +367 -0
- package/dist/AdminWrapper.vue_vue_type_script_setup_true_lang-B32IylcT.js.map +1 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js +263 -0
- package/dist/Alert.vue_vue_type_script_setup_true_lang-DxPCS-Hx.js.map +1 -0
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js +580 -0
- package/dist/DateRange.vue_vue_type_script_setup_true_lang-BLVg1Hah.js.map +1 -0
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js +187 -0
- package/dist/ProductBadge.vue_vue_type_script_setup_true_lang-Cmr2f4Cy.js.map +1 -0
- package/dist/_plugin-vue_export-helper-1tPrXgE0.js +11 -0
- package/dist/_plugin-vue_export-helper-1tPrXgE0.js.map +1 -0
- package/dist/components.css +15 -0
- package/dist/composables/index.js +32 -573
- package/dist/composables/index.js.map +1 -1
- package/dist/data/index.js +18 -0
- package/dist/data/index.js.map +1 -0
- package/dist/domain/index.js +8 -0
- package/dist/domain/index.js.map +1 -0
- package/dist/filterHelpers-DgRyoYSa.js +1386 -0
- package/dist/filterHelpers-DgRyoYSa.js.map +1 -0
- package/dist/forms/index.js +6 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/index-DGO_pNgG.js +79 -0
- package/dist/index-DGO_pNgG.js.map +1 -0
- package/dist/index-QK97OdqQ.js +25 -0
- package/dist/index-QK97OdqQ.js.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/navigation/index.js +8 -0
- package/dist/navigation/index.js.map +1 -0
- package/dist/overlays/index.js +8 -0
- package/dist/overlays/index.js.map +1 -0
- package/dist/stores/index.js +14 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/useAdminPage-GhgXp0x8.js +1070 -0
- package/dist/useAdminPage-GhgXp0x8.js.map +1 -0
- package/dist/useTable-DutR1gkg.js +293 -0
- package/dist/useTable-DutR1gkg.js.map +1 -0
- package/package.json +43 -14
- package/src/composables/composables.md +109 -0
- package/src/composables/index.ts +69 -0
- package/src/composables/useAdminPage.ts +462 -0
- package/src/composables/useConfirmation.ts +358 -0
- package/src/composables/usePageContext.ts +171 -0
- package/src/composables/useStats.ts +361 -0
- package/src/composables/useTable.ts +26 -5
- package/src/composables/useWizard.ts +448 -0
- package/src/data/DataTable.vue +553 -0
- package/src/data/Table/Table.vue +295 -0
- package/src/data/columnHelpers.ts +503 -0
- package/src/data/data.md +106 -0
- package/src/data/filterHelpers.ts +358 -0
- package/src/data/index.ts +31 -0
- package/src/domain/domain.md +102 -0
- package/src/forms/JsonSchemaForm.vue +4 -1
- package/src/forms/forms.md +89 -0
- package/src/index.ts +4 -3
- package/src/navigation/navigation.md +80 -0
- package/src/overlays/overlays.md +86 -0
- package/src/stores/stores.md +82 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stats Composable
|
|
3
|
+
*
|
|
4
|
+
* Derives statistics from reactive data with staggered loading animation.
|
|
5
|
+
* Integrates with uiStats component from @hotelinking/ui.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ref, computed, watch, type Ref, type ComputedRef, type Component } from 'vue';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Color options matching @hotelinking/ui
|
|
12
|
+
*/
|
|
13
|
+
export type StatColor = 'green' | 'red' | 'yellow' | 'blue' | 'gray' | 'purple' | 'orange' | 'pink';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Change type for stat trends
|
|
17
|
+
*/
|
|
18
|
+
export type ChangeType = 'increase' | 'decrease' | 'neutral';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Definition for a single stat
|
|
22
|
+
*/
|
|
23
|
+
export interface StatDefinition<T> {
|
|
24
|
+
/** Unique identifier */
|
|
25
|
+
id: string;
|
|
26
|
+
/** Display name */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Icon component (Heroicons) */
|
|
29
|
+
icon: Component;
|
|
30
|
+
/** Color theme */
|
|
31
|
+
color?: StatColor;
|
|
32
|
+
/** Function to compute stat value from data */
|
|
33
|
+
compute: (items: T[]) => string | number;
|
|
34
|
+
/** Optional: Compute change percentage/value */
|
|
35
|
+
computeChange?: (items: T[], previousItems?: T[]) => string;
|
|
36
|
+
/** Optional: Compute change type (increase/decrease/neutral) */
|
|
37
|
+
computeChangeType?: (items: T[], previousItems?: T[]) => ChangeType;
|
|
38
|
+
/** Show footer with action text */
|
|
39
|
+
showFooter?: boolean;
|
|
40
|
+
/** Action text for footer */
|
|
41
|
+
actionText?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Stat item formatted for uiStats component
|
|
46
|
+
*/
|
|
47
|
+
export interface StatItem {
|
|
48
|
+
id: string;
|
|
49
|
+
name: string;
|
|
50
|
+
stat: string | number;
|
|
51
|
+
icon: Component;
|
|
52
|
+
color: StatColor;
|
|
53
|
+
change?: string;
|
|
54
|
+
changeType?: ChangeType;
|
|
55
|
+
showFooter?: boolean;
|
|
56
|
+
actionText?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Options for useStats
|
|
61
|
+
*/
|
|
62
|
+
export interface UseStatsOptions<T> {
|
|
63
|
+
/** Reactive data source */
|
|
64
|
+
data: Ref<T[]> | ComputedRef<T[]>;
|
|
65
|
+
/** Loading state of the data source */
|
|
66
|
+
loading: Ref<boolean> | ComputedRef<boolean>;
|
|
67
|
+
/** Stat definitions */
|
|
68
|
+
definitions: StatDefinition<T>[];
|
|
69
|
+
/** Delay between each stat reveal (ms) */
|
|
70
|
+
staggerDelay?: number;
|
|
71
|
+
/** Initial delay before first stat reveals (ms) */
|
|
72
|
+
initialDelay?: number;
|
|
73
|
+
/** Previous data for computing changes (optional) */
|
|
74
|
+
previousData?: Ref<T[]> | ComputedRef<T[]>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Return type for useStats
|
|
79
|
+
*/
|
|
80
|
+
export interface UseStatsReturn<T> {
|
|
81
|
+
/** Computed stats array for uiStats */
|
|
82
|
+
stats: ComputedRef<StatItem[]>;
|
|
83
|
+
/** Individual loading states for each stat */
|
|
84
|
+
loadingStates: Ref<boolean[]>;
|
|
85
|
+
/** Combined stats with loading property */
|
|
86
|
+
statsWithLoading: ComputedRef<Array<StatItem & { loading: boolean }>>;
|
|
87
|
+
/** Whether all stats are loaded */
|
|
88
|
+
allLoaded: ComputedRef<boolean>;
|
|
89
|
+
/** Force refresh loading animation */
|
|
90
|
+
refreshAnimation: () => void;
|
|
91
|
+
/** Get a single stat by ID */
|
|
92
|
+
getStat: (id: string) => StatItem | undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Creates a stats composable for deriving statistics from reactive data
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* import { CheckCircleIcon, ClockIcon, XCircleIcon } from '@heroicons/vue/24/outline';
|
|
101
|
+
*
|
|
102
|
+
* const { statsWithLoading } = useStats({
|
|
103
|
+
* data: campaigns,
|
|
104
|
+
* loading: campaignsLoading,
|
|
105
|
+
* definitions: [
|
|
106
|
+
* {
|
|
107
|
+
* id: 'active',
|
|
108
|
+
* name: 'Active Campaigns',
|
|
109
|
+
* icon: CheckCircleIcon,
|
|
110
|
+
* color: 'green',
|
|
111
|
+
* compute: (items) => items.filter(c => c.status === 'active').length,
|
|
112
|
+
* },
|
|
113
|
+
* {
|
|
114
|
+
* id: 'pending',
|
|
115
|
+
* name: 'Pending',
|
|
116
|
+
* icon: ClockIcon,
|
|
117
|
+
* color: 'yellow',
|
|
118
|
+
* compute: (items) => items.filter(c => c.status === 'pending').length,
|
|
119
|
+
* },
|
|
120
|
+
* {
|
|
121
|
+
* id: 'total-sent',
|
|
122
|
+
* name: 'Total Sent',
|
|
123
|
+
* icon: PaperAirplaneIcon,
|
|
124
|
+
* color: 'blue',
|
|
125
|
+
* compute: (items) => items.reduce((sum, c) => sum + c.sentCount, 0).toLocaleString(),
|
|
126
|
+
* computeChange: (items) => '+12%',
|
|
127
|
+
* computeChangeType: () => 'increase',
|
|
128
|
+
* },
|
|
129
|
+
* ],
|
|
130
|
+
* staggerDelay: 100,
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* // In template
|
|
134
|
+
* <div class="grid grid-cols-4 gap-4">
|
|
135
|
+
* <uiStats
|
|
136
|
+
* v-for="stat in statsWithLoading"
|
|
137
|
+
* :key="stat.id"
|
|
138
|
+
* :item="stat"
|
|
139
|
+
* :loading="stat.loading"
|
|
140
|
+
* @statClick="handleStatClick"
|
|
141
|
+
* />
|
|
142
|
+
* </div>
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function useStats<T>(options: UseStatsOptions<T>): UseStatsReturn<T> {
|
|
146
|
+
const {
|
|
147
|
+
data,
|
|
148
|
+
loading,
|
|
149
|
+
definitions,
|
|
150
|
+
staggerDelay = 100,
|
|
151
|
+
initialDelay = 0,
|
|
152
|
+
previousData,
|
|
153
|
+
} = options;
|
|
154
|
+
|
|
155
|
+
// Individual loading states for staggered animation
|
|
156
|
+
const loadingStates = ref<boolean[]>(definitions.map(() => true));
|
|
157
|
+
|
|
158
|
+
// Timers for cleanup
|
|
159
|
+
let timers: ReturnType<typeof setTimeout>[] = [];
|
|
160
|
+
|
|
161
|
+
// Clear all pending timers
|
|
162
|
+
function clearTimers(): void {
|
|
163
|
+
timers.forEach(timer => clearTimeout(timer));
|
|
164
|
+
timers = [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Trigger staggered loading animation
|
|
168
|
+
function triggerStaggeredReveal(): void {
|
|
169
|
+
clearTimers();
|
|
170
|
+
|
|
171
|
+
// Reset all to loading
|
|
172
|
+
loadingStates.value = definitions.map(() => true);
|
|
173
|
+
|
|
174
|
+
// Staggered reveal
|
|
175
|
+
definitions.forEach((_, index) => {
|
|
176
|
+
const timer = setTimeout(() => {
|
|
177
|
+
loadingStates.value[index] = false;
|
|
178
|
+
}, initialDelay + staggerDelay * (index + 1));
|
|
179
|
+
timers.push(timer);
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Watch for loading state changes
|
|
184
|
+
watch(
|
|
185
|
+
() => loading.value,
|
|
186
|
+
(isLoading, wasLoading) => {
|
|
187
|
+
if (wasLoading && !isLoading) {
|
|
188
|
+
// Data just finished loading, trigger reveal
|
|
189
|
+
triggerStaggeredReveal();
|
|
190
|
+
} else if (isLoading && !wasLoading) {
|
|
191
|
+
// Started loading, reset states
|
|
192
|
+
clearTimers();
|
|
193
|
+
loadingStates.value = definitions.map(() => true);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{ immediate: true }
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
// If data is already loaded on mount, trigger reveal
|
|
200
|
+
if (!loading.value) {
|
|
201
|
+
triggerStaggeredReveal();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Computed: Stats derived from data
|
|
205
|
+
const stats = computed<StatItem[]>(() => {
|
|
206
|
+
const items = data.value;
|
|
207
|
+
const prevItems = previousData?.value;
|
|
208
|
+
|
|
209
|
+
return definitions.map(def => {
|
|
210
|
+
const stat: StatItem = {
|
|
211
|
+
id: def.id,
|
|
212
|
+
name: def.name,
|
|
213
|
+
stat: def.compute(items),
|
|
214
|
+
icon: def.icon,
|
|
215
|
+
color: def.color ?? 'gray',
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// Add change information if provided
|
|
219
|
+
if (def.computeChange) {
|
|
220
|
+
stat.change = def.computeChange(items, prevItems);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (def.computeChangeType) {
|
|
224
|
+
stat.changeType = def.computeChangeType(items, prevItems);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Footer options
|
|
228
|
+
if (def.showFooter !== undefined) {
|
|
229
|
+
stat.showFooter = def.showFooter;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (def.actionText) {
|
|
233
|
+
stat.actionText = def.actionText;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return stat;
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Computed: Stats with loading property included
|
|
241
|
+
const statsWithLoading = computed(() =>
|
|
242
|
+
stats.value.map((stat, index) => ({
|
|
243
|
+
...stat,
|
|
244
|
+
loading: loading.value || loadingStates.value[index],
|
|
245
|
+
}))
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Computed: All stats loaded
|
|
249
|
+
const allLoaded = computed(() =>
|
|
250
|
+
!loading.value && loadingStates.value.every(s => !s)
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// Get a single stat by ID
|
|
254
|
+
function getStat(id: string): StatItem | undefined {
|
|
255
|
+
return stats.value.find(s => s.id === id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Manually refresh animation
|
|
259
|
+
function refreshAnimation(): void {
|
|
260
|
+
triggerStaggeredReveal();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
stats,
|
|
265
|
+
loadingStates,
|
|
266
|
+
statsWithLoading,
|
|
267
|
+
allLoaded,
|
|
268
|
+
refreshAnimation,
|
|
269
|
+
getStat,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Helper: Create a simple count stat definition
|
|
275
|
+
*/
|
|
276
|
+
export function countStat<T>(
|
|
277
|
+
id: string,
|
|
278
|
+
name: string,
|
|
279
|
+
icon: Component,
|
|
280
|
+
filter: (item: T) => boolean,
|
|
281
|
+
options?: { color?: StatColor }
|
|
282
|
+
): StatDefinition<T> {
|
|
283
|
+
return {
|
|
284
|
+
id,
|
|
285
|
+
name,
|
|
286
|
+
icon,
|
|
287
|
+
color: options?.color ?? 'gray',
|
|
288
|
+
compute: (items) => items.filter(filter).length,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Helper: Create a sum stat definition
|
|
294
|
+
*/
|
|
295
|
+
export function sumStat<T>(
|
|
296
|
+
id: string,
|
|
297
|
+
name: string,
|
|
298
|
+
icon: Component,
|
|
299
|
+
getValue: (item: T) => number,
|
|
300
|
+
options?: { color?: StatColor; format?: (value: number) => string }
|
|
301
|
+
): StatDefinition<T> {
|
|
302
|
+
return {
|
|
303
|
+
id,
|
|
304
|
+
name,
|
|
305
|
+
icon,
|
|
306
|
+
color: options?.color ?? 'gray',
|
|
307
|
+
compute: (items) => {
|
|
308
|
+
const sum = items.reduce((total, item) => total + getValue(item), 0);
|
|
309
|
+
return options?.format ? options.format(sum) : sum.toLocaleString();
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Helper: Create an average stat definition
|
|
316
|
+
*/
|
|
317
|
+
export function averageStat<T>(
|
|
318
|
+
id: string,
|
|
319
|
+
name: string,
|
|
320
|
+
icon: Component,
|
|
321
|
+
getValue: (item: T) => number,
|
|
322
|
+
options?: { color?: StatColor; decimals?: number; suffix?: string }
|
|
323
|
+
): StatDefinition<T> {
|
|
324
|
+
return {
|
|
325
|
+
id,
|
|
326
|
+
name,
|
|
327
|
+
icon,
|
|
328
|
+
color: options?.color ?? 'gray',
|
|
329
|
+
compute: (items) => {
|
|
330
|
+
if (items.length === 0) return '0';
|
|
331
|
+
const sum = items.reduce((total, item) => total + getValue(item), 0);
|
|
332
|
+
const avg = sum / items.length;
|
|
333
|
+
const formatted = avg.toFixed(options?.decimals ?? 1);
|
|
334
|
+
return options?.suffix ? `${formatted}${options.suffix}` : formatted;
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Helper: Create a percentage stat definition
|
|
341
|
+
*/
|
|
342
|
+
export function percentageStat<T>(
|
|
343
|
+
id: string,
|
|
344
|
+
name: string,
|
|
345
|
+
icon: Component,
|
|
346
|
+
filter: (item: T) => boolean,
|
|
347
|
+
options?: { color?: StatColor; decimals?: number }
|
|
348
|
+
): StatDefinition<T> {
|
|
349
|
+
return {
|
|
350
|
+
id,
|
|
351
|
+
name,
|
|
352
|
+
icon,
|
|
353
|
+
color: options?.color ?? 'gray',
|
|
354
|
+
compute: (items) => {
|
|
355
|
+
if (items.length === 0) return '0%';
|
|
356
|
+
const count = items.filter(filter).length;
|
|
357
|
+
const percentage = (count / items.length) * 100;
|
|
358
|
+
return `${percentage.toFixed(options?.decimals ?? 1)}%`;
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -122,11 +122,13 @@ export function useTable<T extends Record<string, any>>(
|
|
|
122
122
|
|
|
123
123
|
// Computed - Filtering
|
|
124
124
|
const filteredItems = computed(() => {
|
|
125
|
-
|
|
125
|
+
// Ensure activeFilters is always an array
|
|
126
|
+
const filters = Array.isArray(activeFilters.value) ? activeFilters.value : [];
|
|
127
|
+
if (filters.length === 0) return items.value;
|
|
126
128
|
|
|
127
129
|
return items.value.filter(item => {
|
|
128
130
|
// All filters must pass (AND logic by default)
|
|
129
|
-
return
|
|
131
|
+
return filters.every(filter => {
|
|
130
132
|
const { category, operator, value } = filter;
|
|
131
133
|
|
|
132
134
|
if (value === undefined || value === null || value === '') return true;
|
|
@@ -297,8 +299,27 @@ export function useTable<T extends Record<string, any>>(
|
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
// Methods - Filtering
|
|
300
|
-
function applyFilters(filters: SmartFilter[]) {
|
|
301
|
-
|
|
302
|
+
function applyFilters(filters: SmartFilter[] | Record<string, any>) {
|
|
303
|
+
// Handle various filter formats from UI components
|
|
304
|
+
if (Array.isArray(filters)) {
|
|
305
|
+
activeFilters.value = filters;
|
|
306
|
+
} else if (filters && typeof filters === 'object') {
|
|
307
|
+
// Handle object format like { filters: [...] } or extract values
|
|
308
|
+
if ('filters' in filters && Array.isArray(filters.filters)) {
|
|
309
|
+
activeFilters.value = filters.filters;
|
|
310
|
+
} else {
|
|
311
|
+
// Convert object to SmartFilter array
|
|
312
|
+
activeFilters.value = Object.entries(filters)
|
|
313
|
+
.filter(([_, value]) => value !== undefined && value !== null && value !== '')
|
|
314
|
+
.map(([key, value]) => ({
|
|
315
|
+
category: key,
|
|
316
|
+
operator: 'contains',
|
|
317
|
+
value,
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
activeFilters.value = [];
|
|
322
|
+
}
|
|
302
323
|
currentPage.value = 1; // Reset to first page when filters change
|
|
303
324
|
}
|
|
304
325
|
|
|
@@ -314,7 +335,7 @@ export function useTable<T extends Record<string, any>>(
|
|
|
314
335
|
}
|
|
315
336
|
}
|
|
316
337
|
|
|
317
|
-
function handleSmartFiltersApplied(filters: SmartFilter[]) {
|
|
338
|
+
function handleSmartFiltersApplied(filters: SmartFilter[] | Record<string, any>) {
|
|
318
339
|
applyFilters(filters);
|
|
319
340
|
}
|
|
320
341
|
|