@goweekdays/layer-common 0.0.4 → 0.0.7
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/CHANGELOG.md +18 -0
- package/components/Input/NewDate.vue +123 -0
- package/components/Input/Number.vue +39 -17
- package/components/Layout/Header.vue +3 -1
- package/components/SwitchOrg.vue +1 -1
- package/composables/useOrder.ts +22 -0
- package/composables/usePaymentMethod.ts +11 -2
- package/composables/usePromoCode.ts +34 -0
- package/composables/useSubscription.ts +61 -26
- package/composables/useUtils.ts +61 -0
- package/error.vue +41 -0
- package/layouts/plain.vue +7 -0
- package/middleware/auth.ts +25 -0
- package/middleware/org.ts +16 -0
- package/package.json +3 -2
- package/pages/index.vue +3 -0
- package/pages/payment-method-linked.vue +31 -0
- package/pages/require-organization-membership.vue +47 -0
- package/pages/unauthorized.vue +29 -0
- package/public/paymaya-logo.png +0 -0
- package/types/promo-code.d.ts +19 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @goweekdays/layer-common
|
|
2
2
|
|
|
3
|
+
## 0.0.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 3b7164b: add promo code
|
|
8
|
+
|
|
9
|
+
## 0.0.6
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 01873f7: Add error page
|
|
14
|
+
|
|
15
|
+
## 0.0.5
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 0a6bba5: Enhance usePaymentMethod
|
|
20
|
+
|
|
3
21
|
## 0.0.4
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
|
@@ -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>
|
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { ref, computed, nextTick, useAttrs } from "vue";
|
|
3
3
|
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
infinityEnabled: { type: Boolean, default: false }, // Enable ∞ display for 0
|
|
6
|
+
});
|
|
7
|
+
|
|
4
8
|
const modelValue = defineModel<number>(); // Store actual number
|
|
5
|
-
const inputRef = ref<HTMLInputElement | null>(null);
|
|
6
|
-
const attrs = useAttrs();
|
|
9
|
+
const inputRef = ref<HTMLInputElement | null>(null);
|
|
10
|
+
const attrs = useAttrs();
|
|
7
11
|
|
|
8
|
-
let cursorPosition = 0;
|
|
9
|
-
let forceCursorToEnd = false;
|
|
12
|
+
let cursorPosition = 0;
|
|
13
|
+
let forceCursorToEnd = false;
|
|
10
14
|
|
|
11
|
-
// Computed property to format value with commas
|
|
15
|
+
// Computed property to format value with commas or infinity sign
|
|
12
16
|
const formattedValue = computed({
|
|
13
|
-
get: () =>
|
|
14
|
-
|
|
17
|
+
get: () => {
|
|
18
|
+
if (props.infinityEnabled && modelValue.value === 0) return "∞"; // Show ∞ if enabled
|
|
19
|
+
return modelValue.value?.toLocaleString() ?? "";
|
|
20
|
+
},
|
|
15
21
|
set: (val: string) => {
|
|
22
|
+
if (props.infinityEnabled && val === "∞") {
|
|
23
|
+
modelValue.value = 0; // Convert back to 0 internally
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
const rawValue = val.replace(/\D/g, ""); // Remove non-numeric characters
|
|
17
28
|
const numericValue = rawValue ? Number(rawValue) : 0;
|
|
18
29
|
|
|
@@ -30,7 +41,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
30
41
|
const { selectionStart, selectionEnd, value } = inputRef.value;
|
|
31
42
|
const isAllSelected = selectionStart === 0 && selectionEnd === value.length;
|
|
32
43
|
|
|
33
|
-
// Allow only numbers (prevent non-numeric input)
|
|
34
44
|
if (
|
|
35
45
|
!/^\d$/.test(event.key) &&
|
|
36
46
|
![
|
|
@@ -46,7 +56,6 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
46
56
|
return;
|
|
47
57
|
}
|
|
48
58
|
|
|
49
|
-
// If all text is selected and a number is pressed, replace entire value
|
|
50
59
|
if (isAllSelected && /^\d$/.test(event.key)) {
|
|
51
60
|
event.preventDefault();
|
|
52
61
|
modelValue.value = Number(event.key);
|
|
@@ -55,13 +64,11 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
55
64
|
return;
|
|
56
65
|
}
|
|
57
66
|
|
|
58
|
-
// Allow cursor movement with ArrowLeft & ArrowRight
|
|
59
67
|
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
60
68
|
forceCursorToEnd = false;
|
|
61
69
|
return;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
// Force cursor to end when incrementing/decrementing
|
|
65
72
|
if (event.key === "ArrowUp") {
|
|
66
73
|
event.preventDefault();
|
|
67
74
|
modelValue.value += 1;
|
|
@@ -71,21 +78,21 @@ const handleKeyDown = (event: KeyboardEvent) => {
|
|
|
71
78
|
modelValue.value = Math.max(0, modelValue.value - 1);
|
|
72
79
|
forceCursorToEnd = true;
|
|
73
80
|
} else {
|
|
74
|
-
cursorPosition = selectionStart || 0;
|
|
81
|
+
cursorPosition = selectionStart || 0;
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
nextTick(() => restoreCursor());
|
|
78
85
|
};
|
|
79
86
|
|
|
80
|
-
// Restore cursor position
|
|
87
|
+
// Restore cursor position
|
|
81
88
|
const restoreCursor = () => {
|
|
82
89
|
if (!inputRef.value) return;
|
|
83
90
|
const length = formattedValue.value.length;
|
|
84
91
|
|
|
85
92
|
if (forceCursorToEnd) {
|
|
86
|
-
inputRef.value.setSelectionRange(length, length);
|
|
93
|
+
inputRef.value.setSelectionRange(length, length);
|
|
87
94
|
} else {
|
|
88
|
-
inputRef.value.setSelectionRange(cursorPosition, cursorPosition);
|
|
95
|
+
inputRef.value.setSelectionRange(cursorPosition, cursorPosition);
|
|
89
96
|
}
|
|
90
97
|
};
|
|
91
98
|
</script>
|
|
@@ -96,7 +103,22 @@ const restoreCursor = () => {
|
|
|
96
103
|
v-model="formattedValue"
|
|
97
104
|
v-bind="attrs"
|
|
98
105
|
type="text"
|
|
99
|
-
placeholder="Enter a number"
|
|
100
106
|
@keydown="handleKeyDown"
|
|
101
|
-
|
|
107
|
+
>
|
|
108
|
+
<template v-if="$slots.prepend" v-slot:prepend>
|
|
109
|
+
<slot name="prepend"></slot>
|
|
110
|
+
</template>
|
|
111
|
+
|
|
112
|
+
<template v-if="$slots.append" v-slot:append>
|
|
113
|
+
<slot name="append"></slot>
|
|
114
|
+
</template>
|
|
115
|
+
|
|
116
|
+
<template v-if="$slots['append-inner']" v-slot:append-inner>
|
|
117
|
+
<slot name="append-inner"></slot>
|
|
118
|
+
</template>
|
|
119
|
+
|
|
120
|
+
<template v-if="$slots['prepend-inner']" v-slot:prepend-inner>
|
|
121
|
+
<slot name="prepend-inner"></slot>
|
|
122
|
+
</template>
|
|
123
|
+
</v-text-field>
|
|
102
124
|
</template>
|
package/components/SwitchOrg.vue
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
>
|
|
13
13
|
<template #activator="{ props }">
|
|
14
14
|
<v-btn variant="text" class="text-none ml-2" v-bind="props">
|
|
15
|
-
<div class="d-block text-truncate" style="
|
|
15
|
+
<div class="d-block text-truncate" style="width: 130px">
|
|
16
16
|
{{ selectedOrg?.text || "Select organization" }}
|
|
17
17
|
</div>
|
|
18
18
|
<v-icon class="ml-1">mdi-menu-down</v-icon>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export default function useOrder() {
|
|
2
|
+
function getOrders({
|
|
3
|
+
search = "",
|
|
4
|
+
id = "",
|
|
5
|
+
page = 1,
|
|
6
|
+
status = "succeeded",
|
|
7
|
+
} = {}) {
|
|
8
|
+
return useNuxtApp().$api<Record<string, any>>("/api/orders", {
|
|
9
|
+
method: "GET",
|
|
10
|
+
query: {
|
|
11
|
+
search,
|
|
12
|
+
id,
|
|
13
|
+
page,
|
|
14
|
+
status,
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
getOrders,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -7,7 +7,7 @@ export default function usePaymentMethod() {
|
|
|
7
7
|
success_return_url = "",
|
|
8
8
|
failure_return_url = "",
|
|
9
9
|
cancel_return_url = "",
|
|
10
|
-
|
|
10
|
+
category = "individual",
|
|
11
11
|
} = {}) {
|
|
12
12
|
return useNuxtApp().$api<Record<string, any>>(
|
|
13
13
|
"/api/payment-methods/e-wallet",
|
|
@@ -21,7 +21,7 @@ export default function usePaymentMethod() {
|
|
|
21
21
|
success_return_url,
|
|
22
22
|
failure_return_url,
|
|
23
23
|
cancel_return_url,
|
|
24
|
-
|
|
24
|
+
category,
|
|
25
25
|
},
|
|
26
26
|
}
|
|
27
27
|
);
|
|
@@ -81,6 +81,14 @@ export default function usePaymentMethod() {
|
|
|
81
81
|
const cardholderName = useState("cardholderName", () => "");
|
|
82
82
|
const selectedPaymentMethod = useState("selectedPaymentMethod", () => "");
|
|
83
83
|
|
|
84
|
+
function reset() {
|
|
85
|
+
eWalletNumber.value = "";
|
|
86
|
+
cardNumber.value = "";
|
|
87
|
+
cardExpiration.value = "";
|
|
88
|
+
cardSecurityCode.value = "";
|
|
89
|
+
cardholderName.value = "";
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
return {
|
|
85
93
|
linkEWallet,
|
|
86
94
|
linkCard,
|
|
@@ -92,5 +100,6 @@ export default function usePaymentMethod() {
|
|
|
92
100
|
cardSecurityCode,
|
|
93
101
|
cardholderName,
|
|
94
102
|
selectedPaymentMethod,
|
|
103
|
+
reset,
|
|
95
104
|
};
|
|
96
105
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export default function usePromoCode() {
|
|
2
|
+
function add(value: TPromoCode) {
|
|
3
|
+
return useNuxtApp().$api<Record<string, any>>("/api/promo-codes", {
|
|
4
|
+
method: "POST",
|
|
5
|
+
body: value,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getPromoCodes({ search = "", page = 1, status = "active" } = {}) {
|
|
10
|
+
return useNuxtApp().$api<Record<string, any>>("/api/promo-codes", {
|
|
11
|
+
method: "GET",
|
|
12
|
+
query: {
|
|
13
|
+
search,
|
|
14
|
+
page,
|
|
15
|
+
status,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getByCode(code: string) {
|
|
21
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
22
|
+
`/api/promo-codes/code/${code}`,
|
|
23
|
+
{
|
|
24
|
+
method: "GET",
|
|
25
|
+
}
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
add,
|
|
31
|
+
getPromoCodes,
|
|
32
|
+
getByCode,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -13,6 +13,18 @@ export default function useSubscription() {
|
|
|
13
13
|
return useNuxtApp().$api<TSubscription>(`/api/subscriptions/id/${id}`);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function getByAffiliateId(id: string) {
|
|
17
|
+
return useNuxtApp().$api<TSubscription>(
|
|
18
|
+
`/api/subscriptions/affiliate/${id}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getByOrgId(id: string) {
|
|
23
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
24
|
+
`/api/subscriptions/org/${id}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
16
28
|
function getSubscriptions() {
|
|
17
29
|
return useNuxtApp().$api("/api/subscriptions");
|
|
18
30
|
}
|
|
@@ -34,61 +46,84 @@ export default function useSubscription() {
|
|
|
34
46
|
() => "inactive"
|
|
35
47
|
);
|
|
36
48
|
|
|
37
|
-
async function
|
|
49
|
+
async function affSubscriptionStatus() {
|
|
38
50
|
const { currentUser } = useLocalAuth();
|
|
39
51
|
|
|
40
52
|
if (currentUser.value && currentUser.value._id) {
|
|
41
53
|
try {
|
|
42
|
-
const result = await
|
|
43
|
-
affiliateSubscription.value = result.status;
|
|
54
|
+
const result = await getByAffiliateId(currentUser.value._id);
|
|
55
|
+
affiliateSubscription.value = result.status as string;
|
|
44
56
|
} catch (error) {
|
|
45
57
|
console.error("failed to get the subscription", error);
|
|
46
58
|
}
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
});
|
|
62
|
+
const orgSubscription = useState("orgSubscription", () => "inactive");
|
|
63
|
+
|
|
64
|
+
async function orgSubscriptionStatus() {
|
|
65
|
+
const { currentOrg } = useOrg();
|
|
66
|
+
|
|
67
|
+
if (currentOrg.value) {
|
|
68
|
+
try {
|
|
69
|
+
const result = await getByOrgId(currentOrg.value);
|
|
70
|
+
orgSubscription.value = result.status as string;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error("failed to get the subscription", error);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
type
|
|
77
|
+
type TSub = {
|
|
68
78
|
customer_id: string;
|
|
69
79
|
amount: number;
|
|
70
80
|
payment_method_id: string;
|
|
81
|
+
payment_method_type: string;
|
|
82
|
+
payment_method_channel: string;
|
|
83
|
+
payment_method_number: string;
|
|
84
|
+
payment_method_expiry_month?: string;
|
|
85
|
+
payment_method_expiry_year?: string;
|
|
86
|
+
payment_method_cvv?: string;
|
|
87
|
+
payment_method_cardholder_name?: string;
|
|
71
88
|
currency?: string;
|
|
72
|
-
|
|
89
|
+
seats?: number;
|
|
90
|
+
organization?: TOrg;
|
|
73
91
|
billingAddress: TAddress;
|
|
74
92
|
};
|
|
75
93
|
|
|
76
|
-
function initOrgSubscription(value:
|
|
77
|
-
return useNuxtApp().$api
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
function initOrgSubscription(value: TSub) {
|
|
95
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
96
|
+
"/api/subscriptions/organization",
|
|
97
|
+
{
|
|
98
|
+
method: "POST",
|
|
99
|
+
body: value,
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function initAffiliateSubscription(value: TSub) {
|
|
105
|
+
return useNuxtApp().$api<Record<string, any>>(
|
|
106
|
+
"/api/subscriptions/affiliate",
|
|
107
|
+
{
|
|
108
|
+
method: "POST",
|
|
109
|
+
body: value,
|
|
110
|
+
}
|
|
111
|
+
);
|
|
81
112
|
}
|
|
82
113
|
|
|
83
114
|
return {
|
|
84
115
|
add,
|
|
85
116
|
getById,
|
|
117
|
+
getByOrgId,
|
|
86
118
|
getSubscriptions,
|
|
119
|
+
affSubscriptionStatus,
|
|
87
120
|
affiliateSubscription,
|
|
88
|
-
|
|
121
|
+
orgSubscriptionStatus,
|
|
122
|
+
orgSubscription,
|
|
89
123
|
getSubscriptionStatusById,
|
|
90
124
|
cancelSubscription,
|
|
91
125
|
initAffiliateSubscription,
|
|
92
126
|
initOrgSubscription,
|
|
127
|
+
getByAffiliateId,
|
|
93
128
|
};
|
|
94
129
|
}
|
package/composables/useUtils.ts
CHANGED
|
@@ -6,6 +6,31 @@ export default function useUtils() {
|
|
|
6
6
|
return regex.test(v) || "Please enter a valid email address";
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
+
const validateDate = (value: string): boolean | string => {
|
|
10
|
+
const dateRegex = /^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/;
|
|
11
|
+
|
|
12
|
+
if (!dateRegex.test(value)) return "Invalid date format (MM/DD/YYYY)";
|
|
13
|
+
|
|
14
|
+
return true;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const minOneMonthAdvance = (value: string): boolean | string => {
|
|
18
|
+
if (!/^(0[1-9]|1[0-2])\/(0[1-9]|[12][0-9]|3[01])\/\d{4}$/.test(value)) {
|
|
19
|
+
return "Invalid date format (MM/DD/YYYY)";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const [month, day, year] = value.split("/").map(Number);
|
|
23
|
+
const selectedDate = new Date(year, month - 1, day);
|
|
24
|
+
|
|
25
|
+
const currentDate = new Date();
|
|
26
|
+
const minDate = new Date();
|
|
27
|
+
minDate.setMonth(currentDate.getMonth() + 1); // 1 month in advance
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
selectedDate >= minDate || "Date must be at least 1 month in advance"
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
9
34
|
const passwordRule = (v: string) =>
|
|
10
35
|
v.length >= 8 || "Password must be at least 8 characters long";
|
|
11
36
|
|
|
@@ -131,6 +156,39 @@ export default function useUtils() {
|
|
|
131
156
|
maximumFractionDigits: 2,
|
|
132
157
|
});
|
|
133
158
|
|
|
159
|
+
function formatNumber(
|
|
160
|
+
amount: number,
|
|
161
|
+
options: {
|
|
162
|
+
currency?: string;
|
|
163
|
+
locale?: string;
|
|
164
|
+
useSymbol?: boolean;
|
|
165
|
+
decimalPlaces?: number;
|
|
166
|
+
} = {}
|
|
167
|
+
): string {
|
|
168
|
+
const {
|
|
169
|
+
currency,
|
|
170
|
+
locale = "en-US",
|
|
171
|
+
useSymbol = false,
|
|
172
|
+
decimalPlaces = 2,
|
|
173
|
+
} = options;
|
|
174
|
+
|
|
175
|
+
return new Intl.NumberFormat(locale, {
|
|
176
|
+
style: useSymbol && currency ? "currency" : "decimal",
|
|
177
|
+
currency: currency || "USD", // Default currency (ignored if `useSymbol` is false)
|
|
178
|
+
minimumFractionDigits: decimalPlaces,
|
|
179
|
+
maximumFractionDigits: decimalPlaces,
|
|
180
|
+
}).format(amount);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 🔹 Examples:
|
|
184
|
+
// console.log(formatNumber(1234.56)); // "1,234.56" (comma separator, 2 decimals)
|
|
185
|
+
// console.log(formatNumber(1234.56, { decimalPlaces: 0 })); // "1,234" (no decimals)
|
|
186
|
+
// console.log(formatNumber(1234.56, { useSymbol: true, currency: "USD" })); // "$1,234.56"
|
|
187
|
+
// console.log(formatNumber(1234.56, { useSymbol: true, currency: "PHP", decimalPlaces: 0 })); // "₱1,234"
|
|
188
|
+
// console.log(formatNumber(1234.56, { useSymbol: false, decimalPlaces: 0 })); // "1,234"
|
|
189
|
+
// console.log(formatNumber(1234.56, { useSymbol: true, currency: "EUR", locale: "de-DE" })); // "1.234,56 €"
|
|
190
|
+
// console.log(formatNumber(1234.56, { useSymbol: true, currency: "EUR", locale: "de-DE", decimalPlaces: 0 })); // "1.234 €"
|
|
191
|
+
|
|
134
192
|
return {
|
|
135
193
|
requiredRule,
|
|
136
194
|
emailRule,
|
|
@@ -146,5 +204,8 @@ export default function useUtils() {
|
|
|
146
204
|
validateWord,
|
|
147
205
|
getCountries,
|
|
148
206
|
formatter,
|
|
207
|
+
formatNumber,
|
|
208
|
+
validateDate,
|
|
209
|
+
minOneMonthAdvance,
|
|
149
210
|
};
|
|
150
211
|
}
|
package/error.vue
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { NuxtError } from "#app";
|
|
3
|
+
|
|
4
|
+
const props = defineProps({
|
|
5
|
+
error: Object as () => NuxtError,
|
|
6
|
+
});
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<template>
|
|
10
|
+
<v-app>
|
|
11
|
+
<v-main>
|
|
12
|
+
<v-row
|
|
13
|
+
class="fill-height"
|
|
14
|
+
no-gutters
|
|
15
|
+
justify="center"
|
|
16
|
+
align-content="center"
|
|
17
|
+
>
|
|
18
|
+
<v-col cols="9">
|
|
19
|
+
<v-row>
|
|
20
|
+
<v-col cols="12" class="text-center text-h2">
|
|
21
|
+
<span class="font-weight-bold">{{ props.error?.message }}</span>
|
|
22
|
+
</v-col>
|
|
23
|
+
<v-col cols="12">
|
|
24
|
+
<v-row no-gutters justify="center">
|
|
25
|
+
<v-btn
|
|
26
|
+
rounded="xl"
|
|
27
|
+
size="large"
|
|
28
|
+
class="text-none"
|
|
29
|
+
variant="tonal"
|
|
30
|
+
:to="{ name: 'index' }"
|
|
31
|
+
>
|
|
32
|
+
Go to home page
|
|
33
|
+
</v-btn>
|
|
34
|
+
</v-row>
|
|
35
|
+
</v-col>
|
|
36
|
+
</v-row>
|
|
37
|
+
</v-col>
|
|
38
|
+
</v-row>
|
|
39
|
+
</v-main>
|
|
40
|
+
</v-app>
|
|
41
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export default defineNuxtRouteMiddleware(async () => {
|
|
2
|
+
// Ensure middleware runs only on the client side
|
|
3
|
+
if (import.meta.server) return;
|
|
4
|
+
|
|
5
|
+
const { cookieConfig } = useRuntimeConfig().public;
|
|
6
|
+
|
|
7
|
+
// Get access token from cookies
|
|
8
|
+
const accessToken = useCookie("accessToken", cookieConfig).value;
|
|
9
|
+
|
|
10
|
+
if (!accessToken) {
|
|
11
|
+
// Redirect to login page if no access token
|
|
12
|
+
return navigateTo({ name: "index" });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { getCurrentUser } = useLocalAuth();
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await getCurrentUser();
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.log(error);
|
|
21
|
+
|
|
22
|
+
// Redirect to login page if user authentication fails
|
|
23
|
+
return navigateTo({ name: "index" });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const hexSchema = z
|
|
4
|
+
.string()
|
|
5
|
+
.regex(/^[0-9a-fA-F]{24}$/, "Invalid organization ID");
|
|
6
|
+
|
|
7
|
+
export default defineNuxtRouteMiddleware((to) => {
|
|
8
|
+
const { organization } = to.params;
|
|
9
|
+
|
|
10
|
+
if (organization && !hexSchema.safeParse(organization).success) {
|
|
11
|
+
return navigateTo(
|
|
12
|
+
{ name: "require-organization-membership" },
|
|
13
|
+
{ replace: true }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
});
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@goweekdays/layer-common",
|
|
3
3
|
"license": "MIT",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.0.
|
|
5
|
+
"version": "0.0.7",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@mdi/font": "^7.4.47",
|
|
31
|
-
"sass": "^1.80.6"
|
|
31
|
+
"sass": "^1.80.6",
|
|
32
|
+
"zod": "^3.24.2"
|
|
32
33
|
}
|
|
33
34
|
}
|
package/pages/index.vue
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="fill-height" justify="center" align-content="center">
|
|
3
|
+
<v-card class="pa-4" elevation="4" color="success lighten-1">
|
|
4
|
+
<v-card-title class="text-white d-flex align-center">
|
|
5
|
+
<v-icon class="mr-2">mdi-check-circle</v-icon>
|
|
6
|
+
Successfully Linked Payment Method
|
|
7
|
+
</v-card-title>
|
|
8
|
+
|
|
9
|
+
<v-card-actions class="d-flex justify-center">
|
|
10
|
+
<v-btn
|
|
11
|
+
color="white"
|
|
12
|
+
variant="tonal"
|
|
13
|
+
rounded="xl"
|
|
14
|
+
size="large"
|
|
15
|
+
@click="closeWindow"
|
|
16
|
+
>Close</v-btn
|
|
17
|
+
>
|
|
18
|
+
</v-card-actions>
|
|
19
|
+
</v-card>
|
|
20
|
+
</v-row>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup>
|
|
24
|
+
definePageMeta({
|
|
25
|
+
layout: "plain",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const closeWindow = () => {
|
|
29
|
+
window.close();
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="fill-height" align="center" justify="center">
|
|
3
|
+
<v-col cols="12" lg="6" md="6" sm="6" class="text-center text-subtitle-1">
|
|
4
|
+
You must be a member of an organization to view this page. Please join or
|
|
5
|
+
contact your administrator for access.
|
|
6
|
+
<v-row no-gutters justify="center" align="center" class="mt-2">
|
|
7
|
+
<v-btn
|
|
8
|
+
@click="goToAccount"
|
|
9
|
+
rounded="xl"
|
|
10
|
+
variant="tonal"
|
|
11
|
+
class="text-none text-subtitle-2"
|
|
12
|
+
>
|
|
13
|
+
Go to Account Page
|
|
14
|
+
</v-btn>
|
|
15
|
+
<span class="mx-2">or</span>
|
|
16
|
+
<v-btn
|
|
17
|
+
@click="createOrg"
|
|
18
|
+
rounded="xl"
|
|
19
|
+
variant="tonal"
|
|
20
|
+
class="text-none text-subtitle-2"
|
|
21
|
+
>
|
|
22
|
+
Create an Organization
|
|
23
|
+
</v-btn>
|
|
24
|
+
</v-row>
|
|
25
|
+
</v-col>
|
|
26
|
+
</v-row>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
definePageMeta({
|
|
31
|
+
layout: "plain",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const { APP_ACCOUNT, APP_NAME, APP_ORG } = useRuntimeConfig().public;
|
|
35
|
+
|
|
36
|
+
function goToAccount() {
|
|
37
|
+
window.location.href = `${APP_ACCOUNT}/home`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createOrg() {
|
|
41
|
+
if (APP_NAME.toLowerCase() === "org") {
|
|
42
|
+
navigateTo({ name: "organizations-create" });
|
|
43
|
+
} else {
|
|
44
|
+
window.location.href = `${APP_ORG}/organizations/create`;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-row no-gutters class="fill-height" align="center" justify="center">
|
|
3
|
+
<v-col cols="12" lg="6" md="6" sm="6" class="text-center text-subtitle-1">
|
|
4
|
+
You must be a member of an organization to view this page. Please join or
|
|
5
|
+
contact your administrator for access.
|
|
6
|
+
<v-row no-gutters justify="center" class="mt-2">
|
|
7
|
+
<v-btn
|
|
8
|
+
@click="goToAccount"
|
|
9
|
+
rounded="xl"
|
|
10
|
+
variant="tonal"
|
|
11
|
+
class="text-none text-subtitle-2"
|
|
12
|
+
>
|
|
13
|
+
Go to Account Page
|
|
14
|
+
</v-btn>
|
|
15
|
+
</v-row>
|
|
16
|
+
</v-col>
|
|
17
|
+
</v-row>
|
|
18
|
+
</template>
|
|
19
|
+
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
definePageMeta({
|
|
22
|
+
layout: "plain",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
function goToAccount() {
|
|
26
|
+
const { APP_ACCOUNT } = useRuntimeConfig().public;
|
|
27
|
+
window.location.href = `${APP_ACCOUNT}/home`;
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
declare type TPromoTier = {
|
|
2
|
+
min: number;
|
|
3
|
+
max: number;
|
|
4
|
+
price: number;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
declare type TPromoCode = {
|
|
8
|
+
_id?: ObjectId;
|
|
9
|
+
code: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
type: "tiered" | "fixed";
|
|
12
|
+
tiers?: TPromoTier[];
|
|
13
|
+
fixed_rate?: number;
|
|
14
|
+
appliesTo?: string; // "subscription" | "order"
|
|
15
|
+
createdAt?: string; // Date of creation
|
|
16
|
+
expiresAt?: string; // Optional expiry
|
|
17
|
+
assignedTo?: string | ObjectId; // Store the only ape who used it
|
|
18
|
+
status?: "active" | "expired" | "disabled";
|
|
19
|
+
};
|