pukaad-ui-lib 1.240.0 → 1.242.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/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
3
  "configKey": "pukaadUI",
4
- "version": "1.240.0",
4
+ "version": "1.242.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -15,13 +15,15 @@
15
15
  {{ props.label }}
16
16
  <span v-if="props.required" class="text-destructive">*</span>
17
17
  </div>
18
- <div
18
+ <Button
19
19
  v-if="!props.disabledForgotPassword"
20
- class="font-body-medium text-primary"
20
+ variant="text"
21
+ color="primary"
22
+ class="font-body-medium"
21
23
  @click="handleForgotPassword"
22
24
  >
23
25
  ลืมรหัสผ่าน
24
- </div>
26
+ </Button>
25
27
  </ShadFormLabel>
26
28
  </template>
27
29
  </InputTextField>
@@ -46,6 +48,7 @@
46
48
 
47
49
  <script setup>
48
50
  import { ref, computed } from "vue";
51
+ import Button from "../button.vue";
49
52
  const props = defineProps({
50
53
  new: { type: Boolean, required: false, default: false },
51
54
  disabledForgotPassword: { type: Boolean, required: false, default: false },
@@ -124,8 +127,8 @@ const defaultRules = (v) => {
124
127
  if (isNotNoSpace) return "\u0E2B\u0E49\u0E32\u0E21\u0E40\u0E27\u0E49\u0E19\u0E27\u0E23\u0E23\u0E04";
125
128
  return true;
126
129
  };
127
- const handleForgotPassword = () => {
128
- console.log("forgot-password");
130
+ const handleForgotPassword = (e) => {
131
+ emits("forgotPassword", e);
129
132
  };
130
133
  const inputTextFieldRef = ref();
131
134
  const setErrors = (errMsg) => {
@@ -1,6 +1,16 @@
1
1
  export interface ModalEmailOTPProps {
2
2
  email?: string;
3
3
  confirmedText?: string;
4
+ /** Custom API path for requesting OTP. Defaults to /me/email-otp-request */
5
+ requestPath?: string;
6
+ /** Custom request body. When provided, used instead of { email }. */
7
+ requestBody?: Record<string, any>;
8
+ /** Custom API path for verifying OTP. Defaults to /me/email-otp-verify */
9
+ verifyPath?: string;
10
+ /** Extra body fields merged with { code } when verifying. Defaults to { email }. */
11
+ verifyBody?: Record<string, any>;
12
+ /** If provided, sets the initial countdown and skips auto-request on open. */
13
+ initialExpiredTime?: string;
4
14
  }
5
15
  type __VLS_Props = ModalEmailOTPProps;
6
16
  type __VLS_ModelProps = {
@@ -10,10 +20,10 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
20
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
21
  "update:modelValue": (value: boolean) => any;
12
22
  } & {
13
- complete: () => any;
23
+ complete: (data?: any) => any;
14
24
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
25
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
16
- onComplete?: (() => any) | undefined;
26
+ onComplete?: ((data?: any) => any) | undefined;
17
27
  }>, {
18
28
  confirmedText: string;
19
29
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -44,7 +44,12 @@ const { CountdownTime } = useCountDown();
44
44
  const emits = defineEmits(["complete"]);
45
45
  const props = defineProps({
46
46
  email: { type: String, required: false },
47
- confirmedText: { type: String, required: false, default: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19" }
47
+ confirmedText: { type: String, required: false, default: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19" },
48
+ requestPath: { type: String, required: false },
49
+ requestBody: { type: Object, required: false },
50
+ verifyPath: { type: String, required: false },
51
+ verifyBody: { type: Object, required: false },
52
+ initialExpiredTime: { type: String, required: false }
48
53
  });
49
54
  const isOpen = defineModel({ type: Boolean, ...{
50
55
  default: false
@@ -64,16 +69,15 @@ const maskedEmail = computed(() => {
64
69
  });
65
70
  const onVerifyOTP = async () => {
66
71
  try {
67
- await api("/me/email-otp-verify", {
72
+ const path = props.verifyPath ?? "/me/email-otp-verify";
73
+ const body = props.verifyBody ? { ...props.verifyBody, code: valueOTP.value } : { code: valueOTP.value, email: props.email };
74
+ const response = await api(path, {
68
75
  method: "POST",
69
- body: {
70
- code: valueOTP.value,
71
- email: props.email
72
- }
76
+ body
73
77
  });
74
78
  valueOTP.value = "";
75
79
  isOpen.value = false;
76
- emits("complete");
80
+ emits("complete", response?.data);
77
81
  } catch (e) {
78
82
  $toast.error("OTP \u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07");
79
83
  }
@@ -81,16 +85,13 @@ const onVerifyOTP = async () => {
81
85
  const handleSendEmail = async () => {
82
86
  try {
83
87
  isLoading.value = true;
84
- if (!props.email) throw new Error("\u0E01\u0E23\u0E38\u0E13\u0E32\u0E23\u0E30\u0E1A\u0E38 email");
85
- const response = await api(
86
- "/me/email-otp-request",
87
- {
88
- method: "POST",
89
- body: {
90
- email: props.email
91
- }
92
- }
93
- );
88
+ const path = props.requestPath ?? "/me/email-otp-request";
89
+ const body = props.requestBody ?? { email: props.email };
90
+ if (!props.requestBody && !props.email) throw new Error("\u0E01\u0E23\u0E38\u0E13\u0E32\u0E23\u0E30\u0E1A\u0E38 email");
91
+ const response = await api(path, {
92
+ method: "POST",
93
+ body
94
+ });
94
95
  timeExp.value = response.data.expired_time;
95
96
  } catch (err) {
96
97
  $toast.error("\u0E1E\u0E1A\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14 \u0E1A\u0E32\u0E07\u0E2D\u0E22\u0E48\u0E32\u0E07 !");
@@ -101,7 +102,11 @@ const handleSendEmail = async () => {
101
102
  watch(isOpen, (v) => {
102
103
  if (v) {
103
104
  valueOTP.value = "";
104
- handleSendEmail();
105
+ if (props.initialExpiredTime) {
106
+ timeExp.value = props.initialExpiredTime;
107
+ } else {
108
+ handleSendEmail();
109
+ }
105
110
  } else {
106
111
  timeExp.value = "";
107
112
  }
@@ -1,6 +1,16 @@
1
1
  export interface ModalEmailOTPProps {
2
2
  email?: string;
3
3
  confirmedText?: string;
4
+ /** Custom API path for requesting OTP. Defaults to /me/email-otp-request */
5
+ requestPath?: string;
6
+ /** Custom request body. When provided, used instead of { email }. */
7
+ requestBody?: Record<string, any>;
8
+ /** Custom API path for verifying OTP. Defaults to /me/email-otp-verify */
9
+ verifyPath?: string;
10
+ /** Extra body fields merged with { code } when verifying. Defaults to { email }. */
11
+ verifyBody?: Record<string, any>;
12
+ /** If provided, sets the initial countdown and skips auto-request on open. */
13
+ initialExpiredTime?: string;
4
14
  }
5
15
  type __VLS_Props = ModalEmailOTPProps;
6
16
  type __VLS_ModelProps = {
@@ -10,10 +20,10 @@ type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
20
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
21
  "update:modelValue": (value: boolean) => any;
12
22
  } & {
13
- complete: () => any;
23
+ complete: (data?: any) => any;
14
24
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
25
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
16
- onComplete?: (() => any) | undefined;
26
+ onComplete?: ((data?: any) => any) | undefined;
17
27
  }>, {
18
28
  confirmedText: string;
19
29
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -0,0 +1,13 @@
1
+ type __VLS_ModelProps = {
2
+ modelValue?: boolean;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:modelValue": (value: boolean) => any;
6
+ } & {
7
+ complete: () => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
9
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
10
+ onComplete?: (() => any) | undefined;
11
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
@@ -0,0 +1,146 @@
1
+ <template>
2
+ <component
3
+ v-for="modal in modals"
4
+ :key="modal.key"
5
+ :is="modal.component"
6
+ v-bind="modalProps[modal.key]"
7
+ v-model="modalStates[modal.key]"
8
+ @complete="(data) => handleComplete(modal.key, data)"
9
+ @close="isOpen = false"
10
+ />
11
+ </template>
12
+
13
+ <script setup>
14
+ import { reactive, ref, computed, watch } from "vue";
15
+ import { useNuxtApp } from "nuxt/app";
16
+ import { useApi } from "../../composables/useApi";
17
+ import ModalUserAccountSearch from "@/runtime/components/modal/modal-user-account-search.vue";
18
+ import ModalPhoneOTP from "@/runtime/components/modal/modal-phone-OTP.vue";
19
+ import ModalEmailOTP from "@/runtime/components/modal/modal-email-OTP.vue";
20
+ import ModalPasswordNew from "@/runtime/components/modal/modal-password-new.vue";
21
+ const { $toast } = useNuxtApp();
22
+ const api = useApi();
23
+ const emit = defineEmits(["complete"]);
24
+ const isOpen = defineModel({ type: Boolean, ...{ default: false } });
25
+ const phone = ref("");
26
+ const maskedPhone = ref("");
27
+ const maskedEmail = ref(null);
28
+ const method = ref("sms");
29
+ const expiredTime = ref("");
30
+ const resetToken = ref("");
31
+ const isLoading = ref(false);
32
+ const modals = [
33
+ { key: "accountSearch", component: ModalUserAccountSearch },
34
+ { key: "phoneOTP", component: ModalPhoneOTP },
35
+ { key: "emailOTP", component: ModalEmailOTP },
36
+ { key: "passwordNew", component: ModalPasswordNew }
37
+ ];
38
+ const modalStates = reactive(
39
+ Object.fromEntries(modals.map((m) => [m.key, false]))
40
+ );
41
+ const modalProps = computed(() => ({
42
+ accountSearch: {
43
+ confirmedText: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19"
44
+ },
45
+ phoneOTP: {
46
+ phone: phone.value,
47
+ phoneLabel: maskedPhone.value,
48
+ confirmedText: "\u0E16\u0E31\u0E14\u0E44\u0E1B"
49
+ },
50
+ emailOTP: {
51
+ email: maskedEmail.value ?? "",
52
+ requestPath: "/auth/forgot-password/send-otp",
53
+ requestBody: { phone: phone.value, method: "email" },
54
+ verifyPath: "/auth/forgot-password/verify-otp",
55
+ verifyBody: { phone: phone.value },
56
+ initialExpiredTime: expiredTime.value,
57
+ confirmedText: "\u0E16\u0E31\u0E14\u0E44\u0E1B"
58
+ },
59
+ passwordNew: {
60
+ resetToken: resetToken.value,
61
+ disabledForceLogout: false,
62
+ title: "\u0E15\u0E31\u0E49\u0E07\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48",
63
+ confirmText: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19"
64
+ }
65
+ }));
66
+ const handleComplete = async (key, data) => {
67
+ if (key === "accountSearch") {
68
+ const result = data;
69
+ phone.value = result.phone;
70
+ method.value = result.method;
71
+ maskedPhone.value = result.maskedPhone;
72
+ maskedEmail.value = result.maskedEmail;
73
+ await callSendOTP();
74
+ return;
75
+ }
76
+ if (key === "phoneOTP") {
77
+ await callVerifyOTP(data);
78
+ return;
79
+ }
80
+ if (key === "emailOTP") {
81
+ const token = data?.reset_token;
82
+ if (!token) {
83
+ $toast?.error?.("\u0E44\u0E21\u0E48\u0E1E\u0E1A reset token");
84
+ return;
85
+ }
86
+ resetToken.value = token;
87
+ modalStates.passwordNew = true;
88
+ return;
89
+ }
90
+ if (key === "passwordNew") {
91
+ isOpen.value = false;
92
+ $toast?.success?.("\u0E23\u0E35\u0E40\u0E0B\u0E47\u0E15\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08 !");
93
+ emit("complete");
94
+ return;
95
+ }
96
+ };
97
+ const callSendOTP = async () => {
98
+ isLoading.value = true;
99
+ try {
100
+ const res = await api("/auth/forgot-password/send-otp", {
101
+ method: "POST",
102
+ body: { phone: phone.value, method: method.value }
103
+ });
104
+ expiredTime.value = res.data.expired_time;
105
+ if (method.value === "sms") {
106
+ modalStates.phoneOTP = true;
107
+ } else {
108
+ modalStates.emailOTP = true;
109
+ }
110
+ } catch (e) {
111
+ const msg = e?.data?.message?.description || e?.data?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14\u0E43\u0E19\u0E01\u0E32\u0E23\u0E2A\u0E48\u0E07 OTP";
112
+ $toast?.error?.(msg);
113
+ } finally {
114
+ isLoading.value = false;
115
+ }
116
+ };
117
+ const callVerifyOTP = async (code) => {
118
+ isLoading.value = true;
119
+ try {
120
+ const res = await api("/auth/forgot-password/verify-otp", {
121
+ method: "POST",
122
+ body: { phone: phone.value, code }
123
+ });
124
+ resetToken.value = res.data.reset_token;
125
+ modalStates.passwordNew = true;
126
+ } catch (e) {
127
+ const msg = e?.data?.message?.description || e?.data?.message || "OTP \u0E44\u0E21\u0E48\u0E16\u0E39\u0E01\u0E15\u0E49\u0E2D\u0E07\u0E2B\u0E23\u0E37\u0E2D\u0E2B\u0E21\u0E14\u0E2D\u0E32\u0E22\u0E38";
128
+ $toast?.error?.(msg);
129
+ } finally {
130
+ isLoading.value = false;
131
+ }
132
+ };
133
+ watch(isOpen, (v) => {
134
+ if (v) {
135
+ phone.value = "";
136
+ maskedPhone.value = "";
137
+ maskedEmail.value = null;
138
+ method.value = "sms";
139
+ expiredTime.value = "";
140
+ resetToken.value = "";
141
+ modalStates.accountSearch = true;
142
+ } else {
143
+ modals.forEach((m) => modalStates[m.key] = false);
144
+ }
145
+ });
146
+ </script>
@@ -0,0 +1,13 @@
1
+ type __VLS_ModelProps = {
2
+ modelValue?: boolean;
3
+ };
4
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
5
+ "update:modelValue": (value: boolean) => any;
6
+ } & {
7
+ complete: () => any;
8
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
9
+ "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
10
+ onComplete?: (() => any) | undefined;
11
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
12
+ declare const _default: typeof __VLS_export;
13
+ export default _default;
@@ -2,6 +2,8 @@ interface Props {
2
2
  title?: string;
3
3
  confirmText?: string;
4
4
  loginToken?: string;
5
+ /** When provided, uses forgot-password reset endpoint instead of password/create */
6
+ resetToken?: string;
5
7
  disabledForceLogout?: boolean;
6
8
  }
7
9
  type __VLS_Props = Props;
@@ -14,13 +16,13 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
14
16
  "update:modelValue": (value: boolean) => any;
15
17
  complete: (data: {
16
18
  secId: string;
17
- }) => any;
19
+ } | null) => any;
18
20
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
19
21
  onClose?: (() => any) | undefined;
20
22
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
21
23
  onComplete?: ((data: {
22
24
  secId: string;
23
- }) => any) | undefined;
25
+ } | null) => any) | undefined;
24
26
  }>, {
25
27
  title: string;
26
28
  confirmText: string;
@@ -5,21 +5,23 @@
5
5
  v-model="isOpen"
6
6
  @close="emit('close')"
7
7
  @submit="handleSubmit"
8
+ width="425px"
8
9
  >
9
- <InputPassword
10
- disabled-forgot-password
11
- new
12
- required
13
- label="รหัสผ่าน"
14
- placeholder="กรอกรหัสผ่าน"
15
- v-model="password"
16
- />
17
- <InputCheckbox
18
- v-if="!props.disabledForceLogout"
19
- label="ออกจากระบบ Pukaad ในอุปกรณ์อื่นๆทั้งหมด"
20
- v-model="logoutAll"
21
- />
22
-
10
+ <div class="flex flex-col gap-2">
11
+ <InputPassword
12
+ disabled-forgot-password
13
+ new
14
+ required
15
+ label="รหัสผ่าน"
16
+ placeholder="กรอกรหัสผ่าน"
17
+ v-model="password"
18
+ />
19
+ <InputCheckbox
20
+ v-if="!props.disabledForceLogout"
21
+ label="ออกจากระบบ Pukaad ในอุปกรณ์อื่นๆทั้งหมด"
22
+ v-model="logoutAll"
23
+ />
24
+ </div>
23
25
  <template #footer="{ meta }">
24
26
  <Button
25
27
  type="submit"
@@ -36,15 +38,18 @@
36
38
  <script setup>
37
39
  import { ref, computed, watch } from "vue";
38
40
  import { useNuxtApp, useRuntimeConfig } from "nuxt/app";
41
+ import { useApi } from "../../composables/useApi";
39
42
  const props = defineProps({
40
43
  title: { type: String, required: false, default: "\u0E23\u0E2B\u0E31\u0E2A\u0E1C\u0E48\u0E32\u0E19\u0E43\u0E2B\u0E21\u0E48" },
41
44
  confirmText: { type: String, required: false, default: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19" },
42
45
  loginToken: { type: String, required: false },
46
+ resetToken: { type: String, required: false },
43
47
  disabledForceLogout: { type: Boolean, required: false, default: false }
44
48
  });
45
49
  const emit = defineEmits(["complete", "close"]);
46
50
  const { $toast } = useNuxtApp();
47
51
  const config = useRuntimeConfig();
52
+ const api = useApi();
48
53
  const isOpen = defineModel({ type: Boolean, ...{ default: false } });
49
54
  const loading = ref(false);
50
55
  const password = ref("");
@@ -59,6 +64,27 @@ watch(isOpen, (open) => {
59
64
  }
60
65
  });
61
66
  const handleSubmit = async () => {
67
+ if (props.resetToken) {
68
+ loading.value = true;
69
+ try {
70
+ await api("/auth/forgot-password/reset-password", {
71
+ method: "POST",
72
+ body: {
73
+ reset_token: props.resetToken,
74
+ new_password: password.value,
75
+ logout_all: logoutAll.value
76
+ }
77
+ });
78
+ emit("complete", null);
79
+ isOpen.value = false;
80
+ } catch (e) {
81
+ const msg = e?.data?.message?.description || e?.data?.message || "\u0E40\u0E01\u0E34\u0E14\u0E02\u0E49\u0E2D\u0E1C\u0E34\u0E14\u0E1E\u0E25\u0E32\u0E14\u0E43\u0E19\u0E01\u0E32\u0E23\u0E40\u0E0A\u0E37\u0E48\u0E2D\u0E21\u0E15\u0E48\u0E2D";
82
+ $toast?.error?.(msg);
83
+ } finally {
84
+ loading.value = false;
85
+ }
86
+ return;
87
+ }
62
88
  if (!props.loginToken) {
63
89
  $toast?.error?.("\u0E44\u0E21\u0E48\u0E1E\u0E1A login token");
64
90
  return;
@@ -2,6 +2,8 @@ interface Props {
2
2
  title?: string;
3
3
  confirmText?: string;
4
4
  loginToken?: string;
5
+ /** When provided, uses forgot-password reset endpoint instead of password/create */
6
+ resetToken?: string;
5
7
  disabledForceLogout?: boolean;
6
8
  }
7
9
  type __VLS_Props = Props;
@@ -14,13 +16,13 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {},
14
16
  "update:modelValue": (value: boolean) => any;
15
17
  complete: (data: {
16
18
  secId: string;
17
- }) => any;
19
+ } | null) => any;
18
20
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
19
21
  onClose?: (() => any) | undefined;
20
22
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
21
23
  onComplete?: ((data: {
22
24
  secId: string;
23
- }) => any) | undefined;
25
+ } | null) => any) | undefined;
24
26
  }>, {
25
27
  title: string;
26
28
  confirmText: string;
@@ -1,3 +1,10 @@
1
+ export interface ModalUserAccountSearchResult {
2
+ phone: string;
3
+ method: "sms" | "email";
4
+ maskedPhone: string;
5
+ maskedEmail: string | null;
6
+ hasEmail: boolean;
7
+ }
1
8
  type __VLS_Props = {
2
9
  confirmedText?: string;
3
10
  };
@@ -6,11 +13,14 @@ type __VLS_ModelProps = {
6
13
  };
7
14
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
- complete: (...args: any[]) => void;
10
- "update:modelValue": (value: boolean) => void;
16
+ "update:modelValue": (value: boolean) => any;
17
+ } & {
18
+ close: () => any;
19
+ complete: (data: ModalUserAccountSearchResult) => any;
11
20
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
21
+ onClose?: (() => any) | undefined;
12
22
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
13
- onComplete?: ((...args: any[]) => any) | undefined;
23
+ onComplete?: ((data: ModalUserAccountSearchResult) => any) | undefined;
14
24
  }>, {
15
25
  confirmedText: string;
16
26
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
@@ -1,44 +1,156 @@
1
1
  <template>
2
- <Modal title="ค้นหาบัญชีของคุณ" v-model="modelValue">
3
- <div class="flex flex-col gap-[16px]">
4
- <div class="font-body-large text-2xl text-center px-[50px]">
2
+ <Modal
3
+ :title="step === 'search' ? '\u0E04\u0E49\u0E19\u0E2B\u0E32\u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E02\u0E2D\u0E07\u0E04\u0E38\u0E13' : '\u0E40\u0E25\u0E37\u0E2D\u0E01\u0E27\u0E34\u0E18\u0E35\u0E15\u0E23\u0E27\u0E08\u0E2A\u0E2D\u0E1A'"
4
+ v-model="modelValue"
5
+ width="425px"
6
+ :loading="isLoading"
7
+ >
8
+ <!-- Step 1: phone + recaptcha -->
9
+ <div v-if="step === 'search'" class="flex flex-col gap-[24px]">
10
+ <p class="font-body-large">
5
11
  ป้อนเบอร์โทรศัพท์ที่เชื่อมโยงกับบัญชีของคุณ เพื่อเปลี่ยนรหัสผ่าน
6
- </div>
12
+ </p>
7
13
  <InputPhone
8
14
  label="เบอร์โทรศัพท์"
9
15
  placeholder="กรอกเบอร์โทรศัพท์"
10
16
  v-model="phone"
11
- full-width
12
17
  />
13
18
  <InputRecaptcha ref="recaptchaRef" name="recaptcha" v-model="recaptcha" />
14
19
  </div>
20
+
21
+ <!-- Step 2: select OTP method -->
22
+ <div v-else-if="step === 'list'" class="flex flex-col">
23
+ <template v-for="(item, index) in accountItems" :key="index">
24
+ <div class="py-[12px] px-[16px]">
25
+ <InputRadio
26
+ :item="item.value"
27
+ label-position="left"
28
+ v-model="selectedMethod"
29
+ >
30
+ <template v-slot:[`label-${item.value}-text`]>
31
+ <div
32
+ class="flex flex-col w-full text-gray font-body-medium-prominent"
33
+ >
34
+ <div v-if="item.type === 'email'">ส่งรหัสไปทางอีเมล</div>
35
+ <div v-else-if="item.type === 'phone'">ส่งรหัสไปทาง SMS</div>
36
+ <div>{{ item.label }}</div>
37
+ </div>
38
+ </template>
39
+ </InputRadio>
40
+ </div>
41
+ <Divider v-if="index !== accountItems.length - 1" />
42
+ </template>
43
+ </div>
44
+
45
+ <!-- Footer slot (must be direct child of Modal) -->
15
46
  <template #footer>
16
47
  <Button
17
- full-width
18
- variant="primary"
19
- :disabled="!phone || !recaptcha"
20
- @click="onNext"
48
+ v-if="step === 'search'"
49
+ class="w-full"
50
+ color="primary"
51
+ :disabled="!phone || !recaptcha || isLoading"
52
+ @click="onSearchNext"
21
53
  >
22
- {{ props.confirmedText }}
54
+ ถัดไป
23
55
  </Button>
56
+ <div v-else-if="step === 'list'" class="flex flex-col gap-[8px] w-full">
57
+ <Button
58
+ class="w-full"
59
+ color="primary"
60
+ :disabled="!selectedMethod"
61
+ @click="onListConfirm"
62
+ >
63
+ {{ props.confirmedText }}
64
+ </Button>
65
+ <Button variant="text" color="primary" class="w-full" @click="goBack"
66
+ >ไม่ใช่คุณใช่ไหม?</Button
67
+ >
68
+ </div>
24
69
  </template>
25
70
  </Modal>
26
71
  </template>
27
72
 
28
73
  <script setup>
29
- import { ref } from "vue";
30
- const emit = defineEmits(["complete"]);
74
+ import { ref, computed, watch } from "vue";
75
+ import { useNuxtApp } from "nuxt/app";
76
+ import { useApi } from "../../composables/useApi";
77
+ import Button from "../button.vue";
78
+ const { $toast } = useNuxtApp();
79
+ const api = useApi();
80
+ const emit = defineEmits(["complete", "close"]);
31
81
  const props = defineProps({
32
82
  confirmedText: { type: String, required: false, default: "\u0E22\u0E37\u0E19\u0E22\u0E31\u0E19" }
33
83
  });
34
- const recaptchaRef = ref(null);
35
- const modelValue = defineModel({ type: Boolean, ...{
36
- default: false
37
- } });
84
+ const modelValue = defineModel({ type: Boolean, ...{ default: false } });
85
+ const step = ref("search");
86
+ const isLoading = ref(false);
38
87
  const phone = ref("");
39
88
  const recaptcha = ref("");
40
- const onNext = () => {
41
- emit("complete");
89
+ const recaptchaRef = ref(null);
90
+ const maskedPhone = ref("");
91
+ const maskedEmail = ref(null);
92
+ const hasEmail = ref(false);
93
+ const selectedMethod = ref(null);
94
+ const accountItems = computed(() => {
95
+ const items = [
96
+ { type: "phone", value: "sms", label: maskedPhone.value }
97
+ ];
98
+ if (hasEmail.value && maskedEmail.value) {
99
+ items.push({ type: "email", value: "email", label: maskedEmail.value });
100
+ }
101
+ return items;
102
+ });
103
+ const onSearchNext = async () => {
104
+ isLoading.value = true;
105
+ try {
106
+ const res = await api("/auth/forgot-password/find-account", {
107
+ method: "POST",
108
+ body: { phone: phone.value, token: recaptcha.value }
109
+ });
110
+ maskedPhone.value = res.data.masked_phone;
111
+ maskedEmail.value = res.data.masked_email;
112
+ hasEmail.value = res.data.has_email;
113
+ selectedMethod.value = null;
114
+ step.value = "list";
115
+ } catch (e) {
116
+ const msg = e?.data?.message?.description || e?.data?.message || "\u0E44\u0E21\u0E48\u0E1E\u0E1A\u0E1A\u0E31\u0E0D\u0E0A\u0E35\u0E17\u0E35\u0E48\u0E43\u0E0A\u0E49\u0E40\u0E1A\u0E2D\u0E23\u0E4C\u0E42\u0E17\u0E23\u0E19\u0E35\u0E49";
117
+ $toast?.error?.(msg);
118
+ } finally {
119
+ isLoading.value = false;
120
+ }
121
+ };
122
+ const onListConfirm = () => {
123
+ if (!selectedMethod.value) return;
124
+ emitComplete();
125
+ };
126
+ const emitComplete = () => {
127
+ emit("complete", {
128
+ phone: phone.value,
129
+ method: selectedMethod.value ?? "sms",
130
+ maskedPhone: maskedPhone.value,
131
+ maskedEmail: maskedEmail.value,
132
+ hasEmail: hasEmail.value
133
+ });
42
134
  modelValue.value = false;
43
135
  };
136
+ const goBack = () => {
137
+ step.value = "search";
138
+ phone.value = "";
139
+ recaptcha.value = "";
140
+ selectedMethod.value = null;
141
+ maskedPhone.value = "";
142
+ maskedEmail.value = null;
143
+ hasEmail.value = false;
144
+ };
145
+ watch(modelValue, (v) => {
146
+ if (!v) {
147
+ step.value = "search";
148
+ phone.value = "";
149
+ recaptcha.value = "";
150
+ selectedMethod.value = null;
151
+ maskedPhone.value = "";
152
+ maskedEmail.value = null;
153
+ hasEmail.value = false;
154
+ }
155
+ });
44
156
  </script>
@@ -1,3 +1,10 @@
1
+ export interface ModalUserAccountSearchResult {
2
+ phone: string;
3
+ method: "sms" | "email";
4
+ maskedPhone: string;
5
+ maskedEmail: string | null;
6
+ hasEmail: boolean;
7
+ }
1
8
  type __VLS_Props = {
2
9
  confirmedText?: string;
3
10
  };
@@ -6,11 +13,14 @@ type __VLS_ModelProps = {
6
13
  };
7
14
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
8
15
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
- complete: (...args: any[]) => void;
10
- "update:modelValue": (value: boolean) => void;
16
+ "update:modelValue": (value: boolean) => any;
17
+ } & {
18
+ close: () => any;
19
+ complete: (data: ModalUserAccountSearchResult) => any;
11
20
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
21
+ onClose?: (() => any) | undefined;
12
22
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
13
- onComplete?: ((...args: any[]) => any) | undefined;
23
+ onComplete?: ((data: ModalUserAccountSearchResult) => any) | undefined;
14
24
  }>, {
15
25
  confirmedText: string;
16
26
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.240.0",
3
+ "version": "1.242.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,18 +0,0 @@
1
- import type { ModalUserAccountListProps, ModalUserAccountListItem } from "@/types/components/modal/modal-user-account-list";
2
- type __VLS_Props = ModalUserAccountListProps;
3
- type __VLS_ModelProps = {
4
- modelValue?: boolean;
5
- };
6
- type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
7
- declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
- "update:modelValue": (value: boolean) => any;
9
- } & {
10
- complete: (value: ModalUserAccountListItem | null) => any;
11
- }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
12
- "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
13
- onComplete?: ((value: ModalUserAccountListItem | null) => any) | undefined;
14
- }>, {
15
- items: ModalUserAccountListItem[];
16
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
- declare const _default: typeof __VLS_export;
18
- export default _default;
@@ -1,51 +0,0 @@
1
- <template>
2
- <Modal title="เลือกวิธีตรวจสอบ" v-model="modelValue">
3
- <template v-for="(item, index) in props.items" :key="index">
4
- <div class="py-[12px] px-[16px]">
5
- <InputRadio :item="item.value" label-position="left" v-model="selected">
6
- <template v-slot:[`label-${item.value}-text`]>
7
- <div
8
- class="flex flex-col w-full text-gray font-body-medium-prominent"
9
- >
10
- <div v-if="item.type === 'email'">ส่งรหัสไปทางอีเมล</div>
11
- <div v-else-if="item.type === 'phone'">ส่งรหัสไปทาง SMS</div>
12
- <div>{{ item.label }}</div>
13
- </div>
14
- </template>
15
- </InputRadio>
16
- </div>
17
- <Divider v-if="index !== props.items.length - 1" />
18
- </template>
19
-
20
- <template #footer>
21
- <Button
22
- color="primary"
23
- full-width
24
- @click="onConfirm"
25
- :disabled="!selected"
26
- >
27
- ยืนยัน
28
- </Button>
29
- </template>
30
- </Modal>
31
- </template>
32
-
33
- <script setup>
34
- import { ref } from "vue";
35
- const emit = defineEmits(["complete"]);
36
- const props = defineProps({
37
- items: { type: Array, required: false, default: () => [] }
38
- });
39
- const selected = ref(null);
40
- const modelValue = defineModel({ type: Boolean, ...{
41
- default: false
42
- } });
43
- const onConfirm = () => {
44
- const selectedItem = props.items.find(
45
- (item) => item.value === selected.value
46
- );
47
- emit("complete", selectedItem ?? null);
48
- modelValue.value = false;
49
- selected.value = null;
50
- };
51
- </script>
@@ -1,18 +0,0 @@
1
- import type { ModalUserAccountListProps, ModalUserAccountListItem } from "@/types/components/modal/modal-user-account-list";
2
- type __VLS_Props = ModalUserAccountListProps;
3
- type __VLS_ModelProps = {
4
- modelValue?: boolean;
5
- };
6
- type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
7
- declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
8
- "update:modelValue": (value: boolean) => any;
9
- } & {
10
- complete: (value: ModalUserAccountListItem | null) => any;
11
- }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
12
- "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
13
- onComplete?: ((value: ModalUserAccountListItem | null) => any) | undefined;
14
- }>, {
15
- items: ModalUserAccountListItem[];
16
- }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
- declare const _default: typeof __VLS_export;
18
- export default _default;