@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.
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/.editorconfig +12 -0
- package/.github/workflows/main.yml +17 -0
- package/.github/workflows/publish.yml +39 -0
- package/.nuxtrc +1 -0
- package/.playground/app.vue +41 -0
- package/.playground/eslint.config.mjs +6 -0
- package/.playground/nuxt.config.ts +22 -0
- package/.playground/pages/feedback.vue +30 -0
- package/CHANGELOG.md +263 -0
- package/README.md +73 -0
- package/app.vue +3 -0
- package/components/AccessCardAddForm.vue +363 -0
- package/components/AccessManagement.vue +420 -0
- package/components/Avatar/Main.vue +68 -0
- package/components/BillingMain.vue +66 -0
- package/components/BtnUploadFile.vue +139 -0
- package/components/BuildingForm.vue +303 -0
- package/components/BuildingManagement/buildings.vue +335 -0
- package/components/BuildingManagement/units.vue +350 -0
- package/components/BuildingUnitFormAdd.vue +441 -0
- package/components/BuildingUnitFormEdit.vue +429 -0
- package/components/CameraForm.vue +264 -0
- package/components/CameraMain.vue +352 -0
- package/components/Card/DeleteConfirmation.vue +51 -0
- package/components/Card/MemberInfoSummary.vue +44 -0
- package/components/Card/Toggle.vue +25 -0
- package/components/Chat/Bubbles.vue +53 -0
- package/components/Chat/Information.vue +416 -0
- package/components/Chat/ListCard.vue +62 -0
- package/components/Chat/Message.vue +158 -0
- package/components/Chat/Navigation.vue +150 -0
- package/components/ConfirmDialog.vue +66 -0
- package/components/Container/Standard.vue +33 -0
- package/components/DashboardPlaceholder.vue +1524 -0
- package/components/Dialog/DeleteConfirmation.vue +51 -0
- package/components/Dialog/ReplaceAutofillPrompt.vue +49 -0
- package/components/Dialog/UpdateMoreAction.vue +103 -0
- package/components/DocumentForm.vue +187 -0
- package/components/DocumentManagement.vue +376 -0
- package/components/Editor.vue +95 -0
- package/components/EntryPassMain.vue +518 -0
- package/components/Feedback/Form.vue +173 -0
- package/components/FeedbackDetail.vue +599 -0
- package/components/FeedbackMain.vue +588 -0
- package/components/FormDialog.vue +65 -0
- package/components/ImageCarousel.vue +138 -0
- package/components/Input/Date.vue +177 -0
- package/components/Input/DateTimePicker.vue +131 -0
- package/components/Input/File.vue +236 -0
- package/components/Input/FileV2.vue +234 -0
- package/components/Input/InputPhoneNumberV2.vue +164 -0
- package/components/Input/ListGroupSelection.vue +96 -0
- package/components/Input/NRICNumber.vue +53 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +124 -0
- package/components/Input/Password.vue +22 -0
- package/components/Input/PhoneNumber.vue +188 -0
- package/components/Input/VehicleNumber.vue +49 -0
- package/components/InputLabel.vue +22 -0
- package/components/InvitationForm.vue +359 -0
- package/components/InvitationMain.vue +310 -0
- package/components/Layout/Header.vue +129 -0
- package/components/Layout/NavigationDrawer.vue +44 -0
- package/components/ListItem.vue +35 -0
- package/components/ListView.vue +87 -0
- package/components/LocalPagination.vue +31 -0
- package/components/MemberMain.vue +459 -0
- package/components/NFC/NFCPatrolReportMain.vue +591 -0
- package/components/NFC/NFCPatrolRouteForm.vue +596 -0
- package/components/NFC/NFCPatrolRouteMain.vue +539 -0
- package/components/NFC/NFCTagForm.vue +236 -0
- package/components/NFC/NFCTagMain.vue +337 -0
- package/components/NFC/PatrolSettings.vue +130 -0
- package/components/NavigationItem.vue +83 -0
- package/components/NumberSettingField.vue +107 -0
- package/components/OnlineFormConfigurationForm.vue +290 -0
- package/components/OnlineFormsConfiguration.vue +429 -0
- package/components/PeopleForm.vue +452 -0
- package/components/PlaceholderComponent.vue +34 -0
- package/components/RolePermissionFormCreate.vue +161 -0
- package/components/RolePermissionFormPreviewUpdate.vue +183 -0
- package/components/RolePermissionMain.vue +361 -0
- package/components/SearchVehicleNumberUser.vue +91 -0
- package/components/ServiceProviderFormCreate.vue +154 -0
- package/components/ServiceProviderMain.vue +547 -0
- package/components/SignaturePad.vue +73 -0
- package/components/Snackbar.vue +23 -0
- package/components/SpecificAttr.vue +53 -0
- package/components/SupplyManagement.vue +292 -0
- package/components/SwitchContext.vue +108 -0
- package/components/TableList.vue +150 -0
- package/components/TableListSecondary.vue +164 -0
- package/components/TableMain.vue +142 -0
- package/components/TableWithButton.vue +94 -0
- package/components/VehicleUpdateMoreAction.vue +84 -0
- package/components/VideoPlayer.vue +125 -0
- package/components/VisitorForm.vue +659 -0
- package/components/VisitorFormSelection.vue +53 -0
- package/components/VisitorManagement.vue +490 -0
- package/components/WorkOrder/Create.vue +284 -0
- package/components/WorkOrder/Detail.vue +71 -0
- package/components/WorkOrder/ListView.vue +96 -0
- package/components/WorkOrder/Main.vue +489 -0
- package/components/Workorder.vue +1 -0
- package/composables/useAddress.ts +107 -0
- package/composables/useBuilding.ts +250 -0
- package/composables/useBuildingUnit.ts +116 -0
- package/composables/useCard.ts +46 -0
- package/composables/useCommonPermission.ts +207 -0
- package/composables/useCustomer.ts +113 -0
- package/composables/useCustomerSite.ts +56 -0
- package/composables/useDashboard.ts +31 -0
- package/composables/useDashboardData.ts +425 -0
- package/composables/useDocument.ts +57 -0
- package/composables/useFacility.ts +246 -0
- package/composables/useFeedback.ts +119 -0
- package/composables/useFile.ts +55 -0
- package/composables/useInvoice.ts +18 -0
- package/composables/useLocal.ts +131 -0
- package/composables/useLocalAuth.ts +137 -0
- package/composables/useLocalSetup.ts +13 -0
- package/composables/useMember.ts +111 -0
- package/composables/useNFCPatrolRoute.ts +77 -0
- package/composables/useNFCPatrolSettings.ts +19 -0
- package/composables/useNFCPatrolTag.ts +53 -0
- package/composables/useOnlineForm.ts +67 -0
- package/composables/useOrg.ts +129 -0
- package/composables/usePDFDownload.ts +25 -0
- package/composables/usePaymentMethod.ts +101 -0
- package/composables/usePeople.ts +81 -0
- package/composables/usePermission.ts +54 -0
- package/composables/usePhoneCountries.ts +561 -0
- package/composables/usePrice.ts +15 -0
- package/composables/usePromoCode.ts +36 -0
- package/composables/useRecapPermission.ts +26 -0
- package/composables/useRole.ts +104 -0
- package/composables/useSecurityUtils.ts +18 -0
- package/composables/useServiceProvider.ts +224 -0
- package/composables/useSite.ts +109 -0
- package/composables/useSiteEntryPassSettings.ts +46 -0
- package/composables/useSiteSettings.ts +123 -0
- package/composables/useSubscription.ts +150 -0
- package/composables/useUser.ts +132 -0
- package/composables/useUtils.ts +445 -0
- package/composables/useVerification.ts +34 -0
- package/composables/useVisitor.ts +120 -0
- package/composables/useWorkOrder.ts +85 -0
- package/error.vue +41 -0
- package/layouts/plain.vue +7 -0
- package/middleware/01.auth.ts +20 -0
- package/middleware/02.org.ts +21 -0
- package/middleware/03.customer.ts +13 -0
- package/middleware/member.ts +4 -0
- package/nuxt.config.ts +54 -0
- package/package.json +39 -0
- package/pages/index.vue +3 -0
- package/pages/payment-method-linked.vue +31 -0
- package/pages/require-customer.vue +56 -0
- package/pages/require-organization-membership.vue +47 -0
- package/pages/unauthorized.vue +29 -0
- package/plugins/API.ts +21 -0
- package/plugins/iconify.client.ts +5 -0
- package/plugins/secure-member.client.ts +86 -0
- package/plugins/vuetify.ts +62 -0
- package/public/bg-camera.jpg +0 -0
- package/public/bg-city.jpg +0 -0
- package/public/bg-condo.jpg +0 -0
- package/public/images/icons/delete-icon.png +0 -0
- package/public/sprite.svg +1 -0
- package/tsconfig.json +3 -0
- package/types/address.d.ts +13 -0
- package/types/building.d.ts +27 -0
- package/types/camera.d.ts +31 -0
- package/types/card.d.ts +22 -0
- package/types/customer.d.ts +27 -0
- package/types/document.d.ts +6 -0
- package/types/feedback.d.ts +68 -0
- package/types/local.d.ts +74 -0
- package/types/member.d.ts +21 -0
- package/types/online-form.d.ts +15 -0
- package/types/org.d.ts +13 -0
- package/types/people.d.ts +24 -0
- package/types/permission.d.ts +25 -0
- package/types/phone-number.d.ts +10 -0
- package/types/price.d.ts +17 -0
- package/types/promo-code.d.ts +19 -0
- package/types/role.d.ts +11 -0
- package/types/select.d.ts +4 -0
- package/types/service-provider.d.ts +15 -0
- package/types/site.d.ts +20 -0
- package/types/subscription.d.ts +23 -0
- package/types/user.d.ts +19 -0
- package/types/verification.d.ts +20 -0
- package/types/visitor.d.ts +42 -0
- package/types/work-order.d.ts +42 -0
- 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>
|