@htlkg/data 0.0.21 → 0.0.23
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/hooks/index.d.ts +702 -94
- package/dist/hooks/index.js +793 -56
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +802 -65
- package/dist/index.js.map +1 -1
- package/dist/mutations/index.js +4 -4
- package/dist/mutations/index.js.map +1 -1
- package/dist/queries/index.js +5 -5
- package/dist/queries/index.js.map +1 -1
- package/package.json +11 -12
- package/src/hooks/accounts/index.ts +2 -0
- package/src/hooks/{useAccounts.ts → accounts/useAccounts.ts} +48 -5
- package/src/hooks/accounts/usePaginatedAccounts.ts +166 -0
- package/src/hooks/brands/index.ts +2 -0
- package/src/hooks/{useBrands.ts → brands/useBrands.ts} +1 -1
- package/src/hooks/brands/usePaginatedBrands.ts +206 -0
- package/src/hooks/contacts/index.ts +2 -0
- package/src/hooks/contacts/useContacts.ts +176 -0
- package/src/hooks/contacts/usePaginatedContacts.ts +268 -0
- package/src/hooks/createPaginatedDataHook.ts +359 -0
- package/src/hooks/data-hook-errors.property.test.ts +4 -4
- package/src/hooks/data-hook-filters.property.test.ts +4 -4
- package/src/hooks/data-hooks.property.test.ts +4 -4
- package/src/hooks/index.ts +101 -8
- package/src/hooks/productInstances/index.ts +1 -0
- package/src/hooks/{useProductInstances.ts → productInstances/useProductInstances.ts} +9 -6
- package/src/hooks/products/index.ts +1 -0
- package/src/hooks/{useProducts.ts → products/useProducts.ts} +4 -5
- package/src/hooks/reservations/index.ts +2 -0
- package/src/hooks/reservations/usePaginatedReservations.ts +258 -0
- package/src/hooks/{useReservations.ts → reservations/useReservations.ts} +65 -10
- package/src/hooks/users/index.ts +2 -0
- package/src/hooks/users/usePaginatedUsers.ts +213 -0
- package/src/hooks/{useUsers.ts → users/useUsers.ts} +1 -1
- package/src/mutations/accounts/accounts.test.ts +287 -0
- package/src/mutations/{accounts.ts → accounts/accounts.ts} +2 -2
- package/src/mutations/accounts/index.ts +1 -0
- package/src/mutations/brands/brands.test.ts +292 -0
- package/src/mutations/{brands.ts → brands/brands.ts} +2 -2
- package/src/mutations/brands/index.ts +1 -0
- package/src/mutations/reservations/index.ts +1 -0
- package/src/mutations/{reservations.test.ts → reservations/reservations.test.ts} +1 -1
- package/src/mutations/{reservations.ts → reservations/reservations.ts} +2 -2
- package/src/mutations/users/index.ts +1 -0
- package/src/mutations/users/users.test.ts +289 -0
- package/src/mutations/{users.ts → users/users.ts} +2 -2
- package/src/queries/accounts/accounts.test.ts +228 -0
- package/src/queries/accounts/index.ts +1 -0
- package/src/queries/brands/brands.test.ts +288 -0
- package/src/queries/brands/index.ts +1 -0
- package/src/queries/products/index.ts +1 -0
- package/src/queries/products/products.test.ts +347 -0
- package/src/queries/reservations/index.ts +1 -0
- package/src/queries/users/index.ts +1 -0
- package/src/queries/users/users.test.ts +301 -0
- /package/src/queries/{accounts.ts → accounts/accounts.ts} +0 -0
- /package/src/queries/{brands.ts → brands/brands.ts} +0 -0
- /package/src/queries/{products.ts → products/products.ts} +0 -0
- /package/src/queries/{reservations.test.ts → reservations/reservations.test.ts} +0 -0
- /package/src/queries/{reservations.ts → reservations/reservations.ts} +0 -0
- /package/src/queries/{users.ts → users/users.ts} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated Brands Hooks
|
|
3
|
+
*
|
|
4
|
+
* Vue composables for fetching brands with server-side pagination.
|
|
5
|
+
* Provides separate hooks for active and deleted brands to enable
|
|
6
|
+
* efficient tab-based filtering in large datasets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Ref, ComputedRef } from "vue";
|
|
10
|
+
import type { Brand } from "@htlkg/core/types";
|
|
11
|
+
import {
|
|
12
|
+
createPaginatedDataHook,
|
|
13
|
+
ACTIVE_FILTER,
|
|
14
|
+
DELETED_FILTER,
|
|
15
|
+
type PaginatedHookOptions,
|
|
16
|
+
type PaginationState,
|
|
17
|
+
} from "../createPaginatedDataHook";
|
|
18
|
+
|
|
19
|
+
/** Extended Brand type with computed fields */
|
|
20
|
+
export type BrandWithCounts = Brand & {
|
|
21
|
+
accountName?: string;
|
|
22
|
+
enabledProductCount?: number;
|
|
23
|
+
totalProductCount?: number;
|
|
24
|
+
deletedAt?: string;
|
|
25
|
+
deletedBy?: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export interface UsePaginatedBrandsOptions extends PaginatedHookOptions {
|
|
29
|
+
/** Filter by account ID */
|
|
30
|
+
accountId?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UsePaginatedBrandsReturn {
|
|
34
|
+
/** Reactive array of brands */
|
|
35
|
+
brands: Ref<BrandWithCounts[]>;
|
|
36
|
+
/** Loading state (true during any fetch) */
|
|
37
|
+
loading: Ref<boolean>;
|
|
38
|
+
/** Loading state for initial fetch */
|
|
39
|
+
initialLoading: Ref<boolean>;
|
|
40
|
+
/** Loading state for loadMore */
|
|
41
|
+
loadingMore: Ref<boolean>;
|
|
42
|
+
/** Error state */
|
|
43
|
+
error: Ref<Error | null>;
|
|
44
|
+
/** Pagination state */
|
|
45
|
+
pagination: Ref<PaginationState>;
|
|
46
|
+
/** Whether there are more items to load */
|
|
47
|
+
hasMore: ComputedRef<boolean>;
|
|
48
|
+
/** Load next page of data */
|
|
49
|
+
loadMore: () => Promise<void>;
|
|
50
|
+
/** Refetch data (resets to first page) */
|
|
51
|
+
refetch: () => Promise<void>;
|
|
52
|
+
/** Reset data and pagination state */
|
|
53
|
+
reset: () => void;
|
|
54
|
+
/** Update search filter and refetch from server (searches ALL data) */
|
|
55
|
+
setSearchFilter: (filter: any) => Promise<void>;
|
|
56
|
+
/** Current search filter */
|
|
57
|
+
searchFilter: Ref<any>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build additional filter from hook options
|
|
62
|
+
*/
|
|
63
|
+
function buildFilter(options: UsePaginatedBrandsOptions): any {
|
|
64
|
+
const conditions: any[] = [];
|
|
65
|
+
|
|
66
|
+
if (options.accountId) {
|
|
67
|
+
conditions.push({ accountId: { eq: options.accountId } });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
71
|
+
conditions.push(options.filter);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (conditions.length === 0) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
if (conditions.length === 1) {
|
|
78
|
+
return conditions[0];
|
|
79
|
+
}
|
|
80
|
+
return { and: conditions };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Transform brand data
|
|
85
|
+
*/
|
|
86
|
+
function transformBrand(brand: any): BrandWithCounts {
|
|
87
|
+
return {
|
|
88
|
+
...brand,
|
|
89
|
+
accountName: brand.account?.name || "",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Selection set for brand queries
|
|
95
|
+
*/
|
|
96
|
+
const BRAND_SELECTION_SET = [
|
|
97
|
+
"id",
|
|
98
|
+
"name",
|
|
99
|
+
"accountId",
|
|
100
|
+
"logo",
|
|
101
|
+
"timezone",
|
|
102
|
+
"status",
|
|
103
|
+
"settings",
|
|
104
|
+
"deletedAt",
|
|
105
|
+
"deletedBy",
|
|
106
|
+
"account.name",
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Internal hook for active brands
|
|
111
|
+
*/
|
|
112
|
+
const useActiveBrandsInternal = createPaginatedDataHook<BrandWithCounts, UsePaginatedBrandsOptions>({
|
|
113
|
+
model: "Brand",
|
|
114
|
+
dataPropertyName: "brands",
|
|
115
|
+
defaultPageSize: 25,
|
|
116
|
+
selectionSet: BRAND_SELECTION_SET,
|
|
117
|
+
transform: transformBrand,
|
|
118
|
+
buildFilter,
|
|
119
|
+
baseFilter: ACTIVE_FILTER,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Internal hook for deleted brands
|
|
124
|
+
*/
|
|
125
|
+
const useDeletedBrandsInternal = createPaginatedDataHook<BrandWithCounts, UsePaginatedBrandsOptions>({
|
|
126
|
+
model: "Brand",
|
|
127
|
+
dataPropertyName: "brands",
|
|
128
|
+
defaultPageSize: 25,
|
|
129
|
+
selectionSet: BRAND_SELECTION_SET,
|
|
130
|
+
transform: transformBrand,
|
|
131
|
+
buildFilter,
|
|
132
|
+
baseFilter: DELETED_FILTER,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Composable for fetching active (non-deleted) brands with pagination
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```typescript
|
|
140
|
+
* import { useActiveBrands } from '@htlkg/data/hooks';
|
|
141
|
+
*
|
|
142
|
+
* const { brands, loading, hasMore, loadMore, refetch } = useActiveBrands({
|
|
143
|
+
* pageSize: 25,
|
|
144
|
+
* });
|
|
145
|
+
*
|
|
146
|
+
* // Load more when user scrolls or clicks "Load More"
|
|
147
|
+
* async function onLoadMore() {
|
|
148
|
+
* await loadMore();
|
|
149
|
+
* }
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* @example With account filter
|
|
153
|
+
* ```typescript
|
|
154
|
+
* const { brands, loading } = useActiveBrands({
|
|
155
|
+
* accountId: 'account-123',
|
|
156
|
+
* pageSize: 50
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function useActiveBrands(options: UsePaginatedBrandsOptions = {}): UsePaginatedBrandsReturn {
|
|
161
|
+
const result = useActiveBrandsInternal(options);
|
|
162
|
+
return {
|
|
163
|
+
brands: result.brands as Ref<BrandWithCounts[]>,
|
|
164
|
+
loading: result.loading,
|
|
165
|
+
initialLoading: result.initialLoading,
|
|
166
|
+
loadingMore: result.loadingMore,
|
|
167
|
+
error: result.error,
|
|
168
|
+
pagination: result.pagination,
|
|
169
|
+
hasMore: result.hasMore,
|
|
170
|
+
loadMore: result.loadMore,
|
|
171
|
+
refetch: result.refetch,
|
|
172
|
+
reset: result.reset,
|
|
173
|
+
setSearchFilter: result.setSearchFilter,
|
|
174
|
+
searchFilter: result.searchFilter,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Composable for fetching deleted brands with pagination
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* ```typescript
|
|
183
|
+
* import { useDeletedBrands } from '@htlkg/data/hooks';
|
|
184
|
+
*
|
|
185
|
+
* const { brands, loading, hasMore, loadMore, refetch } = useDeletedBrands({
|
|
186
|
+
* pageSize: 25,
|
|
187
|
+
* });
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
export function useDeletedBrands(options: UsePaginatedBrandsOptions = {}): UsePaginatedBrandsReturn {
|
|
191
|
+
const result = useDeletedBrandsInternal(options);
|
|
192
|
+
return {
|
|
193
|
+
brands: result.brands as Ref<BrandWithCounts[]>,
|
|
194
|
+
loading: result.loading,
|
|
195
|
+
initialLoading: result.initialLoading,
|
|
196
|
+
loadingMore: result.loadingMore,
|
|
197
|
+
error: result.error,
|
|
198
|
+
pagination: result.pagination,
|
|
199
|
+
hasMore: result.hasMore,
|
|
200
|
+
loadMore: result.loadMore,
|
|
201
|
+
refetch: result.refetch,
|
|
202
|
+
reset: result.reset,
|
|
203
|
+
setSearchFilter: result.setSearchFilter,
|
|
204
|
+
searchFilter: result.searchFilter,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useContacts Hook
|
|
3
|
+
*
|
|
4
|
+
* Vue composable for fetching and managing contact data with reactive state.
|
|
5
|
+
* Provides loading states, error handling, search, pagination, and refetch capabilities.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Ref, ComputedRef } from "vue";
|
|
9
|
+
import type { Contact } from "@htlkg/core/types";
|
|
10
|
+
import { createDataHook, type BaseHookOptions } from "../createDataHook";
|
|
11
|
+
|
|
12
|
+
export interface UseContactsOptions extends BaseHookOptions {
|
|
13
|
+
/** Filter by brand ID */
|
|
14
|
+
brandId?: string;
|
|
15
|
+
/** Search query (searches email, firstName, lastName) */
|
|
16
|
+
search?: string;
|
|
17
|
+
/** Filter by GDPR consent */
|
|
18
|
+
gdprConsent?: boolean;
|
|
19
|
+
/** Filter by marketing opt-in */
|
|
20
|
+
marketingOptIn?: boolean;
|
|
21
|
+
/** Filter by tags (contact must have at least one of these tags) */
|
|
22
|
+
tags?: string[];
|
|
23
|
+
/** Pagination token for fetching next page */
|
|
24
|
+
nextToken?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseContactsReturn {
|
|
28
|
+
/** Reactive array of contacts */
|
|
29
|
+
contacts: Ref<Contact[]>;
|
|
30
|
+
/** Computed array of contacts with GDPR consent */
|
|
31
|
+
consentedContacts: ComputedRef<Contact[]>;
|
|
32
|
+
/** Computed array of contacts opted-in for marketing */
|
|
33
|
+
marketingContacts: ComputedRef<Contact[]>;
|
|
34
|
+
/** Loading state */
|
|
35
|
+
loading: Ref<boolean>;
|
|
36
|
+
/** Error state */
|
|
37
|
+
error: Ref<Error | null>;
|
|
38
|
+
/** Refetch contacts */
|
|
39
|
+
refetch: () => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build filter from hook options
|
|
44
|
+
*/
|
|
45
|
+
function buildFilter(options: UseContactsOptions): any {
|
|
46
|
+
const conditions: any[] = [];
|
|
47
|
+
|
|
48
|
+
// Filter by brand
|
|
49
|
+
if (options.brandId) {
|
|
50
|
+
conditions.push({ brandId: { eq: options.brandId } });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Search across email, firstName, lastName
|
|
54
|
+
if (options.search) {
|
|
55
|
+
conditions.push({
|
|
56
|
+
or: [
|
|
57
|
+
{ email: { contains: options.search } },
|
|
58
|
+
{ firstName: { contains: options.search } },
|
|
59
|
+
{ lastName: { contains: options.search } },
|
|
60
|
+
],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Filter by GDPR consent
|
|
65
|
+
if (options.gdprConsent !== undefined) {
|
|
66
|
+
conditions.push({ gdprConsent: { eq: options.gdprConsent } });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Filter by marketing opt-in
|
|
70
|
+
if (options.marketingOptIn !== undefined) {
|
|
71
|
+
conditions.push({ marketingOptIn: { eq: options.marketingOptIn } });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Filter by tags (contact must have at least one of these tags)
|
|
75
|
+
if (options.tags && options.tags.length > 0) {
|
|
76
|
+
const tagConditions = options.tags.map((tag) => ({
|
|
77
|
+
tags: { contains: tag },
|
|
78
|
+
}));
|
|
79
|
+
conditions.push({ or: tagConditions });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Include any additional custom filter
|
|
83
|
+
if (options.filter) {
|
|
84
|
+
conditions.push(options.filter);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (conditions.length === 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (conditions.length === 1) {
|
|
92
|
+
return conditions[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { and: conditions };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Internal hook created by factory
|
|
100
|
+
*/
|
|
101
|
+
const useContactsInternal = createDataHook<
|
|
102
|
+
Contact,
|
|
103
|
+
UseContactsOptions,
|
|
104
|
+
{ consentedContacts: Contact[]; marketingContacts: Contact[] }
|
|
105
|
+
>({
|
|
106
|
+
model: "Contact",
|
|
107
|
+
dataPropertyName: "contacts",
|
|
108
|
+
buildFilter,
|
|
109
|
+
computedProperties: {
|
|
110
|
+
consentedContacts: (contacts) =>
|
|
111
|
+
contacts.filter((c) => c.gdprConsent === true),
|
|
112
|
+
marketingContacts: (contacts) =>
|
|
113
|
+
contacts.filter((c) => c.marketingOptIn === true),
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Composable for fetching and managing contacts
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* import { useContacts } from '@htlkg/data/hooks';
|
|
123
|
+
*
|
|
124
|
+
* const { contacts, loading, error, refetch } = useContacts({
|
|
125
|
+
* brandId: 'brand-123',
|
|
126
|
+
* limit: 25
|
|
127
|
+
* });
|
|
128
|
+
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example With search
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const { contacts, loading } = useContacts({
|
|
133
|
+
* brandId: 'brand-123',
|
|
134
|
+
* search: 'john',
|
|
135
|
+
* limit: 25
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*
|
|
139
|
+
* @example With GDPR and marketing filters
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const { contacts, marketingContacts } = useContacts({
|
|
142
|
+
* brandId: 'brand-123',
|
|
143
|
+
* gdprConsent: true,
|
|
144
|
+
* marketingOptIn: true
|
|
145
|
+
* });
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @example With computed properties
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const { contacts, consentedContacts, marketingContacts } = useContacts({
|
|
151
|
+
* brandId: 'brand-123'
|
|
152
|
+
* });
|
|
153
|
+
*
|
|
154
|
+
* // consentedContacts - contacts with GDPR consent
|
|
155
|
+
* // marketingContacts - contacts opted-in for marketing
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @example With tag filtering
|
|
159
|
+
* ```typescript
|
|
160
|
+
* const { contacts } = useContacts({
|
|
161
|
+
* brandId: 'brand-123',
|
|
162
|
+
* tags: ['vip', 'returning']
|
|
163
|
+
* });
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export function useContacts(options: UseContactsOptions = {}): UseContactsReturn {
|
|
167
|
+
const result = useContactsInternal(options);
|
|
168
|
+
return {
|
|
169
|
+
contacts: result.contacts as Ref<Contact[]>,
|
|
170
|
+
consentedContacts: result.consentedContacts as ComputedRef<Contact[]>,
|
|
171
|
+
marketingContacts: result.marketingContacts as ComputedRef<Contact[]>,
|
|
172
|
+
loading: result.loading,
|
|
173
|
+
error: result.error,
|
|
174
|
+
refetch: result.refetch,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paginated Contacts Hooks
|
|
3
|
+
*
|
|
4
|
+
* Vue composables for fetching contacts with server-side pagination.
|
|
5
|
+
* Provides separate hooks for active and deleted contacts to enable
|
|
6
|
+
* efficient tab-based filtering in large datasets.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Ref, ComputedRef } from "vue";
|
|
10
|
+
import type { Contact } from "@htlkg/core/types";
|
|
11
|
+
import {
|
|
12
|
+
createPaginatedDataHook,
|
|
13
|
+
ACTIVE_FILTER,
|
|
14
|
+
DELETED_FILTER,
|
|
15
|
+
type PaginatedHookOptions,
|
|
16
|
+
type PaginationState,
|
|
17
|
+
} from "../createPaginatedDataHook";
|
|
18
|
+
|
|
19
|
+
/** Extended Contact type with computed fields */
|
|
20
|
+
export type ContactWithRelations = Contact & {
|
|
21
|
+
brandName?: string;
|
|
22
|
+
name?: string;
|
|
23
|
+
deletedAt?: string;
|
|
24
|
+
deletedBy?: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export interface UsePaginatedContactsOptions extends PaginatedHookOptions {
|
|
28
|
+
/** Filter by brand ID */
|
|
29
|
+
brandId?: string;
|
|
30
|
+
/** Filter by account ID */
|
|
31
|
+
accountId?: string;
|
|
32
|
+
/** Search query (searches email, firstName, lastName) */
|
|
33
|
+
search?: string;
|
|
34
|
+
/** Filter by GDPR consent */
|
|
35
|
+
gdprConsent?: boolean;
|
|
36
|
+
/** Filter by marketing opt-in */
|
|
37
|
+
marketingOptIn?: boolean;
|
|
38
|
+
/** Filter by tags (contact must have at least one of these tags) */
|
|
39
|
+
tags?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface UsePaginatedContactsReturn {
|
|
43
|
+
/** Reactive array of contacts */
|
|
44
|
+
contacts: Ref<ContactWithRelations[]>;
|
|
45
|
+
/** Loading state (true during any fetch) */
|
|
46
|
+
loading: Ref<boolean>;
|
|
47
|
+
/** Loading state for initial fetch */
|
|
48
|
+
initialLoading: Ref<boolean>;
|
|
49
|
+
/** Loading state for loadMore */
|
|
50
|
+
loadingMore: Ref<boolean>;
|
|
51
|
+
/** Error state */
|
|
52
|
+
error: Ref<Error | null>;
|
|
53
|
+
/** Pagination state */
|
|
54
|
+
pagination: Ref<PaginationState>;
|
|
55
|
+
/** Whether there are more items to load */
|
|
56
|
+
hasMore: ComputedRef<boolean>;
|
|
57
|
+
/** Load next page of data */
|
|
58
|
+
loadMore: () => Promise<void>;
|
|
59
|
+
/** Refetch data (resets to first page) */
|
|
60
|
+
refetch: () => Promise<void>;
|
|
61
|
+
/** Reset data and pagination state */
|
|
62
|
+
reset: () => void;
|
|
63
|
+
/** Update search filter and refetch from server (searches ALL data) */
|
|
64
|
+
setSearchFilter: (filter: any) => Promise<void>;
|
|
65
|
+
/** Current search filter */
|
|
66
|
+
searchFilter: Ref<any>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Build additional filter from hook options
|
|
71
|
+
*/
|
|
72
|
+
function buildFilter(options: UsePaginatedContactsOptions): any {
|
|
73
|
+
const conditions: any[] = [];
|
|
74
|
+
|
|
75
|
+
if (options.brandId) {
|
|
76
|
+
conditions.push({ brandId: { eq: options.brandId } });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options.accountId) {
|
|
80
|
+
conditions.push({ accountId: { eq: options.accountId } });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Search across email, firstName, lastName
|
|
84
|
+
if (options.search) {
|
|
85
|
+
conditions.push({
|
|
86
|
+
or: [
|
|
87
|
+
{ email: { contains: options.search } },
|
|
88
|
+
{ firstName: { contains: options.search } },
|
|
89
|
+
{ lastName: { contains: options.search } },
|
|
90
|
+
],
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Filter by GDPR consent
|
|
95
|
+
if (options.gdprConsent !== undefined) {
|
|
96
|
+
conditions.push({ gdprConsent: { eq: options.gdprConsent } });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Filter by marketing opt-in
|
|
100
|
+
if (options.marketingOptIn !== undefined) {
|
|
101
|
+
conditions.push({ marketingOptIn: { eq: options.marketingOptIn } });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Filter by tags (contact must have at least one of these tags)
|
|
105
|
+
if (options.tags && options.tags.length > 0) {
|
|
106
|
+
const tagConditions = options.tags.map((tag) => ({
|
|
107
|
+
tags: { contains: tag },
|
|
108
|
+
}));
|
|
109
|
+
conditions.push({ or: tagConditions });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (options.filter && Object.keys(options.filter).length > 0) {
|
|
113
|
+
conditions.push(options.filter);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (conditions.length === 0) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
if (conditions.length === 1) {
|
|
120
|
+
return conditions[0];
|
|
121
|
+
}
|
|
122
|
+
return { and: conditions };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Transform contact data
|
|
127
|
+
*/
|
|
128
|
+
function transformContact(contact: any): ContactWithRelations {
|
|
129
|
+
return {
|
|
130
|
+
...contact,
|
|
131
|
+
name: `${contact.firstName || ""} ${contact.lastName || ""}`.trim() || contact.email,
|
|
132
|
+
brandName: contact.brand?.name || "",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Selection set for contact queries
|
|
138
|
+
*/
|
|
139
|
+
const CONTACT_SELECTION_SET = [
|
|
140
|
+
"id",
|
|
141
|
+
"email",
|
|
142
|
+
"phone",
|
|
143
|
+
"firstName",
|
|
144
|
+
"lastName",
|
|
145
|
+
"locale",
|
|
146
|
+
"brandId",
|
|
147
|
+
"accountId",
|
|
148
|
+
"gdprConsent",
|
|
149
|
+
"gdprConsentDate",
|
|
150
|
+
"marketingOptIn",
|
|
151
|
+
"preferences",
|
|
152
|
+
"tags",
|
|
153
|
+
"totalVisits",
|
|
154
|
+
"lastVisitDate",
|
|
155
|
+
"firstVisitDate",
|
|
156
|
+
"source",
|
|
157
|
+
"status",
|
|
158
|
+
"createdAt",
|
|
159
|
+
"updatedAt",
|
|
160
|
+
"deletedAt",
|
|
161
|
+
"deletedBy",
|
|
162
|
+
"brand.name",
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Internal hook for active contacts
|
|
167
|
+
*/
|
|
168
|
+
const useActiveContactsInternal = createPaginatedDataHook<ContactWithRelations, UsePaginatedContactsOptions>({
|
|
169
|
+
model: "Contact",
|
|
170
|
+
dataPropertyName: "contacts",
|
|
171
|
+
defaultPageSize: 25,
|
|
172
|
+
selectionSet: CONTACT_SELECTION_SET,
|
|
173
|
+
transform: transformContact,
|
|
174
|
+
buildFilter,
|
|
175
|
+
baseFilter: ACTIVE_FILTER,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Internal hook for deleted contacts
|
|
180
|
+
*/
|
|
181
|
+
const useDeletedContactsInternal = createPaginatedDataHook<ContactWithRelations, UsePaginatedContactsOptions>({
|
|
182
|
+
model: "Contact",
|
|
183
|
+
dataPropertyName: "contacts",
|
|
184
|
+
defaultPageSize: 25,
|
|
185
|
+
selectionSet: CONTACT_SELECTION_SET,
|
|
186
|
+
transform: transformContact,
|
|
187
|
+
buildFilter,
|
|
188
|
+
baseFilter: DELETED_FILTER,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Composable for fetching active (non-deleted) contacts with pagination
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* import { useActiveContacts } from '@htlkg/data/hooks';
|
|
197
|
+
*
|
|
198
|
+
* const { contacts, loading, hasMore, loadMore, refetch, setSearchFilter } = useActiveContacts({
|
|
199
|
+
* pageSize: 25,
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* // Load more when user scrolls or clicks "Load More"
|
|
203
|
+
* async function onLoadMore() {
|
|
204
|
+
* await loadMore();
|
|
205
|
+
* }
|
|
206
|
+
*
|
|
207
|
+
* // Server-side search (searches ALL contacts in database)
|
|
208
|
+
* async function onSearch(filter: any) {
|
|
209
|
+
* await setSearchFilter(filter);
|
|
210
|
+
* }
|
|
211
|
+
* ```
|
|
212
|
+
*
|
|
213
|
+
* @example With filters
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const { contacts, loading } = useActiveContacts({
|
|
216
|
+
* brandId: 'brand-123',
|
|
217
|
+
* gdprConsent: true,
|
|
218
|
+
* pageSize: 50
|
|
219
|
+
* });
|
|
220
|
+
* ```
|
|
221
|
+
*/
|
|
222
|
+
export function useActiveContacts(options: UsePaginatedContactsOptions = {}): UsePaginatedContactsReturn {
|
|
223
|
+
const result = useActiveContactsInternal(options);
|
|
224
|
+
return {
|
|
225
|
+
contacts: result.contacts as Ref<ContactWithRelations[]>,
|
|
226
|
+
loading: result.loading,
|
|
227
|
+
initialLoading: result.initialLoading,
|
|
228
|
+
loadingMore: result.loadingMore,
|
|
229
|
+
error: result.error,
|
|
230
|
+
pagination: result.pagination,
|
|
231
|
+
hasMore: result.hasMore,
|
|
232
|
+
loadMore: result.loadMore,
|
|
233
|
+
refetch: result.refetch,
|
|
234
|
+
reset: result.reset,
|
|
235
|
+
setSearchFilter: result.setSearchFilter,
|
|
236
|
+
searchFilter: result.searchFilter,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Composable for fetching deleted contacts with pagination
|
|
242
|
+
*
|
|
243
|
+
* @example
|
|
244
|
+
* ```typescript
|
|
245
|
+
* import { useDeletedContacts } from '@htlkg/data/hooks';
|
|
246
|
+
*
|
|
247
|
+
* const { contacts, loading, hasMore, loadMore, refetch } = useDeletedContacts({
|
|
248
|
+
* pageSize: 25,
|
|
249
|
+
* });
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function useDeletedContacts(options: UsePaginatedContactsOptions = {}): UsePaginatedContactsReturn {
|
|
253
|
+
const result = useDeletedContactsInternal(options);
|
|
254
|
+
return {
|
|
255
|
+
contacts: result.contacts as Ref<ContactWithRelations[]>,
|
|
256
|
+
loading: result.loading,
|
|
257
|
+
initialLoading: result.initialLoading,
|
|
258
|
+
loadingMore: result.loadingMore,
|
|
259
|
+
error: result.error,
|
|
260
|
+
pagination: result.pagination,
|
|
261
|
+
hasMore: result.hasMore,
|
|
262
|
+
loadMore: result.loadMore,
|
|
263
|
+
refetch: result.refetch,
|
|
264
|
+
reset: result.reset,
|
|
265
|
+
setSearchFilter: result.setSearchFilter,
|
|
266
|
+
searchFilter: result.searchFilter,
|
|
267
|
+
};
|
|
268
|
+
}
|