@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,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Confirmation Composable
|
|
3
|
+
*
|
|
4
|
+
* Manages confirmation dialog state for destructive actions.
|
|
5
|
+
* Works with uiModal from @hotelinking/ui.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ref, computed, type Ref, type ComputedRef, type Component } from 'vue';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Confirmation dialog variant
|
|
12
|
+
*/
|
|
13
|
+
export type ConfirmationVariant = 'danger' | 'warning' | 'info' | 'default';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for a confirmation dialog
|
|
17
|
+
*/
|
|
18
|
+
export interface ConfirmationConfig {
|
|
19
|
+
/** Dialog title */
|
|
20
|
+
title: string;
|
|
21
|
+
/** Dialog message/description */
|
|
22
|
+
message: string;
|
|
23
|
+
/** Text for confirm button */
|
|
24
|
+
confirmText?: string;
|
|
25
|
+
/** Text for cancel button */
|
|
26
|
+
cancelText?: string;
|
|
27
|
+
/** Visual variant */
|
|
28
|
+
variant?: ConfirmationVariant;
|
|
29
|
+
/** Icon component to display */
|
|
30
|
+
icon?: Component;
|
|
31
|
+
/** Whether action is in progress */
|
|
32
|
+
loading?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Internal state for pending confirmation
|
|
37
|
+
*/
|
|
38
|
+
interface PendingConfirmation {
|
|
39
|
+
resolve: (value: boolean) => void;
|
|
40
|
+
reject: (reason?: unknown) => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Options for useConfirmation
|
|
45
|
+
*/
|
|
46
|
+
export interface UseConfirmationOptions {
|
|
47
|
+
/** Default confirm button text */
|
|
48
|
+
defaultConfirmText?: string;
|
|
49
|
+
/** Default cancel button text */
|
|
50
|
+
defaultCancelText?: string;
|
|
51
|
+
/** Default variant */
|
|
52
|
+
defaultVariant?: ConfirmationVariant;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return type for useConfirmation
|
|
57
|
+
*/
|
|
58
|
+
export interface UseConfirmationReturn {
|
|
59
|
+
/** Whether dialog is open */
|
|
60
|
+
isOpen: Ref<boolean>;
|
|
61
|
+
/** Current dialog configuration */
|
|
62
|
+
config: Ref<ConfirmationConfig>;
|
|
63
|
+
/** Whether action is in progress */
|
|
64
|
+
isLoading: Ref<boolean>;
|
|
65
|
+
|
|
66
|
+
/** Open confirmation dialog and wait for response */
|
|
67
|
+
confirm: (options: ConfirmationConfig) => Promise<boolean>;
|
|
68
|
+
/** Handle confirm button click */
|
|
69
|
+
handleConfirm: () => void;
|
|
70
|
+
/** Handle cancel button click */
|
|
71
|
+
handleCancel: () => void;
|
|
72
|
+
/** Close dialog without triggering callbacks */
|
|
73
|
+
close: () => void;
|
|
74
|
+
|
|
75
|
+
/** Convenience: Confirm delete action */
|
|
76
|
+
confirmDelete: (itemName: string, options?: Partial<ConfirmationConfig>) => Promise<boolean>;
|
|
77
|
+
/** Convenience: Confirm bulk delete action */
|
|
78
|
+
confirmBulkDelete: (count: number, itemType?: string, options?: Partial<ConfirmationConfig>) => Promise<boolean>;
|
|
79
|
+
/** Convenience: Confirm destructive action */
|
|
80
|
+
confirmDestructive: (actionName: string, options?: Partial<ConfirmationConfig>) => Promise<boolean>;
|
|
81
|
+
/** Convenience: Confirm with custom async action */
|
|
82
|
+
confirmWithAction: <T>(
|
|
83
|
+
options: ConfirmationConfig,
|
|
84
|
+
action: () => Promise<T>
|
|
85
|
+
) => Promise<{ confirmed: boolean; result?: T; error?: Error }>;
|
|
86
|
+
|
|
87
|
+
/** Props for uiModal component */
|
|
88
|
+
modalProps: ComputedRef<{
|
|
89
|
+
title: string;
|
|
90
|
+
open: boolean;
|
|
91
|
+
modalName: string;
|
|
92
|
+
actions: Array<{ name: string; value: string }>;
|
|
93
|
+
}>;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Creates a confirmation dialog composable
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```typescript
|
|
101
|
+
* const confirmation = useConfirmation();
|
|
102
|
+
*
|
|
103
|
+
* async function handleDelete(item: Item) {
|
|
104
|
+
* const confirmed = await confirmation.confirmDelete(item.name);
|
|
105
|
+
* if (confirmed) {
|
|
106
|
+
* await deleteItem(item.id);
|
|
107
|
+
* }
|
|
108
|
+
* }
|
|
109
|
+
*
|
|
110
|
+
* // Or with async action
|
|
111
|
+
* async function handleDeleteWithAction(item: Item) {
|
|
112
|
+
* const { confirmed, error } = await confirmation.confirmWithAction(
|
|
113
|
+
* { title: 'Delete Item', message: `Delete "${item.name}"?` },
|
|
114
|
+
* () => deleteItem(item.id)
|
|
115
|
+
* );
|
|
116
|
+
* if (confirmed && !error) {
|
|
117
|
+
* showSuccess('Item deleted');
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
120
|
+
*
|
|
121
|
+
* // In template
|
|
122
|
+
* <Modal
|
|
123
|
+
* v-bind="confirmation.modalProps"
|
|
124
|
+
* @modal-action="(e) => e.action === 'confirm' ? confirmation.handleConfirm() : confirmation.handleCancel()"
|
|
125
|
+
* >
|
|
126
|
+
* <p>{{ confirmation.config.message }}</p>
|
|
127
|
+
* </Modal>
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export function useConfirmation(options: UseConfirmationOptions = {}): UseConfirmationReturn {
|
|
131
|
+
const {
|
|
132
|
+
defaultConfirmText = 'Confirm',
|
|
133
|
+
defaultCancelText = 'Cancel',
|
|
134
|
+
defaultVariant = 'default',
|
|
135
|
+
} = options;
|
|
136
|
+
|
|
137
|
+
// State
|
|
138
|
+
const isOpen = ref(false);
|
|
139
|
+
const isLoading = ref(false);
|
|
140
|
+
const config = ref<ConfirmationConfig>({
|
|
141
|
+
title: '',
|
|
142
|
+
message: '',
|
|
143
|
+
confirmText: defaultConfirmText,
|
|
144
|
+
cancelText: defaultCancelText,
|
|
145
|
+
variant: defaultVariant,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Pending promise resolution
|
|
149
|
+
let pending: PendingConfirmation | null = null;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Open confirmation dialog and wait for user response
|
|
153
|
+
*/
|
|
154
|
+
function confirm(confirmConfig: ConfirmationConfig): Promise<boolean> {
|
|
155
|
+
// Set configuration with defaults
|
|
156
|
+
config.value = {
|
|
157
|
+
...confirmConfig,
|
|
158
|
+
confirmText: confirmConfig.confirmText ?? defaultConfirmText,
|
|
159
|
+
cancelText: confirmConfig.cancelText ?? defaultCancelText,
|
|
160
|
+
variant: confirmConfig.variant ?? defaultVariant,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
isOpen.value = true;
|
|
164
|
+
isLoading.value = false;
|
|
165
|
+
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
pending = { resolve, reject };
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Handle confirm button click
|
|
173
|
+
*/
|
|
174
|
+
function handleConfirm(): void {
|
|
175
|
+
if (pending) {
|
|
176
|
+
pending.resolve(true);
|
|
177
|
+
pending = null;
|
|
178
|
+
}
|
|
179
|
+
isOpen.value = false;
|
|
180
|
+
isLoading.value = false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Handle cancel button click
|
|
185
|
+
*/
|
|
186
|
+
function handleCancel(): void {
|
|
187
|
+
if (pending) {
|
|
188
|
+
pending.resolve(false);
|
|
189
|
+
pending = null;
|
|
190
|
+
}
|
|
191
|
+
isOpen.value = false;
|
|
192
|
+
isLoading.value = false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Close dialog without resolving
|
|
197
|
+
*/
|
|
198
|
+
function close(): void {
|
|
199
|
+
if (pending) {
|
|
200
|
+
pending.resolve(false);
|
|
201
|
+
pending = null;
|
|
202
|
+
}
|
|
203
|
+
isOpen.value = false;
|
|
204
|
+
isLoading.value = false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Convenience: Confirm delete action
|
|
209
|
+
*/
|
|
210
|
+
function confirmDelete(
|
|
211
|
+
itemName: string,
|
|
212
|
+
confirmConfig?: Partial<ConfirmationConfig>
|
|
213
|
+
): Promise<boolean> {
|
|
214
|
+
return confirm({
|
|
215
|
+
title: 'Delete Item',
|
|
216
|
+
message: `Are you sure you want to delete "${itemName}"? This action cannot be undone.`,
|
|
217
|
+
confirmText: 'Delete',
|
|
218
|
+
cancelText: 'Cancel',
|
|
219
|
+
variant: 'danger',
|
|
220
|
+
...confirmConfig,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Convenience: Confirm bulk delete action
|
|
226
|
+
*/
|
|
227
|
+
function confirmBulkDelete(
|
|
228
|
+
count: number,
|
|
229
|
+
itemType: string = 'items',
|
|
230
|
+
confirmConfig?: Partial<ConfirmationConfig>
|
|
231
|
+
): Promise<boolean> {
|
|
232
|
+
return confirm({
|
|
233
|
+
title: `Delete ${count} ${itemType}`,
|
|
234
|
+
message: `Are you sure you want to delete ${count} ${itemType}? This action cannot be undone.`,
|
|
235
|
+
confirmText: 'Delete All',
|
|
236
|
+
cancelText: 'Cancel',
|
|
237
|
+
variant: 'danger',
|
|
238
|
+
...confirmConfig,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Convenience: Confirm destructive action
|
|
244
|
+
*/
|
|
245
|
+
function confirmDestructive(
|
|
246
|
+
actionName: string,
|
|
247
|
+
confirmConfig?: Partial<ConfirmationConfig>
|
|
248
|
+
): Promise<boolean> {
|
|
249
|
+
return confirm({
|
|
250
|
+
title: `Confirm ${actionName}`,
|
|
251
|
+
message: `Are you sure you want to ${actionName.toLowerCase()}? This action cannot be undone.`,
|
|
252
|
+
confirmText: actionName,
|
|
253
|
+
cancelText: 'Cancel',
|
|
254
|
+
variant: 'danger',
|
|
255
|
+
...confirmConfig,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Convenience: Confirm with async action execution
|
|
261
|
+
*/
|
|
262
|
+
async function confirmWithAction<T>(
|
|
263
|
+
confirmConfig: ConfirmationConfig,
|
|
264
|
+
action: () => Promise<T>
|
|
265
|
+
): Promise<{ confirmed: boolean; result?: T; error?: Error }> {
|
|
266
|
+
const confirmed = await confirm(confirmConfig);
|
|
267
|
+
|
|
268
|
+
if (!confirmed) {
|
|
269
|
+
return { confirmed: false };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Show loading state
|
|
273
|
+
isLoading.value = true;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
const result = await action();
|
|
277
|
+
isOpen.value = false;
|
|
278
|
+
isLoading.value = false;
|
|
279
|
+
return { confirmed: true, result };
|
|
280
|
+
} catch (error) {
|
|
281
|
+
isLoading.value = false;
|
|
282
|
+
return {
|
|
283
|
+
confirmed: true,
|
|
284
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Props for uiModal component
|
|
291
|
+
*/
|
|
292
|
+
const modalProps = computed(() => ({
|
|
293
|
+
title: config.value.title,
|
|
294
|
+
open: isOpen.value,
|
|
295
|
+
modalName: 'confirmation-dialog',
|
|
296
|
+
actions: [
|
|
297
|
+
{
|
|
298
|
+
name: config.value.cancelText ?? defaultCancelText,
|
|
299
|
+
value: 'cancel',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: config.value.confirmText ?? defaultConfirmText,
|
|
303
|
+
value: 'confirm',
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
// State
|
|
310
|
+
isOpen,
|
|
311
|
+
config,
|
|
312
|
+
isLoading,
|
|
313
|
+
|
|
314
|
+
// Core methods
|
|
315
|
+
confirm,
|
|
316
|
+
handleConfirm,
|
|
317
|
+
handleCancel,
|
|
318
|
+
close,
|
|
319
|
+
|
|
320
|
+
// Convenience methods
|
|
321
|
+
confirmDelete,
|
|
322
|
+
confirmBulkDelete,
|
|
323
|
+
confirmDestructive,
|
|
324
|
+
confirmWithAction,
|
|
325
|
+
|
|
326
|
+
// Modal props
|
|
327
|
+
modalProps,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Global confirmation instance for use across components
|
|
333
|
+
*/
|
|
334
|
+
let globalConfirmation: UseConfirmationReturn | null = null;
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Get or create global confirmation instance
|
|
338
|
+
*
|
|
339
|
+
* @example
|
|
340
|
+
* ```typescript
|
|
341
|
+
* // In any component
|
|
342
|
+
* const confirmation = useGlobalConfirmation();
|
|
343
|
+
* await confirmation.confirmDelete('Item Name');
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
export function useGlobalConfirmation(): UseConfirmationReturn {
|
|
347
|
+
if (!globalConfirmation) {
|
|
348
|
+
globalConfirmation = useConfirmation();
|
|
349
|
+
}
|
|
350
|
+
return globalConfirmation;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Reset global confirmation instance (useful for testing)
|
|
355
|
+
*/
|
|
356
|
+
export function resetGlobalConfirmation(): void {
|
|
357
|
+
globalConfirmation = null;
|
|
358
|
+
}
|
|
@@ -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
|
+
}
|