vue-wiguet-chatweb 0.1.28 → 0.1.29

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.
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>