@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,138 @@
1
+ <template>
2
+ <v-dialog v-model="overlay" v-if="overlay" width="100%" height="100%" opacity="50">
3
+ <v-row align="center" justify="center" class="fill-height" style="position: relative;">
4
+ <v-carousel hide-delimiters width="100%" height="100%" :show-arrows="files.length > 1 ? 'hover' : false"
5
+ v-model="activeIndex">
6
+ <template v-for="x, index in files" :key="x || index">
7
+ <template v-if="fileTypes?.[x] === 'image'">
8
+ <v-carousel-item height="100%" width="100%" rounded="lg">
9
+ <v-row no-gutters class="w-100 h-100" align="center">
10
+ <v-img :lazy-src="getFileUrl(x)" :src="getFileUrl(x)" width="70%" height="70%"
11
+ :alt="'Image Viewer Card -' + index"></v-img>
12
+ </v-row>
13
+ <template v-slot:placeholder>
14
+ <div class="d-flex align-center justify-center fill-height">
15
+ <v-progress-circular color="grey-lighten-4" indeterminate></v-progress-circular>
16
+ </div>
17
+ </template>
18
+ </v-carousel-item>
19
+ </template>
20
+ <template v-else-if="fileTypes?.[x] === 'video'">
21
+ <v-carousel-item>
22
+ <v-row no-gutters class="h-100 w-100" align="center" justify="center">
23
+ <video width="80%" height="80%" controls>
24
+ <source :src="getFileUrl(x)" />
25
+ </video>
26
+ </v-row>
27
+ <template v-slot:placeholder>
28
+ <div class="d-flex align-center justify-center fill-height">
29
+ <v-progress-circular color="grey-lighten-4" indeterminate></v-progress-circular>
30
+ </div>
31
+ </template>
32
+ </v-carousel-item>
33
+ </template>
34
+ <template v-else>
35
+ <v-carousel-item>
36
+ <v-row no-gutters class="h-100 w-100" align="center" justify="center">
37
+ <v-icon size="100" color="white">mdi-file</v-icon>
38
+ </v-row>
39
+ </v-carousel-item>
40
+ </template>
41
+ </template>
42
+
43
+ <template v-slot:prev="{ props }">
44
+ <v-btn color="white" variant="outlined" class="text-white" icon="mdi-chevron-left"
45
+ @click="props.onClick"></v-btn>
46
+ </template>
47
+ <template v-slot:next="{ props }">
48
+ <v-btn color="white" class="text-white" variant="outlined" icon="mdi-chevron-right"
49
+ @click="props.onClick"></v-btn>
50
+ </template>
51
+ </v-carousel>
52
+ <div style="position: absolute; top: 2%; left: 0%; right: 0%; z-index: 2"
53
+ class="cursor-pointer text-white text-h6 custom-shadow d-flex justify-space-between" @click="overlay = false">
54
+ <v-row no-gutters style="position: relative">
55
+ <v-col cols="8" xs="6" sm="2" md="2" lg="2">
56
+ <v-btn prepend-icon="mdi-close" text="Close" rounded="lg" color="secondary" class="ml-2"
57
+ style="position: absolute;"></v-btn>
58
+ </v-col>
59
+
60
+ </v-row>
61
+
62
+ </div>
63
+ <span class="text-white text-16px d-flex w-100 mt-2 justify-center"
64
+ style="position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);">{{ `${activeIndex +
65
+ 1}/${files.length}`
66
+ }}</span>
67
+
68
+ </v-row>
69
+ </v-dialog>
70
+ </template>
71
+
72
+ <script setup lang="ts">
73
+ const props = defineProps({
74
+ activeFileId: {
75
+ type: String,
76
+ required: false,
77
+ },
78
+ files: {
79
+ type: Array as PropType<string[]>,
80
+ default: []
81
+ }
82
+ });
83
+
84
+
85
+ const { getFileUrl, urlToFile } = useFile()
86
+ const overlay = defineModel({ required: true, default: false });
87
+ const activeIndex = ref(0);
88
+ const fileTypes = ref<Record<string, "image" | "video" | "other">>({});
89
+
90
+
91
+ const emit = defineEmits(['share', 'like'])
92
+
93
+ watchEffect(() => {
94
+ if (props.activeFileId && props.files.length > 0) {
95
+ const index =
96
+ props.files?.findIndex((x) => x == props.activeFileId) || 0;
97
+ if (index !== -1) {
98
+ activeIndex.value = index;
99
+ } else activeIndex.value = 0;
100
+ } else {
101
+ return (activeIndex.value = 0);
102
+ }
103
+ });
104
+
105
+ async function resolveFileTypes() {
106
+ fileTypes.value = {}; // reset
107
+
108
+ for (const x of props.files) {
109
+ try {
110
+ const url = getFileUrl(x);
111
+ const file = await urlToFile(url, x);
112
+ const type = file?.type;
113
+
114
+
115
+ if (type?.startsWith("video")) fileTypes.value[x] = "video";
116
+ else if (type?.startsWith("image")) fileTypes.value[x] = "image";
117
+ else fileTypes.value[x] = "other";
118
+
119
+ } catch (err) {
120
+ fileTypes.value[x] = "other";
121
+ }
122
+ }
123
+ }
124
+
125
+ watch(
126
+ () => props.files,
127
+ () => resolveFileTypes(),
128
+ { immediate: true }
129
+ );
130
+
131
+
132
+ </script>
133
+
134
+ <style scoped>
135
+ .custom-shadow {
136
+ text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.8);
137
+ }
138
+ </style>
@@ -0,0 +1,177 @@
1
+ <template>
2
+ <v-input
3
+ :error="!!validationErrMsg"
4
+ :error-messages="validationErrMsg"
5
+ hide-details="auto"
6
+ :name="name + 'custom-input-date'"
7
+ :disabled="props.disabled"
8
+ >
9
+ <template #default>
10
+ <v-row>
11
+ <v-col cols="12" lg="6" md="6">
12
+ <v-select
13
+ v-model="month"
14
+ :items="props.months"
15
+ label="Month"
16
+ density="comfortable"
17
+ :name="props.name + 'custom-input-date-month'"
18
+ hide-details
19
+ :error="!!validationErrMsg"
20
+ :disabled="props.disabled"
21
+ />
22
+ </v-col>
23
+
24
+ <v-col cols="12" lg="3" md="3">
25
+ <v-text-field
26
+ v-model.number="day"
27
+ type="number"
28
+ label="Day"
29
+ hide-details
30
+ placeholder="DD"
31
+ density="comfortable"
32
+ :name="props.name + 'custom-input-date-day'"
33
+ :error="!!validationErrMsg"
34
+ :disabled="props.disabled"
35
+ />
36
+ </v-col>
37
+
38
+ <v-col cols="12" lg="3" md="3">
39
+ <v-text-field
40
+ v-model.number="year"
41
+ type="number"
42
+ label="Year"
43
+ hide-details
44
+ placeholder="YYYY"
45
+ density="comfortable"
46
+ :name="props.name + 'custom-input-date-year'"
47
+ :error="!!validationErrMsg"
48
+ :disabled="props.disabled"
49
+ />
50
+ </v-col>
51
+ </v-row>
52
+ </template>
53
+ </v-input>
54
+ </template>
55
+
56
+ <script setup lang="ts">
57
+ const month = defineModel("month", { default: "" });
58
+ const day = defineModel("day", { default: 0 });
59
+ const year = defineModel("year", { default: 0 });
60
+
61
+ const props = defineProps({
62
+ months: {
63
+ type: Array,
64
+ default: [
65
+ "January",
66
+ "February",
67
+ "March",
68
+ "April",
69
+ "May",
70
+ "June",
71
+ "July",
72
+ "August",
73
+ "September",
74
+ "October",
75
+ "November",
76
+ "December",
77
+ ],
78
+ },
79
+ name: {
80
+ type: String,
81
+ default: "input-date-combo",
82
+ },
83
+ disabled: {
84
+ type: Boolean,
85
+ default: false,
86
+ },
87
+ });
88
+
89
+ const isLeapYear = (year: number): boolean =>
90
+ (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
91
+
92
+ const monthMap: Record<string, number> = {
93
+ January: 1,
94
+ February: 2,
95
+ March: 3,
96
+ April: 4,
97
+ May: 5,
98
+ June: 6,
99
+ July: 7,
100
+ August: 8,
101
+ September: 9,
102
+ October: 10,
103
+ November: 11,
104
+ December: 12,
105
+ };
106
+
107
+ const validateBirthDate = (): string => {
108
+ const _month = monthMap[month.value];
109
+ const _day = day.value;
110
+ const _year = year.value;
111
+
112
+ const errorMessage = "Enter a valid date";
113
+
114
+ if (
115
+ !_month ||
116
+ !_day ||
117
+ !_year ||
118
+ _year < 1904 ||
119
+ _year > new Date().getFullYear()
120
+ ) {
121
+ return errorMessage;
122
+ }
123
+
124
+ const daysInMonth = [
125
+ 31,
126
+ isLeapYear(_year) ? 29 : 28,
127
+ 31,
128
+ 30,
129
+ 31,
130
+ 30,
131
+ 31,
132
+ 31,
133
+ 30,
134
+ 31,
135
+ 30,
136
+ 31,
137
+ ];
138
+
139
+ if (_day < 1 || _day > daysInMonth[_month - 1]) {
140
+ return errorMessage;
141
+ }
142
+
143
+ if (!_month || !_day || !_year) {
144
+ return errorMessage;
145
+ }
146
+
147
+ return "";
148
+ };
149
+
150
+ const validationErrMsg = ref("");
151
+
152
+ const date = computed(() => {
153
+ return {
154
+ day: day.value,
155
+ month: month.value,
156
+ year: year.value,
157
+ };
158
+ });
159
+
160
+ validationErrMsg.value = validateBirthDate();
161
+
162
+ const disabled = computed(() => props.disabled);
163
+
164
+ watch(disabled, (curr) => {
165
+ if (curr) {
166
+ validationErrMsg.value = validateBirthDate();
167
+ }
168
+ });
169
+
170
+ watch(
171
+ date,
172
+ () => {
173
+ validationErrMsg.value = validateBirthDate();
174
+ },
175
+ { deep: true }
176
+ );
177
+ </script>
@@ -0,0 +1,131 @@
1
+ <template>
2
+ <div class="d-flex flex-column">
3
+ <v-text-field v-bind="$attrs" ref="dateTimePickerRef" :model-value="dateTimeFormattedReadOnly" autocomplete="off"
4
+ :placeholder="placeholder" :rules="rules" style="z-index: 10" @click="openDatePicker">
5
+ <template #append-inner>
6
+ <v-icon icon="mdi-calendar" @click.stop="openDatePicker" />
7
+ </template>
8
+ </v-text-field>
9
+ <div class="w-100 d-flex align-end ga-3 hidden-input">
10
+ <input ref="dateInput" type="datetime-local" v-model="dateTime" />
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script setup lang="ts">
16
+ import { ref, computed, watch } from 'vue'
17
+
18
+ const prop = defineProps({
19
+ rules: {
20
+ type: Array as PropType<Array<any>>,
21
+ default: () => []
22
+ },
23
+ placeholder: {
24
+ type: String,
25
+ default: 'MM/DD/YYYY, HH:MM AM/PM'
26
+ }
27
+ })
28
+
29
+ const { formatDateISO8601 } = useUtils()
30
+ const dateTime = defineModel<string | null>({ default: null }) //2025-10-10T13:09 format
31
+ const dateTimeUTC = defineModel<string | null>('utc', { default: null }) // UTC format
32
+
33
+ const dateTimeFormattedReadOnly = ref<string | null>(null)
34
+
35
+
36
+
37
+
38
+ const dateInput = ref<HTMLInputElement | null>(null)
39
+ const dateTimePickerRef = ref<HTMLInputElement | null>(null)
40
+
41
+ const isInitialLoad = ref(true)
42
+
43
+ function openDatePicker() {
44
+ setTimeout(() => {
45
+ dateInput.value?.showPicker?.()
46
+ }, 0)
47
+ }
48
+
49
+ function validate() {
50
+ (dateTimePickerRef.value as any)?.validate()
51
+ }
52
+
53
+ function convertToReadableFormat(dateStr: string): string {
54
+
55
+ if (!dateStr) return "";
56
+ const date = new Date(dateStr)
57
+ const options: Intl.DateTimeFormatOptions = {
58
+ year: 'numeric',
59
+ month: '2-digit',
60
+ day: '2-digit',
61
+ hour: '2-digit',
62
+ minute: '2-digit',
63
+ hour12: true
64
+ }
65
+ const formatted = date.toLocaleString('en-US', options)
66
+ return formatted
67
+ }
68
+
69
+ function handleInitialDate(){
70
+ const dateDefault = dateTime.value
71
+ const dateUTC = dateTimeUTC.value
72
+ if(dateDefault){
73
+ dateTimeFormattedReadOnly.value = convertToReadableFormat(dateDefault)
74
+ const localDate = new Date(dateDefault)
75
+ dateTimeUTC.value = localDate.toISOString()
76
+ } else if (dateUTC){
77
+ dateTimeFormattedReadOnly.value = convertToReadableFormat(dateUTC)
78
+ const localDate = new Date(dateUTC)
79
+ dateTime.value = formatDateISO8601(localDate)
80
+ } else {
81
+ dateTimeFormattedReadOnly.value = null
82
+ }
83
+ }
84
+
85
+
86
+ watch(dateTime, (dateVal) => {
87
+ if (isInitialLoad.value) return // ignore the first run
88
+ if (!dateVal) {
89
+ dateTimeFormattedReadOnly.value = null;
90
+ dateTimeUTC.value = null
91
+ return
92
+ }
93
+
94
+ dateTimeFormattedReadOnly.value = convertToReadableFormat(dateVal)
95
+ const localDate = new Date(dateVal)
96
+ dateTimeUTC.value = localDate.toISOString()
97
+
98
+ }, { immediate: false })
99
+
100
+ watch(dateTimeUTC, () => {
101
+ handleInitialDate()
102
+ }, { immediate: true})
103
+
104
+
105
+ onMounted(async () => {
106
+ await nextTick()
107
+ isInitialLoad.value = false
108
+ // Wait until Vuetify renders its internal input
109
+ const nativeInput = (dateTimePickerRef.value as any)?.$el?.querySelector('input')
110
+ if (nativeInput) {
111
+ nativeInput.addEventListener('click', (e: MouseEvent) => {
112
+ e.stopPropagation()
113
+ openDatePicker()
114
+ })
115
+ }
116
+
117
+ })
118
+
119
+
120
+ defineExpose({
121
+ validate
122
+ })
123
+ </script>
124
+
125
+ <style scoped>
126
+ .hidden-input {
127
+ opacity: 0;
128
+ height: 0;
129
+ width: 1px;
130
+ }
131
+ </style>
@@ -0,0 +1,236 @@
1
+ <template>
2
+ <div>
3
+ <v-row class="mb-4" align="center" no-gutters>
4
+ <v-col
5
+ cols="10"
6
+ class="pr-2"
7
+ v-if="
8
+ props.createdFrom === 'feedback' && props.attachments.length > 0
9
+ ? false
10
+ : true
11
+ "
12
+ >
13
+ <div
14
+ class="d-flex align-center justify-center pa-4 rounded-lg border-dashed border border-grey"
15
+ @dragover.prevent
16
+ @drop="handleDrop"
17
+ style="position: relative; z-index: 1"
18
+ >
19
+ <v-file-input
20
+ ref="dropInput"
21
+ v-model="files"
22
+ multiple
23
+ hide-details
24
+ :max-files="maxFiles"
25
+ style="
26
+ opacity: 0;
27
+ position: absolute;
28
+ inset: 0;
29
+ z-index: 2;
30
+ cursor: pointer;
31
+ "
32
+ @change="onFileChange"
33
+ />
34
+ <div
35
+ class="d-flex align-center"
36
+ style="z-index: 1; pointer-events: none"
37
+ >
38
+ <v-icon size="28" class="mr-2" color="primary"
39
+ >mdi-cloud-upload-outline</v-icon
40
+ >
41
+ <span class="text-body-1 font-weight-medium"
42
+ >Drag and drop files here</span
43
+ >
44
+ </div>
45
+ </div>
46
+ </v-col>
47
+
48
+ <v-col
49
+ cols="2"
50
+ class="d-flex justify-center"
51
+ v-if="
52
+ props.createdFrom === 'feedback' && props.attachments.length > 0
53
+ ? false
54
+ : true
55
+ "
56
+ >
57
+ <v-btn
58
+ color="primary-button"
59
+ min-width="55"
60
+ width="55"
61
+ height="55"
62
+ elevation="0"
63
+ @click="selectAttachment"
64
+ >
65
+ <v-icon size="20">mdi-camera-outline</v-icon>
66
+ </v-btn>
67
+ </v-col>
68
+ </v-row>
69
+
70
+ <v-sheet v-if="attachments.length > 0" elevation="0" class="py-3" rounded>
71
+ <v-row no-gutters>
72
+ <v-col
73
+ v-for="(file, index) in attachments"
74
+ :key="index"
75
+ cols="12"
76
+ class="d-flex align-center pa-2 mr-2 mb-2 rounded bg-white border-sm"
77
+ >
78
+ <div class="mr-3">
79
+ <!-- <v-img
80
+ v-if="!localErroredImages.includes(file)"
81
+ :src="file"
82
+ width="40"
83
+ height="40"
84
+ class="rounded"
85
+ cover
86
+ @error="onImageError(file)"
87
+ />
88
+ <v-img
89
+ v-else
90
+ :src="getThumbnail(file)"
91
+ width="40"
92
+ height="40"
93
+ class="rounded"
94
+ cover
95
+ /> -->
96
+ <v-img
97
+ :src="getThumbnail(file)"
98
+ width="40"
99
+ height="40"
100
+ class="rounded"
101
+ cover
102
+ />
103
+ </div>
104
+
105
+ <div class="flex-grow-1 text-truncate">
106
+ {{ getDisplayName(file) }}
107
+ </div>
108
+
109
+ <v-icon
110
+ size="small"
111
+ @click.stop="$emit('delete', file)"
112
+ v-if="
113
+ props.createdFrom === 'feedback' && props.attachments.length > 0
114
+ ? false
115
+ : true
116
+ "
117
+ >
118
+ mdi-trash-can-outline
119
+ </v-icon>
120
+ </v-col>
121
+ </v-row>
122
+ </v-sheet>
123
+ </div>
124
+ </template>
125
+
126
+ <script setup lang="ts">
127
+ const props = defineProps<{
128
+ attachments: string[];
129
+ erroredImages?: string[];
130
+ maxFiles?: number;
131
+ createdFrom?: string;
132
+ }>();
133
+
134
+ const emit = defineEmits<{
135
+ (e: "add", file: File): void;
136
+ (e: "delete", url: string): void;
137
+ (e: "errored", url: string): void;
138
+ }>();
139
+
140
+ const dropInput = ref<any>(null);
141
+ const files = ref<File[]>([]);
142
+ const localErroredImages = ref<string[]>(props.erroredImages || []);
143
+ // Store file names for display
144
+ const fileNamesMap = ref<Record<string, string>>({});
145
+
146
+ watch(
147
+ () => props.erroredImages,
148
+ (val) => {
149
+ if (val) {
150
+ localErroredImages.value = val;
151
+ }
152
+ }
153
+ );
154
+
155
+ function handleDrop(event: DragEvent) {
156
+ const droppedFiles = event.dataTransfer?.files;
157
+ if (droppedFiles?.length) {
158
+ files.value = Array.from(droppedFiles);
159
+ onFileChange();
160
+ }
161
+ }
162
+
163
+ function selectAttachment() {
164
+ const input = dropInput.value?.$el?.querySelector(
165
+ "input[type='file']"
166
+ ) as HTMLInputElement | null;
167
+ input?.click();
168
+ }
169
+
170
+ function onFileChange() {
171
+ if (files.value.length) {
172
+ console.log(
173
+ "onFileChange triggered with files:",
174
+ files.value.map((f) => f.name)
175
+ );
176
+
177
+ const maxFiles = props.maxFiles || 5;
178
+ if (files.value.length > maxFiles) {
179
+ console.warn(`Too many files selected. Maximum allowed: ${maxFiles}`);
180
+ files.value = [];
181
+ return;
182
+ }
183
+
184
+ files.value.forEach((file) => {
185
+ console.log(`Emitting 'add' event for file: ${file.name}`);
186
+ // For each file, we'll also need to store its original name
187
+ // This will be associated with the file URL in the parent component
188
+ emit("add", file);
189
+ });
190
+
191
+ files.value = [];
192
+ }
193
+ }
194
+
195
+ function onImageError(file: string) {
196
+ console.log(`Image error for file: ${file}`);
197
+ emit("errored", file);
198
+ }
199
+
200
+ function getThumbnail(fileUrl: string): string {
201
+ // if (fileUrl.endsWith(".pdf")) return "mdi-file-pdf-outline";
202
+ // if (fileUrl.match(/\.(doc|docx)$/i))
203
+ // return "/images/file-thumbnails/word.png";
204
+ // if (fileUrl.match(/\.(xls|xlsx)$/i))
205
+ // return "/images/file-thumbnails/excel.png";
206
+ // return "/images/file-thumbnails/file.png";
207
+ return `/api/public/${fileUrl}`;
208
+ }
209
+
210
+ // Modified to try to display the friendly name
211
+ function getFileName(fileUrl: string): string {
212
+ try {
213
+ const url = new URL(fileUrl);
214
+ return decodeURIComponent(url.pathname.split("/").pop() || "");
215
+ } catch (e) {
216
+ return fileUrl.split("/").pop() || "";
217
+ }
218
+ }
219
+
220
+ // This will use our stored file names when available
221
+ function getDisplayName(fileUrl: string): string {
222
+ // If we have a friendly name stored, use it
223
+ if (fileNamesMap.value[fileUrl]) {
224
+ return fileNamesMap.value[fileUrl];
225
+ }
226
+ // Otherwise fall back to the ID/URL-based name
227
+ return getFileName(fileUrl);
228
+ }
229
+
230
+ // Method to update file names map - will be called from parent
231
+ defineExpose({
232
+ updateFileName: (url: string, name: string) => {
233
+ fileNamesMap.value[url] = name;
234
+ },
235
+ });
236
+ </script>