vue-wiguet-chatweb 0.1.27 → 0.1.29

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. package/README.md +68 -68
  2. package/dist/components/Chat.vue.d.ts +2 -5
  3. package/dist/components/DangerIcon.vue.d.ts +1 -1
  4. package/dist/components/IconAttach.vue.d.ts +1 -1
  5. package/dist/components/IconChat.vue.d.ts +1 -1
  6. package/dist/components/IconClose.vue.d.ts +1 -1
  7. package/dist/components/IconSend.vue.d.ts +1 -1
  8. package/dist/components/IconTelegram.vue.d.ts +1 -1
  9. package/dist/components/IconWhatsApp.vue.d.ts +1 -1
  10. package/dist/components/Loader.vue.d.ts +1 -1
  11. package/dist/components/MessageList.vue.d.ts +2 -3
  12. package/dist/components/ODialog/IPropsDialog.d.ts +1 -0
  13. package/dist/components/ODialog/ODialog.vue.d.ts +15 -13
  14. package/dist/components/Widget.vue.d.ts +2 -1
  15. package/dist/dto/app.dto.d.ts +1 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/main.d.ts +1 -0
  18. package/dist/store/config.d.ts +1 -0
  19. package/dist/store/index.d.ts +1 -0
  20. package/dist/style.css +1 -1
  21. package/dist/vue-wiguet-chatweb.js +1769 -1829
  22. package/dist/vue-wiguet-chatweb.umd.cjs +26 -26
  23. package/package.json +66 -66
  24. package/src/assets/emojis/AngryIcon.svg +4 -4
  25. package/src/assets/emojis/HappiestIcon.svg +4 -4
  26. package/src/assets/emojis/HappyIcon.svg +4 -4
  27. package/src/assets/emojis/NeutralIcon.svg +4 -4
  28. package/src/assets/emojis/SadIcon.svg +4 -4
  29. package/src/components/Chat.vue +691 -691
  30. package/src/components/ChatMessage.vue +102 -102
  31. package/src/components/DangerIcon.vue +12 -12
  32. package/src/components/IconAttach.vue +24 -24
  33. package/src/components/IconChat.vue +23 -23
  34. package/src/components/IconClose.vue +5 -5
  35. package/src/components/IconSend.vue +8 -8
  36. package/src/components/IconTelegram.vue +28 -28
  37. package/src/components/IconWhatsApp.vue +39 -39
  38. package/src/components/IconWidget.vue +45 -45
  39. package/src/components/Loader.vue +31 -31
  40. package/src/components/LoadingComponent.vue +111 -111
  41. package/src/components/MessageList.vue +357 -357
  42. package/src/components/ODialog/IPropsDialog.ts +4 -4
  43. package/src/components/ODialog/IPropsSidebar.ts +13 -13
  44. package/src/components/ODialog/ODialog.vue +106 -84
  45. package/src/components/Widget.vue +187 -205
  46. package/src/components/__tests__/Chat.spec.ts +5 -5
  47. package/src/components/__tests__/ChatMessage.spec.ts +5 -5
  48. package/src/components/__tests__/MessageList.spec.ts +47 -47
  49. package/dist/App.vue.d.ts +0 -2
  50. package/dist/components/ChatMessage.vue.d.ts +0 -12
@@ -1,691 +1,691 @@
1
- <template>
2
- <div class="widget">
3
- <div class="header-widget">
4
- <h4 class="title-chat">{{ titlePrincipal }}</h4>
5
- <button @click="() => toggleChat()" class="btn-close">
6
- <IconClose class="pointer" />
7
- </button>
8
- </div>
9
- <div class="messages-container" ref="messageContainerRef">
10
- <div class="loader" v-if="isLoading">
11
- <Loader />
12
- </div>
13
- <MessageList
14
- v-if="messagesData.data.length > 0"
15
- :messages="messagesData.data"
16
- :canLoadMoreMessages="messagesData.canLoadMoreMessages"
17
- @loadMore="getMessages"
18
- @retry="retryMessage"
19
- @on-qualifying="(args) => onQualifying(args)"
20
- @see="(message: Message) => {
21
- currentDialogView = DIALOG_VIEWS.SEE
22
- urlFileMessage = message.urlFile
23
- dialog.title = 'Imagen'
24
- dialog.modelValue = true
25
- }"
26
- />
27
- <div class="fit" v-else>
28
- <span class="center">No tienes mensajes</span>
29
- </div>
30
- </div>
31
-
32
- <div class="w-full">
33
- <form v-if="!isDisabledBoxMessage" class="message-send" @submit.prevent="(event) => submitMessage()">
34
- <div class="form-message">
35
- <div class="jl-inputgroup-chat">
36
- <textarea
37
- v-model="message"
38
- class="jl2-input-chat"
39
- placeholder="Escribe tu mensaje"
40
- required
41
- ref="textAreaRef"
42
- @input="() => autoAdjustHeight()"
43
- @keydown.enter="
44
- (event) => {
45
- !isMobile && event.preventDefault();
46
- }
47
- "
48
- @keyup.enter="saltoDeLineaOEnviar"
49
- />
50
-
51
- <input
52
- type="file"
53
- ref="fileInputRef"
54
- @change="onFileSelect"
55
- accept="image/*"
56
- style="display: none;"
57
- key="fileInputKey"
58
-
59
- />
60
- <button
61
- type="button"
62
- class="pointer btn-primary"
63
- title="Adjuntar archivo"
64
- @click="
65
- () => {
66
- fileInputRef.value = '';
67
- fileInputKey++;
68
- fileInputRef?.click();
69
- }
70
- "
71
- >
72
- <IconAttach style="width: 20px; height: 20px" />
73
- </button>
74
-
75
- <button type="submit" class="pointer btn-primary">
76
- <IconSend style="width: 20px; height: 20px" />
77
- </button>
78
- </div>
79
- </div>
80
- </form>
81
- <span class="message-send-block" v-else>
82
- Necesitamos que nos califique la atención para continuar
83
- </span>
84
- </div>
85
- </div>
86
-
87
- <ODialog v-bind="dialog">
88
- <div v-if="currentDialogView === DIALOG_VIEWS.UPLOAD" class="flex flex-col gap-3 justify-center items-center">
89
- <img v-for="(urlFile, i) in urlFiles" :key="i" :src="urlFile.toString()" alt="Image" width="400" />
90
-
91
- <form
92
- class="message-send"
93
- @submit.prevent="
94
- (event) => {
95
- submitMessage();
96
- dialog.modelValue = false;
97
- }
98
- "
99
- >
100
- <div class="form-message">
101
- <div class="jl-inputgroup-chat">
102
- <textarea
103
- v-model="message"
104
- autofocus
105
- class="jl2-input-chat"
106
- placeholder="Escribe tu mensaje"
107
- ref="textAreaRef"
108
- @input="() => autoAdjustHeight()"
109
- @keydown.enter="
110
- (event) => {
111
- !isMobile && event.preventDefault();
112
- }
113
- "
114
- @keyup.enter="(event)=>{
115
- saltoDeLineaOEnviar(event, MESSAGE_TYPE_CODES.IMAGEN)
116
- dialog.modelValue = false;
117
- }"
118
- />
119
-
120
- <button type="submit" class="pointer btn-primary">
121
- <IconSend style="width: 20px; height: 20px" />
122
- </button>
123
- </div>
124
- </div>
125
- </form>
126
- </div>
127
- <div v-else>
128
- <img v-if="urlFileMessage" :src="urlFileMessage" alt="Image" style="width: 55vw;" />
129
- </div>
130
- </ODialog>
131
- </template>
132
-
133
- <script setup lang="ts">
134
- import {
135
- ref,
136
- onMounted,
137
- nextTick,
138
- PropType,
139
- watch,
140
- onUnmounted,
141
- reactive,
142
- } from "vue";
143
- import { v4 as uuidv4 } from "uuid";
144
-
145
- import {
146
- type SendMessageBody,
147
- ChatInformation,
148
- ListMessageBody,
149
- Message,
150
- } from "../dto/app.dto";
151
- import IconClose from "./IconClose.vue";
152
- import IconSend from "./IconSend.vue";
153
- import {
154
- getMessagesApi,
155
- sendMessageApi,
156
- setVistoToTrueApi,
157
- updateMessageApi,
158
- } from "../store/index";
159
- import { getInformationApi } from "../store";
160
- import MessageList from "./MessageList.vue";
161
- import Loader from "./Loader.vue";
162
- import { searchFromLast } from "../resources/functions.helpers";
163
- import { io, Socket } from "socket.io-client";
164
- import { APP_TYPE } from "../dto/chat.dto";
165
- import { MESSAGE_TYPE_CODES, TypeMessageTypeCodes } from "../resources/constants/message-type.constant";
166
- import { useMobile } from "../hooks/useMobile";
167
- import IconAttach from "./IconAttach.vue";
168
- import ODialog from "./ODialog/ODialog.vue";
169
- import { IPropsDialog } from "./ODialog/IPropsDialog";
170
-
171
- const enum DIALOG_VIEWS {
172
- UPLOAD,
173
- SEE
174
- }
175
-
176
- //DATA
177
- const message = ref("");
178
- const notViewed = ref(0);
179
- const fileInputRef = ref();
180
- const fileInputKey = ref(0);
181
- const currentDialogView = ref(DIALOG_VIEWS.SEE)
182
- const urlFileMessage = ref<string>()
183
-
184
- const messagesData = ref<{ data: Message[]; canLoadMoreMessages: boolean }>({
185
- data: [],
186
- canLoadMoreMessages: false,
187
- });
188
-
189
- const appChatId = ref("");
190
- const isLoading = ref(false);
191
-
192
- const emit = defineEmits([
193
- "show-toast",
194
- "show-confirm",
195
- "new-message",
196
- "clear-new-messages",
197
- "not-viewed-total",
198
- "onQualifying",
199
- ]);
200
-
201
- const props = defineProps({
202
- titlePrincipal: {
203
- type: String,
204
- default: "Comunicación en linea para consultas",
205
- },
206
- toggleChat: { type: Function, required: true },
207
- tokenAuth: { type: String, required: true },
208
- user: {
209
- type: Object as PropType<{
210
- nombreCompleto: string;
211
- ci: string;
212
- msPersonaId: number;
213
- }>,
214
- required: true,
215
- },
216
- visible: { type: Boolean, required: true },
217
- });
218
-
219
- const messageContainerRef = ref<HTMLElement | null>(null);
220
-
221
- watch(
222
- () => props.visible,
223
- async (current) => {
224
- if (!current) return;
225
-
226
- emit("clear-new-messages");
227
- scrollToBottom();
228
-
229
- if (notViewed.value > 0) {
230
- setVistoToTrueApi(appChatId.value, props.tokenAuth);
231
- }
232
-
233
- if (appChatId.value) return;
234
-
235
- const resp = await getInformationApi(props.tokenAuth);
236
-
237
- if (resp) {
238
- appChatId.value = resp.appChat.id;
239
- }
240
- }
241
- );
242
-
243
- function saltoDeLineaOEnviar(event: KeyboardEvent, messageTypeCode?: TypeMessageTypeCodes) {
244
- if(isMobile.value) {
245
- autoAdjustHeight();
246
- return;
247
- }
248
-
249
- if (event.key === "Enter" && event.shiftKey) {
250
- const val = (event.target as any)?.value || "";
251
- (event.target as any).value = val + "\n";
252
- autoAdjustHeight();
253
- return;
254
- }
255
-
256
- submitMessage({ codigoTipoMensaje: messageTypeCode });
257
- }
258
-
259
- function createMessage(message: string, codigoTipoMensaje?: TypeMessageTypeCodes) {
260
- if (message?.length > 300) {
261
- emit("show-toast", {
262
- severity: "warn",
263
- summary: "Error",
264
- detail: "El mensaje no puede superar los 300 caracteres",
265
- life: 5000,
266
- });
267
- return;
268
- }
269
-
270
- if (!message.trim() && codigoTipoMensaje !== MESSAGE_TYPE_CODES.IMAGEN) {
271
- emit("show-toast", {
272
- severity: "warn",
273
- summary: "Error",
274
- detail: "Por favor ingrese un mensaje",
275
- life: 5000,
276
- });
277
- return;
278
- }
279
-
280
- const messageType = codigoTipoMensaje
281
- ? {
282
- code: codigoTipoMensaje,
283
- }
284
- : undefined;
285
-
286
- const newMessage: Message = {
287
- id: uuidv4(),
288
- chatId: information.value?.chat?.id,
289
- message,
290
- visto: true,
291
- multimedia: false,
292
- esCliente: true,
293
- appChatId: appChatId.value,
294
- createdAt: new Date().toISOString(),
295
- updatedAt: new Date().toISOString(),
296
- file: inputFiles.value?.[0],
297
- urlFile: urlFiles.value?.[0].toString(),
298
- messageType,
299
- sender: {
300
- nombreCompleto: props.user.nombreCompleto,
301
- ci: props.user.ci,
302
- msPersonaId: props.user.msPersonaId,
303
- },
304
- response: undefined
305
- };
306
-
307
- return newMessage
308
- }
309
-
310
- function updateMessageStatus(
311
- newMessage: Message,
312
- idxMessageToCommunicate?: number,
313
- messageSaved?: Message,
314
- tipoCalificacionId?: number
315
- ) {
316
- if (idxMessageToCommunicate == null) throw new Error('idx is required')
317
-
318
- if (!messageSaved) {
319
- if (tipoCalificacionId) {
320
- const messageAResponder = messagesData.value.data[idxMessageToCommunicate]
321
- messageAResponder.response = undefined
322
- } else {
323
- messagesData.value.data[idxMessageToCommunicate].error = {
324
- error: true,
325
- id: newMessage.id,
326
- };
327
- }
328
-
329
- emit("show-toast", {
330
- severity: "error",
331
- summary: "Error",
332
- detail: "Ocurrio un error al enviar el mensaje, intente nuevamente",
333
- life: 5000,
334
- });
335
-
336
- return
337
- }
338
-
339
- let messageUpdated = { ...messageSaved, chatId: newMessage?.chatId }
340
-
341
- if (tipoCalificacionId) {
342
- const messageAResponder = messagesData.value.data[idxMessageToCommunicate]
343
- messageAResponder.response = messageSaved
344
-
345
- messageUpdated = { ...messageAResponder, chatId: newMessage?.chatId }
346
- } else {
347
- messagesData.value.data[idxMessageToCommunicate] = messageUpdated;
348
- }
349
-
350
- return messageUpdated;
351
- }
352
-
353
- const submitMessage = async (
354
- messageExtraData?: { mensajeARespondeId?: string, tipoCalificacionId?: number, codigoTipoMensaje?: TypeMessageTypeCodes}
355
- ) => {
356
-
357
- const newMessage = createMessage(message.value, messageExtraData?.codigoTipoMensaje);
358
- message.value = '';
359
- if (!newMessage) return;
360
-
361
- let idxMessageToCommunicate = -1;
362
- if (messageExtraData?.tipoCalificacionId) {
363
- idxMessageToCommunicate = messagesData.value.data.findIndex((val) => val.id === messageExtraData?.mensajeARespondeId )
364
- const messageAResponder = messagesData.value.data[idxMessageToCommunicate]
365
-
366
- if ( messageAResponder ) {
367
- messageAResponder.response = newMessage
368
- }
369
- } else {
370
- idxMessageToCommunicate = messagesData.value.data.push(newMessage) - 1;
371
- }
372
-
373
- const body = {
374
- message: message.value,
375
- appChatId: appChatId.value,
376
- ...messageExtraData,
377
- codigoTipoMensaje: messageExtraData?.codigoTipoMensaje,
378
- files: inputFiles.value,
379
- }
380
- sendApi(body).then((messageSaved) => {
381
- let messageUpdated = updateMessageStatus(
382
- newMessage,
383
- idxMessageToCommunicate,
384
- messageSaved,
385
- messageExtraData?.tipoCalificacionId
386
- )
387
-
388
- if(!messageUpdated) return
389
-
390
- isDisabledBoxMessage.value = !!messagesData.value.data.find((msg) => msg.messageType?.code === MESSAGE_TYPE_CODES.PREGUNTA && msg.response == null);
391
-
392
- socketService.value?.emit(
393
- "sendMessage",
394
- { roomId: information?.value?.appChat.id, message: messageUpdated },
395
- (response: any) => {
396
- console.log("🚀 ~ socketService.value.emit ~ response:", response);
397
- }
398
- );
399
- });
400
-
401
- message.value = "";
402
- scrollToBottom();
403
- textAreaRef.value && (textAreaRef.value.style.height = "20px");
404
- };
405
-
406
- const sendApi = async (bodyParam: Omit<SendMessageBody, 'esCliente'>) => {
407
- const body: SendMessageBody = {
408
- ...bodyParam,
409
- esCliente: true,
410
- };
411
- return sendMessageApi(body, props.tokenAuth);
412
- };
413
-
414
- const getMessages = async () => {
415
- const lastMessagesId = messagesData.value.data[0]?.id;
416
- const body: ListMessageBody = {
417
- lastMessagesId,
418
- appChatId: appChatId.value,
419
- limit: 10,
420
- };
421
-
422
- isLoading.value = true;
423
- const resp = await getMessagesApi({ body, token: props.tokenAuth });
424
- isLoading.value = false;
425
-
426
- isDisabledBoxMessage.value = !!resp.data.find((msg) => msg.messageType?.code === MESSAGE_TYPE_CODES.PREGUNTA && msg.response == null);
427
-
428
- messagesData.value.data.unshift(
429
- ...resp.data.sort((a, b) => -b.createdAt.localeCompare(a.createdAt))
430
- );
431
-
432
- messagesData.value.canLoadMoreMessages =
433
- resp.pagination.total > resp.pagination.size;
434
-
435
- if (lastMessagesId && messageContainerRef.value?.scrollHeight) {
436
- mantainElementsOnViewport(messageContainerRef.value?.scrollHeight);
437
- }
438
-
439
- if (!lastMessagesId) scrollToBottom();
440
- };
441
-
442
- const retryMessage = async (message: Message) => {
443
- emit("show-confirm", async () => {
444
- if (!message.error?.id) return;
445
-
446
- const msg = await sendApi({
447
- message: message.message,
448
- appChatId: appChatId.value
449
- });
450
-
451
- if (!msg) {
452
- emit("show-toast", {
453
- severity: "error",
454
- summary: "Error",
455
- detail: "Ocurrio un error al enviar el mensaje, intente nuevamente",
456
- life: 5000,
457
- });
458
- } else {
459
- const idx = searchFromLast<Message>(
460
- messagesData.value.data,
461
- "id",
462
- message.error.id
463
- );
464
-
465
- messagesData.value.data[idx] = { ...msg, error: undefined };
466
-
467
- socketService.value?.emit("sendMessage", {
468
- roomId: information?.value?.appChat.id,
469
- message,
470
- });
471
- }
472
-
473
- scrollToBottom();
474
- });
475
- };
476
-
477
- const scrollToBottom = () => {
478
- nextTick(() => {
479
- if (messageContainerRef.value) {
480
- messageContainerRef.value.scrollTop =
481
- messageContainerRef.value.scrollHeight;
482
- }
483
- });
484
- };
485
-
486
- const mantainElementsOnViewport = (scrollHeightBeforeAdd: number) => {
487
- nextTick(() => {
488
- const objDiv = messageContainerRef.value;
489
- if (objDiv) {
490
- objDiv.scrollTop = objDiv.scrollHeight - scrollHeightBeforeAdd;
491
- }
492
- });
493
- };
494
-
495
- const textAreaRef = ref<HTMLTextAreaElement>();
496
-
497
- const fontSpace = 14;
498
-
499
- function autoAdjustHeight() {
500
- if (!textAreaRef.value) return;
501
-
502
- textAreaRef.value.style.height =
503
- textAreaRef.value.scrollHeight - fontSpace + "px";
504
-
505
- if (textAreaRef.value.scrollHeight === textAreaRef.value.clientHeight)
506
- return;
507
-
508
- textAreaRef.value.style.height = textAreaRef.value.scrollHeight + "px";
509
- }
510
-
511
- const socketService = ref<Socket>();
512
- const information = ref<ChatInformation>();
513
-
514
- function connectMsWebSocket(
515
- userChat: ChatInformation,
516
- app: APP_TYPE = APP_TYPE.WEBCHAT
517
- ) {
518
- if (!userChat) throw new Error("user chat is required");
519
-
520
- socketService.value = io(window.VITE_SOCKET_URI, {
521
- query: {
522
- // TODO: confirmar si se quita o no
523
- usuarioId: `${userChat?.chat?.persona?.funcionarioId}`,
524
- aplicacion: app,
525
- },
526
- extraHeaders: { Authorization: props.tokenAuth },
527
- });
528
-
529
- socketService.value.removeAllListeners();
530
-
531
- socketService.value.on("connect", () => {
532
- console.log("Conectado al servidor de sockets");
533
-
534
- socketService.value?.emit("joinRoom", `${userChat?.appChat?.id}`);
535
- });
536
-
537
- socketService.value.on("disconnect", () => {
538
- console.log("Desconectado del servidor de sockets");
539
- });
540
-
541
- socketService.value.on("receiveMessage", (data: any) => {
542
- console.log("Mensaje recibido:", data.message, userChat);
543
- const indexMessage = messagesData.value.data.findIndex((msg) => msg.id === data.message.id);
544
-
545
- if (
546
- data.message.sender.msPersonaId === userChat?.chat?.persona?.msPersonaId && indexMessage === -1
547
- )
548
- return;
549
-
550
- if (indexMessage !== -1) {
551
- messagesData.value.data[indexMessage].response = data.message.response;
552
- } else {
553
- messagesData.value.data.push(data.message);
554
- }
555
-
556
- isDisabledBoxMessage.value = !!messagesData.value.data.find((msg) => msg.messageType?.code === MESSAGE_TYPE_CODES.PREGUNTA && msg.response == null);
557
-
558
- setVistoToTrueApi(data.message.appChatId, props.tokenAuth);
559
- scrollToBottom();
560
- !props.visible && emit("new-message");
561
- });
562
- }
563
-
564
- const { isMobile } = useMobile()
565
-
566
- function onQualifying({ message: messageParam, emoji }: { message: Message, emoji: { iconUnicode: string, value: number } }) {
567
- const callback = async () => {
568
- if (!messageParam.id || !emoji) return;
569
-
570
- message.value = emoji.iconUnicode
571
-
572
- submitMessage({
573
- tipoCalificacionId: emoji.value,
574
- mensajeARespondeId: messageParam.id
575
- }).then();
576
- }
577
-
578
- emit('onQualifying', { message: emoji.iconUnicode , callback})
579
- };
580
-
581
- const isDisabledBoxMessage = ref(false);
582
-
583
- // Adjuntar
584
- function onFileSelect() {
585
- inputFiles.value = [];
586
- urlFiles.value = [];
587
-
588
- const filesParam = fileInputRef.value?.files ?? []
589
-
590
- const file = filesParam?.[0];
591
-
592
- inputFiles.value = filesParam;
593
-
594
- if (!file) return;
595
-
596
- urlFiles.value.push(URL.createObjectURL(file))
597
-
598
- currentDialogView.value = DIALOG_VIEWS.UPLOAD;
599
- dialog.title = 'Preparar imagen';
600
- dialog.modelValue = true;
601
- }
602
-
603
- // Dialog
604
-
605
- const inputFiles = ref<(File & { objectURL: string })[]>([]);
606
- const urlFiles = ref<Array<string>>([]);
607
-
608
- const dialog = reactive<IPropsDialog>({
609
- modelValue: false,
610
- 'onUpdate:modelValue': (args) => {
611
- dialog.modelValue = args;
612
-
613
- if (args) return;
614
-
615
- urlFiles.value = [];
616
- inputFiles.value = [];
617
- },
618
- title: 'Preparar imagen'
619
- });
620
-
621
- //
622
-
623
- onMounted(async () => {
624
- if (appChatId.value) return;
625
-
626
- const resp = await getInformationApi(props.tokenAuth);
627
-
628
- if (!resp) return;
629
-
630
- information.value = resp;
631
- appChatId.value = resp.appChat.id;
632
- notViewed.value = resp.appChat.totalNoVistosCliente;
633
- connectMsWebSocket(resp);
634
- getMessages();
635
- emit("not-viewed-total", resp.appChat.totalNoVistosCliente);
636
- });
637
-
638
- onUnmounted(() => {
639
- socketService.value?.off();
640
- });
641
- </script>
642
-
643
- <style scoped>
644
- .btn-primary {
645
- padding: 10px 12px;
646
- &:hover {
647
- background-color: rgb(242, 139, 12, 0.1);
648
- }
649
- }
650
-
651
- .btn-close {
652
- padding: 0;
653
- background-color: transparent;
654
- border: none;
655
- display: flex;
656
- align-items: center;
657
- border-radius: 50%;
658
- &:hover {
659
- background-color: rgba(202, 202, 202, 0.534);
660
- }
661
- }
662
-
663
- .messages-container {
664
- position: relative;
665
- }
666
- .loader {
667
- position: absolute;
668
- top: 18px;
669
- z-index: 5;
670
- left: 50%;
671
- transform: translate(-50%, -50%);
672
- }
673
-
674
- .fit {
675
- width: 100%;
676
- height: 100%;
677
- position: relative;
678
- }
679
- .center {
680
- position: absolute;
681
- top: 50%;
682
- left: 50%;
683
- transform: translate(-50%, -50%);
684
- }
685
-
686
- .message-send-block {
687
- display: block;
688
- margin: 1.5rem;
689
- text-align: center;
690
- }
691
- </style>
1
+ <template>
2
+ <div class="widget">
3
+ <div class="header-widget">
4
+ <h4 class="title-chat">{{ titlePrincipal }}</h4>
5
+ <button @click="() => toggleChat()" class="btn-close">
6
+ <IconClose class="pointer" />
7
+ </button>
8
+ </div>
9
+ <div class="messages-container" ref="messageContainerRef">
10
+ <div class="loader" v-if="isLoading">
11
+ <Loader />
12
+ </div>
13
+ <MessageList
14
+ v-if="messagesData.data.length > 0"
15
+ :messages="messagesData.data"
16
+ :canLoadMoreMessages="messagesData.canLoadMoreMessages"
17
+ @loadMore="getMessages"
18
+ @retry="retryMessage"
19
+ @on-qualifying="(args) => onQualifying(args)"
20
+ @see="(message: Message) => {
21
+ currentDialogView = DIALOG_VIEWS.SEE
22
+ urlFileMessage = message.urlFile
23
+ dialog.title = 'Imagen'
24
+ dialog.modelValue = true
25
+ }"
26
+ />
27
+ <div class="fit" v-else>
28
+ <span class="center">No tienes mensajes</span>
29
+ </div>
30
+ </div>
31
+
32
+ <div class="w-full">
33
+ <form v-if="!isDisabledBoxMessage" class="message-send" @submit.prevent="(event) => submitMessage()">
34
+ <div class="form-message">
35
+ <div class="jl-inputgroup-chat">
36
+ <textarea
37
+ v-model="message"
38
+ class="jl2-input-chat"
39
+ placeholder="Escribe tu mensaje"
40
+ required
41
+ ref="textAreaRef"
42
+ @input="() => autoAdjustHeight()"
43
+ @keydown.enter="
44
+ (event) => {
45
+ !isMobile && event.preventDefault();
46
+ }
47
+ "
48
+ @keyup.enter="saltoDeLineaOEnviar"
49
+ />
50
+
51
+ <input
52
+ type="file"
53
+ ref="fileInputRef"
54
+ @change="onFileSelect"
55
+ accept="image/*"
56
+ style="display: none;"
57
+ key="fileInputKey"
58
+
59
+ />
60
+ <button
61
+ type="button"
62
+ class="pointer btn-primary"
63
+ title="Adjuntar archivo"
64
+ @click="
65
+ () => {
66
+ fileInputRef.value = '';
67
+ fileInputKey++;
68
+ fileInputRef?.click();
69
+ }
70
+ "
71
+ >
72
+ <IconAttach style="width: 20px; height: 20px" />
73
+ </button>
74
+
75
+ <button type="submit" class="pointer btn-primary">
76
+ <IconSend style="width: 20px; height: 20px" />
77
+ </button>
78
+ </div>
79
+ </div>
80
+ </form>
81
+ <span class="message-send-block" v-else>
82
+ Necesitamos que nos califique la atención para continuar
83
+ </span>
84
+ </div>
85
+ </div>
86
+
87
+ <ODialog v-bind="dialog">
88
+ <div v-if="currentDialogView === DIALOG_VIEWS.UPLOAD" class="flex flex-col gap-3 justify-center items-center">
89
+ <img v-for="(urlFile, i) in urlFiles" :key="i" :src="urlFile.toString()" alt="Image" width="400" />
90
+
91
+ <form
92
+ class="message-send"
93
+ @submit.prevent="
94
+ (event) => {
95
+ submitMessage();
96
+ dialog.modelValue = false;
97
+ }
98
+ "
99
+ >
100
+ <div class="form-message">
101
+ <div class="jl-inputgroup-chat">
102
+ <textarea
103
+ v-model="message"
104
+ autofocus
105
+ class="jl2-input-chat"
106
+ placeholder="Escribe tu mensaje"
107
+ ref="textAreaRef"
108
+ @input="() => autoAdjustHeight()"
109
+ @keydown.enter="
110
+ (event) => {
111
+ !isMobile && event.preventDefault();
112
+ }
113
+ "
114
+ @keyup.enter="(event)=>{
115
+ saltoDeLineaOEnviar(event, MESSAGE_TYPE_CODES.IMAGEN)
116
+ dialog.modelValue = false;
117
+ }"
118
+ />
119
+
120
+ <button type="submit" class="pointer btn-primary">
121
+ <IconSend style="width: 20px; height: 20px" />
122
+ </button>
123
+ </div>
124
+ </div>
125
+ </form>
126
+ </div>
127
+ <div v-else>
128
+ <img v-if="urlFileMessage" :src="urlFileMessage" alt="Image" style="width: 55vw;" />
129
+ </div>
130
+ </ODialog>
131
+ </template>
132
+
133
+ <script setup lang="ts">
134
+ import {
135
+ ref,
136
+ onMounted,
137
+ nextTick,
138
+ PropType,
139
+ watch,
140
+ onUnmounted,
141
+ reactive,
142
+ } from "vue";
143
+ import { v4 as uuidv4 } from "uuid";
144
+
145
+ import {
146
+ type SendMessageBody,
147
+ ChatInformation,
148
+ ListMessageBody,
149
+ Message,
150
+ } from "../dto/app.dto";
151
+ import IconClose from "./IconClose.vue";
152
+ import IconSend from "./IconSend.vue";
153
+ import {
154
+ getMessagesApi,
155
+ sendMessageApi,
156
+ setVistoToTrueApi,
157
+ updateMessageApi,
158
+ } from "../store/index";
159
+ import { getInformationApi } from "../store";
160
+ import MessageList from "./MessageList.vue";
161
+ import Loader from "./Loader.vue";
162
+ import { searchFromLast } from "../resources/functions.helpers";
163
+ import { io, Socket } from "socket.io-client";
164
+ import { APP_TYPE } from "../dto/chat.dto";
165
+ import { MESSAGE_TYPE_CODES, TypeMessageTypeCodes } from "../resources/constants/message-type.constant";
166
+ import { useMobile } from "../hooks/useMobile";
167
+ import IconAttach from "./IconAttach.vue";
168
+ import ODialog from "./ODialog/ODialog.vue";
169
+ import { IPropsDialog } from "./ODialog/IPropsDialog";
170
+
171
+ const enum DIALOG_VIEWS {
172
+ UPLOAD,
173
+ SEE
174
+ }
175
+
176
+ //DATA
177
+ const message = ref("");
178
+ const notViewed = ref(0);
179
+ const fileInputRef = ref();
180
+ const fileInputKey = ref(0);
181
+ const currentDialogView = ref(DIALOG_VIEWS.SEE)
182
+ const urlFileMessage = ref<string>()
183
+
184
+ const messagesData = ref<{ data: Message[]; canLoadMoreMessages: boolean }>({
185
+ data: [],
186
+ canLoadMoreMessages: false,
187
+ });
188
+
189
+ const appChatId = ref("");
190
+ const isLoading = ref(false);
191
+
192
+ const emit = defineEmits([
193
+ "show-toast",
194
+ "show-confirm",
195
+ "new-message",
196
+ "clear-new-messages",
197
+ "not-viewed-total",
198
+ "onQualifying",
199
+ ]);
200
+
201
+ const props = defineProps({
202
+ titlePrincipal: {
203
+ type: String,
204
+ default: "Comunicación en linea para consultas",
205
+ },
206
+ toggleChat: { type: Function, required: true },
207
+ tokenAuth: { type: String, required: true },
208
+ user: {
209
+ type: Object as PropType<{
210
+ nombreCompleto: string;
211
+ ci: string;
212
+ msPersonaId: number;
213
+ }>,
214
+ required: true,
215
+ },
216
+ visible: { type: Boolean, required: true },
217
+ });
218
+
219
+ const messageContainerRef = ref<HTMLElement | null>(null);
220
+
221
+ watch(
222
+ () => props.visible,
223
+ async (current) => {
224
+ if (!current) return;
225
+
226
+ emit("clear-new-messages");
227
+ scrollToBottom();
228
+
229
+ if (notViewed.value > 0) {
230
+ setVistoToTrueApi(appChatId.value, props.tokenAuth);
231
+ }
232
+
233
+ if (appChatId.value) return;
234
+
235
+ const resp = await getInformationApi(props.tokenAuth);
236
+
237
+ if (resp) {
238
+ appChatId.value = resp.appChat.id;
239
+ }
240
+ }
241
+ );
242
+
243
+ function saltoDeLineaOEnviar(event: KeyboardEvent, messageTypeCode?: TypeMessageTypeCodes) {
244
+ if(isMobile.value) {
245
+ autoAdjustHeight();
246
+ return;
247
+ }
248
+
249
+ if (event.key === "Enter" && event.shiftKey) {
250
+ const val = (event.target as any)?.value || "";
251
+ (event.target as any).value = val + "\n";
252
+ autoAdjustHeight();
253
+ return;
254
+ }
255
+
256
+ submitMessage({ codigoTipoMensaje: messageTypeCode });
257
+ }
258
+
259
+ function createMessage(message: string, codigoTipoMensaje?: TypeMessageTypeCodes) {
260
+ if (message?.length > 300) {
261
+ emit("show-toast", {
262
+ severity: "warn",
263
+ summary: "Error",
264
+ detail: "El mensaje no puede superar los 300 caracteres",
265
+ life: 5000,
266
+ });
267
+ return;
268
+ }
269
+
270
+ if (!message.trim() && codigoTipoMensaje !== MESSAGE_TYPE_CODES.IMAGEN) {
271
+ emit("show-toast", {
272
+ severity: "warn",
273
+ summary: "Error",
274
+ detail: "Por favor ingrese un mensaje",
275
+ life: 5000,
276
+ });
277
+ return;
278
+ }
279
+
280
+ const messageType = codigoTipoMensaje
281
+ ? {
282
+ code: codigoTipoMensaje,
283
+ }
284
+ : undefined;
285
+
286
+ const newMessage: Message = {
287
+ id: uuidv4(),
288
+ chatId: information.value?.chat?.id,
289
+ message,
290
+ visto: true,
291
+ multimedia: false,
292
+ esCliente: true,
293
+ appChatId: appChatId.value,
294
+ createdAt: new Date().toISOString(),
295
+ updatedAt: new Date().toISOString(),
296
+ file: inputFiles.value?.[0],
297
+ urlFile: urlFiles.value?.[0].toString(),
298
+ messageType,
299
+ sender: {
300
+ nombreCompleto: props.user.nombreCompleto,
301
+ ci: props.user.ci,
302
+ msPersonaId: props.user.msPersonaId,
303
+ },
304
+ response: undefined
305
+ };
306
+
307
+ return newMessage
308
+ }
309
+
310
+ function updateMessageStatus(
311
+ newMessage: Message,
312
+ idxMessageToCommunicate?: number,
313
+ messageSaved?: Message,
314
+ tipoCalificacionId?: number
315
+ ) {
316
+ if (idxMessageToCommunicate == null) throw new Error('idx is required')
317
+
318
+ if (!messageSaved) {
319
+ if (tipoCalificacionId) {
320
+ const messageAResponder = messagesData.value.data[idxMessageToCommunicate]
321
+ messageAResponder.response = undefined
322
+ } else {
323
+ messagesData.value.data[idxMessageToCommunicate].error = {
324
+ error: true,
325
+ id: newMessage.id,
326
+ };
327
+ }
328
+
329
+ emit("show-toast", {
330
+ severity: "error",
331
+ summary: "Error",
332
+ detail: "Ocurrio un error al enviar el mensaje, intente nuevamente",
333
+ life: 5000,
334
+ });
335
+
336
+ return
337
+ }
338
+
339
+ let messageUpdated = { ...messageSaved, chatId: newMessage?.chatId }
340
+
341
+ if (tipoCalificacionId) {
342
+ const messageAResponder = messagesData.value.data[idxMessageToCommunicate]
343
+ messageAResponder.response = messageSaved
344
+
345
+ messageUpdated = { ...messageAResponder, chatId: newMessage?.chatId }
346
+ } else {
347
+ messagesData.value.data[idxMessageToCommunicate] = messageUpdated;
348
+ }
349
+
350
+ return messageUpdated;
351
+ }
352
+
353
+ const submitMessage = async (
354
+ messageExtraData?: { mensajeARespondeId?: string, tipoCalificacionId?: number, codigoTipoMensaje?: TypeMessageTypeCodes}
355
+ ) => {
356
+
357
+ const newMessage = createMessage(message.value, messageExtraData?.codigoTipoMensaje);
358
+ message.value = '';
359
+ if (!newMessage) return;
360
+
361
+ let idxMessageToCommunicate = -1;
362
+ if (messageExtraData?.tipoCalificacionId) {
363
+ idxMessageToCommunicate = messagesData.value.data.findIndex((val) => val.id === messageExtraData?.mensajeARespondeId )
364
+ const messageAResponder = messagesData.value.data[idxMessageToCommunicate]
365
+
366
+ if ( messageAResponder ) {
367
+ messageAResponder.response = newMessage
368
+ }
369
+ } else {
370
+ idxMessageToCommunicate = messagesData.value.data.push(newMessage) - 1;
371
+ }
372
+
373
+ const body = {
374
+ message: message.value,
375
+ appChatId: appChatId.value,
376
+ ...messageExtraData,
377
+ codigoTipoMensaje: messageExtraData?.codigoTipoMensaje,
378
+ files: inputFiles.value,
379
+ }
380
+ sendApi(body).then((messageSaved) => {
381
+ let messageUpdated = updateMessageStatus(
382
+ newMessage,
383
+ idxMessageToCommunicate,
384
+ messageSaved,
385
+ messageExtraData?.tipoCalificacionId
386
+ )
387
+
388
+ if(!messageUpdated) return
389
+
390
+ isDisabledBoxMessage.value = !!messagesData.value.data.find((msg) => msg.messageType?.code === MESSAGE_TYPE_CODES.PREGUNTA && msg.response == null);
391
+
392
+ socketService.value?.emit(
393
+ "sendMessage",
394
+ { roomId: information?.value?.appChat.id, message: messageUpdated },
395
+ (response: any) => {
396
+ console.log("🚀 ~ socketService.value.emit ~ response:", response);
397
+ }
398
+ );
399
+ });
400
+
401
+ message.value = "";
402
+ scrollToBottom();
403
+ textAreaRef.value && (textAreaRef.value.style.height = "20px");
404
+ };
405
+
406
+ const sendApi = async (bodyParam: Omit<SendMessageBody, 'esCliente'>) => {
407
+ const body: SendMessageBody = {
408
+ ...bodyParam,
409
+ esCliente: true,
410
+ };
411
+ return sendMessageApi(body, props.tokenAuth);
412
+ };
413
+
414
+ const getMessages = async () => {
415
+ const lastMessagesId = messagesData.value.data[0]?.id;
416
+ const body: ListMessageBody = {
417
+ lastMessagesId,
418
+ appChatId: appChatId.value,
419
+ limit: 10,
420
+ };
421
+
422
+ isLoading.value = true;
423
+ const resp = await getMessagesApi({ body, token: props.tokenAuth });
424
+ isLoading.value = false;
425
+
426
+ isDisabledBoxMessage.value = !!resp.data.find((msg) => msg.messageType?.code === MESSAGE_TYPE_CODES.PREGUNTA && msg.response == null);
427
+
428
+ messagesData.value.data.unshift(
429
+ ...resp.data.sort((a, b) => -b.createdAt.localeCompare(a.createdAt))
430
+ );
431
+
432
+ messagesData.value.canLoadMoreMessages =
433
+ resp.pagination.total > resp.pagination.size;
434
+
435
+ if (lastMessagesId && messageContainerRef.value?.scrollHeight) {
436
+ mantainElementsOnViewport(messageContainerRef.value?.scrollHeight);
437
+ }
438
+
439
+ if (!lastMessagesId) scrollToBottom();
440
+ };
441
+
442
+ const retryMessage = async (message: Message) => {
443
+ emit("show-confirm", async () => {
444
+ if (!message.error?.id) return;
445
+
446
+ const msg = await sendApi({
447
+ message: message.message,
448
+ appChatId: appChatId.value
449
+ });
450
+
451
+ if (!msg) {
452
+ emit("show-toast", {
453
+ severity: "error",
454
+ summary: "Error",
455
+ detail: "Ocurrio un error al enviar el mensaje, intente nuevamente",
456
+ life: 5000,
457
+ });
458
+ } else {
459
+ const idx = searchFromLast<Message>(
460
+ messagesData.value.data,
461
+ "id",
462
+ message.error.id
463
+ );
464
+
465
+ messagesData.value.data[idx] = { ...msg, error: undefined };
466
+
467
+ socketService.value?.emit("sendMessage", {
468
+ roomId: information?.value?.appChat.id,
469
+ message,
470
+ });
471
+ }
472
+
473
+ scrollToBottom();
474
+ });
475
+ };
476
+
477
+ const scrollToBottom = () => {
478
+ nextTick(() => {
479
+ if (messageContainerRef.value) {
480
+ messageContainerRef.value.scrollTop =
481
+ messageContainerRef.value.scrollHeight;
482
+ }
483
+ });
484
+ };
485
+
486
+ const mantainElementsOnViewport = (scrollHeightBeforeAdd: number) => {
487
+ nextTick(() => {
488
+ const objDiv = messageContainerRef.value;
489
+ if (objDiv) {
490
+ objDiv.scrollTop = objDiv.scrollHeight - scrollHeightBeforeAdd;
491
+ }
492
+ });
493
+ };
494
+
495
+ const textAreaRef = ref<HTMLTextAreaElement>();
496
+
497
+ const fontSpace = 14;
498
+
499
+ function autoAdjustHeight() {
500
+ if (!textAreaRef.value) return;
501
+
502
+ textAreaRef.value.style.height =
503
+ textAreaRef.value.scrollHeight - fontSpace + "px";
504
+
505
+ if (textAreaRef.value.scrollHeight === textAreaRef.value.clientHeight)
506
+ return;
507
+
508
+ textAreaRef.value.style.height = textAreaRef.value.scrollHeight + "px";
509
+ }
510
+
511
+ const socketService = ref<Socket>();
512
+ const information = ref<ChatInformation>();
513
+
514
+ function connectMsWebSocket(
515
+ userChat: ChatInformation,
516
+ app: APP_TYPE = APP_TYPE.WEBCHAT
517
+ ) {
518
+ if (!userChat) throw new Error("user chat is required");
519
+
520
+ socketService.value = io(window.VITE_SOCKET_URI, {
521
+ query: {
522
+ // TODO: confirmar si se quita o no
523
+ usuarioId: `${userChat?.chat?.persona?.funcionarioId}`,
524
+ aplicacion: app,
525
+ },
526
+ extraHeaders: { Authorization: props.tokenAuth },
527
+ });
528
+
529
+ socketService.value.removeAllListeners();
530
+
531
+ socketService.value.on("connect", () => {
532
+ console.log("Conectado al servidor de sockets");
533
+
534
+ socketService.value?.emit("joinRoom", `${userChat?.appChat?.id}`);
535
+ });
536
+
537
+ socketService.value.on("disconnect", () => {
538
+ console.log("Desconectado del servidor de sockets");
539
+ });
540
+
541
+ socketService.value.on("receiveMessage", (data: any) => {
542
+ console.log("Mensaje recibido:", data.message, userChat);
543
+ const indexMessage = messagesData.value.data.findIndex((msg) => msg.id === data.message.id);
544
+
545
+ if (
546
+ data.message.sender.msPersonaId === userChat?.chat?.persona?.msPersonaId && indexMessage === -1
547
+ )
548
+ return;
549
+
550
+ if (indexMessage !== -1) {
551
+ messagesData.value.data[indexMessage].response = data.message.response;
552
+ } else {
553
+ messagesData.value.data.push(data.message);
554
+ }
555
+
556
+ isDisabledBoxMessage.value = !!messagesData.value.data.find((msg) => msg.messageType?.code === MESSAGE_TYPE_CODES.PREGUNTA && msg.response == null);
557
+
558
+ setVistoToTrueApi(data.message.appChatId, props.tokenAuth);
559
+ scrollToBottom();
560
+ !props.visible && emit("new-message");
561
+ });
562
+ }
563
+
564
+ const { isMobile } = useMobile()
565
+
566
+ function onQualifying({ message: messageParam, emoji }: { message: Message, emoji: { iconUnicode: string, value: number } }) {
567
+ const callback = async () => {
568
+ if (!messageParam.id || !emoji) return;
569
+
570
+ message.value = emoji.iconUnicode
571
+
572
+ submitMessage({
573
+ tipoCalificacionId: emoji.value,
574
+ mensajeARespondeId: messageParam.id
575
+ }).then();
576
+ }
577
+
578
+ emit('onQualifying', { message: emoji.iconUnicode , callback})
579
+ };
580
+
581
+ const isDisabledBoxMessage = ref(false);
582
+
583
+ // Adjuntar
584
+ function onFileSelect() {
585
+ inputFiles.value = [];
586
+ urlFiles.value = [];
587
+
588
+ const filesParam = fileInputRef.value?.files ?? []
589
+
590
+ const file = filesParam?.[0];
591
+
592
+ inputFiles.value = filesParam;
593
+
594
+ if (!file) return;
595
+
596
+ urlFiles.value.push(URL.createObjectURL(file))
597
+
598
+ currentDialogView.value = DIALOG_VIEWS.UPLOAD;
599
+ dialog.title = 'Preparar imagen';
600
+ dialog.modelValue = true;
601
+ }
602
+
603
+ // Dialog
604
+
605
+ const inputFiles = ref<(File & { objectURL: string })[]>([]);
606
+ const urlFiles = ref<Array<string>>([]);
607
+
608
+ const dialog = reactive<IPropsDialog>({
609
+ modelValue: false,
610
+ 'onUpdate:modelValue': (args) => {
611
+ dialog.modelValue = args;
612
+
613
+ if (args) return;
614
+
615
+ urlFiles.value = [];
616
+ inputFiles.value = [];
617
+ },
618
+ title: 'Preparar imagen'
619
+ });
620
+
621
+ //
622
+
623
+ onMounted(async () => {
624
+ if (appChatId.value) return;
625
+
626
+ const resp = await getInformationApi(props.tokenAuth);
627
+
628
+ if (!resp) return;
629
+
630
+ information.value = resp;
631
+ appChatId.value = resp.appChat.id;
632
+ notViewed.value = resp.appChat.totalNoVistosCliente;
633
+ connectMsWebSocket(resp);
634
+ getMessages();
635
+ emit("not-viewed-total", resp.appChat.totalNoVistosCliente);
636
+ });
637
+
638
+ onUnmounted(() => {
639
+ socketService.value?.off();
640
+ });
641
+ </script>
642
+
643
+ <style scoped>
644
+ .btn-primary {
645
+ padding: 10px 12px;
646
+ &:hover {
647
+ background-color: rgb(242, 139, 12, 0.1);
648
+ }
649
+ }
650
+
651
+ .btn-close {
652
+ padding: 0;
653
+ background-color: transparent;
654
+ border: none;
655
+ display: flex;
656
+ align-items: center;
657
+ border-radius: 50%;
658
+ &:hover {
659
+ background-color: rgba(202, 202, 202, 0.534);
660
+ }
661
+ }
662
+
663
+ .messages-container {
664
+ position: relative;
665
+ }
666
+ .loader {
667
+ position: absolute;
668
+ top: 18px;
669
+ z-index: 5;
670
+ left: 50%;
671
+ transform: translate(-50%, -50%);
672
+ }
673
+
674
+ .fit {
675
+ width: 100%;
676
+ height: 100%;
677
+ position: relative;
678
+ }
679
+ .center {
680
+ position: absolute;
681
+ top: 50%;
682
+ left: 50%;
683
+ transform: translate(-50%, -50%);
684
+ }
685
+
686
+ .message-send-block {
687
+ display: block;
688
+ margin: 1.5rem;
689
+ text-align: center;
690
+ }
691
+ </style>