@eeplatform/nuxt-layer-common 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +37 -0
- package/.playground/nuxt.config.ts +20 -0
- package/CHANGELOG.md +7 -0
- package/README.md +73 -0
- package/app.vue +3 -0
- package/components/AddPaymentMethod.vue +585 -0
- package/components/BtnUploadFile.vue +139 -0
- package/components/ConfirmDialog.vue +66 -0
- package/components/Container/Standard.vue +33 -0
- package/components/Input/Date.vue +177 -0
- package/components/Input/ListGroupSelection.vue +93 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +124 -0
- package/components/Input/Password.vue +35 -0
- package/components/InputLabel.vue +18 -0
- package/components/InvitationMain.vue +195 -0
- package/components/Layout/Header.vue +285 -0
- package/components/Layout/NavigationDrawer.vue +52 -0
- package/components/LinkHome.vue +9 -0
- package/components/ListItem.vue +35 -0
- package/components/LocalPagination.vue +41 -0
- package/components/MemberMain.vue +452 -0
- package/components/NavigationItem.vue +73 -0
- package/components/PlaceholderComponent.vue +34 -0
- package/components/RolePermissionFormCreate.vue +179 -0
- package/components/RolePermissionFormPreviewUpdate.vue +184 -0
- package/components/RolePermissionMain.vue +376 -0
- package/components/Snackbar.vue +23 -0
- package/components/SpecificAttr.vue +57 -0
- package/components/Std/Pagination.vue +52 -0
- package/components/SwitchContext.vue +109 -0
- package/components/SwitchOrg.vue +159 -0
- package/components/TableList.vue +130 -0
- package/composables/useAddress.ts +144 -0
- package/composables/useChartOfAccount.ts +62 -0
- package/composables/useCommonPermission.ts +130 -0
- package/composables/useFile.ts +29 -0
- package/composables/useInvoice.ts +42 -0
- package/composables/useLocal.ts +63 -0
- package/composables/useLocalAuth.ts +157 -0
- package/composables/useLocalSetup.ts +46 -0
- package/composables/useMember.ts +107 -0
- package/composables/useOrder.ts +22 -0
- package/composables/useOrg.ts +106 -0
- package/composables/useOrgPermission.ts +27 -0
- package/composables/usePayment.ts +22 -0
- package/composables/usePaymentMethod.ts +347 -0
- package/composables/usePermission.ts +54 -0
- package/composables/usePrice.ts +15 -0
- package/composables/usePromoCode.ts +43 -0
- package/composables/useRecapPermission.ts +26 -0
- package/composables/useRole.ts +89 -0
- package/composables/useSchoolPermission.ts +13 -0
- package/composables/useSubscription.ts +264 -0
- package/composables/useUser.ts +102 -0
- package/composables/useUtils.ts +294 -0
- package/composables/useVerification.ts +19 -0
- package/error.vue +41 -0
- package/eslint.config.js +3 -0
- package/layouts/plain.vue +7 -0
- package/middleware/01.auth.ts +14 -0
- package/middleware/org.ts +16 -0
- package/nuxt.config.ts +48 -0
- package/package.json +35 -0
- package/pages/index.vue +3 -0
- package/pages/payment-method-cancel-link.vue +31 -0
- package/pages/payment-method-failed-link.vue +31 -0
- package/pages/payment-method-linked.vue +31 -0
- package/pages/require-organization-membership.vue +47 -0
- package/pages/unauthorized.vue +29 -0
- package/plugins/API.ts +58 -0
- package/plugins/iconify.client.ts +5 -0
- package/plugins/vuetify.ts +55 -0
- package/public/bdo-logo.svg +4 -0
- package/public/bpi-logo.svg +74 -0
- package/public/chinabank-logo.svg +120 -0
- package/public/gcash-logo.png +0 -0
- package/public/gcash-logo.svg +65 -0
- package/public/grabpay-logo.svg +99 -0
- package/public/paymaya-logo.jpg +0 -0
- package/public/paymaya-logo.png +0 -0
- package/public/paymaya-logo.svg +25 -0
- package/public/qrph-c567ff0f-ab6d-4662-86bf-24c6c731d8a8-logo.svg +20 -0
- package/public/rcbc-logo.svg +15 -0
- package/public/shopeepay-logo.svg +89 -0
- package/public/ubp-logo.svg +88 -0
- package/tsconfig.json +3 -0
- package/types/address.d.ts +13 -0
- package/types/invoice.d.ts +28 -0
- package/types/local.d.ts +25 -0
- package/types/member.d.ts +12 -0
- package/types/org.d.ts +13 -0
- package/types/payment-method.d.ts +11 -0
- package/types/payment.d.ts +18 -0
- package/types/permission.d.ts +14 -0
- package/types/price.d.ts +17 -0
- package/types/promo-code.d.ts +19 -0
- package/types/role.d.ts +13 -0
- package/types/subscription.d.ts +29 -0
- package/types/user.d.ts +21 -0
- package/types/verification.d.ts +15 -0
- package/types/xendit.d.ts +3 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-input
|
|
3
|
+
:label="label"
|
|
4
|
+
:error="!!errorMessage"
|
|
5
|
+
:error-messages="errorMessage"
|
|
6
|
+
:hide-details="readOnly ?? !!value"
|
|
7
|
+
class="btn-input-file"
|
|
8
|
+
>
|
|
9
|
+
<template #default>
|
|
10
|
+
<v-row no-gutters>
|
|
11
|
+
<v-col cols="12" v-if="value || readOnly">
|
|
12
|
+
<v-row no-gutters>
|
|
13
|
+
<v-card
|
|
14
|
+
variant="outlined"
|
|
15
|
+
border="thin"
|
|
16
|
+
class="px-4 text-caption font-weight-medium"
|
|
17
|
+
:rounded="readOnly ? '' : '0 s'"
|
|
18
|
+
@click="emit('click:view')"
|
|
19
|
+
height="36"
|
|
20
|
+
>
|
|
21
|
+
<v-row no-gutters class="fill-height" align="center">
|
|
22
|
+
{{ value ? "View attachment" : "No file attached" }}
|
|
23
|
+
</v-row>
|
|
24
|
+
</v-card>
|
|
25
|
+
<v-card
|
|
26
|
+
v-if="!readOnly"
|
|
27
|
+
variant="outlined"
|
|
28
|
+
border="s-0 thin"
|
|
29
|
+
rounded="0 e"
|
|
30
|
+
class="px-4 d-inline"
|
|
31
|
+
@click="emit('click:close')"
|
|
32
|
+
height="36"
|
|
33
|
+
>
|
|
34
|
+
<v-row no-gutters class="fill-height" align="center">
|
|
35
|
+
<v-icon size="small"> mdi-close </v-icon>
|
|
36
|
+
</v-row>
|
|
37
|
+
</v-card>
|
|
38
|
+
</v-row>
|
|
39
|
+
</v-col>
|
|
40
|
+
|
|
41
|
+
<v-col cols="12" v-else>
|
|
42
|
+
<v-btn
|
|
43
|
+
variant="outlined"
|
|
44
|
+
border="thin"
|
|
45
|
+
class="text-none"
|
|
46
|
+
:prepend-icon="value ? '' : 'mdi-tray-arrow-up'"
|
|
47
|
+
@click="selectAttachment"
|
|
48
|
+
:disabled="loading"
|
|
49
|
+
:loading="loading"
|
|
50
|
+
>
|
|
51
|
+
Add file
|
|
52
|
+
</v-btn>
|
|
53
|
+
</v-col>
|
|
54
|
+
</v-row>
|
|
55
|
+
|
|
56
|
+
<v-file-input
|
|
57
|
+
v-show="false"
|
|
58
|
+
v-model="attachment"
|
|
59
|
+
multiple
|
|
60
|
+
accept="image/*"
|
|
61
|
+
:class="`attachment-input-${inputClass}`"
|
|
62
|
+
@change="uploadAttachment"
|
|
63
|
+
:name="name + 'custom-input-file'"
|
|
64
|
+
></v-file-input>
|
|
65
|
+
</template>
|
|
66
|
+
</v-input>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<script setup lang="ts">
|
|
70
|
+
const value = defineModel<string>({ required: true, default: "" });
|
|
71
|
+
const attachment = ref<File | null>(null);
|
|
72
|
+
const loading = ref(false);
|
|
73
|
+
|
|
74
|
+
const emit = defineEmits(["click:view", "click:close", "upload"]);
|
|
75
|
+
|
|
76
|
+
const { addFile } = useFile();
|
|
77
|
+
const inputClass = Date.now().toString();
|
|
78
|
+
|
|
79
|
+
function selectAttachment() {
|
|
80
|
+
const input = document.querySelector(
|
|
81
|
+
`.attachment-input-${inputClass} input`
|
|
82
|
+
) as HTMLInputElement;
|
|
83
|
+
input.click();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function uploadAttachment(e: Event) {
|
|
87
|
+
loading.value = true;
|
|
88
|
+
const files = (e.target as HTMLInputElement).files;
|
|
89
|
+
|
|
90
|
+
if (files && files.length > 0) {
|
|
91
|
+
const file = files[0];
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const res = await addFile(file);
|
|
95
|
+
value.value = res.id as string;
|
|
96
|
+
await emit("upload", res);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.log(error);
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
console.log("No file selected");
|
|
102
|
+
}
|
|
103
|
+
loading.value = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Other props (optional)
|
|
107
|
+
const { label, rules, name, readOnly } = defineProps({
|
|
108
|
+
label: {
|
|
109
|
+
type: String,
|
|
110
|
+
default: "Rating",
|
|
111
|
+
},
|
|
112
|
+
// Array of validation functions: (number) => true | string
|
|
113
|
+
rules: {
|
|
114
|
+
type: Array<Function>,
|
|
115
|
+
default: () => [],
|
|
116
|
+
},
|
|
117
|
+
name: {
|
|
118
|
+
type: String,
|
|
119
|
+
required: true,
|
|
120
|
+
default: "button-upload-file",
|
|
121
|
+
},
|
|
122
|
+
readOnly: {
|
|
123
|
+
type: Boolean,
|
|
124
|
+
default: false,
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Compute the first failed validation rule, if any.
|
|
130
|
+
* Each rule returns true (if valid) or a string (if invalid).
|
|
131
|
+
*/
|
|
132
|
+
const errorMessage = computed(() => {
|
|
133
|
+
for (const rule of rules) {
|
|
134
|
+
const result = rule(attachment.value || value.value);
|
|
135
|
+
return typeof result === "string" ? result : null;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog v-model="dialog" max-width="400" persistent>
|
|
3
|
+
<v-card class="rounded-xl pa-6" elevation="2">
|
|
4
|
+
<!-- Close Button -->
|
|
5
|
+
<v-btn
|
|
6
|
+
icon
|
|
7
|
+
variant="text"
|
|
8
|
+
size="small"
|
|
9
|
+
class="position-absolute"
|
|
10
|
+
style="top: 8px; right: 8px"
|
|
11
|
+
@click="closeDialog"
|
|
12
|
+
>
|
|
13
|
+
<v-icon>mdi-close</v-icon>
|
|
14
|
+
</v-btn>
|
|
15
|
+
|
|
16
|
+
<v-card-text class="text-center">
|
|
17
|
+
<v-row v-if="imageSrc" justify="center" class="py-10">
|
|
18
|
+
<v-img height="120" :src="imageSrc" alt="Dialog Image" contain />
|
|
19
|
+
</v-row>
|
|
20
|
+
|
|
21
|
+
<!-- Title Slot -->
|
|
22
|
+
<div class="text-h6 font-weight-medium mb-2">
|
|
23
|
+
<slot name="title"></slot>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Description Slot -->
|
|
27
|
+
<div class="text-body-1 mb-4">
|
|
28
|
+
<slot name="description"></slot>
|
|
29
|
+
</div>
|
|
30
|
+
</v-card-text>
|
|
31
|
+
|
|
32
|
+
<!-- Footer Slot -->
|
|
33
|
+
<v-card-actions>
|
|
34
|
+
<slot name="footer"></slot>
|
|
35
|
+
</v-card-actions>
|
|
36
|
+
</v-card>
|
|
37
|
+
</v-dialog>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script lang="ts" setup>
|
|
41
|
+
const props = defineProps({
|
|
42
|
+
modelValue: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false,
|
|
45
|
+
},
|
|
46
|
+
imageSrc: {
|
|
47
|
+
type: String,
|
|
48
|
+
default: "",
|
|
49
|
+
},
|
|
50
|
+
loading: {
|
|
51
|
+
type: Boolean,
|
|
52
|
+
default: false,
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits(["update:modelValue", "submit"]);
|
|
57
|
+
|
|
58
|
+
const dialog = computed({
|
|
59
|
+
get: () => props.modelValue,
|
|
60
|
+
set: (value) => emit("update:modelValue", value),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function closeDialog() {
|
|
64
|
+
dialog.value = false;
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters>
|
|
3
|
+
<v-col cols="12">
|
|
4
|
+
<span
|
|
5
|
+
class="font-weight-medium text-subtitle-1 text-decoration-underline cursor-pointer"
|
|
6
|
+
@click="emit('back')"
|
|
7
|
+
>
|
|
8
|
+
Back
|
|
9
|
+
</span>
|
|
10
|
+
</v-col>
|
|
11
|
+
<v-col cols="12">
|
|
12
|
+
<span class="text-h5 font-weight-medium">{{ props.title }}</span>
|
|
13
|
+
</v-col>
|
|
14
|
+
|
|
15
|
+
<v-col cols="12" class="mt-4">
|
|
16
|
+
<slot name="default"> </slot>
|
|
17
|
+
</v-col>
|
|
18
|
+
|
|
19
|
+
<v-col cols="12" class="mt-4">
|
|
20
|
+
<slot name="footer"> </slot>
|
|
21
|
+
</v-col>
|
|
22
|
+
</v-row>
|
|
23
|
+
</template>
|
|
24
|
+
|
|
25
|
+
<script setup lang="ts">
|
|
26
|
+
const emit = defineEmits(["back"]);
|
|
27
|
+
const props = defineProps({
|
|
28
|
+
title: {
|
|
29
|
+
type: String,
|
|
30
|
+
default: "",
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
</script>
|
|
@@ -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,93 @@
|
|
|
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 }" class="pl-1">
|
|
65
|
+
<v-list-item-action start>
|
|
66
|
+
<v-checkbox-btn :model-value="isSelected"></v-checkbox-btn>
|
|
67
|
+
</v-list-item-action>
|
|
68
|
+
</template>
|
|
69
|
+
</v-list-item>
|
|
70
|
+
</template>
|
|
71
|
+
</v-list-group>
|
|
72
|
+
</template>
|
|
73
|
+
</v-list>
|
|
74
|
+
</v-card>
|
|
75
|
+
</v-input>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
const selected = defineModel<Array<string>>({ default: [] });
|
|
80
|
+
const attrs = useAttrs();
|
|
81
|
+
const props = defineProps({
|
|
82
|
+
items: {
|
|
83
|
+
type: Object,
|
|
84
|
+
required: true,
|
|
85
|
+
default: () => ({}),
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const selectedActionCount = (resource: string) => {
|
|
90
|
+
return selected.value.filter((permission) => permission.startsWith(resource))
|
|
91
|
+
.length;
|
|
92
|
+
};
|
|
93
|
+
</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>
|