@7365admin1/layer-common 1.8.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 (198) hide show
  1. package/.changeset/README.md +8 -0
  2. package/.changeset/config.json +11 -0
  3. package/.editorconfig +12 -0
  4. package/.github/workflows/main.yml +17 -0
  5. package/.github/workflows/publish.yml +39 -0
  6. package/.nuxtrc +1 -0
  7. package/.playground/app.vue +41 -0
  8. package/.playground/eslint.config.mjs +6 -0
  9. package/.playground/nuxt.config.ts +22 -0
  10. package/.playground/pages/feedback.vue +30 -0
  11. package/CHANGELOG.md +263 -0
  12. package/README.md +73 -0
  13. package/app.vue +3 -0
  14. package/components/AccessCardAddForm.vue +363 -0
  15. package/components/AccessManagement.vue +420 -0
  16. package/components/Avatar/Main.vue +68 -0
  17. package/components/BillingMain.vue +66 -0
  18. package/components/BtnUploadFile.vue +139 -0
  19. package/components/BuildingForm.vue +303 -0
  20. package/components/BuildingManagement/buildings.vue +335 -0
  21. package/components/BuildingManagement/units.vue +350 -0
  22. package/components/BuildingUnitFormAdd.vue +441 -0
  23. package/components/BuildingUnitFormEdit.vue +429 -0
  24. package/components/CameraForm.vue +264 -0
  25. package/components/CameraMain.vue +352 -0
  26. package/components/Card/DeleteConfirmation.vue +51 -0
  27. package/components/Card/MemberInfoSummary.vue +44 -0
  28. package/components/Card/Toggle.vue +25 -0
  29. package/components/Chat/Bubbles.vue +53 -0
  30. package/components/Chat/Information.vue +416 -0
  31. package/components/Chat/ListCard.vue +62 -0
  32. package/components/Chat/Message.vue +158 -0
  33. package/components/Chat/Navigation.vue +150 -0
  34. package/components/ConfirmDialog.vue +66 -0
  35. package/components/Container/Standard.vue +33 -0
  36. package/components/DashboardPlaceholder.vue +1524 -0
  37. package/components/Dialog/DeleteConfirmation.vue +51 -0
  38. package/components/Dialog/ReplaceAutofillPrompt.vue +49 -0
  39. package/components/Dialog/UpdateMoreAction.vue +103 -0
  40. package/components/DocumentForm.vue +187 -0
  41. package/components/DocumentManagement.vue +376 -0
  42. package/components/Editor.vue +95 -0
  43. package/components/EntryPassMain.vue +518 -0
  44. package/components/Feedback/Form.vue +173 -0
  45. package/components/FeedbackDetail.vue +599 -0
  46. package/components/FeedbackMain.vue +588 -0
  47. package/components/FormDialog.vue +65 -0
  48. package/components/ImageCarousel.vue +138 -0
  49. package/components/Input/Date.vue +177 -0
  50. package/components/Input/DateTimePicker.vue +131 -0
  51. package/components/Input/File.vue +236 -0
  52. package/components/Input/FileV2.vue +234 -0
  53. package/components/Input/InputPhoneNumberV2.vue +164 -0
  54. package/components/Input/ListGroupSelection.vue +96 -0
  55. package/components/Input/NRICNumber.vue +53 -0
  56. package/components/Input/NewDate.vue +123 -0
  57. package/components/Input/Number.vue +124 -0
  58. package/components/Input/Password.vue +22 -0
  59. package/components/Input/PhoneNumber.vue +188 -0
  60. package/components/Input/VehicleNumber.vue +49 -0
  61. package/components/InputLabel.vue +22 -0
  62. package/components/InvitationForm.vue +359 -0
  63. package/components/InvitationMain.vue +310 -0
  64. package/components/Layout/Header.vue +129 -0
  65. package/components/Layout/NavigationDrawer.vue +44 -0
  66. package/components/ListItem.vue +35 -0
  67. package/components/ListView.vue +87 -0
  68. package/components/LocalPagination.vue +31 -0
  69. package/components/MemberMain.vue +459 -0
  70. package/components/NFC/NFCPatrolReportMain.vue +591 -0
  71. package/components/NFC/NFCPatrolRouteForm.vue +596 -0
  72. package/components/NFC/NFCPatrolRouteMain.vue +539 -0
  73. package/components/NFC/NFCTagForm.vue +236 -0
  74. package/components/NFC/NFCTagMain.vue +337 -0
  75. package/components/NFC/PatrolSettings.vue +130 -0
  76. package/components/NavigationItem.vue +83 -0
  77. package/components/NumberSettingField.vue +107 -0
  78. package/components/OnlineFormConfigurationForm.vue +290 -0
  79. package/components/OnlineFormsConfiguration.vue +429 -0
  80. package/components/PeopleForm.vue +452 -0
  81. package/components/PlaceholderComponent.vue +34 -0
  82. package/components/RolePermissionFormCreate.vue +161 -0
  83. package/components/RolePermissionFormPreviewUpdate.vue +183 -0
  84. package/components/RolePermissionMain.vue +361 -0
  85. package/components/SearchVehicleNumberUser.vue +91 -0
  86. package/components/ServiceProviderFormCreate.vue +154 -0
  87. package/components/ServiceProviderMain.vue +547 -0
  88. package/components/SignaturePad.vue +73 -0
  89. package/components/Snackbar.vue +23 -0
  90. package/components/SpecificAttr.vue +53 -0
  91. package/components/SupplyManagement.vue +292 -0
  92. package/components/SwitchContext.vue +108 -0
  93. package/components/TableList.vue +150 -0
  94. package/components/TableListSecondary.vue +164 -0
  95. package/components/TableMain.vue +142 -0
  96. package/components/TableWithButton.vue +94 -0
  97. package/components/VehicleUpdateMoreAction.vue +84 -0
  98. package/components/VideoPlayer.vue +125 -0
  99. package/components/VisitorForm.vue +659 -0
  100. package/components/VisitorFormSelection.vue +53 -0
  101. package/components/VisitorManagement.vue +490 -0
  102. package/components/WorkOrder/Create.vue +284 -0
  103. package/components/WorkOrder/Detail.vue +71 -0
  104. package/components/WorkOrder/ListView.vue +96 -0
  105. package/components/WorkOrder/Main.vue +489 -0
  106. package/components/Workorder.vue +1 -0
  107. package/composables/useAddress.ts +107 -0
  108. package/composables/useBuilding.ts +250 -0
  109. package/composables/useBuildingUnit.ts +116 -0
  110. package/composables/useCard.ts +46 -0
  111. package/composables/useCommonPermission.ts +207 -0
  112. package/composables/useCustomer.ts +113 -0
  113. package/composables/useCustomerSite.ts +56 -0
  114. package/composables/useDashboard.ts +31 -0
  115. package/composables/useDashboardData.ts +425 -0
  116. package/composables/useDocument.ts +57 -0
  117. package/composables/useFacility.ts +246 -0
  118. package/composables/useFeedback.ts +119 -0
  119. package/composables/useFile.ts +55 -0
  120. package/composables/useInvoice.ts +18 -0
  121. package/composables/useLocal.ts +131 -0
  122. package/composables/useLocalAuth.ts +137 -0
  123. package/composables/useLocalSetup.ts +13 -0
  124. package/composables/useMember.ts +111 -0
  125. package/composables/useNFCPatrolRoute.ts +77 -0
  126. package/composables/useNFCPatrolSettings.ts +19 -0
  127. package/composables/useNFCPatrolTag.ts +53 -0
  128. package/composables/useOnlineForm.ts +67 -0
  129. package/composables/useOrg.ts +129 -0
  130. package/composables/usePDFDownload.ts +25 -0
  131. package/composables/usePaymentMethod.ts +101 -0
  132. package/composables/usePeople.ts +81 -0
  133. package/composables/usePermission.ts +54 -0
  134. package/composables/usePhoneCountries.ts +561 -0
  135. package/composables/usePrice.ts +15 -0
  136. package/composables/usePromoCode.ts +36 -0
  137. package/composables/useRecapPermission.ts +26 -0
  138. package/composables/useRole.ts +104 -0
  139. package/composables/useSecurityUtils.ts +18 -0
  140. package/composables/useServiceProvider.ts +224 -0
  141. package/composables/useSite.ts +109 -0
  142. package/composables/useSiteEntryPassSettings.ts +46 -0
  143. package/composables/useSiteSettings.ts +123 -0
  144. package/composables/useSubscription.ts +150 -0
  145. package/composables/useUser.ts +132 -0
  146. package/composables/useUtils.ts +445 -0
  147. package/composables/useVerification.ts +34 -0
  148. package/composables/useVisitor.ts +120 -0
  149. package/composables/useWorkOrder.ts +85 -0
  150. package/error.vue +41 -0
  151. package/layouts/plain.vue +7 -0
  152. package/middleware/01.auth.ts +20 -0
  153. package/middleware/02.org.ts +21 -0
  154. package/middleware/03.customer.ts +13 -0
  155. package/middleware/member.ts +4 -0
  156. package/nuxt.config.ts +54 -0
  157. package/package.json +39 -0
  158. package/pages/index.vue +3 -0
  159. package/pages/payment-method-linked.vue +31 -0
  160. package/pages/require-customer.vue +56 -0
  161. package/pages/require-organization-membership.vue +47 -0
  162. package/pages/unauthorized.vue +29 -0
  163. package/plugins/API.ts +21 -0
  164. package/plugins/iconify.client.ts +5 -0
  165. package/plugins/secure-member.client.ts +86 -0
  166. package/plugins/vuetify.ts +62 -0
  167. package/public/bg-camera.jpg +0 -0
  168. package/public/bg-city.jpg +0 -0
  169. package/public/bg-condo.jpg +0 -0
  170. package/public/images/icons/delete-icon.png +0 -0
  171. package/public/sprite.svg +1 -0
  172. package/tsconfig.json +3 -0
  173. package/types/address.d.ts +13 -0
  174. package/types/building.d.ts +27 -0
  175. package/types/camera.d.ts +31 -0
  176. package/types/card.d.ts +22 -0
  177. package/types/customer.d.ts +27 -0
  178. package/types/document.d.ts +6 -0
  179. package/types/feedback.d.ts +68 -0
  180. package/types/local.d.ts +74 -0
  181. package/types/member.d.ts +21 -0
  182. package/types/online-form.d.ts +15 -0
  183. package/types/org.d.ts +13 -0
  184. package/types/people.d.ts +24 -0
  185. package/types/permission.d.ts +25 -0
  186. package/types/phone-number.d.ts +10 -0
  187. package/types/price.d.ts +17 -0
  188. package/types/promo-code.d.ts +19 -0
  189. package/types/role.d.ts +11 -0
  190. package/types/select.d.ts +4 -0
  191. package/types/service-provider.d.ts +15 -0
  192. package/types/site.d.ts +20 -0
  193. package/types/subscription.d.ts +23 -0
  194. package/types/user.d.ts +19 -0
  195. package/types/verification.d.ts +20 -0
  196. package/types/visitor.d.ts +42 -0
  197. package/types/work-order.d.ts +42 -0
  198. package/utils/phoneMasks.ts +1703 -0
@@ -0,0 +1,234 @@
1
+ <template>
2
+ <v-row no-gutters class="w-100 pb-5" @click="resetErrorMessage">
3
+
4
+ <!-- VIEW MODE -->
5
+ <template v-if="viewMode">
6
+ <div class="w-100">
7
+ <v-file-upload-item v-for="({ file }, idx) in filesCollection" :key="fileKey(file)" :file="file" lines="one" nav
8
+ @click="handleClickItem(file)">
9
+ <template #prepend>
10
+ <v-avatar size="32" rounded></v-avatar>
11
+ </template>
12
+
13
+ <!-- delete hidden in view mode -->
14
+ <template #clear>
15
+ <!-- empty on purpose -->
16
+ </template>
17
+ </v-file-upload-item>
18
+ </div>
19
+ </template>
20
+
21
+ <!-- NORMAL MODE -->
22
+ <template v-else>
23
+ <v-file-upload v-model="uploadFiles" density="compact" @update:model-value="handleUpdateValue"
24
+ :loading="processing" :disabled="processing" :height="height" :title="title" :accept="accept"
25
+ :name="`upload_images`" class="text-caption w-100" clearable :multiple="multiple">
26
+ <template #item="{ props: itemProps, file }">
27
+ <v-file-upload-item v-bind="itemProps" lines="one" nav @click="handleClickItem(file)">
28
+ <template #prepend>
29
+ <v-avatar size="32" rounded></v-avatar>
30
+ </template>
31
+
32
+ <!-- delete button NOT shown in view mode -->
33
+ <template #clear="{ props: clearProps }">
34
+ <v-btn v-if="!viewMode" color="primary" @click.stop="handleRemove(file)"></v-btn>
35
+ </template>
36
+ </v-file-upload-item>
37
+ </template>
38
+ </v-file-upload>
39
+ </template>
40
+
41
+ <v-row no-gutters class="w-100" v-if="errorMessage">
42
+ <p class="text-error w-100 text-center text-subtitle-2">{{ errorMessage }}</p>
43
+ </v-row>
44
+
45
+ <ImageCarousel v-model="showImageCarousel" :files="idsArray" :active-file-id="activeImageId" />
46
+ </v-row>
47
+ </template>
48
+
49
+
50
+ <script setup lang="ts">
51
+ import { nextTick, ref, onMounted, watch } from 'vue'
52
+
53
+ const props = defineProps({
54
+ height: {
55
+ type: [Number, String],
56
+ default: 68,
57
+ },
58
+ multiple: {
59
+ type: Boolean,
60
+ default: false,
61
+ },
62
+ maxLength: {
63
+ type: Number,
64
+ default: 10,
65
+ },
66
+ title: {
67
+ type: String,
68
+ default: 'Upload Images'
69
+ },
70
+ accept: {
71
+ type: String,
72
+ default: "image/*"
73
+ },
74
+ viewMode: {
75
+ type: Boolean,
76
+ default: false
77
+ }
78
+ })
79
+
80
+ const { addFile, deleteFile, getFileUrl, urlToFile } = useFile()
81
+
82
+ const showImageCarousel = ref(false)
83
+ const activeImageId = ref("")
84
+
85
+ // The parent v-model binding
86
+ const idsArray = defineModel<string[]>({ default: [] })
87
+
88
+ const uploadFiles = ref<File[]>([])
89
+ const filesCollection = ref<{ file: File; id: string }[]>([])
90
+ const errorMessage = ref('')
91
+ const processing = ref(false)
92
+
93
+ function fileKey(f: File) {
94
+ return `${f.name}_${f.size}_${f.lastModified}`
95
+ }
96
+
97
+ async function handleRemove(removedFile: File) {
98
+ const key = fileKey(removedFile)
99
+ const arr = [...filesCollection.value]
100
+
101
+ const removedItem = arr.find((item) => fileKey(item.file) === key)
102
+ if (!removedItem) return
103
+
104
+ filesCollection.value = arr.filter((item) => item.id !== removedItem.id)
105
+ uploadFiles.value = uploadFiles.value.filter((f) => fileKey(f) !== key)
106
+
107
+
108
+ idsArray.value = filesCollection.value.map((x) => x.id)
109
+
110
+ }
111
+
112
+ async function handleClickItem(file: File) {
113
+ const isImageOrVideo =
114
+ file.type.startsWith('image/') ||
115
+ /\.(jpg|jpeg|png|gif|webp|bmp|svg|mp4|mov|mkv|avi)$/i.test(file.name)
116
+
117
+
118
+ const found = filesCollection.value.find((item) => fileKey(item.file) === fileKey(file))
119
+ if (!found) return
120
+
121
+
122
+ if (isImageOrVideo) {
123
+ activeImageId.value = found.id
124
+ showImageCarousel.value = true
125
+ return;
126
+ }
127
+
128
+ await downloadFile(found.id, file.name)
129
+ }
130
+
131
+ async function downloadFile(id: string, filename: string) {
132
+ try {
133
+ const url = await getFileUrl(id)
134
+ const link = document.createElement('a')
135
+ link.href = url
136
+ link.download = filename
137
+ link.target = '_blank'
138
+
139
+ document.body.appendChild(link)
140
+ link.click()
141
+ document.body.removeChild(link)
142
+ } catch (err) {
143
+ console.error('Failed to download file', err)
144
+ errorMessage.value = 'Failed to download file.'
145
+ }
146
+ }
147
+
148
+
149
+
150
+
151
+
152
+ async function handleUpdateValue(value: File[]) {
153
+ await nextTick()
154
+ const max = props.maxLength
155
+ const existingLength = filesCollection.value.length
156
+ errorMessage.value = ''
157
+
158
+ if (existingLength + value.length > max) {
159
+ value = value.slice(0, max - existingLength)
160
+ errorMessage.value = `Max allowed images is ${max}`
161
+ }
162
+
163
+ const collectionKeys = filesCollection.value.map((x) => fileKey(x.file))
164
+ const addedFiles = value.filter((f) => !collectionKeys.includes(fileKey(f)))
165
+
166
+ processing.value = true
167
+ try {
168
+ for (const file of addedFiles) {
169
+ const res = await addFile(file) // should return { id, url }
170
+ if (res?.id) {
171
+ filesCollection.value = [...filesCollection.value, { file, id: res.id }]
172
+ }
173
+ }
174
+
175
+ uploadFiles.value = filesCollection.value.map((x) => x.file)
176
+
177
+ } catch (err) {
178
+ console.error('Upload failed', err)
179
+ errorMessage.value = 'Failed to upload some files.'
180
+ } finally {
181
+ processing.value = false
182
+ }
183
+ }
184
+
185
+
186
+ async function loadFilesFromIds(ids: string[]) {
187
+ const result: { file: File; id: string }[] = []
188
+ for (const id of ids) {
189
+ try {
190
+ const url = await getFileUrl(id)
191
+ const name = decodeURIComponent(url.split('/').pop() || `file_${id}`)
192
+ const file = await urlToFile(url, name)
193
+ result.push({ file, id })
194
+ } catch (err) {
195
+ console.warn('Failed to load file from ID:', id, err)
196
+ }
197
+ }
198
+ return result
199
+ }
200
+
201
+
202
+ function resetErrorMessage() {
203
+ errorMessage.value = ''
204
+ }
205
+
206
+
207
+ onMounted(async () => {
208
+ setTimeout(async () => {
209
+ if (idsArray.value.length > 0) {
210
+ const loadedFiles = await loadFilesFromIds(idsArray.value)
211
+ filesCollection.value = loadedFiles
212
+ uploadFiles.value = loadedFiles.map((x) => x.file)
213
+ }
214
+ }, 200)
215
+ })
216
+
217
+ watch(filesCollection, () => {
218
+ idsArray.value = [...filesCollection.value.map(x => x.id)]
219
+ }, { deep: true })
220
+
221
+
222
+ </script>
223
+
224
+
225
+ <style scoped>
226
+ * :deep(.v-file-upload-title) {
227
+ font-size: 1rem;
228
+ font-weight: 500;
229
+ }
230
+
231
+ * :deep(.v-file-upload-items) {
232
+ min-width: 100%;
233
+ }
234
+ </style>
@@ -0,0 +1,164 @@
1
+ <template>
2
+ <v-row no-gutters class="mb-5">
3
+ <v-col cols="12" class="d-flex ga-2">
4
+ <v-select v-model="selectedCode" :variant="variant" :items="countries" item-title="code" item-value="code"
5
+ hide-details class="px-0" :density="density" style="max-width: 95px" :rules="[...props.rules]"
6
+ :readonly="props.readOnly" @update:model-value="handleUpdateCountry">
7
+ <template v-slot:item="{ props: itemProps, item }">
8
+ <v-list-item v-bind="itemProps" :title="item.raw.name" :subtitle="item.raw.dial_code" width="300" />
9
+ </template>
10
+ </v-select>
11
+ <v-mask-input v-model="input" :mask="currentMask" :rules="[...props.rules, validatePhone]" ref="maskRef" :key="`mask-key-${maskKey}`"
12
+ :loading="loading" :readonly="props.readOnly" :variant="variant" hint="Enter a valid phone number"
13
+ hide-details persistent-hint return-masked-value :prefix="phonePrefix || '###'" persistent-placeholder
14
+ :density="density" :placeholder="placeholder || currentMask"></v-mask-input>
15
+ </v-col>
16
+ <span class="text-error text-caption w-100" v-if="errorMessage && !hideDetails">{{ errorMessage }}</span>
17
+ </v-row>
18
+
19
+ </template>
20
+
21
+ <script setup lang="ts">
22
+ import { ref, computed, type PropType } from 'vue'
23
+ import type { ValidationRule } from 'vuetify/lib/types.mjs'
24
+ //@ts-ignore
25
+ import phoneMasks from '~/utils/phoneMasks'
26
+
27
+ const props = defineProps({
28
+ rules: {
29
+ type: Array as PropType<ValidationRule[]>,
30
+ default: []
31
+ },
32
+ variant: {
33
+ type: String as PropType<any>,
34
+ default: "outlined"
35
+ },
36
+ density: {
37
+ type: String as PropType<Density>,
38
+ default: "default"
39
+ },
40
+ placeholder: {
41
+ type: String,
42
+ },
43
+ hideDetails: {
44
+ type: Boolean,
45
+ default: false
46
+ },
47
+ loading: {
48
+ type: Boolean,
49
+ default: false
50
+ },
51
+ readOnly: {
52
+ type: Boolean,
53
+ default: false
54
+ }
55
+ })
56
+
57
+ type Density = 'default' | 'comfortable' | 'compact';
58
+
59
+ type TPhoneMask =
60
+ {
61
+ name: string
62
+ flag: string
63
+ code: string;
64
+ dial_code: string;
65
+ regex: string;
66
+ }
67
+
68
+ const phone = defineModel({ default: '' })
69
+ const input = ref('')
70
+ const selectedCode = ref('SG')
71
+ const countries = phoneMasks
72
+ const errorMessage = ref('')
73
+ const maskRef = ref()
74
+ const maskKey = ref(0)
75
+
76
+ const currentMask = computed(() => {
77
+ const country = phoneMasks.find((c: TPhoneMask) => c.code === selectedCode.value)
78
+ if (!country) return '############'
79
+ return generateMaskFromRegex(country.regex)
80
+ })
81
+
82
+
83
+
84
+ const validatePhone = (): boolean | string => {
85
+ if(props.readOnly) return true;
86
+ const value = phone.value
87
+ if (!value) {
88
+ errorMessage.value = ''
89
+ return true;
90
+ }
91
+ const country = phoneMasks.find((c: any) => c.code === selectedCode.value)
92
+ if (!country) {
93
+ errorMessage.value = ''
94
+ return true
95
+ }
96
+
97
+ const regex = new RegExp(country.regex)
98
+ const isValid = regex.test(value)
99
+
100
+ errorMessage.value = isValid ? '' : `Invalid ${country.name} phone number`
101
+ return isValid
102
+ }
103
+
104
+
105
+
106
+ function generateMaskFromRegex(regex: string): string {
107
+ let pattern = regex.replace(/^\^|\$$/g, '');
108
+
109
+ pattern = pattern.replace(/\(\?:\+?\d+\)\?/g, '');
110
+ pattern = pattern.replace(/\+?\d{1,4}/, '');
111
+
112
+ pattern = pattern.replace(/\\d\{(\d+)\}/g, (_, count) => '#'.repeat(Number(count)));
113
+
114
+ pattern = pattern.replace(/\\d/g, '#');
115
+
116
+ pattern = pattern.replace(/\\/g, '');
117
+ pattern = pattern.replace(/\(\?:/g, '');
118
+ pattern = pattern.trim();
119
+
120
+ return pattern;
121
+ }
122
+
123
+ const phonePrefix = computed(() => {
124
+ const country = phoneMasks.find((c: TPhoneMask) => c.code === selectedCode.value)
125
+ return country?.dial_code || ''
126
+ })
127
+
128
+
129
+ function handleUpdateCountry() {
130
+ phone.value = ''
131
+ }
132
+
133
+ const emit = defineEmits(['update:modelValue'])
134
+
135
+ watch(phone, (newVal) => {
136
+ emit('update:modelValue', newVal)
137
+ })
138
+
139
+ watch(input, (newInput) => {
140
+ const prefix = phonePrefix.value
141
+
142
+ if (!newInput) {
143
+ return
144
+ }
145
+
146
+ phone.value = prefix + newInput
147
+ })
148
+
149
+
150
+ onMounted(() => {
151
+ if (!phone.value) return
152
+
153
+ const found = phoneMasks.find((c: any) => phone.value?.startsWith(c?.dial_code))
154
+ if (found) {
155
+ selectedCode.value = found.code
156
+ }
157
+
158
+ input.value = phone.value.replace(found?.dial_code || '', '')
159
+ maskKey.value++
160
+ })
161
+
162
+
163
+
164
+ </script>
@@ -0,0 +1,96 @@
1
+ <template>
2
+ <v-input v-bind="attrs">
3
+ <v-card width="100%" v-bind="attrs">
4
+ <v-list
5
+ v-model:selected="selected"
6
+ lines="two"
7
+ :select-strategy="attrs.readonly ? 'classic' : 'leaf'"
8
+ class="pa-0"
9
+ density="compact"
10
+ read-only
11
+ open-strategy="single"
12
+ >
13
+ <template
14
+ v-for="(permission, permissionKey, permissionIndex) in props.items"
15
+ :key="permissionKey"
16
+ >
17
+ <v-divider v-if="permissionIndex > 0"></v-divider>
18
+ <v-list-group :value="permissionKey" fluid>
19
+ <template v-slot:activator="{ props }">
20
+ <v-list-item v-bind="props" density="compact">
21
+ <span class="text-capitalize">
22
+ {{ String(permissionKey).replace(/-/g, " ") }}
23
+ </span>
24
+
25
+ <template #prepend>
26
+ <v-chip class="mr-2" small>
27
+ {{ selectedActionCount(String(permissionKey)) }}
28
+ </v-chip>
29
+ </template>
30
+ </v-list-item>
31
+ </template>
32
+
33
+ <template v-for="(item, itemKey) in permission" :key="itemKey">
34
+ <v-divider></v-divider>
35
+ <v-list-item v-if="attrs.readonly" density="compact">
36
+ <template #title class="pl-2">
37
+ <span class="text-subtitle-2 text-capitalize">
38
+ {{ String(itemKey).replace(/-/g, " ") }}
39
+ </span>
40
+ </template>
41
+
42
+ <template #subtitle class="pl-2">
43
+ <span class="text-subtitle-2">{{ item.description }}</span>
44
+ </template>
45
+ </v-list-item>
46
+
47
+ <v-list-item
48
+ v-else
49
+ :value="`${permissionKey}:${itemKey}`"
50
+ density="compact"
51
+ >
52
+ <template #title class="pl-2">
53
+ <span class="text-subtitle-2 text-capitalize">
54
+ {{ String(itemKey).replace(/-/g, " ") }}
55
+ </span>
56
+ </template>
57
+
58
+ <template #subtitle class="pl-2">
59
+ <span class="text-subtitle-2 text-capitalize">
60
+ {{ String(item.description).replace(/-/g, " ") }}
61
+ </span>
62
+ </template>
63
+
64
+ <template #prepend="{ isSelected, select }" class="pl-1">
65
+ <v-list-item-action start>
66
+ <v-checkbox-btn
67
+ :model-value="isSelected"
68
+ @update:model-value="select"
69
+ ></v-checkbox-btn>
70
+ </v-list-item-action>
71
+ </template>
72
+ </v-list-item>
73
+ </template>
74
+ </v-list-group>
75
+ </template>
76
+ </v-list>
77
+ </v-card>
78
+ </v-input>
79
+ </template>
80
+
81
+ <script setup lang="ts">
82
+ const selected = defineModel<Array<string>>({ default: [] });
83
+ const attrs = useAttrs();
84
+ const props = defineProps({
85
+ items: {
86
+ type: Object,
87
+ required: true,
88
+ default: () => ({}),
89
+ },
90
+ });
91
+
92
+ const selectedActionCount = (resource: string) => {
93
+ return selected.value.filter((permission) => permission.startsWith(resource))
94
+ .length;
95
+ };
96
+ </script>
@@ -0,0 +1,53 @@
1
+ <template>
2
+ <v-text-field
3
+ v-model.trim="model"
4
+ :rules="rules"
5
+ :maxlength="maxlength"
6
+ :placeholder="placeholder"
7
+ :counter="maxlength"
8
+ @input="onInput"
9
+ :loading="loading"
10
+ outlined
11
+ clearable
12
+ />
13
+ </template>
14
+
15
+ <script setup>
16
+
17
+ const props = defineProps({
18
+ placeholder: {
19
+ type: String,
20
+ default: ''
21
+ },
22
+ rules: {
23
+ type: Array,
24
+ default: () => []
25
+ },
26
+ maxlength: {
27
+ type: [Number, String],
28
+ default: false
29
+ },
30
+ loading: {
31
+ type: Boolean,
32
+ default: false
33
+ }
34
+ })
35
+
36
+ const emit = defineEmits(['update:modelValue'])
37
+
38
+ const model = defineModel({required: true})
39
+
40
+ function onInput(event) {
41
+ const value = typeof event === 'string' ? event : event?.target?.value || ''
42
+
43
+ let formatted = value.replace(/[^A-Za-z0-9]/g, '')
44
+ formatted = formatted.toUpperCase()
45
+
46
+ model.value = formatted || ""
47
+ }
48
+
49
+
50
+ watch(model, (newVal) => {
51
+ emit('update:modelValue', newVal)
52
+ })
53
+ </script>
@@ -0,0 +1,123 @@
1
+ <script setup lang="ts">
2
+ import { defineModel, defineProps, computed, ref } from "vue";
3
+
4
+ const props = defineProps<{
5
+ rules?: ((value: string) => boolean | string)[]; // Accepts external validation rules
6
+ }>();
7
+
8
+ const dateValue = defineModel<string>({ default: "" });
9
+ const inputRef = ref<HTMLInputElement | null>(null);
10
+
11
+ const formatDate = (event: Event) => {
12
+ const input = event.target as HTMLInputElement;
13
+ let value = input.value.replace(/\D/g, ""); // Remove non-numeric characters
14
+
15
+ // Format as MM/DD/YYYY
16
+ let formattedValue = value
17
+ .slice(0, 8)
18
+ .replace(/(\d{2})(\d{0,2})?(\d{0,4})?/, (_, m, d, y) =>
19
+ [m, d, y].filter(Boolean).join("/")
20
+ );
21
+
22
+ // Preserve cursor position
23
+ const cursorPosition = input.selectionStart ?? 0;
24
+ const slashCountBefore = (dateValue.value.match(/\//g) || []).length;
25
+ const slashCountAfter = (formattedValue.match(/\//g) || []).length;
26
+ const cursorOffset = slashCountAfter - slashCountBefore;
27
+
28
+ // Only update if value changed to prevent unnecessary reactivity updates
29
+ if (dateValue.value !== formattedValue) {
30
+ dateValue.value = formattedValue;
31
+ setTimeout(() => {
32
+ input.setSelectionRange(
33
+ cursorPosition + cursorOffset,
34
+ cursorPosition + cursorOffset
35
+ );
36
+ });
37
+ }
38
+ };
39
+
40
+ // Compute combined validation rules
41
+ const computedRules = computed(() => {
42
+ return props.rules ? [...props.rules] : [];
43
+ });
44
+
45
+ // Handle arrow key increments with cursor preservation
46
+ const handleArrowKeys = (event: KeyboardEvent) => {
47
+ if (!dateValue.value) return;
48
+
49
+ const input = event.target as HTMLInputElement;
50
+ const cursorPosition = input.selectionStart ?? 0; // Store cursor position
51
+ dateValue.value.split("/").map(Number);
52
+
53
+ let updatedDate = dateValue.value;
54
+
55
+ // Determine which part to modify
56
+ if (cursorPosition <= 2) {
57
+ updatedDate = modifyDatePart(
58
+ dateValue.value,
59
+ "month",
60
+ event.key === "ArrowUp" ? 1 : -1
61
+ );
62
+ } else if (cursorPosition <= 5) {
63
+ updatedDate = modifyDatePart(
64
+ dateValue.value,
65
+ "day",
66
+ event.key === "ArrowUp" ? 1 : -1
67
+ );
68
+ } else {
69
+ updatedDate = modifyDatePart(
70
+ dateValue.value,
71
+ "year",
72
+ event.key === "ArrowUp" ? 1 : -1
73
+ );
74
+ }
75
+
76
+ if (dateValue.value !== updatedDate) {
77
+ dateValue.value = updatedDate;
78
+ setTimeout(() => {
79
+ input.setSelectionRange(cursorPosition, cursorPosition);
80
+ });
81
+ }
82
+
83
+ event.preventDefault();
84
+ };
85
+
86
+ const modifyDatePart = (
87
+ date: string,
88
+ part: "month" | "day" | "year",
89
+ change: number
90
+ ) => {
91
+ let [month, day, year] = date.split("/").map(Number);
92
+
93
+ if (part === "month") {
94
+ month = Math.max(1, Math.min(12, month + change));
95
+ const maxDays = new Date(year, month, 0).getDate();
96
+ day = Math.min(day, maxDays); // Adjust day to fit new month's max days
97
+ } else if (part === "day") {
98
+ const maxDays = new Date(year, month, 0).getDate();
99
+ day = Math.max(1, Math.min(maxDays, day + change));
100
+ } else if (part === "year") {
101
+ year += change;
102
+ const maxDays = new Date(year, month, 0).getDate();
103
+ day = Math.min(day, maxDays); // Adjust day to fit new year's month
104
+ }
105
+
106
+ return `${String(month).padStart(2, "0")}/${String(day).padStart(
107
+ 2,
108
+ "0"
109
+ )}/${year}`;
110
+ };
111
+ </script>
112
+
113
+ <template>
114
+ <v-text-field
115
+ ref="inputRef"
116
+ v-model="dateValue"
117
+ placeholder="MM/DD/YYYY"
118
+ @input="formatDate"
119
+ @keydown.up="handleArrowKeys"
120
+ @keydown.down="handleArrowKeys"
121
+ :rules="computedRules"
122
+ ></v-text-field>
123
+ </template>