@htlkg/components 0.0.2 → 0.0.3

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.
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Page Context Composable
3
+ *
4
+ * Provides access to common page context data (user, brand, routes)
5
+ * within Vue components, without prop drilling.
6
+ */
7
+
8
+ import { computed, inject, type InjectionKey, type ComputedRef } from "vue";
9
+ import { useStore } from "@nanostores/vue";
10
+ import { atom } from "nanostores";
11
+ import { routes, type Routes } from "@htlkg/core";
12
+
13
+ /**
14
+ * User information from authentication
15
+ */
16
+ export interface PageUser {
17
+ username: string;
18
+ email?: string;
19
+ isAdmin: boolean;
20
+ isSuperAdmin: boolean;
21
+ brandIds?: number[];
22
+ accountIds?: number[];
23
+ roles?: string[];
24
+ }
25
+
26
+ /**
27
+ * Brand context for brand-scoped pages
28
+ */
29
+ export interface PageBrand {
30
+ id: number;
31
+ name: string;
32
+ logo?: string;
33
+ }
34
+
35
+ /**
36
+ * Full page context
37
+ */
38
+ export interface PageContext {
39
+ user: PageUser | null;
40
+ brand?: PageBrand;
41
+ brandId?: number;
42
+ isAdmin: boolean;
43
+ isSuperAdmin: boolean;
44
+ routes: Routes;
45
+ }
46
+
47
+ /**
48
+ * Injection key for providing page context
49
+ */
50
+ export const PAGE_CONTEXT_KEY: InjectionKey<PageContext> = Symbol("pageContext");
51
+
52
+ /**
53
+ * Nanostore for user state (set by AdminLayout)
54
+ */
55
+ export const $user = atom<PageUser | null>(null);
56
+
57
+ /**
58
+ * Nanostore for current brand (set by BrandLayout)
59
+ */
60
+ export const $currentBrand = atom<PageBrand | null>(null);
61
+
62
+ /**
63
+ * Set user in the store (called from layout)
64
+ */
65
+ export function setUser(user: PageUser | null): void {
66
+ $user.set(user);
67
+ }
68
+
69
+ /**
70
+ * Set current brand in the store (called from layout)
71
+ */
72
+ export function setCurrentBrand(brand: PageBrand | null): void {
73
+ $currentBrand.set(brand);
74
+ }
75
+
76
+ /**
77
+ * Get page context within a Vue component
78
+ *
79
+ * This composable provides access to:
80
+ * - Current user information
81
+ * - Current brand (if in brand context)
82
+ * - Admin/superadmin status
83
+ * - Type-safe routes
84
+ *
85
+ * @example
86
+ * ```vue
87
+ * <script setup>
88
+ * import { usePageContext } from '@htlkg/components';
89
+ *
90
+ * const { user, isAdmin, routes } = usePageContext();
91
+ *
92
+ * function goToAccounts() {
93
+ * window.location.href = routes.admin.accounts();
94
+ * }
95
+ * </script>
96
+ * ```
97
+ *
98
+ * @example With provider pattern
99
+ * ```vue
100
+ * // In parent component
101
+ * import { provide, PAGE_CONTEXT_KEY } from '@htlkg/components';
102
+ * provide(PAGE_CONTEXT_KEY, { user, brand, isAdmin: true, routes });
103
+ *
104
+ * // In child component
105
+ * const context = usePageContext();
106
+ * ```
107
+ */
108
+ export function usePageContext(): ComputedRef<PageContext> {
109
+ // Try injection first (from provider)
110
+ const injected = inject(PAGE_CONTEXT_KEY, null);
111
+ if (injected) {
112
+ return computed(() => injected);
113
+ }
114
+
115
+ // Fallback to nanostores
116
+ const user = useStore($user);
117
+ const brand = useStore($currentBrand);
118
+
119
+ return computed(() => ({
120
+ user: user.value,
121
+ brand: brand.value ?? undefined,
122
+ brandId: brand.value?.id,
123
+ isAdmin: user.value?.isAdmin ?? false,
124
+ isSuperAdmin: user.value?.isSuperAdmin ?? false,
125
+ routes,
126
+ }));
127
+ }
128
+
129
+ /**
130
+ * Check if user has access to a specific brand
131
+ */
132
+ export function useHasAccessToBrand(brandId: number): ComputedRef<boolean> {
133
+ const context = usePageContext();
134
+
135
+ return computed(() => {
136
+ const user = context.value.user;
137
+ if (!user) return false;
138
+ if (user.isAdmin || user.isSuperAdmin) return true;
139
+ return user.brandIds?.includes(brandId) ?? false;
140
+ });
141
+ }
142
+
143
+ /**
144
+ * Check if user has access to a specific account
145
+ */
146
+ export function useHasAccessToAccount(accountId: number): ComputedRef<boolean> {
147
+ const context = usePageContext();
148
+
149
+ return computed(() => {
150
+ const user = context.value.user;
151
+ if (!user) return false;
152
+ if (user.isAdmin || user.isSuperAdmin) return true;
153
+ return user.accountIds?.includes(accountId) ?? false;
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Get user roles
159
+ */
160
+ export function useUserRoles(): ComputedRef<string[]> {
161
+ const context = usePageContext();
162
+ return computed(() => context.value.user?.roles ?? []);
163
+ }
164
+
165
+ /**
166
+ * Check if user has a specific role
167
+ */
168
+ export function useHasRole(role: string): ComputedRef<boolean> {
169
+ const roles = useUserRoles();
170
+ return computed(() => roles.value.includes(role));
171
+ }
@@ -122,11 +122,13 @@ export function useTable<T extends Record<string, any>>(
122
122
 
123
123
  // Computed - Filtering
124
124
  const filteredItems = computed(() => {
125
- if (activeFilters.value.length === 0) return items.value;
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 activeFilters.value.every(filter => {
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
- activeFilters.value = filters;
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