@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,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>
|