pukaad-ui-lib 1.212.1 → 1.213.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.212.1",
4
+ "version": "1.213.0",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -8,11 +8,12 @@ type __VLS_ModelProps = {
8
8
  };
9
9
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
- submit: (...args: any[]) => void;
12
- "update:modelValue": (value: boolean) => void;
13
- "update:data": (value: SuggestPlaceData) => void;
11
+ "update:modelValue": (value: boolean) => any;
12
+ "update:data": (value: SuggestPlaceData) => any;
13
+ } & {
14
+ submit: (data: SuggestPlaceData) => any;
14
15
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
- onSubmit?: ((...args: any[]) => any) | undefined;
16
+ onSubmit?: ((data: SuggestPlaceData) => any) | undefined;
16
17
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
17
18
  "onUpdate:data"?: ((value: SuggestPlaceData) => any) | undefined;
18
19
  }>, {
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <Drawer class="w-[888px]" :title="drawerTitle" v-model="isOpen">
2
+ <Drawer class="w-[888px]" :title="drawerTitle" :loading="loading" v-model="isOpen">
3
3
  <!-- backoffice: แสดงแบบมี tabs -->
4
4
  <ShadTabs v-if="props.state === 'backoffice'">
5
5
  <ShadTabsList>
@@ -97,12 +97,19 @@
97
97
 
98
98
  <script setup>
99
99
  import { computed, ref } from "vue";
100
+ import { useNuxtApp } from "nuxt/app";
101
+ import { useApi } from "@/runtime/composables/useApi";
102
+ import { usePresignedUpload } from "@/runtime/composables/usePresignedUpload";
100
103
  const props = defineProps({
101
104
  state: { type: String, required: false, default: "personal" }
102
105
  });
103
106
  const emit = defineEmits(["submit"]);
107
+ const { $toast } = useNuxtApp();
108
+ const api = useApi();
109
+ const { uploadFiles } = usePresignedUpload();
104
110
  const isOpen = defineModel({ type: Boolean, ...{ default: false } });
105
111
  const formData = defineModel("data", { type: Object, ...{ default: () => ({}) } });
112
+ const loading = ref(false);
106
113
  const photoColumns = ref(false);
107
114
  const videoColumns = ref(false);
108
115
  if (!formData.value.photos) formData.value.photos = [];
@@ -115,8 +122,63 @@ const isFormValid = computed(() => {
115
122
  const hasCategories = !!(d.categories && d.categories.length > 0);
116
123
  return addressComplete && hasLatLng && hasBusinessName && hasCategories;
117
124
  });
118
- const onSubmit = () => {
119
- emit("submit", formData.value);
125
+ const endpointMap = {
126
+ personal: "/personal/suggest-places",
127
+ business: "/business/suggest-places",
128
+ backoffice: "/office/suggest-places"
129
+ };
130
+ const onSubmit = async () => {
131
+ loading.value = true;
132
+ try {
133
+ const d = formData.value;
134
+ const [photoUrls, videoUrls, reviewPhotoUrls] = await Promise.all([
135
+ uploadFiles(d.photos ?? [], "suggest-place-media"),
136
+ uploadFiles(d.videos ?? [], "suggest-place-media"),
137
+ uploadFiles(d.reviewPhotos ?? [], "suggest-place-review")
138
+ ]);
139
+ const categoryIds = (d.categories ?? []).map(
140
+ (c) => typeof c === "object" ? String(c.id ?? c.value) : String(c)
141
+ );
142
+ const endpoint = endpointMap[props.state] ?? "/personal/suggest-places";
143
+ const res = await api(endpoint, {
144
+ method: "POST",
145
+ body: {
146
+ business_name: d.businessName ?? "",
147
+ name_th: d.nameTh ?? "",
148
+ name_en: d.nameEn ?? "",
149
+ description: d.description ?? "",
150
+ address: {
151
+ province_id: d.address?.province_id,
152
+ amphur_id: d.address?.amphur_id,
153
+ tambon_id: d.address?.tambon_id,
154
+ zipcode: d.address?.zipcode,
155
+ detail_address: d.address?.detail_address ?? "",
156
+ full_address: d.address?.full_address ?? ""
157
+ },
158
+ latLng: d.latLng ?? null,
159
+ phone: d.phone ?? "",
160
+ extra_phones: d.extraPhones ?? [],
161
+ contact_channels: d.contactChannels ?? [],
162
+ opening_hours: d.openingHours ?? null,
163
+ categories: categoryIds,
164
+ is_review: d.isReview ?? false,
165
+ rating: d.rating ?? 0,
166
+ review_description: d.reviewDescription ?? "",
167
+ review_photos: reviewPhotoUrls,
168
+ photos: photoUrls,
169
+ videos: videoUrls
170
+ }
171
+ });
172
+ $toast?.success?.(res?.message || "\u0E41\u0E19\u0E30\u0E19\u0E33\u0E2A\u0E16\u0E32\u0E19\u0E17\u0E35\u0E48\u0E2A\u0E33\u0E40\u0E23\u0E47\u0E08");
173
+ emit("submit", formData.value);
174
+ isOpen.value = false;
175
+ } catch (e) {
176
+ $toast?.error?.(
177
+ e?.data?.error || 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"
178
+ );
179
+ } finally {
180
+ loading.value = false;
181
+ }
120
182
  };
121
183
  const drawerTitle = computed(() => {
122
184
  if (props.state === "personal" || props.state === "business") {
@@ -8,11 +8,12 @@ type __VLS_ModelProps = {
8
8
  };
9
9
  type __VLS_PublicProps = __VLS_Props & __VLS_ModelProps;
10
10
  declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
11
- submit: (...args: any[]) => void;
12
- "update:modelValue": (value: boolean) => void;
13
- "update:data": (value: SuggestPlaceData) => void;
11
+ "update:modelValue": (value: boolean) => any;
12
+ "update:data": (value: SuggestPlaceData) => any;
13
+ } & {
14
+ submit: (data: SuggestPlaceData) => any;
14
15
  }, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
15
- onSubmit?: ((...args: any[]) => any) | undefined;
16
+ onSubmit?: ((data: SuggestPlaceData) => any) | undefined;
16
17
  "onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
17
18
  "onUpdate:data"?: ((value: SuggestPlaceData) => any) | undefined;
18
19
  }>, {
@@ -19,6 +19,7 @@ export interface SuggestPlaceData {
19
19
  reviewPhotos?: any[];
20
20
  photos?: any[];
21
21
  videos?: any[];
22
+ contactChannels?: any[];
22
23
  }
23
24
  type __VLS_Props = {
24
25
  state?: "personal" | "business" | "backoffice";
@@ -15,6 +15,9 @@
15
15
  show-counter
16
16
  :limit="180"
17
17
  free-text
18
+ :fetch-fn="fetchApprovedPlaces"
19
+ value-key="business_name"
20
+ label-key="business_name"
18
21
  />
19
22
  <InputTextField
20
23
  v-if="extraNameCount >= 1"
@@ -52,7 +55,7 @@
52
55
  placeholder="เพิ่มหมวดหมู่ที่เกี่ยวข้องกับสถานที่"
53
56
  :limit="3"
54
57
  show-counter
55
- :items="[]"
58
+ :options="categoryOptions"
56
59
  required
57
60
  multiple
58
61
  />
@@ -64,9 +67,17 @@
64
67
  :limit="220"
65
68
  show-counter
66
69
  />
67
- <InputDateOpening name="openingHours" v-model="modelValue.openingHours" />
70
+ <InputDateOpening
71
+ name="openingHours"
72
+ v-model="modelValue.openingHours"
73
+ />
68
74
  <div class="flex flex-col gap-[8px]">
69
- <InputPhone name="phone" v-model="modelValue.phone" label="เบอร์โทรศัพท์" placeholder="กรอกเบอร์โทรศัพท์" />
75
+ <InputPhone
76
+ name="phone"
77
+ v-model="modelValue.phone"
78
+ label="เบอร์โทรศัพท์"
79
+ placeholder="กรอกเบอร์โทรศัพท์"
80
+ />
70
81
  <InputPhone
71
82
  v-for="(item, index) in modelValue.extraPhones"
72
83
  :key="index"
@@ -84,12 +95,26 @@
84
95
  <Icon name="lucide:plus" />
85
96
  เพิ่มเบอร์โทรศัพท์
86
97
  </Button>
98
+ <InputLink
99
+ name="contactChannels"
100
+ format="contact_channel"
101
+ default-first
102
+ v-model="modelValue.contactChannels"
103
+ />
87
104
  </div>
88
105
  <template v-if="props.state === 'personal'">
89
106
  <div class="flex flex-col gap-[16px]">
90
- <InputCheckbox name="isReview" v-model="modelValue.isReview" label="คุณต้องการรีวิวสถานที่นี้" />
107
+ <InputCheckbox
108
+ name="isReview"
109
+ v-model="modelValue.isReview"
110
+ label="คุณต้องการรีวิวสถานที่นี้"
111
+ />
91
112
  <template v-if="modelValue.isReview">
92
- <InputRating name="rating" v-model="modelValue.rating" class="flex py-4 justify-center" />
113
+ <InputRating
114
+ name="rating"
115
+ v-model="modelValue.rating"
116
+ class="flex py-4 justify-center"
117
+ />
93
118
  <InputTextarea
94
119
  name="reviewDescription"
95
120
  v-model="modelValue.reviewDescription"
@@ -104,7 +129,12 @@
104
129
  </div>
105
130
  <div class="font-body-small text-gray">สูงสุด 9 รายการ</div>
106
131
  </div>
107
- <InputFile name="reviewPhotos" v-model="modelValue.reviewPhotos" accept="image/*" :limit="9" />
132
+ <InputFile
133
+ name="reviewPhotos"
134
+ v-model="modelValue.reviewPhotos"
135
+ accept="image/*"
136
+ :limit="9"
137
+ />
108
138
  <div class="font-body-small text-gray w-[250px]">
109
139
  รองรับไฟล์ *.jpg *.jpeg *.png *.webp *.bmp *.gif
110
140
  ขนาดไฟล์ไม่เกิน 30 mb
@@ -138,11 +168,13 @@
138
168
  </template>
139
169
 
140
170
  <script setup>
141
- import { ref, computed } from "vue";
171
+ import { ref, computed, onMounted } from "vue";
172
+ import { useApi } from "@/runtime/composables/useApi";
142
173
  import SuggestPlaceMap from "./suggest-place-map.vue";
143
174
  const props = defineProps({
144
175
  state: { type: String, required: false, default: "personal" }
145
176
  });
177
+ const api = useApi();
146
178
  const modelValue = defineModel({ type: Object, ...{
147
179
  default: () => ({})
148
180
  } });
@@ -157,4 +189,48 @@ const isAddressCompleted = computed(() => {
157
189
  const v = modelValue.value.address || {};
158
190
  return !!(v.province_id && v.amphur_id && v.tambon_id && v.zipcode);
159
191
  });
192
+ const listEndpointMap = {
193
+ personal: "/personal/suggest-places",
194
+ business: "/business/suggest-places",
195
+ backoffice: "/office/suggest-places"
196
+ };
197
+ const fetchApprovedPlaces = async (page, pageSize, search) => {
198
+ const base = listEndpointMap[props.state] ?? "/personal/suggest-places";
199
+ const res = await api(base, {
200
+ query: {
201
+ q: search ?? "",
202
+ status: "PENDING,APPROVED",
203
+ page,
204
+ page_size: pageSize
205
+ }
206
+ });
207
+ const data = (res.data ?? []).map((item) => ({
208
+ ...item,
209
+ description: item.address?.full_address ?? ""
210
+ }));
211
+ return {
212
+ data,
213
+ meta: { total_pages: res.meta?.total_pages ?? 1 }
214
+ };
215
+ };
216
+ const categoryOptions = ref([]);
217
+ const categoryEndpointMap = {
218
+ personal: "/personal/suggest-places/categories",
219
+ business: "/business/suggest-places/categories",
220
+ backoffice: "/office/suggest-places/categories"
221
+ };
222
+ onMounted(async () => {
223
+ try {
224
+ const endpoint = categoryEndpointMap[props.state] ?? "/personal/suggest-places/categories";
225
+ const res = await api(
226
+ endpoint
227
+ );
228
+ categoryOptions.value = (res.data ?? []).map((c) => ({
229
+ label: c.category_name,
230
+ value: c.id
231
+ }));
232
+ } catch {
233
+ categoryOptions.value = [];
234
+ }
235
+ });
160
236
  </script>
@@ -19,6 +19,7 @@ export interface SuggestPlaceData {
19
19
  reviewPhotos?: any[];
20
20
  photos?: any[];
21
21
  videos?: any[];
22
+ contactChannels?: any[];
22
23
  }
23
24
  type __VLS_Props = {
24
25
  state?: "personal" | "business" | "backoffice";
@@ -1,6 +1,7 @@
1
1
  import type { InputLinkItem, ContactChannel, InputLinkProps } from "@/types/components/input/input-link";
2
2
  type __VLS_Props = InputLinkProps & {
3
3
  modelValue?: InputLinkItem[] | ContactChannel[];
4
+ defaultFirst?: boolean;
4
5
  };
5
6
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
6
7
  "update:modelValue": (value: InputLinkItem[] | ContactChannel[]) => any;
@@ -10,6 +11,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
10
11
  name: string;
11
12
  modelValue: InputLinkItem[] | ContactChannel[];
12
13
  format: "legacy" | "contact_channel";
14
+ defaultFirst: boolean;
13
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
16
  declare const _default: typeof __VLS_export;
15
17
  export default _default;
@@ -37,11 +37,12 @@
37
37
  </template>
38
38
 
39
39
  <script setup>
40
- import { computed, ref, nextTick, watch } from "vue";
40
+ import { computed, ref, nextTick, watch, onMounted } from "vue";
41
41
  const props = defineProps({
42
42
  name: { type: String, required: false, default: "link" },
43
43
  format: { type: String, required: false, default: "legacy" },
44
- modelValue: { type: Array, required: false, default: () => [] }
44
+ modelValue: { type: Array, required: false, default: () => [] },
45
+ defaultFirst: { type: Boolean, required: false, default: false }
45
46
  });
46
47
  const emit = defineEmits(["update:modelValue"]);
47
48
  const listSocial = [
@@ -208,6 +209,11 @@ const listOption = computed(() => {
208
209
  });
209
210
  return arr;
210
211
  });
212
+ onMounted(() => {
213
+ if (props.defaultFirst && linkValue.value.length === 0) {
214
+ addLink();
215
+ }
216
+ });
211
217
  const addLink = () => {
212
218
  const link = {
213
219
  name: "Email",
@@ -1,6 +1,7 @@
1
1
  import type { InputLinkItem, ContactChannel, InputLinkProps } from "@/types/components/input/input-link";
2
2
  type __VLS_Props = InputLinkProps & {
3
3
  modelValue?: InputLinkItem[] | ContactChannel[];
4
+ defaultFirst?: boolean;
4
5
  };
5
6
  declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
6
7
  "update:modelValue": (value: InputLinkItem[] | ContactChannel[]) => any;
@@ -10,6 +11,7 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {
10
11
  name: string;
11
12
  modelValue: InputLinkItem[] | ContactChannel[];
12
13
  format: "legacy" | "contact_channel";
14
+ defaultFirst: boolean;
13
15
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
14
16
  declare const _default: typeof __VLS_export;
15
17
  export default _default;
@@ -0,0 +1,9 @@
1
+ export interface FileItem {
2
+ file?: File;
3
+ url: string;
4
+ description?: string;
5
+ duration?: string;
6
+ }
7
+ export declare const usePresignedUpload: () => {
8
+ uploadFiles: (files: FileItem[], state: string) => Promise<string[]>;
9
+ };
@@ -0,0 +1,45 @@
1
+ import { $fetch } from "ofetch";
2
+ import { useApi } from "./useApi.js";
3
+ export const usePresignedUpload = () => {
4
+ const api = useApi();
5
+ const uploadFiles = async (files, state) => {
6
+ if (!files || files.length === 0) return [];
7
+ const newFiles = files.filter((item) => !!item.file);
8
+ const existingUrls = files.filter((item) => !item.file && item.url && !item.url.startsWith("blob:")).map((item) => item.url);
9
+ if (newFiles.length === 0) return existingUrls;
10
+ const res = await api("/storage/presigned-urls", {
11
+ method: "POST",
12
+ body: {
13
+ state,
14
+ file_name: newFiles.map((item) => item.file.name)
15
+ }
16
+ });
17
+ let presignedItems;
18
+ const d = res.data;
19
+ if (d.items) {
20
+ presignedItems = d.items;
21
+ } else {
22
+ presignedItems = [
23
+ {
24
+ original_name: d.original_name,
25
+ upload_url: d.upload_url,
26
+ public_url: d.public_url,
27
+ filename: d.filename
28
+ }
29
+ ];
30
+ }
31
+ await Promise.all(
32
+ newFiles.map(
33
+ (item, index) => $fetch(presignedItems[index].upload_url, {
34
+ method: "PUT",
35
+ body: item.file,
36
+ headers: {
37
+ "Content-Type": item.file.type
38
+ }
39
+ })
40
+ )
41
+ );
42
+ return [...existingUrls, ...presignedItems.map((item) => item.public_url)];
43
+ };
44
+ return { uploadFiles };
45
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pukaad-ui-lib",
3
- "version": "1.212.1",
3
+ "version": "1.213.0",
4
4
  "description": "pukaad-ui for MeMSG",
5
5
  "repository": {
6
6
  "type": "git",