bt-core-app 0.0.0

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.
Files changed (53) hide show
  1. package/.vscode/extensions.json +3 -0
  2. package/README.md +45 -0
  3. package/index.html +13 -0
  4. package/package.json +39 -0
  5. package/public/vite.svg +1 -0
  6. package/src/assets/vue.svg +1 -0
  7. package/src/components/BT-Btn.vue +40 -0
  8. package/src/components/BT-Col.vue +36 -0
  9. package/src/components/BT-Span.vue +21 -0
  10. package/src/components/Dialog-Confirm.vue +47 -0
  11. package/src/components/Dialog-Select-Date.vue +89 -0
  12. package/src/components/Dialog-Select.vue +140 -0
  13. package/src/components/Dialog-Text.vue +59 -0
  14. package/src/composables/actions-tracker.ts +99 -0
  15. package/src/composables/actions.ts +354 -0
  16. package/src/composables/api.ts +471 -0
  17. package/src/composables/auth.ts +382 -0
  18. package/src/composables/cosmetics.ts +178 -0
  19. package/src/composables/csv.ts +198 -0
  20. package/src/composables/dates.ts +79 -0
  21. package/src/composables/demo.ts +25 -0
  22. package/src/composables/dialogs.ts +115 -0
  23. package/src/composables/document-meta.ts +40 -0
  24. package/src/composables/draggable.ts +189 -0
  25. package/src/composables/filters.ts +256 -0
  26. package/src/composables/forage.ts +49 -0
  27. package/src/composables/helpers.ts +694 -0
  28. package/src/composables/id.ts +20 -0
  29. package/src/composables/list.ts +601 -0
  30. package/src/composables/navigation.ts +214 -0
  31. package/src/composables/presets.ts +20 -0
  32. package/src/composables/pwa.ts +89 -0
  33. package/src/composables/resizable.ts +382 -0
  34. package/src/composables/rules.ts +40 -0
  35. package/src/composables/stores.ts +797 -0
  36. package/src/composables/track.ts +55 -0
  37. package/src/composables/urls.ts +11 -0
  38. package/src/core.ts +92 -0
  39. package/src/index.ts +16 -0
  40. package/src/types.ts +13 -0
  41. package/src/useApi.ts +68 -0
  42. package/src/vite-env.d.ts +1 -0
  43. package/test/api.test.ts +84 -0
  44. package/test/forage.test.ts +31 -0
  45. package/test/helpers.test.ts +231 -0
  46. package/test/navigation.test.ts +99 -0
  47. package/test/stores-last-update.test.ts +138 -0
  48. package/test/stores-session.test.ts +118 -0
  49. package/test/track.test.ts +29 -0
  50. package/test/utils.ts +15 -0
  51. package/tsconfig.json +33 -0
  52. package/tsconfig.node.json +11 -0
  53. package/vite.config.ts +19 -0
@@ -0,0 +1,382 @@
1
+ import { isLengthyArray } from '@/composables/helpers';
2
+ import { DateTime } from 'luxon'
3
+ import { type RemovableRef, useStorage } from '@vueuse/core'
4
+ import { type BTDemo } from '@/composables/demo'
5
+ import { useRouter } from 'vue-router'
6
+ import { toValue } from 'vue'
7
+
8
+ export interface AuthItem {
9
+ children?: AuthItem[]
10
+ ignoreSuspension?: boolean
11
+ permissions?: string[]
12
+ requiresAuth?: boolean
13
+ subscriptions?: string[]
14
+ }
15
+
16
+ export interface AuthSubscription {
17
+ code: string
18
+ value: number
19
+ }
20
+
21
+ export interface BaseAuthCredentials {
22
+ expiresOn?: string
23
+ isGlobalAdmin?: boolean
24
+ isLoggedIn?: boolean
25
+ isSuspended?: boolean
26
+ permissions?: string
27
+ subscriptionCode?: string
28
+ timeZone?: string
29
+ token?: string
30
+ userID?: string
31
+ userPermissions?: string[]
32
+ }
33
+
34
+ // const state = useStorage<BaseAuthCredentials>('auth-credentials', {})
35
+ // let defaultsSet = false
36
+ let defaultTimeZone = 'Australia/Melbourne'
37
+
38
+ // export type FindAuthItem = (navName?: string | AuthItem) => AuthItem | undefined
39
+ export type GetAuthUrl = (redirectPath?: string, state?: string) => string
40
+
41
+ export interface CreateAuthOptions {
42
+ defaultTimeZone?: string,
43
+ demo?: BTDemo,
44
+ /**expiry token date format. Defaults to 'd/MM/yyyy h:mm:ss a' */
45
+ expiryTokenFormat?: string
46
+ /**retrieve the auth item */
47
+ getAuthItem: (navName?: string | AuthItem) => AuthItem | null
48
+ /**retrieve the url to navigate to for the OAuth 2.0 process */
49
+ getAuthUrl: GetAuthUrl
50
+ /**sets current credentials on top of default function*/
51
+ setCredentials?: (state: RemovableRef<any>, payload: any) => void
52
+ /**suboptions */
53
+ subscriptionOptions?: AuthSubscription[]
54
+ }
55
+
56
+ export interface BTAuth {
57
+ authState: string
58
+ canEdit: (navName?: string) => boolean
59
+ canEditPermit: (permit: string) => boolean
60
+ canView: (navName?: string) => boolean
61
+ canViewPermit: (permit: string) => boolean
62
+ credentials: any
63
+ doShow: (subcodes?: string[], permissions?: string[], action?: 'view' | 'edit') => boolean
64
+ doShowByNav: (navName?: string | AuthItem, includeChildren?: boolean) => boolean
65
+ getAuthUrl: (redirectPath?: string) => string
66
+ getTimeZone: () => string
67
+ login: (redirectPath?: string) => void
68
+ logout: (navNameRedirect?: string) => void
69
+ setAuth: (jwtToken?: string) => void
70
+ tryLogin: () => boolean | undefined
71
+ }
72
+
73
+ // /**returns current timezone */
74
+ // export function useTimeZone() {
75
+ // return state.value.timeZone ?? defaultTimeZone
76
+ // }
77
+
78
+ // export function useAuthData() {
79
+ // return state.value
80
+ // }
81
+
82
+ export function createAuth(options: CreateAuthOptions): BTAuth {
83
+ const router = useRouter()
84
+ const expiryFormat = options.expiryTokenFormat ?? 'd/MM/yyyy h:mm:ss a'
85
+ const authState = useStorage<string>('auth-credentials-state', (Math.random().toString(36).substring(4, 19) + Math.random().toString(12).substring(1, 11)))
86
+ const state = useStorage<BaseAuthCredentials>('auth-credentials', {})
87
+
88
+ // if (!defaultsSet) {
89
+ // setDefaults()
90
+ // defaultsSet = true
91
+ // }
92
+
93
+ // const { endDemo, isDemoing } = useDemo()
94
+
95
+ /**can edit if navName is undefined, isGlobalAdmin, not suspended, or user permissions allow editing of this navItem */
96
+ function canEdit(navName?: string): boolean {
97
+ if (navName == null) return true
98
+
99
+ var navItem = options.getAuthItem(navName);
100
+ if (navItem == null) return false
101
+ if (navItem.requiresAuth === false) return true
102
+
103
+ const mState = toValue(state)
104
+ if (mState.isSuspended && navItem.ignoreSuspension !== true) return false
105
+ if (mState.isGlobalAdmin) return true
106
+
107
+ if (navItem.permissions != null) {
108
+ for (var ii = 0; ii < navItem.permissions.length; ii++) {
109
+ if (!canEditPermit(navItem.permissions[ii])) {
110
+ return false
111
+ }
112
+ }
113
+ }
114
+
115
+ return true;
116
+ }
117
+
118
+ /**purely tests for whether this permit is in the user's permissions list.
119
+ * Or else the user just needs the 'everything.edit' permission.
120
+ */
121
+ function canEditPermit(permit: string): boolean {
122
+ const mState = toValue(state)
123
+
124
+ if (mState.userPermissions != null) {
125
+ for (var i = 0; i < mState.userPermissions.length; i++) {
126
+ var parts = mState.userPermissions[i].split('.');
127
+ if (mState.userPermissions[i] == 'everything.edit' ||
128
+ permit == parts[0] && parts.length > 1 && parts[1] == 'edit') {
129
+ return true;
130
+ }
131
+ }
132
+ }
133
+
134
+ return false
135
+ }
136
+
137
+ /**can view if navName is undefined, isGlobalAdmin, not susepended, or user permissions allow viewing of this navItem */
138
+ function canView(navName: string | undefined): boolean {
139
+ if (navName == null) return true
140
+
141
+ var navItem = options.getAuthItem(navName);
142
+ if (navItem == null) return false
143
+ if (navItem.requiresAuth === false) return true
144
+
145
+ const mState = toValue(state)
146
+ if (mState.isSuspended && navItem.ignoreSuspension !== true) return false
147
+ if (mState.isGlobalAdmin) return true
148
+
149
+ if (navItem.permissions != null) {
150
+ for (var ii = 0; ii < navItem.permissions.length; ii++) {
151
+ if (!canViewPermit(navItem.permissions[ii])) {
152
+ return false;
153
+ }
154
+ }
155
+ }
156
+
157
+ return true;
158
+ }
159
+
160
+ /**purely tests for whether this permit is in the user's permissions list.
161
+ * Or else the user just needs the 'everything.view' permission or '[permit].edit'.
162
+ */
163
+ function canViewPermit(permit: string): boolean {
164
+ const mState = toValue(state)
165
+
166
+ if (mState.userPermissions != null) {
167
+ for (var i = 0; i < mState.userPermissions.length; i++) {
168
+ var parts = mState.userPermissions[i].split('.');
169
+ if (permit == parts[0] || permit == 'everything' ||
170
+ mState.userPermissions[i] == 'everything.view' ||
171
+ mState.userPermissions[i] == 'everything.edit') {
172
+ return true;
173
+ }
174
+ }
175
+ }
176
+
177
+ return false;
178
+ }
179
+
180
+ /**doesn't check for suspension
181
+ * if user
182
+ */
183
+ function doShow(subcodes?: string[], permissions?: string[], action?: 'view' | 'edit'): boolean {
184
+ let doShowRes = true
185
+
186
+ const mState = toValue(state)
187
+ if (mState.isGlobalAdmin) return true
188
+
189
+ if (permissions != null && permissions.length > 0) {
190
+ permissions.forEach((permit: string) => {
191
+ if (action == 'edit') {
192
+ if (!canEditPermit(permit)) {
193
+ doShowRes = false
194
+ }
195
+ }
196
+ else {
197
+ //otherwise it will be view
198
+ if (!canViewPermit(permit)) {
199
+ doShowRes = false
200
+ }
201
+ }
202
+ })
203
+ }
204
+
205
+ if (subcodes != null && subcodes.length > 0) {
206
+ if (!subcodes.some((subCode: string) => isWithinSubscription(subCode))) {
207
+ doShowRes = false
208
+ }
209
+ }
210
+
211
+ return doShowRes
212
+ }
213
+
214
+ function doShowByNav(navName?: string | AuthItem, includeChildren: boolean = false) {
215
+ var navItem = options.getAuthItem(navName);
216
+ let doShowRes = false;
217
+
218
+ if (navItem == null) return false
219
+
220
+ let subcodes = navItem.subscriptions ?? []
221
+ let permissions = navItem.permissions ?? []
222
+ let oRes = doShow(subcodes, permissions, 'view');
223
+
224
+ if (oRes || !includeChildren || !isLengthyArray(navItem.children))
225
+ return oRes
226
+
227
+ if (navItem?.children != null) {
228
+ navItem.children.forEach(child => {
229
+ if (doShowByNav(child, true)) {
230
+ doShowRes = true;
231
+ }
232
+ })
233
+ }
234
+
235
+ return doShowRes;
236
+ }
237
+
238
+ function getAuthUrl(redirectPath?: string) {
239
+ return options.getAuthUrl(redirectPath, authState.value)
240
+ }
241
+
242
+ /**undefined or unfound is worth 0 */
243
+ function getSubscriptionCodeValue(subCode?: string) {
244
+ if (subCode == null) return 0
245
+ let subList = options.subscriptionOptions ?? []
246
+ return subList.find(x => x.code == subCode)?.value ?? 0
247
+ }
248
+
249
+ /**identifies whether user's subscription level allows for this subCode */
250
+ function isWithinSubscription(subCode?: string) {
251
+ const mState = toValue(state)
252
+
253
+ if (mState.subscriptionCode == null) return false
254
+ if (mState.subscriptionCode == subCode) return true
255
+
256
+ const subLevelNeeded = getSubscriptionCodeValue(subCode)
257
+ const subCompanyLevel = getSubscriptionCodeValue(mState.subscriptionCode)
258
+
259
+ return subLevelNeeded <= subCompanyLevel
260
+ }
261
+
262
+ /**decrypts jwt token */
263
+ function jwtDecrypt(token: string) {
264
+ const base64Url = token.split('.')[1];
265
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
266
+ const jsonPayload = decodeURIComponent(
267
+ atob(base64)
268
+ .split("")
269
+ .map(function(c) {
270
+ return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2);
271
+ })
272
+ .join("")
273
+ );
274
+
275
+ return JSON.parse(jsonPayload);
276
+ }
277
+
278
+ /**clears credentials and potentially redirects */
279
+ function logout(navNameRedirect?: string) {
280
+ state.value = undefined
281
+
282
+ //ensure time zone still exists
283
+ setDefaults()
284
+
285
+ //clear stores
286
+
287
+ //maybe redirect
288
+ if (navNameRedirect != null) {
289
+ if (router.currentRoute.value.name != navNameRedirect) {
290
+ router.push({ name: navNameRedirect })
291
+ }
292
+ }
293
+ }
294
+
295
+ /**Redirects to OAuth 2.0 process if not yet logged in and token expired. Ends demo. */
296
+ function login(redirectPath?: string) {
297
+ const mState = toValue(state)
298
+
299
+ if (!mState.isLoggedIn) {
300
+ options.demo?.endDemo()
301
+ if (tokenExpired()) {
302
+ logout()
303
+ window.location.href = getAuthUrl(redirectPath) //}response_type=code&client_id=appClient1&redirect_path=${redirectPath}` : getAuthUrl()
304
+ }
305
+ }
306
+ }
307
+
308
+ /**if not demoing, then will decrypt the jwtToken and attempt to set credentials */
309
+ function setAuth(jwtToken?: string) {
310
+ if (jwtToken == null) return
311
+
312
+ if (options.demo == null || !options.demo.isDemoing.value) {
313
+ const d = jwtDecrypt(jwtToken)
314
+
315
+ setDefaultAuth(d)
316
+
317
+ if (options.setCredentials != null) {
318
+ options.setCredentials(state as RemovableRef<any>, d)
319
+ }
320
+ }
321
+ }
322
+
323
+ function setDefaultAuth(d: any) {
324
+ const v = state.value
325
+ v.expiresOn = d.ExpiresOn
326
+ v.isGlobalAdmin = d.IsGlobalAdmin == 'True'
327
+ v.isLoggedIn = true
328
+ v.permissions = d.Permissions
329
+ v.subscriptionCode = d.Subscription
330
+ // v.subscriptionsInUse = d.SubscriptionsInUse
331
+ v.token = d
332
+ v.userID = d.UserLoginID
333
+ v.userPermissions = d.Permissions?.split(',') ?? []
334
+ }
335
+
336
+ function setDefaults() {
337
+ state.value.timeZone ??= (options.defaultTimeZone ?? defaultTimeZone)
338
+ }
339
+
340
+ function getTimeZone() {
341
+ return state.value.timeZone ?? options.defaultTimeZone ?? defaultTimeZone
342
+ }
343
+
344
+ /**returns whether or not logged in. Potentially redirects to OAauth 2.0 process if logged in and expired */
345
+ function tryLogin() {
346
+ const mState = toValue(state)
347
+
348
+ //what if no internet?
349
+ if (mState.isLoggedIn && tokenExpired()) {
350
+ logout()
351
+ window.location.href = getAuthUrl()
352
+ }
353
+
354
+ return mState.isLoggedIn
355
+ }
356
+
357
+ function tokenExpired() {
358
+ const mState = toValue(state)
359
+ if (mState == null || mState.expiresOn == null) return true
360
+
361
+ const e1 = DateTime.fromFormat(mState.expiresOn, expiryFormat)
362
+ const e2 = DateTime.utc()
363
+ return e1 <= e2
364
+ }
365
+
366
+ return {
367
+ authState: authState.value,
368
+ canEdit,
369
+ canEditPermit,
370
+ canView,
371
+ canViewPermit,
372
+ credentials: state.value,
373
+ doShow,
374
+ doShowByNav,
375
+ getAuthUrl,
376
+ getTimeZone,
377
+ login,
378
+ logout,
379
+ setAuth,
380
+ tryLogin
381
+ }
382
+ }
@@ -0,0 +1,178 @@
1
+ import { useStorage } from '@vueuse/core'
2
+ import { useTheme } from 'vuetify';
3
+
4
+ interface CosmeticData {
5
+ dark?: BaseCosmeticTheme,
6
+ drawer?: boolean,
7
+ drawerStick?: boolean,
8
+ light?: BaseCosmeticTheme,
9
+ theme?: string
10
+ }
11
+
12
+ export interface BaseCosmeticTheme {
13
+ primary: string,
14
+ secondary: string,
15
+ accent: string,
16
+ error: string,
17
+ info: string,
18
+ success: string,
19
+ warning: string,
20
+ }
21
+
22
+ const defaultLight: BaseCosmeticTheme = {
23
+ primary: '#192233',
24
+ secondary: '#192233',
25
+ accent: '#2a76a2',
26
+ error: '#FF5252',
27
+ info: '#2196F3',
28
+ success: '#4CAF50',
29
+ warning: '#FB8C00'
30
+ }
31
+
32
+ const defaultDark: BaseCosmeticTheme = {
33
+ primary: '#1d5474',
34
+ secondary: '#192233',
35
+ accent: '#7fcbf7',
36
+ error: '#FF5252',
37
+ info: '#2196F3',
38
+ success: '#4CAF50',
39
+ warning: '#FB8C00'
40
+ }
41
+
42
+ export interface UseCosmeticsOptions<T extends BaseCosmeticTheme> {
43
+ defaultDarkTheme?: T
44
+ defaultLightTheme?: T
45
+ defaultDrawer?: boolean
46
+ defaultDrawerStick?: boolean
47
+ defaultTheme?: string
48
+ }
49
+
50
+ export interface BTCosmetics {
51
+ state: CosmeticData
52
+ resetCosmetics: (toDefault: boolean) => void
53
+ setTemporaryColor: (color: string) => void
54
+ toggleDrawer: () => void
55
+ toggleDrawerStick: () => void
56
+ toggleLightDark: () => void
57
+ undoTemporaryColor: () => void
58
+ }
59
+
60
+ export function createCosmetics<T extends BaseCosmeticTheme>(options: UseCosmeticsOptions<T>): BTCosmetics {
61
+ const themeManager = useTheme();
62
+ const state = useStorage<CosmeticData>('cosmetics', {
63
+ theme: options.defaultTheme ?? 'light',
64
+ light: options.defaultLightTheme ?? defaultLight,
65
+ dark: options.defaultDarkTheme ?? defaultDark,
66
+ drawer: options.defaultDrawer ?? true,
67
+ drawerStick: options.defaultDrawerStick ?? false
68
+ })
69
+
70
+ // if (!defaultsSet) {
71
+ // state.value.theme ??= (options.defaultTheme ?? 'light')
72
+ // state.value.light ??= (options.defaultLightTheme ?? defaultLight)
73
+ // state.value.dark ??= (options.defaultDarkTheme ?? defaultDark)
74
+ // state.value.drawer ??= (options.defaultDrawer ?? true)
75
+ // state.value.drawerStick ??= (options.defaultDrawerStick ?? false)
76
+ // defaultsSet = true
77
+ // }
78
+
79
+ /**used for initiating the themes and colors from storage when the web app is loaded */
80
+ // function loadCosmetics() {
81
+ themeManager.global.name.value = state.value.theme!
82
+
83
+ //load dark colors
84
+ let darkColors = themeManager.themes.value.dark.colors
85
+ let storedDarkTheme = state.value.dark!
86
+ const darkThemeKeys = Object.keys(storedDarkTheme)
87
+ darkThemeKeys.forEach(darkKey => {
88
+ const k = darkKey as keyof typeof storedDarkTheme
89
+ darkColors[darkKey] = storedDarkTheme[k]
90
+ })
91
+
92
+ let lightColors = themeManager.themes.value.light.colors
93
+ let storedLightTheme = state.value.light!
94
+ const lightThemeKeys = Object.keys(storedLightTheme)
95
+ lightThemeKeys.forEach(lightKey => {
96
+ const lKey = lightKey as keyof typeof storedLightTheme
97
+ lightColors[lKey] = storedLightTheme[lKey]
98
+ })
99
+ // }
100
+
101
+ /**resets to the default colors or last saved colors of the current theme */
102
+ function resetCosmetics(toDefault: boolean) {
103
+
104
+ themeManager.global.name.value = state.value.theme!
105
+
106
+ if (themeManager.global.name.value == 'dark') {
107
+ //dark
108
+ if (toDefault)
109
+ state.value.dark = { ...(options.defaultDarkTheme ?? defaultDark) };
110
+
111
+ themeManager.themes.value.dark = {
112
+ ...themeManager.themes.value.dark,
113
+ colors: {
114
+ ...themeManager.themes.value.dark.colors,
115
+ ...state.value.dark
116
+ }
117
+ }
118
+ }
119
+ else {
120
+ //light
121
+ if (toDefault)
122
+ state.value.light = { ...(options.defaultLightTheme ?? defaultLight) };
123
+
124
+ themeManager.themes.value.light = {
125
+ ...themeManager.themes.value.light,
126
+ colors: {
127
+ ...themeManager.themes.value.light.colors,
128
+ ...state.value.light
129
+ }
130
+ }
131
+ }
132
+ }
133
+
134
+ /**sets a temporary primary color that is not saved but is applied to cosmetics */
135
+ function setTemporaryColor(color: string) {
136
+ const dark = themeManager.themes.value.dark
137
+ const light = themeManager.themes.value.light
138
+
139
+ dark.colors.primary = color
140
+ light.colors.primary = color
141
+
142
+ document.querySelector('meta[name="theme-color"]')?.setAttribute("content", color)
143
+ }
144
+
145
+ function toggleDrawer() {
146
+ state.value.drawer = !state.value.drawer
147
+ }
148
+
149
+ function toggleDrawerStick() {
150
+ state.value.drawerStick = !state.value.drawerStick
151
+ }
152
+
153
+ function toggleLightDark() {
154
+ state.value.theme = state.value.theme == 'dark' ? 'light' : 'dark'
155
+ themeManager.global.name.value = state.value.theme
156
+ }
157
+
158
+ function undoTemporaryColor() {
159
+ const dark = themeManager.themes.value.dark
160
+ const light = themeManager.themes.value.light
161
+
162
+ dark.colors.primary = (state.value.dark ?? options.defaultDarkTheme ?? defaultDark).primary
163
+ light.colors.primary = (state.value.light ?? options.defaultLightTheme ?? defaultLight).primary
164
+
165
+ document.querySelector('meta[name="theme-color"]')?.setAttribute("content", '')
166
+ }
167
+
168
+ return {
169
+ state: state.value,
170
+ // loadCosmetics,
171
+ resetCosmetics,
172
+ setTemporaryColor,
173
+ toggleDrawer,
174
+ toggleDrawerStick,
175
+ toggleLightDark,
176
+ undoTemporaryColor
177
+ }
178
+ }