Docker Desktop

Izing+ Facebook + Instagram
2024

Passo a Passo

				
					IZING.IO + FACEBOOK + INSTAGRAM

# Download GIT, Node, Docker Desktop e Ngrok (dev local)
- criar banco de dados

docker run --name postgresql -e TZ="America/Sao_Paulo" --restart=always -e POSTGRES_USER=izing -e POSTGRES_PASSWORD=password -p 5432:5432 -v /data:/var/lib/postgresql/dataizing -d postgres

docker run -e TZ="America/Sao_Paulo" --name redis-izing -p 6379:6379 -d --restart=always redis:latest redis-server --appendonly yes --requirepass "password"

# Clonar repositório
- git clone https://github.com/ldurans/izing.open.io.git
- config env

# Instalar back
- cd backend
- npm i -f
- npx sequelize db:migrate
- npx sequelize db:seed:all
- npm run dev:server

# Instalar front
- cd frontend
- npm i -f
- export NODE_OPTIONS=--openssl-legacy-provider && npm quasar build -P -m -pwa
- export NODE_OPTIONS=--openssl-legacy-provider && npx quasar dev

###### Construir integração

# Ngrok
- ngrok http 3100
- https://7401-2804-3d34-5005-ec01-00-1.ngrok-free.app

# Back
- backend/package.json
# executar npm i -f (backend)

- backend/src/database/seeds/20200904070004-create-default-settings.ts

- backend/src/models/Contact.ts
- backend/src/models/Whatsapp.ts

- backend/src/helpers/ConvertMp3ToMp4.ts
- backend/src/helpers/DownloadFiles.ts
- backend/src/helpers/SetChannelWebhook.ts
- backend/src/helpers/ShowHubToken.ts

- backend/src/services/WbotNotificame/CreateChannelsService.ts
- backend/src/services/WbotNotificame/CreateMessageService.ts
- backend/src/services/WbotNotificame/CreateOrUpdateTicketService.ts
- backend/src/services/WbotNotificame/FindOrCreateContactService.ts
- backend/src/services/WbotNotificame/HubMessageListener.ts
- backend/src/services/WbotNotificame/ListChannels.ts
- backend/src/services/WbotNotificame/SendMediaMessageService.ts
- backend/src/services/WbotNotificame/SendTextMessageService.ts
- backend/src/services/WbotNotificame/UpdateMessageAck.ts
- backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts

- backend/src/controllers/ChannelHubController.ts
- backend/src/controllers/ContactController.ts
- backend/src/controllers/MessageHubController.ts
- backend/src/controllers/WebhookHubController.ts

- backend/src/routes/hubChannelRoutes.ts
- backend/src/routes/hubMessageRoutes.ts
- backend/src/routes/hubWebhookRoutes.ts
- backend/src/routes/index.ts

# Front:

- frontend/public/hub-logo.png
- frontend/public/hub_facebook-logo.png
- frontend/public/hub_instagram-logo.png

- frontend/src/pages/atendimento/InputMensagem.vue
- frontend/src/pages/configuracoes/Index.vue
- frontend/src/pages/contatos/ContatoModal.vue

- frontend/src/pages/sessaoWhatsapp/Index.vue
- frontend/src/pages/sessaoWhatsapp/ModalWhatsapp.vue

- frontend/src/service/hub.js
				
			

BACKEND

package.json

				
					{"name":"backend","version":"2.0.0","description":"izing.open.io - sistema de atendimentos multicanal","main":"index.js","scripts":{"build":"tsc","watch":"tsc -w","start":"nodemon --inspect  dist\/server.js","dev:server":"ts-node-dev --inspect=9229 --respawn --transpile-only --ignore node_modules src\/server.ts","pretest":"NODE_ENV=test sequelize db:migrate && NODE_ENV=test sequelize db:seed:all","test":"NODE_ENV=test jest","posttest":"NODE_ENV=test sequelize db:migrate:undo:all","db:migrate":"rm -Rf dist && npm run build && npx sequelize db:migrate","db:seed":"npx sequelize db:seed:all"},"engines":{"node":">=18"},"author":"Durans (lumardyelson@gmail.com)","private":true,"license":"AGPL-3.0-or-later","dependencies":{"@androz2091\/insta.js":"^1.6.1","@ffmpeg-installer\/ffmpeg":"^1.1.0","@sentry\/node":"5.27.0","@types\/pino":"^6.3.5","amqplib":"^0.10.3","asterisk-manager":"^0.2.0","axios":"^0.21.1","bcryptjs":"^2.4.3","body-parser":"^1.19.0","bull":"3.22.8","bull-board":"^1.4.1","content-disposition":"^0.5.4","cookie-parser":"^1.4.5","cors":"^2.8.5","date-fns":"^2.16.1","date-fns-tz":"^1.1.3","dotenv":"^8.2.0","express":"^4.17.1","express-async-errors":"^3.1.1","file-type":"^19.5.0","fluent-ffmpeg":"^2.1.2","helmet":"^4.4.1","http-graceful-shutdown":"^2.4.0","instagram_mqtt":"^1.2.2","instagram-private-api":"^1.45.3","ioredis":"^5.2.5","is-base64":"^1.1.0","jsonwebtoken":"^8.5.1","lodash":"^4.17.21","messaging-api-messenger":"^1.1.0","mime-types":"^2.1.29","multer":"^1.4.2","mustache":"^4.2.0","newrelic":"^8.16.0","notificamehubsdk":"^0.0.19","pg":"^8.4.1","pino":"^6.10.0","pino-http":"^5.6.0","pino-pretty":"^9.1.1","qrcode-terminal":"^0.12.0","redis":"^3.1.2","reflect-metadata":"^0.1.13","sequelize":"^5.22.5","sequelize-typescript":"^1.1.0","sharp":"^0.32.0","slugify":"^1.6.1","socket.io":"^3.1.2","socket.io-redis":"^6.1.1","telegraf":"^4.0.3","uuid":"^8.3.2","whatsapp-web.js":"github:ldurans\/whatsapp-web.js#webpack-exodus","winston":"^3.3.3","xlsx":"^0.18.5","yup":"^0.29.3"},"devDependencies":{"@types\/amqplib":"^0.10.1","@types\/bcryptjs":"^2.4.2","@types\/bluebird":"^3.5.32","@types\/body-parser":"^1.19.0","@types\/bull":"^3.15.0","@types\/content-disposition":"^0.5.4","@types\/cookie-parser":"^1.4.2","@types\/cors":"^2.8.7","@types\/express":"^4.17.8","@types\/factory-girl":"^5.0.2","@types\/faker":"^5.1.3","@types\/fluent-ffmpeg":"^2.1.20","@types\/helmet":"^4.0.0","@types\/is-base64":"^1.1.0","@types\/jest":"^26.0.15","@types\/jsonwebtoken":"^8.5.0","@types\/lodash":"^4.14.176","@types\/mime-types":"^2.1.0","@types\/multer":"^1.4.4","@types\/mustache":"^4.2.2","@types\/node":"^14.11.8","@types\/pino-http":"^5.4.2","@types\/redis":"^2.8.28","@types\/sequelize":"^4.28.14","@types\/sharp":"^0.31.1","@types\/socket.io":"^2.1.13","@types\/socket.io-redis":"^1.0.27","@types\/supertest":"^2.0.10","@types\/uuid":"^8.3.0","@types\/validator":"^13.1.0","@types\/yup":"^0.29.8","@typescript-eslint\/eslint-plugin":"^4.4.0","@typescript-eslint\/parser":"^4.4.0","eslint":"^7.10.0","eslint-config-airbnb-base":"^14.2.0","eslint-config-prettier":"^6.12.0","eslint-import-resolver-typescript":"^2.3.0","eslint-plugin-import":"^2.22.1","eslint-plugin-prettier":"^3.1.4","factory-girl":"^5.0.4","faker":"^5.1.0","javascript-obfuscator":"^4.0.2","jest":"^26.6.0","nodemon":"^2.0.4","prettier":"^2.1.2","sequelize-cli":"^6.4.1","supertest":"^5.0.0","ts-jest":"^26.4.1","ts-node-dev":"^1.0.0-pre.63","typegram":"^3.5.1","typescript":"^4.8.4"}}
				
			

DATABASE

backend\src\database\seeds\20200904070004-create-default-settings.ts

				
					import { QueryInterface } from "sequelize";

module.exports = {
  up: (queryInterface: QueryInterface) => {
    return queryInterface.sequelize.query(
      `
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('userCreation', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 1);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('NotViewTicketsQueueUndefined', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 2);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('NotViewTicketsChatBot', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 3);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('DirectTicketsToWallets', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 4);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('NotViewAssignedTickets', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 6);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('botTicketActive', '3', '2020-12-12 16:08:45.354', '2022-07-01 21:10:02.076', 1, 5);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('ignoreGroupMsg', 'enabled', '2022-12-16 16:08:45.354' , '2022-12-16 21:10:02.076', 1, 7);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('rejectCalls', 'disabled', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 9);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('callRejectMessage', 'As chamadas de voz e vídeo estão desabilitas para esse WhatsApp, favor enviar uma mensagem de texto.', '2020-12-12 16:08:45.354', '2020-12-12 16:08:45.354', 1, 10);
      INSERT INTO public."Settings" ("key", value, "createdAt", "updatedAt", "tenantId", id) VALUES('hubToken', 'disabled', '2020-12-12 16:08:45.354', '2022-07-01 21:10:02.076', 1, 11);
      `
    );
  },

  down: (queryInterface: QueryInterface) => {
    return queryInterface.bulkDelete("Tenants", {});
  }
};

				
			

MODELS

backend\src\models\Contact.ts

				
					import {
  Table,
  Column,
  CreatedAt,
  UpdatedAt,
  Model,
  PrimaryKey,
  AutoIncrement,
  AllowNull,
  Default,
  HasMany,
  BeforeCreate,
  ForeignKey,
  BelongsTo,
  BelongsToMany
} from "sequelize-typescript";
import Campaign from "./Campaign";
import CampaignContacts from "./CampaignContacts";
import ContactCustomField from "./ContactCustomField";
import ContactWallet from "./ContactWallet";
// import Message from "./Message";
import Tags from "./Tag";
import Tenant from "./Tenant";
import Ticket from "./Ticket";
import ContactTag from "./ContactTag";
import User from "./User";

@Table
class Contact extends Model<Contact> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @Column
  name: string;

  @AllowNull(true)
  @Column
  number: string;

  @AllowNull(true)
  @Default(null)
  @Column
  email: string;

  @Column
  profilePicUrl: string;

  @AllowNull(true)
  @Default(null)
  @Column
  pushname: string;

  @AllowNull(true)
  @Default(null)
  @Column
  telegramId: string;

  @AllowNull(true)
  @Default(null)
  @Column
  messengerId: string;

  @AllowNull(true)
  @Default(null)
  @Column
  instagramPK: number;

  @Default(false)
  @Column
  isUser: boolean;

  @Default(false)
  @Column
  isWAContact: boolean;

  @Default(false)
  @Column
  isGroup: boolean;

  @CreatedAt
  createdAt: Date;

  @UpdatedAt
  updatedAt: Date;

  @HasMany(() => Ticket)
  tickets: Ticket[];

  @HasMany(() => ContactCustomField)
  extraInfo: ContactCustomField[];

  @BelongsToMany(() => Tags, () => ContactTag, "contactId", "tagId")
  tags: Tags[];

  @BelongsToMany(() => User, () => ContactWallet, "contactId", "walletId")
  wallets: ContactWallet[];

  @HasMany(() => ContactWallet)
  contactWallets: ContactWallet[];

  @HasMany(() => CampaignContacts)
  campaignContacts: CampaignContacts[];

  @BelongsToMany(
    () => Campaign,
    () => CampaignContacts,
    "contactId",
    "campaignId"
  )
  campaign: Campaign[];

  @ForeignKey(() => Tenant)
  @Column
  tenantId: number;

  @BelongsTo(() => Tenant)
  tenant: Tenant;
}

export default Contact;

				
			

backend\src\models\Whatsapp.ts

				
					import { sign } from "jsonwebtoken";
import {
  Table,
  Column,
  CreatedAt,
  UpdatedAt,
  Model,
  DataType,
  PrimaryKey,
  AutoIncrement,
  Default,
  AllowNull,
  HasMany,
  Unique,
  ForeignKey,
  BelongsTo,
  AfterUpdate,
  BeforeCreate,
  BeforeUpdate
  // DefaultScope
} from "sequelize-typescript";
import webHooks from "../config/webHooks.dev.json";

import authConfig from "../config/auth";

import Queue from "../libs/Queue";
import ApiConfig from "./ApiConfig";
import Tenant from "./Tenant";
import Ticket from "./Ticket";
import ChatFlow from "./ChatFlow";

// @DefaultScope(() => ({
//   where: { isDeleted: false }
// }))
@Table
class Whatsapp extends Model<Whatsapp> {
  @PrimaryKey
  @AutoIncrement
  @Column
  id: number;

  @AllowNull
  @Unique
  @Column(DataType.TEXT)
  name: string;

  @Column(DataType.TEXT)
  session: string;

  @Column(DataType.TEXT)
  qrcode: string;

  @Column
  status: string;

  @Column
  battery: string;

  @Column
  plugged: boolean;

  @Default(true)
  @Column
  isActive: boolean;

  @Default(false)
  @Column
  isDeleted: boolean;

  @Column
  retries: number;

  @Default(false)
  @AllowNull
  @Column
  isDefault: boolean;

  @Default(null)
  @AllowNull
  @Column
  tokenTelegram: string;

  @Default(null)
  @AllowNull
  @Column
  instagramUser: string;

  @Default(null)
  @AllowNull
  @Column
  instagramKey: string;

  @Default(null)
  @AllowNull
  @Column
  fbPageId: string;

  @Default(null)
  @AllowNull
  @Column(DataType.JSONB)
  // eslint-disable-next-line @typescript-eslint/ban-types
  fbObject: object;

  @Default("whatsapp")
  @Column(DataType.ENUM("whatsapp", "telegram", "instagram", "messenger"))
  type: string;

  @CreatedAt
  createdAt: Date;

  @UpdatedAt
  updatedAt: Date;

  @Column
  number: string;

  @Column(DataType.JSONB)
  // eslint-disable-next-line @typescript-eslint/ban-types
  phone: object;

  @HasMany(() => Ticket)
  tickets: Ticket[];

  @ForeignKey(() => Tenant)
  @Column
  tenantId: number;

  @BelongsTo(() => Tenant)
  tenant: Tenant;

  @ForeignKey(() => ChatFlow)
  @Column
  chatFlowId: number;

  @BelongsTo(() => ChatFlow)
  chatFlow: ChatFlow;

  @Default(null)
  @AllowNull
  @Column(DataType.ENUM("360", "gupshup"))
  wabaBSP: string;

  @Default(null)
  @AllowNull
  @Column(DataType.TEXT)
  tokenAPI: string;

  @Default(null)
  @AllowNull
  @Column(DataType.TEXT)
  tokenHook: string;

  @Default(null)
  @AllowNull
  @Column(DataType.TEXT)
  farewellMessage: string;

  @Column(DataType.VIRTUAL)
  get UrlWabaWebHook(): string | null {
    const key = this.getDataValue("tokenHook");
    const wabaBSP = this.getDataValue("wabaBSP");
    let BACKEND_URL;
    BACKEND_URL = process.env.BACKEND_URL;
    if (process.env.NODE_ENV === "dev") {
      BACKEND_URL = webHooks.urlWabahooks;
    }
    return `${BACKEND_URL}/wabahooks/${wabaBSP}/${key}`;
  }

  @Column(DataType.VIRTUAL)
  get UrlMessengerWebHook(): string | null {
    const key = this.getDataValue("tokenHook");
    let BACKEND_URL;
    BACKEND_URL = process.env.BACKEND_URL;
    if (process.env.NODE_ENV === "dev") {
      BACKEND_URL = webHooks.urlWabahooks;
    }
    return `${BACKEND_URL}/fb-messenger-hooks/${key}`;
  }

  @AfterUpdate
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  static async HookStatus(instance: Whatsapp & any): Promise<void> {
    const { status, name, qrcode, number, tenantId, id: sessionId } = instance;
    const payload: any = {
      name,
      number,
      status,
      qrcode,
      timestamp: Date.now(),
      type: "hookSessionStatus"
    };

    const apiConfig: any = await ApiConfig.findAll({
      where: { tenantId, sessionId }
    });

    if (!apiConfig) return;

    await Promise.all(
      apiConfig.map((api: ApiConfig) => {
        if (api.urlServiceStatus) {
          if (api.authToken) {
            payload.authToken = api.authToken;
          }
          return Queue.add("WebHooksAPI", {
            url: api.urlServiceStatus,
            type: payload.type,
            payload
          });
        }
      })
    );
  }

  @BeforeUpdate
  @BeforeCreate
  static async CreateTokenWebHook(instance: Whatsapp): Promise<void> {
    const { secret } = authConfig;

    if (
      !instance?.tokenHook &&
      (instance.type === "waba" || instance.type === "messenger")
    ) {
      const tokenHook = sign(
        {
          tenantId: instance.tenantId,
          whatsappId: instance.id
          // wabaBSP: instance.wabaBSP
        },
        secret,
        {
          expiresIn: "10000d"
        }
      );

      instance.tokenHook = tokenHook;
    }
  }
}

export default Whatsapp;

				
			

HELPERS

backend\src\helpers\ConvertMp3ToMp4.ts

				
					import ffmpeg from "fluent-ffmpeg"; 
import { path as ffmpegPath } from "@ffmpeg-installer/ffmpeg"; 
import fs from "fs"; 
import { logger } from "../utils/logger";

// CONVERTER MP3 PARA MP4
const convertMp3ToMp4 = (input: string, outputMP4: string): Promise<void> => {
  return new Promise((resolve, reject) => {
    ffmpeg.setFfmpegPath(ffmpegPath);
    logger.info(`Converting ${input} to ${outputMP4}`);
    
    if (!fs.existsSync(input)) {
      const errorMsg = `Input file does not exist: ${input}`;
      logger.error(errorMsg);
      return reject(new Error(errorMsg));
    }

    ffmpeg(input)
      .inputFormat("mp3")
      .output(outputMP4)
      .outputFormat("mp4")
      .on("start", (commandLine) => {
        logger.info(`Spawned Ffmpeg with command: ${commandLine}`);
      })
      .on("error", (error: Error) => {
        logger.info(`Encoding Error: ${error.message}`);
        reject(error);
      })
      .on("progress", (progress) => {
        logger.info(`Processing: ${progress.percent}% done`);
      })
      .on("end", () => {
        logger.info("Video Transcoding succeeded !");
        resolve();
      })
      .run();
  });
};

export { convertMp3ToMp4 };

				
			

backend\src\helpers\DownloadFiles.ts

				
					import axios from "axios";
import { extname, join } from "path";
import { writeFile } from "fs/promises";
import mime from "mime-types";
import { logger } from "../utils/logger";

export const downloadFiles = async (url: string) => {
  try {
    const { data } = await axios.get(url, {
      responseType: "arraybuffer"
    });

    let type

    if (url.includes('com/ig_messaging_cdn')) {
      const { fileTypeFromBuffer } = await (eval(
        'import("file-type")'
      ) as Promise<typeof import("file-type")>);
      
      const fileTypeResult = await fileTypeFromBuffer(data);

      if (!fileTypeResult) {
          throw new Error('Não foi possível determinar o tipo do arquivo.');
      }

      type = fileTypeResult.ext
    } else {
      type = url.split("?")[0].split(".").pop();
    }
     
    logger.info("type " + type);

    const filename = `${new Date().getTime()}.${type}`;

    const filePath = `${__dirname}/../../public/${filename}`;

    await writeFile(
      join(__dirname, "..", "..", "public", filename),
      data,
      "base64"
    );

    const mimeType = mime.lookup(filePath);
    const extension = extname(filePath);
    const originalname = url.split("/").pop();

    const media = {
      mimeType,
      extension,
      filename,
      data,
      originalname
    };

    return media;
  } catch (error) {
    logger.warn("e1 " +  error)
    throw error;
  }
};

				
			

backend\src\helpers\SetChannelWebhook.ts

				
					import Whatsapp from "../models/Whatsapp";
import { IChannel } from "../controllers/ChannelHubController";
import { showHubToken } from "./ShowHubToken";

import { Client, MessageSubscription } from "notificamehubsdk";
import { logger } from "../utils/logger";

export const setChannelWebhook = async (
  whatsapp: IChannel,
  whatsappId: string
) => {
  const notificameHubToken = await showHubToken(whatsapp.tenantId.toString());

  const client = new Client(notificameHubToken);

  const url = `${process.env.BACKEND_URL}/hub-webhook/${whatsapp.number}`;
  // const url = `https://7401-2804-3d34-5005-ec01-00-1.ngrok-free.app/hub-webhook/${whatsapp.number}`;

  const subscription = new MessageSubscription(
    {
      url
    },
    {
      channel: whatsapp.number
    }
  );

  client
    .createSubscription(subscription)
    .then((response: any) => {
      logger.info("Webhook subscribed " + response);
    })
    .catch((error: any) => {
      logger.warn("Webhook subscribed e " + error);
    });

  await Whatsapp.update(
    {
      status: "CONNECTED"
    },
    {
      where: {
        id: whatsappId
      }
    }
  );
};

				
			

backend\src\helpers\ShowHubToken.ts

				
					import Setting from "../models/Setting";

export const showHubToken = async (tenantId: string): Promise<string> => {

  const hubToken = await Setting.findOne({
    where: { key: "hubToken", tenantId }
  });

  console.log()

  if (!hubToken?.value || typeof hubToken?.value !== 'string') {
    throw new Error("Notificame Hub token not found");
  }

  return hubToken.value;
};

				
			

SERVICES

backend\src\services\WbotNotificame\CreateChannelsService.ts

				
					import Tenant from "../../models/Tenant";
import AppError from "../../errors/AppError";
import Whatsapp from "../../models/Whatsapp";
import { IChannel } from "../../controllers/ChannelHubController";
import { getIO } from "../../libs/socket";

interface Request {
  tenantId: number;
  channels: IChannel[];
}

interface Response {
  whatsapps: Whatsapp[];
}

const CreateChannelsService = async ({
  tenantId,
  channels = []
}: Request): Promise<Response> => {
  const tenant = await Tenant.findOne({
    where: {
      id: tenantId
    }
  });

  if (tenant !== null) {
    let whatsappCount = await Whatsapp.count({
      where: {
        tenantId
      }
    });

    whatsappCount += channels.length;
  }

  channels = channels.map(channel => {
    return {
      ...channel,
      status: "CONNECTED",
      tenantId
    };
  });

  const whatsapps = await Whatsapp.bulkCreate(channels, {
    ignoreDuplicates: true
  });

  return { whatsapps };
};

export default CreateChannelsService;

				
			

backend\src\services\WbotNotificame\CreateMessageService.ts

				
					import Message from "../../models/Message";
import { logger } from "../../utils/logger";
import socketEmit from "../../helpers/socketEmit";
import { v4 as uuidv4 } from 'uuid';
import Ticket from "../../models/Ticket";

interface MessageData {
  id: string;
  contactId: number;
  body: string;
  ticketId: number;
  fromMe: boolean;
  tenantId: number;
  fileName?: string;
  mediaType?: string;
  originalName?: string;
  scheduleDate?: string | Date;
  ack?: any;
}

const CreateMessageService = async (
  messageData: MessageData
): Promise<Message | undefined> => {
  logger.info("creating message ");
  // logger.info("messageData " + JSON.stringify(messageData));

  const message = await Message.findOne({
    where: {
      messageId: messageData.id,
      tenantId: messageData.tenantId
    }
  });

  if(message){
    logger.info("creating message exists");
    return
  }

  const {
    id,
    contactId,
    body,
    ticketId,
    fromMe,
    tenantId,
    fileName,
    mediaType,
    originalName,
    scheduleDate,
    ack
  } = messageData;

  if ((!body || body === "") && (!fileName || fileName === "")) {
    return;
  }

  const idHub = uuidv4();

  const data: any = {
    contactId,
    body,
    ticketId,
    fromMe,
    tenantId,
    messageId: id,
    ack: 2,
    status: "sended",
    idFront: idHub,
    read: false,
    scheduleDate
  };

  if (fileName) {
    data.mediaUrl = fileName;
    data.mediaType = mediaType === "photo" ? "image" : mediaType;
    data.body = data.mediaUrl;
  }

  // logger.info("creating message data" + JSON.stringify(data));

  try {
    const newMessage = await Message.create(data);

    const messageCreated = await Message.findByPk(newMessage.id, {
      include: [
        {
          model: Ticket,
          as: "ticket",
          where: { tenantId },
          include: ["contact"]
        },
        {
          model: Message,
          as: "quotedMsg",
          include: ["contact"]
        }
      ]
    });

    if (!messageCreated) {
      throw new Error("ERR_CREATING_MESSAGE_SYSTEM 2");
    }

    socketEmit({
      tenantId,
      type: "chat:create",
      payload: messageCreated
    });
    
    return newMessage;
  } catch (error) {
    logger.warn("e8" + error);
  }
};

export default CreateMessageService;

				
			

backend\src\services\WbotNotificame\CreateOrUpdateTicketService.ts

				
					import { Op } from "sequelize";
import Ticket from "../../models/Ticket";
import Whatsapp from "../../models/Whatsapp";
import { IContent } from "./HubMessageListener";
import { getIO } from "../../libs/socket";
import Tenant from "../../models/Tenant";
import Contact from "../../models/Contact";
import User from "../../models/User";
import { logger } from "../../utils/logger";
import socketEmit from "../../helpers/socketEmit";

interface TicketData {
  contactId: number;
  channel: string;
  contents: IContent[];
  whatsapp: Whatsapp;
}

const CreateOrUpdateTicketService = async (
  ticketData: TicketData
): Promise<Ticket> => {
  logger.info("creating ticket ");
  const { contactId, channel, contents, whatsapp } = ticketData;
  const io = getIO();

  const SettingTenant = await Tenant.findOne({
    where: {  id: whatsapp.tenantId }
  });

  let statusCondition = ["open", "pending"];

  let orderClause: [string, string][] = [];

  const ticketExists = await Ticket.findOne({
    where: {
      status: {
        [Op.or]: statusCondition
        // [Op.or]: ["open", "pending"]
        // // manter historico groupTickets
        // [Op.or]: ["open", "pending", "closed"]
      },
      tenantId: whatsapp.tenantId,
      whatsappId: whatsapp.id,
      contactId: contactId
    },
    order: orderClause.length > 0 ? orderClause : undefined,
    include: [
      {
        model: Contact,
        as: "contact",
        include: [
          "extraInfo",
          "tags",
          {
            association: "wallets",
            attributes: ["id", "name"]
          }
        ]
      },
      {
        model: User,
        as: "user",
        attributes: ["id", "name", "isOnline"]
      },
      {
        association: "whatsapp",
        attributes: ["id", "name", "tokenAPI", "chatFlowId", "status", "bmToken", "wabaVersion"]
      }
    ]
  });

  if (ticketExists) {
    logger.info("ticket exists ");

    let newStatus = ticketExists.status;
    let newQueueId = ticketExists.queueId;

    if (ticketExists.status === "closed") {
      newStatus = "pending";
    }

    await ticketExists.update({
      answered: false,
      lastMessage: contents[0].text,
      status: newStatus,
      queueId: newQueueId
    });

    logger.info("ticket queue updated " + newQueueId);

    await ticketExists.reload({
      include: [
        {
          association: "contact"
        },
        {
          association: "user"
        },
        {
          association: "queue"
        },
        {
          association: "whatsapp"
        }
      ]
    });

    socketEmit({
      tenantId: whatsapp.tenantId,
      type: "ticket:update",
      payload: ticketExists
    });

    return ticketExists;
  }

  const newTicket = await Ticket.create({
    status: "pending",
    channel: "hub_" + channel,
    lastMessage: contents[0].text,
    contactId,
    whatsappId: whatsapp.id,
    tenantId: whatsapp.tenantId,
  });

  await newTicket.reload({
    include: [
      {
        association: "contact"
      },
      {
        association: "user"
      },
      {
        association: "queue"
      },
      {
        association: "whatsapp"
      }
    ]
  });

  socketEmit({
    tenantId: whatsapp.tenantId,
    type: "ticket:update",
    payload: newTicket
  });

  return newTicket;
};

export default CreateOrUpdateTicketService;

				
			

backend\src\services\WbotNotificame\FindOrCreateContactService.ts

				
					import Contact from "../../models/Contact";
import Whatsapp from "../../models/Whatsapp";

interface HubContact {
  name: string;
  firstName: string;
  lastName: string;
  picture: string;
  from: string;
  whatsapp: Whatsapp;
  channel: string;
}

const FindOrCreateContactService = async (
  contact: HubContact
): Promise<Contact> => {
  const { name, picture, firstName, lastName, from, whatsapp, channel } = contact;

  let numberFb
  let numberIg
  let contactExists

  if(channel === 'facebook'){
    numberFb = from
    contactExists = await Contact.findOne({
      where: {
        messengerId: from,
        tenantId: whatsapp.tenantId,
      }
    });
  }

  if(channel === 'instagram'){
    numberIg = from
    contactExists = await Contact.findOne({
      where: {
        instagramPK: from,
        tenantId: whatsapp.tenantId,
      }
    });
  }

  if (contactExists) {
    await contactExists.update({ name: name || firstName || 'Name Unavailable' , firstName, lastName, profilePicUrl: picture })
    return contactExists;
  }

  const newContact = await Contact.create({
    name: name || firstName || '',
    firstName,
    lastName,
    profilePicUrl: picture,
    number: null,
    tenantId: whatsapp.tenantId,
    messengerId: numberFb || null,
    instagramPK: numberIg || null
  });

  return newContact;
};

export default FindOrCreateContactService;

				
			

backend\src\services\WbotNotificame\HubMessageListener.ts

				
					import Whatsapp from "../../models/Whatsapp";

import socketEmit from "../../helpers/socketEmit";
import { downloadFiles } from "../../helpers/DownloadFiles";

import { logger } from "../../utils/logger";

import CreateMessageService from "./CreateMessageService";
import FindOrCreateContactService from "./FindOrCreateContactService";
import { UpdateMessageAck } from "./UpdateMessageAck";
import FindOrCreateTicketService from "../TicketServices/FindOrCreateTicketService";

export interface HubInMessage {
  type: "MESSAGE";
  id: string;
  timestamp: string;
  subscriptionId: string;
  channel: "telegram" | "whatsapp" | "facebook" | "instagram" | "sms" | "email";
  direction: "IN";
  message: {
    id: string;
    from: string;
    to: string;
    direction: "IN";
    channel:
      | "telegram"
      | "whatsapp"
      | "facebook"
      | "instagram"
      | "sms"
      | "email";
    visitor: {
      name: string;
      firstName: string;
      lastName: string;
      picture: string;
    };
    contents: IContent[];
    timestamp: string;
  };
}

export interface IContent {
  type: "text" | "image" | "audio" | "video" | "file" | "location";
  text?: string;
  url?: string;
  fileUrl?: string;
  latitude?: number;
  longitude?: number;
  filename?: string;
  fileSize?: number;
  fileMimeType?: string;
}

export interface HubConfirmationSentMessage {
  type: "MESSAGE_STATUS";
  timestamp: string;
  subscriptionId: string;
  channel: "telegram" | "whatsapp" | "facebook" | "instagram" | "sms" | "email";
  messageId: string;
  contentIndex: number;
  messageStatus: {
    timestamp: string;
    code: "SENT" | "REJECTED";
    description: string;
  };
}

const verifySentMessageStatus = (message: HubConfirmationSentMessage) => {
  const {
    messageStatus: { code }
  } = message;

  const isMessageSent = code === "SENT";

  if (isMessageSent) {
    return true;
  }

  return false;
};

const HubMessageListener = async (
  message: any | HubInMessage | HubConfirmationSentMessage,
  whatsapp: Whatsapp,
  medias: Express.Multer.File[]
) => {
  logger.info("HubMessageListener " + JSON.stringify(message));


  if(message.direction === 'IN'){
    message.fromMe = false
  }

  // const ignoreEvent =
  //   message?.message.visitor?.name === "" || !message?.message.visitor?.name;
  const ignoreEvent = message.direction === 'OUT'
  if (ignoreEvent) {
    return;
  }

  const isMessageFromMe = message.type === "MESSAGE_STATUS";

  logger.info("HubMessageListener MESSAGE_STATUS " + isMessageFromMe)

  if (isMessageFromMe) {
    const isMessageSent = verifySentMessageStatus(
      message as HubConfirmationSentMessage
    );

    if (isMessageSent) {
      logger.info("HubMessageListener: message sent ");
      UpdateMessageAck(message.messageId);
    } else {
      logger.info("HubMessageListener: message not sent " + message.messageStatus.code + " - " + message.messageStatus.description);
    }

    return;
  }

  const {
    message: { id, from, channel, contents, visitor }
  } = message as HubInMessage;

  try {
    const contact = await FindOrCreateContactService({
      ...visitor,
      from,
      whatsapp,
      channel
    });

    const unreadMessages = 1

    message.body = contents[0].text
    
    const ticket = await FindOrCreateTicketService({
      contact,
      whatsappId: whatsapp.id!,
      unreadMessages,
      tenantId: whatsapp.tenantId,
      groupContact: undefined,
      msg: message,
      channel: 'hub_' + channel
    });

    if (contents[0]?.type === "text") {
      await CreateMessageService({
        id,
        contactId: contact.id,
        body: contents[0].text || '',
        ticketId: ticket.id,
        fromMe: false,
        tenantId: whatsapp.tenantId
      });
      await ticket.update({lastMessage: contents[0].text})
      socketEmit({
        tenantId: whatsapp.tenantId,
        type: "ticket:update",
        payload: ticket
      });
    } else if (contents[0]?.fileUrl) {
      const media = await downloadFiles(contents[0].fileUrl);

      if (typeof media.mimeType === "string") {
        await CreateMessageService({
          id,
          contactId: contact.id,
          body: contents[0].text || '',
          ticketId: ticket.id,
          fromMe: false,
          tenantId: whatsapp.tenantId,
          fileName: `${media.filename}`,
          mediaType: media.mimeType.split("/")[0],
          originalName: media.originalname
        });
        await ticket.update({lastMessage: media.filename || ''})
        socketEmit({
          tenantId: whatsapp.tenantId,
          type: "ticket:update",
          payload: ticket
        });
      } else {
        // Lidar com o caso em que mimeType é false, se necessário
        logger.warn("Unable to determine the media type")
      }
    }

    if (ticket?.isFarewellMessage) {
      return;
    }

  } catch (error: any) {
    logger.warn("e4 " + error)
  }

};

export default HubMessageListener;

				
			

backend\src\services\WbotNotificame\ListChannels.ts

				
					import { showHubToken } from "../../helpers/ShowHubToken";
import { logger } from "../../utils/logger";
import { Client } from "notificamehubsdk";
require("dotenv").config();

const ListChannels = async (tenantId: string): Promise<any> => {
  try {
    const notificameHubToken = await showHubToken(tenantId);

    if (!notificameHubToken) {
      throw new Error("NOTIFICAMEHUB_TOKEN_NOT_FOUND");
    }

    const client = new Client(notificameHubToken);

    const response = await client.listChannels();
    logger.info("" + JSON.stringify(response));
    return response;
  } catch (error: any) {
    logger.warn(" Error in ListChannels: ", error);
    if (error instanceof Error) {
      throw new Error(error.message);
    } else {
      throw new Error("An unknown error occurred: " + JSON.stringify(error));
    }
  }
};

export default ListChannels;

				
			

backend\src\services\WbotNotificame\SendMediaMessageService.ts

				
					require("dotenv").config();
import { Client, FileContent } from "notificamehubsdk";
import Contact from "../../models/Contact";
import CreateMessageService from "./CreateMessageService";
import { showHubToken } from "../../helpers/ShowHubToken";
import { logger } from "../../utils/logger";
import Whatsapp from "../../models/Whatsapp";
import { convertMp3ToMp4 } from "../../helpers/ConvertMp3ToMp4";
import Ticket from "../../models/Ticket";
import socketEmit from "../../helpers/socketEmit";
import { v4 as uuidV4 } from "uuid";

export const SendMediaMessageService = async (
  media: Express.Multer.File,
  message: string,
  ticketId: number,
  contact: Contact,
  whatsapp: any
) => {

  let channel
  let mediaUrl

  const ticket = await Ticket.findOne({
    where: { id: ticketId }
  });

  if(!whatsapp.tenantId || !whatsapp.type || !whatsapp.number){
    channel = await Whatsapp.findOne({
      where: { id: whatsapp.id }
    });
    whatsapp = channel
    // whatsapp.type = channel?.type
  }
  
  const notificameHubToken = await showHubToken(
    whatsapp.tenantId.toString()
  );

  logger.info("Chamou hub send media");

  const client = new Client(notificameHubToken);

  logger.info("ticket?.channel " + ticket?.channel);

  const channelClient = client.setChannel(ticket?.channel.split('hub_')[1]);

  try{
    message = message.replace(/\n/g, " ");
  } catch(e){
    logger.warn("Replacing newlines: " + e.message);
  }

  logger.info("media " + JSON.stringify(media));

  // const backendUrl = process.env.BACKEND_URL;
  const backendUrl = 'https://7401-2804-3d34-5005-ec01-00-1.ngrok-free.app';
  
  const filename = encodeURIComponent(media.filename);
  mediaUrl = `${backendUrl}/public/${filename}`;

  if (media.mimetype.includes("image")) {
    if (ticket?.channel.split('hub_')[1] === "telegram") {
      media.mimetype = "photo";
    } else {
      media.mimetype = "image";
    }
  } else if (
    (ticket?.channel.split('hub_')[1] === "telegram" || ticket?.channel.split('hub_')[1] === "facebook") &&
    media.mimetype.includes("audio")
  ) {
    media.mimetype = "audio";
  } else if (
    (ticket?.channel.split('hub_')[1] === "telegram" || ticket?.channel.split('hub_')[1] === "facebook") &&
    media.mimetype.includes("video")
  ) {
    media.mimetype = "video";
  } else if (ticket?.channel.split('hub_')[1] === "telegram" || ticket?.channel.split('hub_')[1] === "facebook") {
    media.mimetype = "file";
  }

  try {

    if (media.originalname.includes('.mp3') && ticket?.channel.split('hub_')[1] === "instagram") {
      const inputPath = media.path;
      const outputMP4Path = `${media.destination}/${media.filename.split('.')[0]}.mp4`;
      try {
        await convertMp3ToMp4(inputPath, outputMP4Path);
        media.filename = outputMP4Path.split('/').pop() ?? 'default.mp4';
        mediaUrl = `${backendUrl}/public/${media.filename}`;
        media.originalname = media.filename
        media.mimetype = 'audio'
      } catch(e){
  
      }
    }

    if (media.originalname.includes('.mp4') && ticket?.channel.split('hub_')[1] === "instagram") {
      media.mimetype = 'video'
    }

    const content = new FileContent(
      mediaUrl,
      media.mimetype,
      media.filename,
      media.filename
    );

    let contactNumber

    if(ticket?.channel === 'hub_facebook'){
      contactNumber = contact.messengerId
    }
    if(ticket?.channel === 'hub_instagram'){
      contactNumber = contact.instagramPK
    }

    logger.info("whatsapp.number " + whatsapp.number + " contactNumber "  + contactNumber + " content "  + content + " message "  + message);

    let response = await channelClient.sendMessage(
      whatsapp.number,
      contactNumber,
      content
    );

    logger.info("Hub response: " + JSON.stringify(response));

    let data: any;

    try {
      const jsonStart = response.indexOf("{");
      const jsonResponse = response.substring(jsonStart);
      data = JSON.parse(jsonResponse);
    } catch (error) {
      data = response;
    }

    const newMessage = await CreateMessageService({
      id: data?.id || uuidV4(),
      contactId: contact.id,
      body: `${media.filename}`,
      ticketId,
      fromMe: true,
      tenantId: whatsapp.tenantId,
      fileName: `${media.filename}`,
      mediaType: media.mimetype.split("/")[0] || media.mimetype,
      originalName: media.originalname
    });

    if(ticket){
      await ticket.update({lastMessage: media.filename || '', aswered: true})
      socketEmit({
        tenantId: whatsapp.tenantId,
        type: "ticket:update",
        payload: ticket
      });
    }

    return newMessage;
  } catch (error) {
    logger.warn("e6 " + JSON.stringify(error));
  }
};

				
			

backend\src\services\WbotNotificame\SendTextMessageService.ts

				
					require("dotenv").config();
import { Client, TextContent } from "notificamehubsdk";
import Contact from "../../models/Contact";
import CreateMessageService from "./CreateMessageService";
import { showHubToken } from "../../helpers/ShowHubToken";
import { logger } from "../../utils/logger";
import Whatsapp from "../../models/Whatsapp";
import Ticket from "../../models/Ticket";
import socketEmit from "../../helpers/socketEmit";
import { pupa } from "../../utils/pupa";
import User from "../../models/User";

export const SendTextMessageService = async (
  message: string,
  ticketId: number,
  contact: Contact,
  whatsapp: any
) => {

  let channel

  const ticket = await Ticket.findOne({
    where: { id: ticketId },
    include: [
      {
        model: Contact
      },
      {
        model: User
      }
    ]
  });

  let body = pupa(message || "", {
    protocol: ticket?.protocol || '',
    name: ticket?.contact?.name || '',
  });

  if(!whatsapp.tenantId || !whatsapp.type || !whatsapp.number){
    channel = await Whatsapp.findOne({
      where: { number: whatsapp.number }
    });
    whatsapp = channel
  }

  const notificameHubToken = await showHubToken(
    whatsapp.tenantId.toString()
  );

  const client = new Client(notificameHubToken);

  logger.info("ticket?.channel " + ticket?.channel);

  const channelClient = client.setChannel(ticket?.channel.split('hub_')[1]);

  const content = new TextContent(body);

  let contactNumber

  if(ticket?.channel === 'hub_facebook'){
    contactNumber = contact.messengerId
  }
  if(ticket?.channel === 'hub_instagram'){
    contactNumber = contact.instagramPK
  }
  
  try {
    logger.info("whatsapp.number " + whatsapp.number + " contactNumber "  + contactNumber + " content "  + content + " message "  + body);

    let response = await channelClient.sendMessage(
      whatsapp.number,
      contactNumber,
      content
    );

    logger.info("" + JSON.stringify(response));

    let data: any;

    try {
      const jsonStart = response.indexOf("{");
      const jsonResponse = response.substring(jsonStart);
      data = JSON.parse(jsonResponse);
    } catch (error) {
      data = response;
    }

    const newMessage = await CreateMessageService({
      id: data.id,
      contactId: contact.id,
      body: body,
      ticketId,
      fromMe: true,
      tenantId: whatsapp.tenantId
    });

    if(ticket){
      await ticket.update({lastMessage: body || '', aswered: true})
      socketEmit({
        tenantId: whatsapp.tenantId,
        type: "ticket:update",
        payload: ticket
      });
    }

    return newMessage;
  } catch (error) {
    logger.warn("e7 " + JSON.stringify(error));
  }
};

				
			

backend\src\services\WbotNotificame\UpdateMessageAck.ts

				
					import Message from "../../models/Message";

export const UpdateMessageAck = async (messageId: string): Promise<void> => {
  const message = await Message.findOne({
    where: {
      id: messageId
    }
  });

  if (!message) {
    return;
  }

  await message.update({
    ack: 4
  });
};

				
			

backend\src\services\WbotServices\StartAllWhatsAppsSessions.ts

				
					import { Op } from "sequelize";
// import { initInstaBot } from "../../libs/InstaBot";
import Whatsapp from "../../models/Whatsapp";
import { StartInstaBotSession } from "../InstagramBotServices/StartInstaBotSession";
import { StartMessengerBot } from "../MessengerChannelServices/StartMessengerBot";
import { StartTbotSession } from "../TbotServices/StartTbotSession";
import { StartWaba360 } from "../WABA360/StartWaba360";
import { StartWhatsAppSession } from "./StartWhatsAppSession";
import { setChannelWebhook } from "../../helpers/SetChannelWebhook";
// import { StartTbotSession } from "../TbotServices/StartTbotSession";

export const StartAllWhatsAppsSessions = async (): Promise<void> => {
  const whatsapps = await Whatsapp.findAll({
    where: {
      [Op.or]: [
        {
          [Op.and]: {
            type: {
              [Op.in]: ["instagram", "telegram", "waba", "messenger", "hub_facebook", "hub_instagram"]
            },
            status: {
              [Op.notIn]: ["DISCONNECTED"]
            }
          }
        },
        {
          [Op.and]: {
            type: "whatsapp"
          },
          status: {
            [Op.notIn]: ["DISCONNECTED", "qrcode"]
            // "DISCONNECTED"
          }
        }
      ],
      isActive: true
    }
  });
  const whatsappSessions = whatsapps.filter(w => w.type === "whatsapp");
  const telegramSessions = whatsapps.filter(
    w => w.type === "telegram" && !!w.tokenTelegram
  );
  const instagramSessions = whatsapps.filter(w => w.type === "instagram");
  const waba360Sessions = whatsapps.filter(w => w.type === "waba");
  const messengerSessions = whatsapps.filter(w => w.type === "messenger");
  const hubSessions = whatsapps.filter(w => w.type.includes("hub"));

  if (whatsappSessions.length > 0) {
    whatsappSessions.forEach(whatsapp => {
      StartWhatsAppSession(whatsapp);
    });
  }

  if (telegramSessions.length > 0) {
    telegramSessions.forEach(whatsapp => {
      StartTbotSession(whatsapp);
    });
  }

  if (waba360Sessions.length > 0) {
    waba360Sessions.forEach(channel => {
      if (channel.tokenAPI && channel.wabaBSP === "360") {
        StartWaba360(channel);
      }
    });
  }

  if (instagramSessions.length > 0) {
    instagramSessions.forEach(channel => {
      if (channel.instagramKey) {
        StartInstaBotSession(channel);
      }
    });
  }

  if (messengerSessions.length > 0) {
    messengerSessions.forEach(channel => {
      if (channel.tokenAPI) {
        StartMessengerBot(channel);
      }
    });
  }

  if (hubSessions.length > 0) {
    hubSessions.forEach(channel => {
      setChannelWebhook(channel, channel.id.toString());
    });
  }
};

				
			

CONTROLLERS

backend\src\controllers\ChannelHubController.ts

				
					import { Request, Response } from "express";
import CreateChannelsService from "../services/WbotNotificame/CreateChannelsService";
import { getIO } from "../libs/socket";
import ListChannels from "../services/WbotNotificame/ListChannels";
import { setChannelWebhook } from "../helpers/SetChannelWebhook";

export interface IChannel {
  name: string;
  status?: string;
  isDefault?: boolean;
  tenantId: string | number;
  type: string;
  profilePic?: string;
  phone?: any;
  number?: any;
}

export const store = async (req: Request, res: Response): Promise<Response> => {
  const { channels = [] } = req.body;
  const tenantId = Number(req.user.tenantId);

  const { whatsapps } = await CreateChannelsService({
    tenantId,
    channels
  });

  whatsapps.forEach(whatsapp => {
    setTimeout(() => {
      const whatsappChannel: IChannel = {
        name: whatsapp.name,
        status: whatsapp.status,
        isDefault: whatsapp.isDefault,
        tenantId: whatsapp.tenantId,
        type: whatsapp.type,
        phone: whatsapp.phone
      };
      setChannelWebhook(whatsappChannel, whatsapp.id.toString());
    }, 2000);
  });

  console.log('3')
  const io = getIO();

  whatsapps.forEach(whatsapp => {
    io.emit(`${tenantId}:whatsapp`, {
      action: "update",
      whatsapp
    });
  });

  return res.status(200).json(whatsapps);
};

export const index = async (req: Request, res: Response): Promise<Response> => {
  const { tenantId } = req.user;

  try {
    const channels = await ListChannels(tenantId.toString());
    return res.status(200).json(channels);
  } catch (error) {
    return res.status(500).json({ error: error.message });
  }
};

				
			

backend\src\controllers\ContactController.ts

				
					import * as Yup from "yup";
import { Request, Response } from "express";
import { head } from "lodash";
import XLSX from "xlsx";
import path from "path";
import { v4 as uuidV4 } from "uuid";
import fs from "fs";
import ListContactsService from "../services/ContactServices/ListContactsService";
import CreateContactService from "../services/ContactServices/CreateContactService";
import ShowContactService from "../services/ContactServices/ShowContactService";
import UpdateContactService from "../services/ContactServices/UpdateContactService";
import DeleteContactService from "../services/ContactServices/DeleteContactService";
import UpdateContactTagsService from "../services/ContactServices/UpdateContactTagsService";

import CheckIsValidContact from "../services/WbotServices/CheckIsValidContact";
import GetProfilePicUrl from "../services/WbotServices/GetProfilePicUrl";
import AppError from "../errors/AppError";
import UpdateContactWalletsService from "../services/ContactServices/UpdateContactWalletsService";
import SyncContactsWhatsappInstanceService from "../services/WbotServices/SyncContactsWhatsappInstanceService";
import Whatsapp from "../models/Whatsapp";
import { ImportFileContactsService } from "../services/WbotServices/ImportFileContactsService";
import Contact from "../models/Contact";

type IndexQuery = {
  searchParam: string;
  pageNumber: string;
};

interface ExtraInfo {
  name: string;
  value: string;
}
interface ContactData {
  name: string;
  number: string;
  email?: string;
  extraInfo?: ExtraInfo[];
  wallets?: null | number[] | string[];
}

export const index = async (req: Request, res: Response): Promise<Response> => {
  const { tenantId, id: userId, profile } = req.user;
  const { searchParam, pageNumber } = req.query as IndexQuery;

  const { contacts, count, hasMore } = await ListContactsService({
    searchParam,
    pageNumber,
    tenantId,
    profile,
    userId
  });

  return res.json({ contacts, count, hasMore });
};

export const store = async (req: Request, res: Response): Promise<Response> => {
  const { tenantId } = req.user;
  const newContact: ContactData = req.body;
  newContact.number = newContact.number.replace("-", "").replace(" ", "");

  const schema = Yup.object().shape({
    name: Yup.string().required(),
    // number: Yup.string()
    //   .required()
    //   .matches(/^\d+$/, "Invalid number format. Only numbers is allowed.")
  });

  try {
    await schema.validate(newContact);
  } catch (err) {
    throw new AppError(err.message);
  }

  const waNumber = await CheckIsValidContact(newContact.number, tenantId);

  const profilePicUrl = await GetProfilePicUrl(newContact.number, tenantId);

  const contact = await CreateContactService({
    ...newContact,
    number: waNumber.user,
    profilePicUrl,
    tenantId
  });

  return res.status(200).json(contact);
};

export const show = async (req: Request, res: Response): Promise<Response> => {
  const { contactId } = req.params;
  const { tenantId } = req.user;

  const contact = await ShowContactService({ id: contactId, tenantId });

  return res.status(200).json(contact);
};

export const update = async (
  req: Request,
  res: Response
): Promise<Response> => {
  const contactData: ContactData = req.body;
  const { tenantId } = req.user;

  const schema = Yup.object().shape({
    name: Yup.string(),
    // number: Yup.string().matches(
    //   /^\d+$/,
    //   "Invalid number format. Only numbers is allowed."
    // )
  });

  try {
    await schema.validate(contactData);
  } catch (err) {
    throw new AppError(err.message);
  }

  let waNumber
  let contact

  const { contactId } = req.params;

  console.log(contactData.number)
  if(contactData.number !== 'null' && contactData.number !== ''){
    waNumber = await CheckIsValidContact(contactData.number, tenantId);

    contactData.number = waNumber.user;
    contact = await UpdateContactService({
      contactData,
      contactId,
      tenantId
    });

  } else {
    contact = await UpdateContactService({
      contactData,
      contactId,
      tenantId
    });
  }

  return res.status(200).json(contact);
};

export const remove = async (
  req: Request,
  res: Response
): Promise<Response> => {
  const { contactId } = req.params;
  const { tenantId } = req.user;

  await DeleteContactService({ id: contactId, tenantId });

  return res.status(200).json({ message: "Contact deleted" });
};

export const updateContactTags = async (
  req: Request,
  res: Response
): Promise<Response> => {
  const { tags } = req.body;
  const { contactId } = req.params;
  const { tenantId } = req.user;

  const contact = await UpdateContactTagsService({
    tags,
    contactId,
    tenantId
  });

  return res.status(200).json(contact);
};

export const updateContactWallet = async (
  req: Request,
  res: Response
): Promise<Response> => {
  const { wallets } = req.body;
  const { contactId } = req.params;
  const { tenantId } = req.user;

  const contact = await UpdateContactWalletsService({
    wallets,
    contactId,
    tenantId
  });

  return res.status(200).json(contact);
};

export const syncContacts = async (
  req: Request,
  res: Response
): Promise<Response> => {
  const { tenantId } = req.user;
  const sessoes = await Whatsapp.findAll({
    where: {
      tenantId,
      status: "CONNECTED",
      type: "whatsapp"
    }
  });

  if (!sessoes.length) {
    throw new AppError(
      "Não existem sessões ativas para sincronização dos contatos."
    );
  }

  await Promise.all(
    sessoes.map(async s => {
      if (s.id) {
        if (s.id) {
          await SyncContactsWhatsappInstanceService(s.id, +tenantId);
        }
      }
    })
  );

  return res
    .status(200)
    .json({ message: "Contatos estão sendo sincronizados." });
};

export const upload = async (req: Request, res: Response) => {
  const files = req.files as Express.Multer.File[];
  const file: Express.Multer.File = head(files) as Express.Multer.File;
  const { tenantId } = req.user;
  let { tags, wallets } = req.body;

  if (tags) {
    tags = tags.split(",");
  }

  if (wallets) {
    wallets = wallets.split();
  }

  const response = await ImportFileContactsService(
    +tenantId,
    file,
    tags,
    wallets
  );

  // const io = getIO();

  // io.emit(`company-${companyId}-contact`, {
  //   action: "reload",
  //   records: response
  // });

  return res.status(200).json(response);
};

export const exportContacts = async (req: Request, res: Response) => {
  const { tenantId } = req.user;

  const contacts = await Contact.findAll({
    where: { tenantId },
    attributes: ["id", "name", "number", "email"],
    order: [["name", "ASC"]],
    raw: true
  });

  // Cria um novo workbook e worksheet
  const workbook = XLSX.utils.book_new();
  const worksheet = XLSX.utils.json_to_sheet(contacts);

  // Adiciona o worksheet ao workbook
  XLSX.utils.book_append_sheet(workbook, worksheet, "Contatos");

  // Gera o arquivo Excel no formato .xlsx
  const excelBuffer = XLSX.write(workbook, {
    bookType: "xlsx",
    type: "buffer"
  });

  // Define o nome do arquivo
  const fileName = `${uuidV4()}_contatos.xlsx`;
  const filePath = path.join(__dirname, "..", "..", "public", "downloads");
  const file = path.join(filePath, fileName);

  // Cria os diretórios de downloads se eles não existirem
  if (!fs.existsSync(filePath)) {
    fs.mkdirSync(filePath, { recursive: true });
  }

  // Salva o arquivo no diretório de downloads
  fs.writeFile(file, excelBuffer, err => {
    if (err) {
      console.error("Erro ao salvar arquivo:", err);
      return res.status(500).send("Erro ao exportar contatos");
    }
    const { BACKEND_URL } = process.env;
    const downloadLink = `${BACKEND_URL}:${process.env.PROXY_PORT}/public/downloads/${fileName}`;

    res.send({ downloadLink });
  });
};

				
			

backend\src\controllers\MessageHubController.ts

				
					import { Request, Response } from "express";
import Contact from "../models/Contact";
import Ticket from "../models/Ticket";
import { SendTextMessageService } from "../services/WbotNotificame/SendTextMessageService";
import Whatsapp from "../models/Whatsapp";
import { SendMediaMessageService } from "../services/WbotNotificame/SendMediaMessageService";
import { logger } from "../utils/logger";

export const send = async (req: Request, res: Response): Promise<Response> => {
  const { body: message } = req.body;
  const { ticketId } = req.params;
  const medias = req.files as Express.Multer.File[];

  logger.info("Sending hub message controller");

  const ticket = await Ticket.findByPk(ticketId, {
    include: [
      {
        model: Contact,
        as: "contact",
        include: [
          "extraInfo",
          "tags",
          {
            association: "wallets",
            attributes: ["id", "name"]
          }
        ]
      },
      {
        model: Whatsapp,
        as: "whatsapp",
        attributes: ["number", "type", "tenantId"]
      }
    ]
  });

  if (!ticket) {
    return res.status(404).json({ message: "Ticket not found" });
  }

  try {
    if (medias) {
      await Promise.all(
        medias.map(async (media: Express.Multer.File) => {
          await SendMediaMessageService(
            media,
            message,
            ticket.id,
            ticket.contact,
            ticket.whatsapp
          );
        })
      );
    } else {
      await SendTextMessageService(
        message,
        ticket.id,
        ticket.contact,
        ticket.whatsapp
      );
    }

    return res.status(200).json({ message: "Message sent" });
  } catch (error) {
    logger.warn("e " +  error)

    return res.status(400).json({ message: error });
  }

};

				
			

backend\src\controllers\WebhookHubController.ts

				
					import { Request, Response } from "express";
import Whatsapp from "../models/Whatsapp";
import HubMessageListener from "../services/WbotNotificame/HubMessageListener";
import { logger } from "../utils/logger";

export const listen = async (
  req: Request,
  res: Response
): Promise<Response> => {
  logger.info("Webhook received");
  const medias = req.files as Express.Multer.File[];
  const { number } = req.params;

  const whatsapp = await Whatsapp.findOne({
    where: { number: number }
  });

  if (!whatsapp) {
    return res.status(404).json({ message: "Whatsapp channel not found" });
  }

  try {
    await HubMessageListener(req.body, whatsapp, medias);

    return res.status(200).json({ message: "Webhook received" });
  } catch (error) {
    return res.status(400).json({ message: error });
  }
};

				
			

ROUTES

backend\src\routes\hubChannelRoutes.ts

				
					import express from "express";

import * as ChannelController from "../controllers/ChannelHubController";
import isAuth from "../middleware/isAuth";

const hubChannelRoutes = express.Router();

hubChannelRoutes.post("/hub-channel/", isAuth, ChannelController.store);
hubChannelRoutes.get("/hub-channel/", isAuth, ChannelController.index);

export default hubChannelRoutes;

				
			

backend\src\routes\hubMessageRoutes.ts

				
					import express from "express";
import uploadConfig from "../config/upload";

import * as MessageController from "../controllers/MessageHubController";
import isAuth from "../middleware/isAuth";
import multer from "multer";

const hubMessageRoutes = express.Router();
const upload = multer(uploadConfig);

hubMessageRoutes.post(
  "/hub-message/:ticketId",
  isAuth,
  upload.array("medias"),
  MessageController.send
);

export default hubMessageRoutes;

				
			

backend\src\routes\hubWebhookRoutes.ts

				
					import express from "express";
import uploadConfig from "../config/upload";

import * as WebhookController from "../controllers/WebhookHubController";
import multer from "multer";

const hubWebhookRoutes = express.Router();
const upload = multer(uploadConfig);

hubWebhookRoutes.post(
  "/hub-webhook/:number",
  upload.array("medias"),
  WebhookController.listen
);

export default hubWebhookRoutes;

				
			

backend\src\routes\index.ts

				
					import { Router } from "express";

import userRoutes from "./userRoutes";
import authRoutes from "./authRoutes";
import settingRoutes from "./settingRoutes";
import contactRoutes from "./contactRoutes";
import ticketRoutes from "./ticketRoutes";
import whatsappRoutes from "./whatsappRoutes";
import messageRoutes from "./messageRoutes";
import whatsappSessionRoutes from "./whatsappSessionRoutes";
import autoReplyRoutes from "./autoReplyRoutes";
import fastReplyRoutes from "./fastReplyRoutes";
import queueRoutes from "./queueRoutes";
import statisticsRoutes from "./statisticsRoutes";
import tagRoutes from "./tagRoutes";
import campaignRoutes from "./campaignRoutes";
import campaignContactsRoutes from "./campaignContactsRoutes";
import apiConfigRoutes from "./apiConfigRoutes";
import apiExternalRoutes from "./apiExternalRoutes";
import chatFlowRoutes from "./chatFlowRoutes";
import tenantRoutes from "./tenantRoutes";
import WebHooksRoutes from "./WebHooksRoutes";
import adminRoutes from "./adminRoutes";
import facebookRoutes from "./facebookRoutes";
import hubChannelRoutes from "./hubChannelRoutes";
import hubMessageRoutes from "./hubMessageRoutes";
import hubWebhookRoutes from "./hubWebhookRoutes";

const routes = Router();

routes.use(userRoutes);
routes.use("/auth", authRoutes);
routes.use(settingRoutes);
routes.use(contactRoutes);
routes.use(ticketRoutes);
routes.use(whatsappRoutes);
routes.use(messageRoutes);
routes.use(messageRoutes);
routes.use(whatsappSessionRoutes);
routes.use(autoReplyRoutes);
routes.use(queueRoutes);
routes.use(fastReplyRoutes);
routes.use(statisticsRoutes);
routes.use(tagRoutes);
routes.use(campaignRoutes);
routes.use(campaignContactsRoutes);
routes.use(apiConfigRoutes);
routes.use(apiExternalRoutes);
routes.use(chatFlowRoutes);
routes.use(tenantRoutes);
routes.use(WebHooksRoutes);
routes.use(adminRoutes);
routes.use(facebookRoutes);
routes.use(hubChannelRoutes)
routes.use(hubMessageRoutes)
routes.use(hubWebhookRoutes)

export default routes;

				
			

FRONTEND

IMAGENS

frontend\public\hub-logo.png
frontend\public\hub_facebook-logo.png
frontend\public\hub_instagram-logo.png

PAGES

frontend\src\pages\atendimento\Index.vue

				
					<template>
  <div
    class="WAL position-relative bg-grey-3"
    :style="style"
  >
    <q-layout
      class="WAL__layout shadow-3"
      container
      view="lHr LpR lFr"
    >
      <q-drawer
        v-model="drawerTickets"
        @hide="drawerTickets = false"
        show-if-above
        :overlay="$q.screen.lt.md"
        persistent
        :breakpoint="769"
        bordered
        :width="$q.screen.lt.md ? $q.screen.width : 380"
        content-class="hide-scrollbar full-width"
      >
        <q-toolbar
          class="q-gutter-xs full-width"
          style="height: 64px"
        >
          <q-btn-dropdown
            no-caps
            color="black"
            class="text-bold btn-rounded"
            ripple
          >
            <template v-slot:label>
              <div
                :style="{ maxWidth: $q.screen.lt.sm ? '120px' : '' }"
                class="ellipsis"
              >
                {{ username }}
              </div>
            </template>
            <q-list style="min-width: 100px">
              <!-- <q-item
                clickable
                v-close-popup
              >
                <q-item-section>
                  <q-toggle
                    color="blue"
                    :value="$q.dark.isActive"
                    label="Modo escuro"
                    @input="$setConfigsUsuario({isDark: !$q.dark.isActive})"
                  />
                </q-item-section>
              </q-item> -->
              <q-item
                clickable
                v-close-popup
                @click="abrirModalUsuario"
              >
                <q-item-section>Perfil</q-item-section>
              </q-item>
              <q-item
                clickable
                v-close-popup
                @click="efetuarLogout"
              >
                <q-item-section>Sair</q-item-section>
              </q-item>
              <q-separator />

            </q-list>
          </q-btn-dropdown>
          <q-space />
          <q-btn
            color="black"
            class="btn-rounded"
            icon="mdi-home"
            @click="() => $router.push({ name: 'home-dashboard' })"
          >
            <q-tooltip content-class="bg-padrao text-grey-9 text-bold">
              Retornar ao menu
            </q-tooltip>
          </q-btn>
        </q-toolbar>
        <StatusWhatsapp
          v-if="false"
          class="q-mx-sm full-width"
        />
        <q-toolbar
          v-show="toolbarSearch"
          class="row q-gutter-sm q-py-sm items-center"
        >
          <q-separator class="absolute-top" />
          <q-btn
            :icon="!cFiltroSelecionado ? 'mdi-filter-outline' : 'mdi-filter-plus'"
            class="btn-rounded "
            :color="cFiltroSelecionado ? 'deep-orange-9' : 'primary'"
          >
            <q-menu
              content-class="shadow-10 no-scroll"
              square
            >
              <div
                class="row q-pa-sm"
                style="min-width: 350px; max-width: 350px"
              >
                <div class="q-ma-sm">
                  <div class="text-h6 q-mb-md">Filtros Avançados</div>
                  <q-toggle
                    v-if="profile === 'admin'"
                    class="q-ml-lg"
                    v-model="pesquisaTickets.showAll"
                    label="(Admin) - Visualizar Todos"
                    :class="{ 'q-mb-lg': pesquisaTickets.showAll }"
                    @input="debounce(BuscarTicketFiltro(), 700)"
                  />
                  <q-separator
                    class="q-mb-md"
                    v-if="!pesquisaTickets.showAll"
                  />
                  <div v-if="!pesquisaTickets.showAll">
                    <q-select
                      :disable="pesquisaTickets.showAll"
                      rounded
                      dense
                      outlined
                      hide-bottom-space
                      emit-value
                      map-options
                      multiple
                      options-dense
                      use-chips
                      label="Filas"
                      color="primary"
                      v-model="pesquisaTickets.queuesIds"
                      :options="cUserQueues"
                      :input-debounce="700"
                      option-value="id"
                      option-label="queue"
                      @input="debounce(BuscarTicketFiltro(), 700)"
                      input-style="width: 300px; max-width: 300px;"
                    />

                    <q-list
                      dense
                      class="q-my-md"
                    >
                      <q-item
                        tag="label"
                        v-ripple
                      >
                        <q-item-section avatar>
                          <q-checkbox
                            v-model="pesquisaTickets.status"
                            val="open"
                            color="primary"
                            keep-color
                            @input="debounce(BuscarTicketFiltro(), 700)"
                          />
                        </q-item-section>
                        <q-item-section>
                          <q-item-label>Abertos</q-item-label>
                        </q-item-section>
                      </q-item>
                      <q-item
                        tag="label"
                        v-ripple
                      >
                        <q-item-section avatar>
                          <q-checkbox
                            v-model="pesquisaTickets.status"
                            val="pending"
                            color="negative"
                            keep-color
                            @input="debounce(BuscarTicketFiltro(), 700)"
                          />
                        </q-item-section>
                        <q-item-section>
                          <q-item-label>Pendentes</q-item-label>
                        </q-item-section>
                      </q-item>
                      <q-item
                        tag="label"
                        v-ripple
                      >
                        <q-item-section avatar>
                          <q-checkbox
                            v-model="pesquisaTickets.status"
                            val="closed"
                            color="positive"
                            keep-color
                            @input="debounce(BuscarTicketFiltro(), 700)"
                          />
                        </q-item-section>
                        <q-item-section>
                          <q-item-label>Resolvidos</q-item-label>
                        </q-item-section>
                      </q-item>
                    </q-list>
                    <q-separator class="q-mb-md" />
                    <q-toggle
                      v-model="pesquisaTickets.withUnreadMessages"
                      label="Somente Tickets com mensagens não lidas"
                      @input="debounce(BuscarTicketFiltro(), 700)"
                    />
                    <q-toggle
                      v-model="pesquisaTickets.isNotAssignedUser"
                      label="Somente Tickets não atribuidos (sem usuário definido)"
                      @input="debounce(BuscarTicketFiltro(), 700)"
                    />
                  </div>
                  <q-separator
                    class="q-my-md"
                    spaced
                    v-if="!pesquisaTickets.showAll"
                  />
                  <q-btn
                    class="float-right q-my-md"
                    color="negative"
                    label="Fechar"
                    push
                    rounded
                    v-close-popup
                  />
                </div>
              </div>
            </q-menu>
            <q-tooltip content-class="bg-padrao text-grey-9 text-bold">
              Filtro Avançado
            </q-tooltip>
          </q-btn>
          <q-input
            v-model="pesquisaTickets.searchParam"
            dense
            outlined
            rounded
            type="search"
            class="col-grow"
            :debounce="700"
            @input="BuscarTicketFiltro()"
          >
            <template v-slot:append>
              <q-icon name="search" />
            </template>
          </q-input>
          <q-btn
            color="primary"
            class="btn-rounded"
            icon="mdi-book-account-outline"
            @click="$q.screen.lt.md ? modalNovoTicket = true : $router.push({ name: 'chat-contatos' })"
          >
            <q-tooltip content-class="bg-padrao text-grey-9 text-bold">
              Contatos
            </q-tooltip>
          </q-btn>
          <q-separator class="absolute-bottom" />
        </q-toolbar>

        <q-scroll-area
          ref="scrollAreaTickets"
          style="height: calc(100% - 180px)"
          @scroll="onScroll"
        >
          <!-- <q-separator /> -->
          <ItemTicket
            v-for="(ticket, key) in tickets"
            :key="key"
            :ticket="ticket"
            :filas="filas"
          />
          <div v-if="loading">
            <div class="row justify-center q-my-md">
              <q-spinner
                color="white"
                size="3em"
                :thickness="3"
              />
            </div>
            <div class="row col justify-center q-my-sm text-white">
              Carregando...
            </div>
          </div>
        </q-scroll-area>
        <!-- <q-separator /> -->
        <div
          class="absolute-bottom row justify-between"
          style="height: 50px"
        >
          <q-toggle
            size="xl"
            keep-color
            dense
            class="text-bold q-ml-md flex flex-block"
            :icon-color="$q.dark.isActive ? 'black' : 'white'"
            :value="$q.dark.isActive"
            :color="$q.dark.isActive ? 'grey-3' : 'black'"
            checked-icon="mdi-white-balance-sunny"
            unchecked-icon="mdi-weather-sunny"
            @input="$setConfigsUsuario({ isDark: !$q.dark.isActive })"
          >
            <q-tooltip content-class="text-body1">
              {{ $q.dark.isActive ? 'Desativar' : 'Ativar' }} Modo Escuro (Dark Mode)
            </q-tooltip>
          </q-toggle>
          <div class="flex flex-inline q-pt-xs">
            <q-scroll-area
              horizontal
              style="heigth: 40px; width: 300px;"
            >
              <template v-for="item in whatsapps">
                <q-btn
                  rounded
                  flat
                  dense
                  size="18px"
                  :key="item.id"
                  class="q-mx-xs q-pa-none"
                  :style="`opacity: ${item.status === 'CONNECTED' ? 1 : 0.2}`"
                  :icon="`img:${item.type}-logo.png`"
                >
                  <!-- :color="item.status === 'CONNECTED' ? 'positive' : 'negative'"
                  :icon-right="item.status === 'CONNECTED' ? 'mdi-check-all' : 'mdi-alert-circle-outline'" -->
                  <q-tooltip
                    max-height="300px"
                    content-class="bg-blue-1 text-body1 text-grey-9 hide-scrollbar"
                  >
                    <ItemStatusChannel :item="item" />
                  </q-tooltip>
                </q-btn>
              </template>
            </q-scroll-area>

          </div>
        </div>
      </q-drawer>

      <q-page-container>
        <router-view
          :mensagensRapidas="mensagensRapidas"
          :key="ticketFocado.id"
        ></router-view>
      </q-page-container>

      <q-drawer
        v-if="!cRouteContatos && ticketFocado.id"
        v-model="drawerContact"
        show-if-above
        bordered
        side="right"
        content-class="bg-grey-1"
      >
        <div
          class="bg-white full-width no-border-radius q-pa-sm"
          style="height:60px;"
        >
          <span class="q-ml-md text-h6">
            Dados Contato
          </span>
        </div>
        <q-separator />
        <q-scroll-area style="height: calc(100vh - 70px)">
          <div class="q-pa-sm">
            <q-card
              class="bg-white btn-rounded"
              style="width: 100%"
              bordered
              flat
            >
              <q-card-section class="text-center">
                <q-avatar style="border: 1px solid #9e9e9ea1 !important; width: 100px; height: 100px">
                  <q-icon
                    name="mdi-account"
                    style="width: 100px; height: 100px"
                    size="6em"
                    color="grey-5"
                    v-if="!ticketFocado.contact.profilePicUrl"
                  />
                  <q-img
                    :src="ticketFocado.contact.profilePicUrl"
                    style="width: 100px; height: 100px"
                  >
                    <template v-slot:error>
                      <q-icon
                        name="mdi-account"
                        size="1.5em"
                        color="grey-5"
                      />
                    </template>
                  </q-img>
                </q-avatar>
                <div
                  class="text-caption q-mt-md"
                  style="font-size: 14px"
                >
                  {{ ticketFocado.contact.name || '' }}
                </div>
                <div
                  class="text-caption q-mt-sm"
                  style="font-size: 14px"
                  id="number"
                  @click="copyContent(ticketFocado.contact.number || '')"
                >
                  {{ ticketFocado.contact.number || '' }}
                </div>
                <div
                  class="text-caption q-mt-md"
                  style="font-size: 14px"
                  id="email"
                  @click="copyContent(ticketFocado.contact.email || '')"
                >
                  {{ ticketFocado.contact.email || '' }}
                </div>
                <q-btn
                  color="primary"
                  class="q-mt-sm bg-padrao btn-rounded"
                  flat
                  icon="edit"
                  label="Editar Contato"
                  @click="editContact(ticketFocado.contact.id)"
                />
              </q-card-section>
            </q-card>
            <q-card
              class="bg-white btn-rounded q-mt-sm"
              style="width: 100%"
              bordered
              flat
            >
              <q-card-section class="text-bold q-pa-sm ">
                <q-btn
                  flat
                  class="bg-padrao btn-rounded"
                  :color="!$q.dark.isActive ? 'grey-9' : 'white'"
                  label="Logs"
                  icon="mdi-timeline-text-outline"
                  @click="abrirModalLogs"
                />
              </q-card-section>
            </q-card>
            <q-card
              class="bg-white q-mt-sm btn-rounded"
              style="width: 100%"
              bordered
              flat
              :key="ticketFocado.id + $uuid()"
            >
              <q-card-section class="text-bold q-pb-none">
                Etiquetas
                <q-separator />
              </q-card-section>
              <q-card-section class="q-pa-none">
                <q-select
                  square
                  borderless
                  :value="ticketFocado.contact.tags"
                  multiple
                  :options="etiquetas"
                  use-chips
                  option-value="id"
                  option-label="tag"
                  emit-value
                  map-options
                  dropdown-icon="add"
                  @input="tagSelecionada"
                >
                  <template v-slot:option="{ itemProps, itemEvents, opt, selected, toggleOption }">
                    <q-item
                      v-bind="itemProps"
                      v-on="itemEvents"
                    >
                      <q-item-section>
                        <q-item-label v-html="opt.tag"></q-item-label>
                      </q-item-section>
                      <q-item-section side>
                        <q-checkbox
                          :value="selected"
                          @input="toggleOption(opt)"
                        />
                      </q-item-section>
                    </q-item>
                  </template>
                  <template v-slot:selected-item="{ opt }">
                    <q-chip
                      dense
                      square
                      color="white"
                      text-color="primary"
                      class="q-ma-xs row col-12 text-body1"
                    >
                      <q-icon
                        :style="`color: ${opt.color}`"
                        name="mdi-pound-box-outline"
                        size="28px"
                        class="q-mr-sm"
                      />
                      {{ opt.tag }}
                    </q-chip>
                  </template>
                  <template v-slot:no-option="{ itemProps, itemEvents }">
                    <q-item
                      v-bind="itemProps"
                      v-on="itemEvents"
                    >
                      <q-item-section>
                        <q-item-label class="text-negative text-bold">
                          Ops... Sem etiquetas criadas!
                        </q-item-label>
                        <q-item-label caption>
                          Cadastre novas etiquetas na administração de sistemas.
                        </q-item-label>
                      </q-item-section>
                    </q-item>
                  </template>

                </q-select>
              </q-card-section>
            </q-card>
            <q-card
              class="bg-white q-mt-sm btn-rounded"
              style="width: 100%"
              bordered
              flat
              :key="ticketFocado.id + $uuid()"
            >
              <q-card-section class="text-bold q-pb-none">
                Carteira
                <q-separator />
              </q-card-section>
              <q-card-section class="q-pa-none">
                <q-select
                  square
                  borderless
                  :value="ticketFocado.contact.wallets"
                  multiple
                  :max-values="1"
                  :options="usuarios"
                  use-chips
                  option-value="id"
                  option-label="name"
                  emit-value
                  map-options
                  dropdown-icon="add"
                  @input="carteiraDefinida"
                >
                  <template v-slot:option="{ itemProps, itemEvents, opt, selected, toggleOption }">
                    <q-item
                      v-bind="itemProps"
                      v-on="itemEvents"
                    >
                      <q-item-section>
                        <q-item-label v-html="opt.name"></q-item-label>
                      </q-item-section>
                      <q-item-section side>
                        <q-checkbox
                          :value="selected"
                          @input="toggleOption(opt)"
                        />
                      </q-item-section>
                    </q-item>
                  </template>
                  <template v-slot:selected-item="{ opt }">
                    <q-chip
                      dense
                      square
                      color="white"
                      text-color="primary"
                      class="q-ma-xs row col-12 text-body1"
                    >
                      {{ opt.name }}
                    </q-chip>
                  </template>
                  <template v-slot:no-option="{ itemProps, itemEvents }">
                    <q-item
                      v-bind="itemProps"
                      v-on="itemEvents"
                    >
                      <q-item-section>
                        <q-item-label class="text-negative text-bold">
                          Ops... Sem carteiras disponíveis!!
                        </q-item-label>
                      </q-item-section>
                    </q-item>
                  </template>

                </q-select>
              </q-card-section>
            </q-card>
            <q-card
              class="bg-white q-mt-sm btn-rounded"
              style="width: 100%"
              bordered
              flat
              :key="ticketFocado.id + $uuid()"
            >
              <q-card-section class="text-bold q-pb-none">
                Mensagens Agendadas
                <q-separator />
              </q-card-section>
              <q-card-section class="q-pa-none">
                <template v-if="ticketFocado.scheduledMessages">
                  <q-list>
                    <q-item
                      v-for="(message, idx) in ticketFocado.scheduledMessages"
                      :key="idx"
                      clickable
                    >
                      <q-item-section>
                        <q-item-label caption>
                          <b>Agendado para:</b> {{ $formatarData(message.scheduleDate, 'dd/MM/yyyy HH:mm') }}
                          <q-btn
                            flat
                            round
                            dense
                            icon="mdi-trash-can-outline"
                            class="absolute-top-right q-mr-sm"
                            size="sm"
                            @click="deletarMensagem(message)"
                          />
                        </q-item-label>
                        <q-item-label
                          caption
                          lines="2"
                        > <b>Msg:</b> {{ message.mediaName || message.body }}
                        </q-item-label>
                      </q-item-section>
                      <q-tooltip :delay="500">
                        <MensagemChat :mensagens="[message]" />
                      </q-tooltip>
                    </q-item>
                  </q-list>
                </template>
              </q-card-section>
            </q-card>
            <q-card
              class="bg-white q-mt-sm btn-rounded"
              style="width: 100%"
              bordered
              flat
              :key="ticketFocado.id + $uuid()"
            >
              <q-card-section class="text-bold q-pb-none">
                Outras Informações
              </q-card-section>
              <q-card-section class="q-pa-none">
                <template v-if="cIsExtraInfo">
                  <q-list>
                    <q-item
                      v-for="(info, idx) in ticketFocado.contact.extraInfo"
                      :key="idx"
                    >
                      <q-item-section>
                        <q-item-label>{{ info.value }}</q-item-label>
                      </q-item-section>
                    </q-item>
                  </q-list>
                </template>

              </q-card-section>
            </q-card>
          </div>
        </q-scroll-area>
      </q-drawer>

      <ModalNovoTicket :modalNovoTicket.sync="modalNovoTicket" />
      <ContatoModal
        :contactId="selectedContactId"
        :modalContato.sync="modalContato"
        @contatoModal:contato-editado="contatoEditado"
      />

      <ModalUsuario
        :isProfile="true"
        :modalUsuario.sync="modalUsuario"
        :usuarioEdicao.sync="usuario"
      />

      <q-dialog
        v-model="exibirModalLogs"
        no-backdrop-dismiss
        full-height
        position="right"
        @hide="logsTicket = []"
      >
        <q-card style="width: 400px">
          <q-card-section :class="{ 'bg-grey-2': !$q.dark.isActive, 'bg-primary': $q.dark.isActive }">
            <div class="text-h6">Logs Ticket: {{ ticketFocado.id }}
              <q-btn
                icon="close"
                color="negative"
                flat
                class="bg-padrao float-right"
                round
                v-close-popup
              />
            </div>
          </q-card-section>
          <q-card-section class="">
            <q-scroll-area
              style="height: calc(100vh - 200px);"
              class="full-width"
            >
              <q-timeline
                color="black"
                style="width: 360px"
                class="q-pl-sm "
                :class="{ 'text-black': !$q.dark.isActive }"
              >
                <template v-for="(log, idx) in logsTicket">
                  <q-timeline-entry
                    :key="log && log.id || idx"
                    :subtitle="$formatarData(log.createdAt, 'dd/MM/yyyy HH:mm')"
                    :color="messagesLog[log.type] && messagesLog[log.type].color || ''"
                    :icon="messagesLog[log.type] && messagesLog[log.type].icon || ''"
                  >
                    <template v-slot:title>
                      <div
                        :class="{ 'text-white': $q.dark.isActive }"
                        style="width: calc(100% - 20px)"
                      >
                        <div class="row col text-bold text-body2"> {{ log.user && log.user.name || 'Bot' }}:</div>
                        <div class="row col"> {{ messagesLog[log.type] && messagesLog[log.type].message }}</div>
                      </div>
                    </template>
                  </q-timeline-entry>
                </template>
              </q-timeline>
            </q-scroll-area>
          </q-card-section>
        </q-card>
      </q-dialog>

    </q-layout>
    <audio ref="audioNotificationPlay">
      <source
        :src="alertSound"
        type="audio/mp3"
      >
    </audio>
  </div>
</template>

<script type="rocketlazyloadscript">
import ItemStatusChannel from 'src/pages/sessaoWhatsapp/ItemStatusChannel.vue'
import ContatoModal from 'src/pages/contatos/ContatoModal'
import ItemTicket from './ItemTicket'
import { ConsultarLogsTicket, ConsultarTickets, DeletarMensagem } from 'src/service/tickets'
import { mapGetters } from 'vuex'
import mixinSockets from './mixinSockets'
import socketInitial from 'src/layouts/socketInitial'
import ModalNovoTicket from './ModalNovoTicket'
import { ListarFilas } from 'src/service/filas'
const UserQueues = JSON.parse(localStorage.getItem('queues'))
const profile = localStorage.getItem('profile')
const username = localStorage.getItem('username')
const usuario = JSON.parse(localStorage.getItem('usuario'))
import StatusWhatsapp from 'src/components/StatusWhatsapp'
import alertSound from 'src/assets/sound.mp3'
import { ListarWhatsapps } from 'src/service/sessoesWhatsapp'
import { debounce } from 'quasar'
import { format } from 'date-fns'
import ModalUsuario from 'src/pages/usuarios/ModalUsuario'
import { ListarConfiguracoes } from 'src/service/configuracoes'
import { ListarMensagensRapidas } from 'src/service/mensagensRapidas'
import { ListarEtiquetas } from 'src/service/etiquetas'
import { EditarEtiquetasContato, EditarCarteiraContato } from 'src/service/contatos'
import { RealizarLogout } from 'src/service/login'
import { ListarUsuarios } from 'src/service/user'
import MensagemChat from './MensagemChat.vue'
import { messagesLog } from '../../utils/constants'
export default {
  name: 'IndexAtendimento',
  mixins: [mixinSockets, socketInitial],
  components: {
    ItemTicket,
    ModalNovoTicket,
    StatusWhatsapp,
    ContatoModal,
    ModalUsuario,
    MensagemChat,
    ItemStatusChannel
  },
  data () {
    return {
      messagesLog,
      configuracoes: [],
      debounce,
      alertSound,
      usuario,
      usuarios: [],
      username,
      modalUsuario: false,
      toolbarSearch: true,
      drawerTickets: true,
      drawerContact: true,
      loading: false,
      profile,
      modalNovoTicket: false,
      modalContato: false,
      selectedContactId: null,
      filterBusca: '',
      showDialog: false,
      atendimentos: [],
      countTickets: 0,
      pesquisaTickets: {
        searchParam: '',
        pageNumber: 1,
        status: ['open', 'pending'],
        showAll: false,
        count: null,
        queuesIds: [],
        withUnreadMessages: false,
        isNotAssignedUser: false,
        includeNotQueueDefined: true
        // date: new Date(),
      },
      filas: [],
      etiquetas: [],
      mensagensRapidas: [],
      modalEtiquestas: false,
      exibirModalLogs: false,
      logsTicket: []
    }
  },
  watch: {
    // pesquisaTickets: {
    //   handler (v) {
    //     this.$store.commit('SET_FILTER_PARAMS', extend(true, {}, this.pesquisaTickets))
    //     localStorage.setItem('filtrosAtendimento', JSON.stringify(this.pesquisaTickets))
    //   },
    //   deep: true
    //   // immediate: true
    // }
  },
  computed: {
    ...mapGetters([
      'tickets',
      'ticketFocado',
      'hasMore',
      'whatsapps'
    ]),
    cUserQueues () {
      // try {
      //   const filasUsuario = JSON.parse(UserQueues).map(q => {
      //     if (q.isActive) {
      //       return q.id
      //     }
      //   })
      //   return this.filas.filter(f => filasUsuario.includes(f.id)) || []
      // } catch (error) {
      //   return []
      // }
      return UserQueues
    },
    style () {
      return {
        height: this.$q.screen.height + 'px'
      }
    },
    cToolbarSearchHeigthAjust () {
      return this.toolbarSearch ? 58 : 0
    },
    cHeigVerticalTabs () {
      return `${50 + this.cToolbarSearchHeigthAjust}px`
    },
    cRouteContatos () {
      return this.$route.name === 'chat-contatos'
    },
    cFiltroSelecionado () {
      const { queuesIds, showAll, withUnreadMessages, isNotAssignedUser } = this.pesquisaTickets
      return !!(queuesIds?.length || showAll || withUnreadMessages || isNotAssignedUser)
    },
    cIsExtraInfo () {
      return this.ticketFocado?.contact?.extraInfo?.length > 0
    }
  },
  methods: {
    handlerNotifications (data) {
      const options = {
        body: `${data.body} - ${format(new Date(), 'HH:mm')}`,
        icon: data.ticket.contact.profilePicUrl,
        tag: data.ticket.id,
        renotify: true
      }

      const notification = new Notification(
        `Mensagem de ${data.ticket.contact.name}`,
        options
      )

      setTimeout(() => {
        notification.close()
      }, 10000)

      notification.onclick = e => {
        e.preventDefault()
        window.focus()
        this.$store.dispatch('AbrirChatMensagens', data.ticket)
        this.$router.push({ name: 'atendimento' })
        // history.push(`/tickets/${ticket.id}`);
      }

      this.$nextTick(() => {
        // utilizar refs do layout
        this.$refs.audioNotificationPlay.play()
      })
    },
    async listarConfiguracoes () {
      const { data } = await ListarConfiguracoes()
      localStorage.setItem('configuracoes', JSON.stringify(data))
    },
    onScroll (info) {
      if (info.verticalPercentage <= 0.85) return
      this.onLoadMore()
    },
    editContact (contactId) {
      this.selectedContactId = contactId
      this.modalContato = true
    },
    contatoEditado (contato) {
      this.$store.commit('UPDATE_TICKET_FOCADO_CONTACT', contato)
    },
    async consultarTickets (paramsInit = {}) {
      const params = {
        ...this.pesquisaTickets,
        ...paramsInit
      }
      try {
        const { data } = await ConsultarTickets(params)
        this.countTickets = data.count // count total de tickets no status
        this.$store.commit('LOAD_TICKETS', data.tickets)
        this.$store.commit('SET_HAS_MORE', data.hasMore)
      } catch (err) {
        this.$notificarErro('Algum problema', err)
        console.error(err)
      }
      // return () => clearTimeout(delayDebounceFn)
    },
    async BuscarTicketFiltro () {
      this.$store.commit('RESET_TICKETS')
      this.loading = true
      localStorage.setItem('filtrosAtendimento', JSON.stringify(this.pesquisaTickets))
      this.pesquisaTickets = {
        ...this.pesquisaTickets,
        pageNumber: 1
      }
      await this.consultarTickets(this.pesquisaTickets)
      this.loading = false
      this.$setConfigsUsuario({ isDark: this.$q.dark.isActive })
    },
    async onLoadMore () {
      if (this.tickets.length === 0 || !this.hasMore || this.loading) {
        return
      }
      try {
        this.loading = true
        this.pesquisaTickets.pageNumber++
        await this.consultarTickets()
        this.loading = false
      } catch (error) {
        this.loading = false
      }
    },
    async listarFilas () {
      const { data } = await ListarFilas()
      this.filas = data
      localStorage.setItem('filasCadastradas', JSON.stringify(data || []))
    },
    async listarWhatsapps () {
      const { data } = await ListarWhatsapps()
      this.$store.commit('LOAD_WHATSAPPS', data)
    },
    async listarEtiquetas () {
      const { data } = await ListarEtiquetas(true)
      this.etiquetas = data
    },
    async abrirModalUsuario () {
      // if (!usuario.id) {
      //   await this.dadosUsuario()
      // }
      // const { data } = await DadosUsuario(userId)
      // this.usuario = data
      this.modalUsuario = true
    },
    async efetuarLogout () {
      console.log('logout - index atendimento')
      try {
        await RealizarLogout(usuario)
        localStorage.removeItem('token')
        localStorage.removeItem('username')
        localStorage.removeItem('profile')
        localStorage.removeItem('userId')
        localStorage.removeItem('queues')
        localStorage.removeItem('usuario')
        localStorage.removeItem('filtrosAtendimento')

        this.$router.go({ name: 'login', replace: true })
      } catch (error) {
        this.$notificarErro(
          'Não foi possível realizar logout',
          error
        )
      }
    },
    copyContent (content) {
      navigator.clipboard.writeText(content)
        .then(() => {
          // Copiado com sucesso
          console.log('Conteúdo copiado: ', content)
        })
        .catch((error) => {
          // Ocorreu um erro ao copiar
          console.error('Erro ao copiar o conteúdo: ', error)
        })
    },
    deletarMensagem (mensagem) {
      const data = { ...mensagem }
      this.$q.dialog({
        title: 'Atenção!! Deseja realmente deletar a mensagem? ',
        message: 'Mensagens antigas não serão apagadas no cliente.',
        cancel: {
          label: 'Não',
          color: 'primary',
          push: true
        },
        ok: {
          label: 'Sim',
          color: 'negative',
          push: true
        },
        persistent: true
      }).onOk(() => {
        this.loading = true
        DeletarMensagem(data)
          .then(res => {
            this.loading = false
          })
          .catch(error => {
            this.loading = false
            console.error(error)
            this.$notificarErro('Não foi possível apagar a mensagem', error)
          })
      }).onCancel(() => {
      })
    },
    async tagSelecionada (tags) {
      const { data } = await EditarEtiquetasContato(this.ticketFocado.contact.id, [...tags])
      this.contatoEditado(data)
    },
    async carteiraDefinida (wallets) {
      const { data } = await EditarCarteiraContato(this.ticketFocado.contact.id, [...wallets])
      this.contatoEditado(data)
    },
    async listarUsuarios () {
      try {
        const { data } = await ListarUsuarios()
        this.usuarios = data.users
      } catch (error) {
        console.error(error)
        this.$notificarErro('Problema ao carregar usuários', error)
      }
    },
    setValueMenu () {
      this.drawerTickets = !this.drawerTickets
    },
    setValueMenuContact () {
      this.drawerContact = !this.drawerContact
    },
    async abrirModalLogs () {
      const { data } = await ConsultarLogsTicket({ ticketId: this.ticketFocado.id })
      this.logsTicket = data
      this.exibirModalLogs = true
    }
  },
  beforeMount () {
    this.listarFilas()
    this.listarEtiquetas()
    this.listarConfiguracoes()
    const filtros = JSON.parse(localStorage.getItem('filtrosAtendimento'))
    if (!filtros?.pageNumber) {
      localStorage.setItem('filtrosAtendimento', JSON.stringify(this.pesquisaTickets))
    }
  },
  async mounted () {
    this.$root.$on('infor-cabecalo-chat:acao-menu', this.setValueMenu)
    this.$root.$on('update-ticket:info-contato', this.setValueMenuContact)
    this.socketTicketList()
    this.pesquisaTickets = JSON.parse(localStorage.getItem('filtrosAtendimento'))
    this.$root.$on('handlerNotifications', this.handlerNotifications)
    await this.listarWhatsapps()
    await this.consultarTickets()
    await this.listarUsuarios()
    const { data } = await ListarMensagensRapidas()
    this.mensagensRapidas = data
    if (!('Notification' in window)) {
    } else {
      Notification.requestPermission()
    }
    this.userProfile = localStorage.getItem('profile')
    // this.socketInitial()

    // se existir ticket na url, abrir o ticket.
    if (this.$route?.params?.ticketId) {
      const ticketId = this.$route?.params?.ticketId
      if (ticketId && this.tickets.length > 0) {
        const ticket = this.tickets.find(t => t.id === +ticketId)
        if (!ticket) return
        // caso esteja em um tamanho mobile, fechar a drawer dos contatos
        if (this.$q.screen.lt.md && ticket.status !== 'pending') {
          this.$root.$emit('infor-cabecalo-chat:acao-menu')
        }
        console.log('before - AbrirChatMensagens', ticket)
        this.$store.commit('SET_HAS_MORE', true)
        this.$store.dispatch('AbrirChatMensagens', ticket)
      }
    } else {
      console.log('chat-empty')
      this.$router.push({ name: 'chat-empty' })
    }
  },
  destroyed () {
    this.$root.$off('handlerNotifications', this.handlerNotifications)
    this.$root.$off('infor-cabecalo-chat:acao-menu', this.setValueMenu)
    this.$root.$on('update-ticket:info-contato', this.setValueMenuContact)
    // this.socketDisconnect()
    this.$store.commit('TICKET_FOCADO', {})
  }
}
</script>

<style lang="sass">
.upload-btn-wrapper
  position: relative
  overflow: hidden
  display: inline-block

  & input[type="file"]
    font-size: 100px
    position: absolute
    left: 0
    top: 0
    opacity: 0

.WAL
  width: 100%
  height: 100%

  &:before
    content: ''
    height: 127px
    position: fixed
    top: 0
    width: 100%

  &__layout
    margin: 0 auto
    z-index: 4000
    height: 100%
    width: 100%

  &__field.q-field--outlined .q-field__control:before
    border: none

  .q-drawer--standard
    .WAL__drawer-close
      display: none

@media (max-width: 850px)
  .WAL
    padding: 0
    &__layout
      width: 100%
      border-radius: 0

@media (min-width: 691px)
  .WAL
    &__drawer-open
      display: none

.conversation__summary
  margin-top: 4px

.conversation__more
  margin-top: 0!important
  font-size: 1.4rem
</style>

				
			

frontend\src\pages\atendimento\InputMensagem.vue

				
					<template>
  <div>
    <template v-if="ticketFocado.status != 'pending'">

      <div
        class="row absolute-full fit col-12"
        ref="menuFast"
      >
        <q-menu
          :target="$refs.menuFast"
          :key="cMensagensRapidas.length"
          square
          no-focus
          no-parent-event
          class="no-box-shadow no-shadow"
          fit
          :offset="[0, 5]"
          persistent
          max-height="400px"
          @hide="visualizarMensagensRapidas = false"
          :value="textChat.startsWith('/') || visualizarMensagensRapidas"
        >
          <!-- :value="textChat.startsWith('/')" -->
          <q-list
            class="no-shadow no-box-shadow"
            style="min-width: 100px"
            separator
            v-if="!cMensagensRapidas.length"
          >
            <q-item>
              <q-item-section>
                <q-item-label class="text-negative text-bold">Ops... Nada por aqui!</q-item-label>
                <q-item-label caption>Cadastre suas mensagens na administração de sistema.</q-item-label>
              </q-item-section>
            </q-item>
          </q-list>

          <q-list
            class="no-shadow no-box-shadow"
            style="min-width: 100px"
            separator
            v-else
          >
            <q-item
              v-for="resposta in cMensagensRapidas"
              :key="resposta.key"
              clickable
              v-close-popup
              @click="mensagemRapidaSelecionada(resposta.message)"
            >
              <q-item-section>
                <q-item-label class="text-bold"> {{ resposta.key }} </q-item-label>
                <q-item-label
                  caption
                  lines="2"
                > {{ resposta.message }} </q-item-label>
              </q-item-section>
              <q-tooltip content-class="bg-padrao text-grey-9 text-bold">
                {{ resposta.message }}
              </q-tooltip>
            </q-item>
          </q-list>
        </q-menu>
      </div>

      <div
        style="min-height: 80px"
        class="row q-pb-md q-pt-sm bg-white justify-start items-center text-grey-9 relative-position"
      >

        <div
          class="row col-12 q-pa-sm"
          v-if="isScheduleDate"
        >
          <q-datetime-picker
            style="width: 300px"
            dense
            rounded
            hide-bottom-space
            outlined
            stack-label
            bottom-slots
            label="Data/Hora Agendamento"
            mode="datetime"
            color="primary"
            v-model="scheduleDate"
            format24h
          />
        </div>

        <template v-if="!isRecordingAudio">
          <q-btn
            v-if="$q.screen.width > 500"
            flat
            dense
            @click="abrirEnvioArquivo"
            icon="mdi-paperclip"
            :disable="cDisableActions"
            class="bg-padrao btn-rounded q-mx-xs"
            :color="$q.dark.isActive ? 'white' : ''"
          >
            <q-tooltip content-class="text-bold">
              Enviar arquivo
            </q-tooltip>
          </q-btn>
          <q-btn
            v-if="$q.screen.width > 500"
            flat
            dense
            icon="mdi-emoticon-happy-outline"
            :disable="cDisableActions"
            class="bg-padrao btn-rounded q-mx-xs"
            :color="$q.dark.isActive ? 'white' : ''"
          >
            <q-tooltip content-class="text-bold">
              Emoji
            </q-tooltip>
            <q-menu
              anchor="top right"
              self="bottom middle"
              :offset="[5, 40]"
            >
              <VEmojiPicker
                style="width: 40vw"
                :showSearch="false"
                :emojisByRow="20"
                labelSearch="Localizar..."
                lang="pt-BR"
                @select="onInsertSelectEmoji"
              />
            </q-menu>
          </q-btn>
          <q-btn
            v-if="$q.screen.width > 500"
            flat
            dense
            @click="handlSendLinkVideo"
            icon="mdi-message-video"
            :disable="cDisableActions"
            class="bg-padrao btn-rounded q-mx-xs"
            :color="$q.dark.isActive ? 'white' : ''"
          >
            <q-tooltip content-class="text-bold">
              Enviar link para videoconferencia
            </q-tooltip>
          </q-btn>
          <q-toggle
            keep-color
            v-model="sign"
            dense
            @input="handleSign"
            class="q-mx-sm q-ml-md"
            :color="sign ? 'positive' : 'black'"
            type="toggle"
          >
            <q-tooltip>
              {{ sign ? 'Desativar' : 'Ativar' }} Assinatura
            </q-tooltip>
          </q-toggle>
          <q-input
            hide-bottom-space
            :loading="loading"
            :disable="cDisableActions"
            ref="inputEnvioMensagem"
            id="inputEnvioMensagem"
            type="textarea"
            @keydown.exact.enter.prevent="() => textChat.trim().length ? enviarMensagem() : ''"
            v-show="!cMostrarEnvioArquivo"
            class="col-grow q-mx-xs text-grey-10 inputEnvioMensagem"
            bg-color="grey-2"
            color="grey-7"
            placeholder="Digita sua mensagem"
            input-style="max-height: 30vh"
            autogrow
            rounded
            dense
            outlined
            v-model="textChat"
            :value="textChat"
            @paste="handleInputPaste"
          >
            <!-- <template v-slot:hint>
          "Quebra linha: Shift + Enter"
        </template> -->
            <template
              v-slot:prepend
              v-if="$q.screen.width < 500"
            >
              <q-btn
                flat
                icon="mdi-emoticon-happy-outline"
                :disable="cDisableActions"
                dense
                round
                :color="$q.dark.isActive ? 'white' : ''"
              >
                <q-tooltip content-class="text-bold">
                  Emoji
                </q-tooltip>
                <q-menu
                  anchor="top right"
                  self="bottom middle"
                  :offset="[5, 40]"
                >
                  <VEmojiPicker
                    style="width: 40vw"
                    :showSearch="false"
                    :emojisByRow="20"
                    labelSearch="Localizar..."
                    lang="pt-BR"
                    @select="onInsertSelectEmoji"
                  />
                </q-menu>
              </q-btn>
            </template>
            <template v-slot:append>
              <q-btn
                flat
                @click="abrirEnvioArquivo"
                icon="mdi-paperclip"
                :disable="cDisableActions"
                dense
                round
                v-if="$q.screen.width < 500"
                class="bg-padrao btn-rounded"
                :color="$q.dark.isActive ? 'white' : ''"
              >
                <q-tooltip content-class=" text-bold">
                  Enviar arquivo
                </q-tooltip>
              </q-btn>
              <q-btn
                v-if="!ticketFocado?.channel?.includes('hub')"
                dense
                flat
                round
                icon="mdi-message-flash-outline"
                @click="visualizarMensagensRapidas = !visualizarMensagensRapidas"
              >
                <q-tooltip content-class="text-bold">
                  Mensagens Rápidas
                </q-tooltip>
              </q-btn>
            </template>
          </q-input>
          <!-- tamanho maximo por arquivo de 10mb -->
          <q-file
            :loading="loading"
            :disable="cDisableActions"
            ref="PickerFileMessage"
            id="PickerFileMessage"
            v-show="cMostrarEnvioArquivo"
            v-model="arquivos"
            class="col-grow q-mx-xs PickerFileMessage"
            bg-color="blue-grey-1"
            input-style="max-height: 30vh"
            outlined
            use-chips
            multiple
            autogrow
            dense
            rounded
            append
            :max-files="5"
            :max-file-size="15485760"
            :max-total-size="15485760"
            accept=".txt, .xml, .jpg, .png, image/jpeg, .pdf, .doc, .docx, .mp4, .xls, .xlsx, .jpeg, .zip, .ppt, .pptx, image/*"
            @rejected="onRejectedFiles"
          />
          <q-btn
            v-if="textChat || cMostrarEnvioArquivo"
            ref="btnEnviarMensagem"
            @click="enviarMensagem"
            :disabled="ticketFocado.status !== 'open'"
            flat
            icon="mdi-send"
            class="bg-padrao btn-rounded q-mx-xs"
            :color="$q.dark.isActive ? 'white' : ''"
          >
            <q-tooltip content-class=" text-bold">
              Enviar Mensagem
            </q-tooltip>
          </q-btn>
          <q-btn
            v-if="!textChat && !cMostrarEnvioArquivo && !isRecordingAudio"
            @click="handleSartRecordingAudio"
            :disabled="cDisableActions"
            flat
            icon="mdi-microphone"
            class="bg-padrao btn-rounded q-mx-xs"
            :color="$q.dark.isActive ? 'white' : ''"
          >
            <q-tooltip content-class="text-bold">
              Enviar Áudio
            </q-tooltip>
          </q-btn>
        </template>
        <template v-else>
          <div class="full-width items-center row justify-end ">
            <q-skeleton
              animation="pulse-y"
              class="col-grow q-mx-md"
              type="text"
            />
            <div
              style="width: 200px"
              class="flex flex-center items-center"
              v-if="isRecordingAudio"
            >
              <q-btn
                flat
                icon="mdi-close"
                color="negative"
                @click="handleCancelRecordingAudio"
                class="bg-padrao btn-rounded q-mx-xs"
              />
              <RecordingTimer
                class="text-bold"
                :class="{ 'text-white': $q.dark.isActive }"
              />
              <q-btn
                flat
                icon="mdi-send-circle-outline"
                color="positive"
                @click="handleStopRecordingAudio"
                class="bg-padrao btn-rounded q-mx-xs"
              />
            </div>

          </div>
        </template>

        <q-dialog
          v-model="abrirModalPreviewImagem"
          position="right"
          @hide="hideModalPreviewImagem"
          @show="showModalPreviewImagem"
        >
          <q-card
            style="height: 90vh; min-width: 60vw; max-width: 60vw"
            class="q-pa-md"
          >
            <q-card-section>
              <div class="text-h6">{{ urlMediaPreview.title }}
                <q-btn
                  class="float-right"
                  icon="close"
                  color="negative"
                  round
                  outline
                  @click="hideModalPreviewImagem"
                />
              </div>
            </q-card-section>
            <q-card-section>
              <q-img
                :src="urlMediaPreview.src"
                spinner-color="white"
                class="img-responsive mdi-image-auto-adjust q-uploader__file--img"
                style="height: 60vh; min-width: 55vw; max-width: 55vw"
              />
            </q-card-section>
            <q-card-actions align="center">
              <q-btn
                ref="qbtnPasteEnvioMensagem"
                label="Enviar"
                color="primary"
                v-close-popup
                @click="enviarMensagem"
                @keypress.enter.exact="enviarMensagem()"
              />
            </q-card-actions>
            <span class="row col text-caption text-blue-grey-10">* Confirmar envio: Enter</span>
            <span class="row col text-caption text-blue-grey-10">** Cancelar: ESC</span>
          </q-card>
        </q-dialog>
      </div>
    </template>
    <template v-else>
      <div
        style="min-height: 80px"
        class="row q-pb-md q-pt-sm bg-white justify-center items-center text-grey-9 relative-position"
      >
        <q-btn
          push
          rounded
          style="width: 250px"
          class="text-bold"
          color="positive"
          icon="mdi-send-circle"
          label="Iniciar o atendimento"
          @click="iniciarAtendimento(ticketFocado)"
        />

      </div>
    </template>
    <!-- <p
      v-if="!cMostrarEnvioArquivo"
      class="row col text-caption text-blue-grey-10"
    >Quebra linha/Parágrafo: Shift + Enter ||| Enviar Mensagem: Enter</p> -->
  </div>
</template>

<script type="rocketlazyloadscript">
import { LocalStorage, uid } from 'quasar'
import mixinCommon from './mixinCommon'
import { EnviarMensagemTexto } from 'src/service/tickets'
import { EnviarMensagemHub } from 'src/service/hub'
import { VEmojiPicker } from 'v-emoji-picker'
import { mapGetters } from 'vuex'
import RecordingTimer from './RecordingTimer'
import MicRecorder from 'mic-recorder-to-mp3'
const Mp3Recorder = new MicRecorder({ bitRate: 128 })
import mixinAtualizarStatusTicket from './mixinAtualizarStatusTicket'

export default {
  name: 'InputMensagem',
  mixins: [mixinAtualizarStatusTicket, mixinCommon],
  props: {
    replyingMessage: {
      type: Object,
      default: null
    },
    isScheduleDate: {
      type: Boolean,
      default: false
    },
    mensagensRapidas: {
      type: Array,
      default: () => []
    }
  },
  components: {
    VEmojiPicker,
    RecordingTimer
  },
  data () {
    return {
      loading: false,
      abrirFilePicker: false,
      abrirModalPreviewImagem: false,
      isRecordingAudio: false,
      urlMediaPreview: {
        title: '',
        src: ''
      },
      visualizarMensagensRapidas: false,
      arquivos: [],
      textChat: '',
      sign: false,
      scheduleDate: null
    }
  },
  computed: {
    ...mapGetters(['ticketFocado']),
    cMostrarEnvioArquivo () {
      return this.arquivos.length > 0
    },
    cDisableActions () {
      return (this.isRecordingAudio || this.ticketFocado.status !== 'open')
    },
    cMensagensRapidas () {
      let search = this.textChat?.toLowerCase()
      if (search && search.trim().startsWith('/')) {
        search = search.replace('/', '')
      }
      return !search ? this.mensagensRapidas : this.mensagensRapidas.filter(r => r.key.toLowerCase().indexOf(search) !== -1)
      // return this.mensagensRapidas
    }
  },
  methods: {
    openFilePreview (event) {
      const data = event.clipboardData.files[0]
      const urlImg = window.URL.createObjectURL(data)
      return urlImg
    },
    handleInputPaste (e) {
      if (!this.ticketFocado?.id) return
      if (e.clipboardData.files[0]) {
        this.textChat = ''
        this.arquivos = [e.clipboardData.files[0]]
        this.abrirModalPreviewImagem = true
        this.urlMediaPreview = {
          title: `Enviar imagem para ${this.ticketFocado?.contact?.name}`,
          src: this.openFilePreview(e)
        }
        this.$refs.inputEnvioMensagem.focus()
      }
    },
    mensagemRapidaSelecionada (mensagem) {
      this.textChat = mensagem
      setTimeout(() => {
        this.$refs.inputEnvioMensagem.focus()
      }, 300)
    },
    onInsertSelectEmoji (emoji) {
      const self = this
      var tArea = this.$refs.inputEnvioMensagem.$refs.input
      // get cursor's position:
      var startPos = tArea.selectionStart,
        endPos = tArea.selectionEnd,
        cursorPos = startPos,
        tmpStr = tArea.value

      // filter:
      if (!emoji.data) {
        return
      }

      // insert:
      self.txtContent = this.textChat
      self.txtContent = tmpStr.substring(0, startPos) + emoji.data + tmpStr.substring(endPos, tmpStr.length)
      this.textChat = self.txtContent
      // move cursor:
      setTimeout(() => {
        tArea.selectionStart = tArea.selectionEnd = cursorPos + emoji.data.length
      }, 10)
    },
    abrirEnvioArquivo (event) {
      this.textChat = ''
      this.abrirFilePicker = true
      this.$refs.PickerFileMessage.pickFiles(event)
    },
    async handleSartRecordingAudio () {
      try {
        await navigator.mediaDevices.getUserMedia({ audio: true })
        await Mp3Recorder.start()
        this.isRecordingAudio = true
      } catch (error) {
        this.isRecordingAudio = false
      }
    },
    async handleStopRecordingAudio () {
      this.loading = true
      try {
        const [, blob] = await Mp3Recorder.stop().getMp3()
        if (blob.size < 10000) {
          this.loading = false
          this.isRecordingAudio = false
          return
        }

        const formData = new FormData()
        const filename = `${new Date().getTime()}.mp3`
        formData.append('medias', blob, filename)
        formData.append('body', filename)
        formData.append('fromMe', true)
        formData.append('id', uid())
        if (this.isScheduleDate) {
          formData.append('scheduleDate', this.scheduleDate)
        }
        const ticketId = this.ticketFocado.id
        // await EnviarMensagemTexto(ticketId, formData)
        if (this.ticketFocado.channel.includes('hub')) {
          await EnviarMensagemHub(ticketId, formData)
        } else {
          await EnviarMensagemTexto(ticketId, formData)
        }
        this.arquivos = []
        this.textChat = ''
        this.$emit('update:replyingMessage', null)
        this.abrirFilePicker = false
        this.abrirModalPreviewImagem = false
        this.isRecordingAudio = false
        this.loading = false
        setTimeout(() => {
          this.scrollToBottom()
        }, 300)
      } catch (error) {
        this.isRecordingAudio = false
        this.loading = false
        this.$notificarErro('Ocorreu um erro!', error)
      }
    },
    async handleCancelRecordingAudio () {
      try {
        await Mp3Recorder.stop().getMp3()
        this.isRecordingAudio = false
        this.loading = false
      } catch (error) {
        this.$notificarErro('Ocorreu um erro!', error)
      }
    },
    prepararUploadMedia () {
      if (!this.arquivos.length) {
        throw new Error('Não existem arquivos para envio')
      }
      const formData = new FormData()
      formData.append('fromMe', true)
      formData.append('id', uid())
      this.arquivos.forEach(media => {
        formData.append('medias', media)
        formData.append('body', media.name)
        // formData.append('idFront', uid())
        if (this.isScheduleDate) {
          formData.append('scheduleDate', this.scheduleDate)
        }
      })
      return formData
    },
    prepararMensagemTexto () {
      if (this.textChat.trim() === '') {
        throw new Error('Mensagem Inexistente')
      }

      if (this.textChat.trim() && this.textChat.trim().startsWith('/')) {
        let search = this.textChat.trim().toLowerCase()
        search = search.replace('/', '')
        const mensagemRapida = this.cMensagensRapidas.find(m => m.key.toLowerCase() === search)
        if (mensagemRapida?.message) {
          this.textChat = mensagemRapida.message
        } else {
          const error = this.cMensagensRapidas.length > 1
            ? 'Várias mensagens rápidas encontradas. Selecione uma ou digite uma chave única da mensagem.'
            : '/ indica que você deseja enviar uma mensagem rápida, mas nenhuma foi localizada. Cadastre ou apague a / e digite sua mensagem.'
          this.$notificarErro(error)
          this.loading = false
          throw new Error(error)
        }
      }
      let mensagem = this.textChat.trim()
      const username = localStorage.getItem('username')
      if (username && this.sign) {
        mensagem = `*${username}*:\n ${mensagem}`
      }
      const message = {
        read: 1,
        fromMe: true,
        mediaUrl: '',
        body: mensagem,
        scheduleDate: this.isScheduleDate ? this.scheduleDate : null,
        quotedMsg: this.replyingMessage,
        // idFront: uid()
        id: uid()
      }
      if (this.isScheduleDate) {
        message.scheduleDate = this.scheduleDate
      }
      return message
    },
    async enviarMensagem () {
      if (this.isScheduleDate && !this.scheduleDate) {
        this.$notificarErro('Para agendar uma mensagem, informe o campo Data/Hora Agendamento.')
        return
      }
      this.loading = true
      const ticketId = this.ticketFocado.id
      const message = !this.cMostrarEnvioArquivo
        ? this.prepararMensagemTexto()
        : this.prepararUploadMedia()
      try {
        if (!this.cMostrarEnvioArquivo && !this.textChat) return
        // await EnviarMensagemTexto(ticketId, message)
        if (this.ticketFocado.channel.includes('hub')) {
          await EnviarMensagemHub(ticketId, message)
        } else {
          await EnviarMensagemTexto(ticketId, message)
        }
        this.arquivos = []
        this.textChat = ''
        this.$emit('update:replyingMessage', null)
        this.abrirFilePicker = false
        this.abrirModalPreviewImagem = false
        setTimeout(() => {
          this.scrollToBottom()
        }, 300)
      } catch (error) {
        this.isRecordingAudio = false
        this.loading = false
        this.$notificarErro('Ocorreu um erro!', error)
      }
      this.isRecordingAudio = false
      this.loading = false
      setTimeout(() => {
        this.$refs.inputEnvioMensagem.focus()
      }, 300)
    },
    async handlSendLinkVideo () {
      const link = `https://meet.jit.si/${uid()}/${uid()}`
      let mensagem = link
      const username = localStorage.getItem('username')
      if (username && this.sign) {
        mensagem = `*${username}*:\n ${mensagem}`
      }
      const message = {
        read: 1,
        fromMe: true,
        mediaUrl: '',
        body: mensagem,
        scheduleDate: this.isScheduleDate ? this.scheduleDate : null,
        quotedMsg: this.replyingMessage,
        // idFront: uid()
        id: uid()
      }

      this.loading = true
      const ticketId = this.ticketFocado.id
      try {
        // await EnviarMensagemTexto(ticketId, message)
        if (this.ticketFocado.channel.includes('hub')) {
          await EnviarMensagemHub(ticketId, message)
        } else {
          await EnviarMensagemTexto(ticketId, message)
        }
        setTimeout(() => {
          this.scrollToBottom()
        }, 200)
        setTimeout(() => {
          window.open(link, '_blank')
        }, 800)
      } catch (error) {
        this.loading = false
        this.$notificarErro('Ocorreu um erro!', error)
      }
      this.loading = false
    },
    handlerInputMensagem (v) {
      this.textChat = v.target.value
    },
    showModalPreviewImagem () {
      this.$nextTick(() => {
        setTimeout(() => {
          this.$refs.qbtnPasteEnvioMensagem.$el.focus()
        }, 20)
      })
    },
    hideModalPreviewImagem () {
      this.arquivos = []
      this.urlMediaPreview = {}
      this.abrirModalPreviewImagem = false
    },
    onRejectedFiles (rejectedEntries) {
      this.$q.notify({
        html: true,
        message: `Ops... Ocorreu um erro! <br>
        <ul>
          <li>Cada arquivo deve ter no máximo 10MB.</li>
          <li>Em caso de múltiplos arquivos, o tamanho total (soma de todos) deve ser de até 30MB.</li>
        </ul>`,
        type: 'negative',
        progress: true,
        position: 'top',
        actions: [{
          icon: 'close',
          round: true,
          color: 'white'
        }]
      })
    },
    handleSign (state) {
      this.sign = state
      LocalStorage.set('sign', this.sign)
    }
  },
  mounted () {
    this.$root.$on('mensagem-chat:focar-input-mensagem', () => this.$refs.inputEnvioMensagem.focus())
    const self = this
    window.addEventListener('paste', self.handleInputPaste)
    if (![null, undefined].includes(LocalStorage.getItem('sign'))) {
      this.handleSign(LocalStorage.getItem('sign'))
    }
  },
  beforeDestroy () {
    const self = this
    window.removeEventListener('paste', self.handleInputPaste)
  },
  destroyed () {
    this.$root.$off('mensagem-chat:focar-input-mensagem')
  }
}
</script>

<style lang="sass" scoped>
@media (max-width: 850px)
  .inputEnvioMensagem,
  .PickerFileMessage
    width: 150px

@media (min-width: 851px), (max-width: 1360px)
  .inputEnvioMensagem,
  .PickerFileMessage
    width: 200px !important
</style>

				
			

frontend\src\pages\configuracoes\Index.vue

				
					<template>
  <div>
    <q-list class="text-weight-medium">
      <q-item-label
        header
        class="text-bold text-h6 q-mb-lg"
      >Configurações</q-item-label>

      <q-item-label
        caption
        class="q-mt-lg q-pl-sm"
      >Módulo: Atendimento</q-item-label>
      <q-separator spaced />

      <q-item
        tag="label"
        v-ripple
      >
        <q-item-section>
          <q-item-label>Não visualizar Tickets já atribuidos à outros usuários</q-item-label>
          <q-item-label caption>Somente o usuário responsável pelo ticket e/ou os administradores visualizarão a atendimento.</q-item-label>
        </q-item-section>
        <q-item-section avatar>
          <q-toggle
            v-model="NotViewAssignedTickets"
            false-value="disabled"
            true-value="enabled"
            checked-icon="check"
            keep-color
            :color="NotViewAssignedTickets === 'enabled' ? 'green' : 'negative'"
            size="md"
            unchecked-icon="clear"
            @input="atualizarConfiguracao('NotViewAssignedTickets')"
          />
        </q-item-section>
      </q-item>

      <q-item
        tag="label"
        v-ripple
      >
        <q-item-section>
          <q-item-label>Não visualizar Tickets no ChatBot</q-item-label>
          <q-item-label caption>Somente administradores poderão visualizar tickets que estivem interagindo com o ChatBot.</q-item-label>
        </q-item-section>
        <q-item-section avatar>
          <q-toggle
            v-model="NotViewTicketsChatBot"
            false-value="disabled"
            true-value="enabled"
            checked-icon="check"
            keep-color
            :color="NotViewTicketsChatBot === 'enabled' ? 'green' : 'negative'"
            size="md"
            unchecked-icon="clear"
            @input="atualizarConfiguracao('NotViewTicketsChatBot')"
          />
        </q-item-section>
      </q-item>

      <q-item
        tag="label"
        v-ripple
      >
        <q-item-section>
          <q-item-label>Forçar atendimento via Carteira</q-item-label>
          <q-item-label caption>Caso o contato tenha carteira vínculada, o sistema irá direcionar o atendimento somente para os donos da carteira de clientes.</q-item-label>
        </q-item-section>
        <q-item-section avatar>
          <q-toggle
            v-model="DirectTicketsToWallets"
            false-value="disabled"
            true-value="enabled"
            checked-icon="check"
            keep-color
            :color="DirectTicketsToWallets === 'enabled' ? 'green' : 'negative'"
            size="md"
            unchecked-icon="clear"
            @input="atualizarConfiguracao('DirectTicketsToWallets')"
          />
        </q-item-section>
      </q-item>

      <q-item
        tag="label"
        v-ripple
      >
        <q-item-section>
          <q-item-label>Fluxo ativo para o Bot de atendimento</q-item-label>
          <q-item-label caption>Fluxo a ser utilizado pelo Bot para os novos atendimentos</q-item-label>
        </q-item-section>
        <q-item-section avatar>
          <q-select
            style="width: 300px"
            outlined
            dense
            rounded
            v-model="botTicketActive"
            :options="listaChatFlow"
            map-options
            emit-value
            option-value="id"
            option-label="name"
            @input="atualizarConfiguracao('botTicketActive')"
          />
        </q-item-section>
      </q-item>

      <q-item
        tag="label"
        v-ripple
      >
        <q-item-section>
          <q-item-label>Ignorar Mensagens de Grupo</q-item-label>
          <q-item-label caption>Habilitando esta opção o sistema não abrirá ticket para grupos</q-item-label>
        </q-item-section>
        <q-item-section avatar>
          <q-toggle
            v-model="ignoreGroupMsg"
            false-value="disabled"
            true-value="enabled"
            checked-icon="check"
            keep-color
            :color="ignoreGroupMsg === 'enabled' ? 'green' : 'negative'"
            size="md"
            unchecked-icon="clear"
            @input="atualizarConfiguracao('ignoreGroupMsg')"
          />
        </q-item-section>
      </q-item>

      <q-item
        tag="label"
        v-ripple
      >
        <q-item-section>
          <q-item-label>Recusar chamadas no Whatsapp</q-item-label>
          <q-item-label caption>Quando ativo, as ligações de aúdio e vídeo serão recusadas, automaticamente.</q-item-label>
        </q-item-section>
        <q-item-section avatar>
          <q-toggle
            v-model="rejectCalls"
            false-value="disabled"
            true-value="enabled"
            checked-icon="check"
            keep-color
            :color="rejectCalls === 'enabled' ? 'green' : 'negative'"
            size="md"
            unchecked-icon="clear"
            @input="atualizarConfiguracao('rejectCalls')"
          />
        </q-item-section>
      </q-item>

      <div
        class="row q-px-md"
        v-if="rejectCalls === 'enabled'"
      >
        <div class="col-12">
          <q-input
            rounded
            v-model="callRejectMessage"
            type="textarea"
            autogrow
            dense
            outlined
            label="Mensagem ao rejeitar ligação:"
            input-style="min-height: 6vh; max-height: 9vh;"
            debounce="700"
            @input="atualizarConfiguracao('callRejectMessage')"
          />
        </div>
      </div>

      <div class="row q-px-md">
        <q-item tag="label" class="col-8" v-ripple @click="abrirLink">
          <q-item-section>
            <q-item-label>Crie uma conta em hub.notificame.com.br e gere o seu token</q-item-label>
            <q-item-label caption>{{ montarUrlIntegração() }}</q-item-label>
          </q-item-section>
          <q-tooltip content-class="bg-negative text-bold">
            HUB Notificame (Beta)
          </q-tooltip>
        </q-item>

        <div class="col-4">
          <q-input
            class="blur-effect"
            v-model="hubToken"
            type="textarea"
            autogrow
            dense
            outlined
            label="Seu Token Notificame"
            input-style="min-height: 6vh;"
            debounce="700"
            @input="atualizarConfiguracao('hubToken')"
          />
        </div>
      </div>

    </q-list>

  </div>
</template>

<script type="rocketlazyloadscript">
import { ListarChatFlow } from 'src/service/chatFlow'
import { ListarConfiguracoes, AlterarConfiguracao } from 'src/service/configuracoes'
export default {
  name: 'IndexConfiguracoes',
  data () {
    return {
      configuracoes: [],
      listaChatFlow: [],
      NotViewAssignedTickets: null,
      NotViewTicketsChatBot: null,
      DirectTicketsToWallets: null,
      botTicketActive: null,
      ignoreGroupMsg: null,
      rejectCalls: null,
      callRejectMessage: '',
      hubToken: ''
    }
  },
  computed: {
    cBaseUrlIntegração () {
      return 'https://hub.notificame.com.br/signup/registrar?from=@ComunidadeZDG'
    }
  },
  methods: {
    abrirLink () {
      window.open('https://hub.notificame.com.br/signup/registrar?from=@ComunidadeZDG', '_blank')
    },
    montarUrlIntegração () {
      return `${this.cBaseUrlIntegração}`
    },
    async listarConfiguracoes () {
      const { data } = await ListarConfiguracoes()
      this.configuracoes = data
      this.configuracoes.forEach(el => {
        let value = el.value
        if (el.key === 'botTicketActive' && el.value) {
          value = +el.value
        }
        this.$data[el.key] = value
      })
    },
    async listarChatFlow () {
      const { data } = await ListarChatFlow()
      this.listaChatFlow = data.chatFlow
    },
    async atualizarConfiguracao (key) {
      const params = {
        key,
        value: this.$data[key]
      }
      try {
        await AlterarConfiguracao(params)
        this.$q.notify({
          type: 'positive',
          message: 'Configuração alterada!',
          progress: true,
          actions: [{
            icon: 'close',
            round: true,
            color: 'white'
          }]
        })
      } catch (error) {
        console.error('error - AlterarConfiguracao', error)
        this.$data[key] = this.$data[key] === 'enabled' ? 'disabled' : 'enabled'
        this.$notificarErro('Ocorreu um erro!', error)
      }
    }
  },
  async mounted () {
    await this.listarConfiguracoes()
    await this.listarChatFlow()
  }
}
</script>

<style lang="scss" scoped>
.blur-effect {
  filter: blur(5px)
}
</style>

				
			

frontend\src\pages\contatos\ContatoModal.vue

				
					<template>
  <q-dialog
    @show="fetchContact"
    @hide="$emit('update:modalContato', false)"
    :value="modalContato"
    persistent
  >
    <q-card
      class="q-pa-lg"
      style="min-width: 700px"
    >
      <q-card-section>
        <div class="text-h6">
          {{ contactId ? 'Editar Contato' : 'Adicionar Contato'  }}
        </div>
      </q-card-section>
      <q-card-section class="q-pa-sm q-pl-md text-bold">
        Dados Contato
      </q-card-section>
      <q-card-section class="q-pa-sm q-pl-md row q-col-gutter-md">
        <c-input
          class="col-12"
          outlined
          v-model="contato.name"
          :validator="$v.contato.name"
          @blur="$v.contato.name.$touch"
          label="Nome"
        />
        <c-input
          class="col-12"
          outlined
          v-model="contato.number"
          :validator="$v.contato.number"
          @blur="$v.contato.number.$touch"
          mask="+#############"
          placeholder="+DDI DDD 99999 9999"
          fill-mask
          unmasked-value
          hint="Informe número com DDI e DDD"
          label="Número"
        />
        <c-input
          class="col-12"
          outlined
          dense
          rounded
          :validator="$v.contato.email"
          @blur="$v.contato.email.$touch"
          v-model="contato.email"
          label="E-mail"
        />
      </q-card-section>
      <q-card-section class="q-pa-sm q-pl-md text-bold">
        Informações adicionais
      </q-card-section>
      <q-card-section class="q-pa-sm q-pl-md row q-col-gutter-md justify-center">
        <template v-for="(extraInfo, index) in contato.extraInfo">
          <div
            :key="index"
            class="col-12 row justify-center q-col-gutter-sm"
          >
            <q-input
              class="col-6"
              outlined
              dense
              rounded
              v-model="extraInfo.name"
              label="Descrição"
            />
            <q-input
              class="col-5"
              outlined
              dense
              rounded
              label="Informação"
              v-model="extraInfo.value"
            />
            <div class="col q-pt-md">
              <q-btn
                :key="index"
                icon="delete"
                round
                flat
                color="negative"
                @click="removeExtraInfo(index)"
              />
            </div>
          </div>
        </template>
        <div class="col-6">
          <q-btn
            class="full-width"
            color="primary"
            outline
            rounded
            label="Adicionar Informação"
            @click="contato.extraInfo.push({name: null, value: null})"
          />
        </div>
      </q-card-section>
      <q-card-actions
        align="right"
        class="q-mt-lg"
      >
        <q-btn
          rounded
          label="Sair"
          color="negative"
          v-close-popup
          class="q-px-md "
        />
        <q-btn
          class="q-ml-lg q-px-md"
          rounded
          label="Salvar"
          color="positive"
          @click="saveContact"
        />
      </q-card-actions>
    </q-card>
  </q-dialog>
</template>

<script type="rocketlazyloadscript">
import { required, email, minLength, maxLength } from 'vuelidate/lib/validators'
import { ObterContato, CriarContato, EditarContato } from 'src/service/contatos'
import { ListarUsuarios } from 'src/service/user'
export default {
  name: 'ContatoModal',
  props: {
    modalContato: {
      type: Boolean,
      default: false
    },
    contactId: {
      type: Number,
      default: null
    }
  },
  data () {
    return {
      contato: {
        name: null,
        number: null,
        email: '',
        extraInfo: [],
        wallets: []
      },
      usuarios: []
    }
  },
  validations: {
    contato: {
      name: { required, minLength: minLength(3), maxLength: maxLength(50) },
      email: { email },
      number: { minLength: minLength(8) }
    }
  },
  methods: {
    async fetchContact () {
      try {
        await this.listarUsuarios()
        if (!this.contactId) return
        const { data } = await ObterContato(this.contactId)
        this.contato = data
        if (data.number.substring(0, 2) === '55') {
          this.contato.number = data.number.substring(0)
        }
      } catch (error) {
        console.error(error)
        this.$notificarErro('Ocorreu um erro!', error)
      }
    },
    removeExtraInfo (index) {
      const newData = { ...this.contato }
      newData.extraInfo.splice(index, 1)
      this.contato = { ...newData }
    },
    async saveContact () {
      this.$v.contato.$touch()
      if (this.$v.contato.$error) {
        return this.$q.notify({
          type: 'warning',
          progress: true,
          position: 'top',
          message: 'Ops! Verifique os erros...',
          actions: [{
            icon: 'close',
            round: true,
            color: 'white'
          }]
        })
      }

      const contato = {
        ...this.contato,
        number: '' + this.contato.number // inserir o DDI do brasil para consultar o número
      }

      try {
        if (this.contactId) {
          const { data } = await EditarContato(this.contactId, contato)
          this.$emit('contatoModal:contato-editado', data)
          this.$q.notify({
            type: 'info',
            progress: true,
            position: 'top',
            textColor: 'black',
            message: 'Contato editado!',
            actions: [{
              icon: 'close',
              round: true,
              color: 'white'
            }]
          })
        } else {
          const { data } = await CriarContato(contato)
          this.$q.notify({
            type: 'positive',
            progress: true,
            position: 'top',
            message: 'Contato criado!',
            actions: [{
              icon: 'close',
              round: true,
              color: 'white'
            }]
          })
          this.$emit('contatoModal:contato-criado', data)
        }
        this.$emit('update:modalContato', false)
      } catch (error) {
        console.error(error)
        this.$notificarErro('Ocorreu um erro ao criar o contato', error)
      }
    },
    async listarUsuarios () {
      try {
        const { data } = await ListarUsuarios()
        this.usuarios = data.users
      } catch (error) {
        console.error(error)
        this.$notificarErro('Problema ao carregar usuários', error)
      }
    }

  },
  destroyed () {
    this.$v.contato.$reset()
  }
}
</script>

<style lang="scss" scoped>
</style>

				
			

frontend\src\pages\sessaoWhatsapp\Index.vue

				
					<template>
  <div>
    <div class="row col full-width q-pa-sm">
      <q-card
        flat
        class="full-width"
      >
        <q-card-section class="text-h6 text-bold">
          Canais
          <div class="absolute-right q-pa-md">
            <q-btn
              rounded
              color="black"
              icon="mdi-plus"
              label="Adicionar"
              @click="modalWhatsapp = true"
            />
          </div>
        </q-card-section>
      </q-card>
    </div>
    <div class="row full-width">
      <template v-for="item in canais">
        <q-card
          flat
          bordered
          class="col-xs-12 col-sm-5 col-md-4 col-lg-3 q-ma-sm"
          :key="item.id"
        >
          <q-item>
            <q-item-section avatar>
              <q-avatar>
                <q-icon
                  size="40px"
                  :name="`img:${item.type}-logo.png`"
                />
              </q-avatar>
            </q-item-section>
            <q-item-section>
              <q-item-label class="text-h6 text-bold">Nome: {{ item.name }}</q-item-label>
              <q-item-label class="text-h6 text-caption">
                {{ item.type }}
              </q-item-label>
            </q-item-section>
            <q-item-section side>
              <q-btn
                round
                flat
                dense
                icon="mdi-pen"
                @click="handleOpenModalWhatsapp(item)"
                v-if="isAdmin"
              />
            </q-item-section>
          </q-item>
          <q-separator />
          <q-card-section>
            <ItemStatusChannel :item="item" />
            <template v-if="item.type === 'messenger'">
              <div class="text-body2 text-bold q-mt-sm">
                <span> Página: </span>
                {{ item.fbObject && item.fbObject.name || 'Nenhuma página configurada.' }}
              </div>
            </template>
          </q-card-section>
          <q-card-section>
            <q-select
              v-if="!item.type.includes('hub')"
              outlined
              dense
              rounded
              label="Bot"
              v-model="item.chatFlowId"
              :options="listaChatFlow"
              map-options
              emit-value
              option-value="id"
              option-label="name"
              clearable
              @input="handleSaveWhatsApp(item)"
            />
          </q-card-section>
          <q-separator />
          <q-card-actions
            class="q-gutter-md q-pa-md q-pt-none"
            align="center"
          >
            <template v-if="item.type !== 'messenger'">
              <q-btn
                rounded
                v-if="item.type == 'whatsapp' && item.status == 'qrcode'"
                color="blue-5"
                label="QR Code"
                @click="handleOpenQrModal(item, 'btn-qrCode')"
                icon-right="watch_later"
                :disable="!isAdmin"
              />

              <div
                v-if="item.status == 'DISCONNECTED'"
                class="q-gutter-sm"
              >
                <q-btn
                  rounded
                  color="positive"
                  label="Conectar"
                  @click="handleStartWhatsAppSession(item.id)"
                />
                <q-btn
                  rounded
                  v-if="item.status == 'DISCONNECTED' && item.type == 'whatsapp'"
                  color="blue-5"
                  label="Novo QR Code"
                  @click="handleRequestNewQrCode(item, 'btn-qrCode')"
                  icon-right="watch_later"
                  :disable="!isAdmin"
                />
              </div>

              <div
                v-if="item.status == 'OPENING'"
                class="row items-center q-gutter-sm flex flex-inline"
              >
                <div class="text-bold">
                  Conectando
                </div>
                <q-spinner-radio
                  color="positive"
                  size="2em"
                />
                <q-separator
                  vertical
                  spaced=""
                />
              </div>

              <q-btn
                v-if="['OPENING', 'CONNECTED', 'PAIRING', 'TIMEOUT'].includes(item.status) && !item.type.includes('hub')"
                color="negative"
                label="Desconectar"
                @click="handleDisconectWhatsSession(item.id)"
                :disable="!isAdmin"
                class="q-mx-sm"
              />
            </template>
            <q-btn
              color="red"
              icon="mdi-delete"
              @click="deleteWhatsapp(item)"
              :disable="!isAdmin"
              dense
              round
              flat
              class="absolute-bottom-right"
            >
              <q-tooltip>
                Deletar conexáo
              </q-tooltip>
            </q-btn>
          </q-card-actions>
        </q-card>
      </template>
    </div>
    <ModalQrCode
      :abrirModalQR.sync="abrirModalQR"
      :channel="cDadosWhatsappSelecionado"
      @gerar-novo-qrcode="v => handleRequestNewQrCode(v, 'btn-qrCode')"
    />
    <ModalWhatsapp
      :modalWhatsapp.sync="modalWhatsapp"
      :whatsAppEdit.sync="whatsappSelecionado"
      @recarregar-lista="listarWhatsapps"
    />
    <q-inner-loading :showing="loading">
      <q-spinner-gears
        size="50px"
        color="primary"
      />
    </q-inner-loading>
  </div>
</template>

<script type="rocketlazyloadscript">

import { DeletarWhatsapp, DeleteWhatsappSession, StartWhatsappSession, ListarWhatsapps, RequestNewQrCode, UpdateWhatsapp } from 'src/service/sessoesWhatsapp'
import { format, parseISO } from 'date-fns'
import pt from 'date-fns/locale/pt-BR/index'
import ModalQrCode from './ModalQrCode'
import { mapGetters } from 'vuex'
import ModalWhatsapp from './ModalWhatsapp'
import ItemStatusChannel from './ItemStatusChannel'
import { ListarChatFlow } from 'src/service/chatFlow'

const userLogado = JSON.parse(localStorage.getItem('usuario'))

export default {
  name: 'IndexSessoesWhatsapp',
  components: {
    ModalQrCode,
    ModalWhatsapp,
    ItemStatusChannel
  },
  data () {
    return {
      loading: false,
      userLogado,
      isAdmin: false,
      abrirModalQR: false,
      modalWhatsapp: false,
      whatsappSelecionado: {},
      listaChatFlow: [],
      whatsAppId: null,
      canais: [],
      objStatus: {
        qrcode: ''
      },
      columns: [
        {
          name: 'name',
          label: 'Nome',
          field: 'name',
          align: 'left'
        },
        {
          name: 'status',
          label: 'Status',
          field: 'status',
          align: 'center'
        },
        {
          name: 'session',
          label: 'Sessão',
          field: 'status',
          align: 'center'
        },
        {
          name: 'number',
          label: 'Número',
          field: 'number',
          align: 'center'
        },
        {
          name: 'updatedAt',
          label: 'Última Atualização',
          field: 'updatedAt',
          align: 'center',
          format: d => this.formatarData(d, 'dd/MM/yyyy HH:mm')
        },
        {
          name: 'isDefault',
          label: 'Padrão',
          field: 'isDefault',
          align: 'center'
        },
        {
          name: 'acoes',
          label: 'Ações',
          field: 'acoes',
          align: 'center'
        }
      ]
    }
  },
  watch: {
    whatsapps: {
      handler () {
        this.canais = JSON.parse(JSON.stringify(this.whatsapps))
      },
      deep: true
    }
  },
  computed: {
    ...mapGetters(['whatsapps']),
    cDadosWhatsappSelecionado () {
      const { id } = this.whatsappSelecionado
      return this.whatsapps.find(w => w.id === id)
    }
  },
  methods: {
    formatarData (data, formato) {
      return format(parseISO(data), formato, { locale: pt })
    },
    handleOpenQrModal (channel) {
      this.whatsappSelecionado = channel
      this.abrirModalQR = true
    },
    handleOpenModalWhatsapp (whatsapp) {
      this.whatsappSelecionado = whatsapp
      this.modalWhatsapp = true
    },
    async handleDisconectWhatsSession (whatsAppId) {
      this.$q.dialog({
        title: 'Atenção!! Deseja realmente desconectar? ',
        cancel: {
          label: 'Não',
          color: 'primary',
          push: true
        },
        ok: {
          label: 'Sim',
          color: 'negative',
          push: true
        },
        persistent: true
      }).onOk(() => {
        this.loading = true
        DeleteWhatsappSession(whatsAppId).then(() => {
          const whatsapp = this.whatsapps.find(w => w.id === whatsAppId)
          this.$store.commit('UPDATE_WHATSAPPS', {
            ...whatsapp,
            status: 'DISCONNECTED'
          })
        }).finally(f => {
          this.loading = false
        })
      })
    },
    async handleStartWhatsAppSession (whatsAppId) {
      try {
        await StartWhatsappSession(whatsAppId)
      } catch (error) {
        console.error(error)
      }
    },
    async handleRequestNewQrCode (channel, origem) {
      if (channel.type === 'telegram' && !channel.tokenTelegram) {
        this.$notificarErro('Necessário informar o token para Telegram')
      }
      this.loading = true
      try {
        await RequestNewQrCode({ id: channel.id, isQrcode: true })
        setTimeout(() => {
          this.handleOpenQrModal(channel)
        }, 2000)
      } catch (error) {
        console.error(error)
      }
      this.loading = false
    },
    async listarWhatsapps () {
      const { data } = await ListarWhatsapps()
      this.$store.commit('LOAD_WHATSAPPS', data)
    },
    async deleteWhatsapp (whatsapp) {
      this.$q.dialog({
        title: 'Atenção!! Deseja realmente deletar? ',
        message: 'Não é uma boa ideia apagar se já tiver gerado atendimentos para esse whatsapp.',
        cancel: {
          label: 'Não',
          color: 'primary',
          push: true
        },
        ok: {
          label: 'Sim',
          color: 'negative',
          push: true
        },
        persistent: true
      }).onOk(() => {
        this.loading = true
        DeletarWhatsapp(whatsapp.id).then(r => {
          this.$store.commit('DELETE_WHATSAPPS', whatsapp.id)
        }).finally(f => {
          this.loading = false
        })
      })
    },
    async listarChatFlow () {
      const { data } = await ListarChatFlow()
      this.listaChatFlow = data.chatFlow
    },
    async handleSaveWhatsApp (whatsapp) {
      try {
        await UpdateWhatsapp(whatsapp.id, whatsapp)
        this.$q.notify({
          type: 'positive',
          progress: true,
          position: 'top',
          message: `Whatsapp ${whatsapp.id ? 'editado' : 'criado'} com sucesso!`,
          actions: [{
            icon: 'close',
            round: true,
            color: 'white'
          }]
        })
      } catch (error) {
        console.error(error)
        return this.$q.notify({
          type: 'error',
          progress: true,
          position: 'top',
          message: 'Ops! Verifique os erros... O nome da conexão não pode existir na plataforma, é um identificador único.',
          actions: [{
            icon: 'close',
            round: true,
            color: 'white'
          }]
        })
      }
    }
  },
  mounted () {
    this.isAdmin = localStorage.getItem('profile')
    this.listarWhatsapps()
    this.listarChatFlow()
  }
}
</script>

<style lang="scss" scoped>
</style>

				
			

frontend\src\pages\sessaoWhatsapp\ModalWhatsapp.vue

				
					<template>
  <q-dialog
    :value="modalWhatsapp"
    @hide="fecharModal"
    @show="abrirModal"
    persistent
  >
    <q-card
      class="q-pa-md"
      style="width: 500px"
    >
      <q-card-section>
        <div class="text-h6">
          <q-icon
            size="50px"
            class="q-mr-md"
            :name="whatsapp.type ? `img:${whatsapp.type}-logo.png` : 'mdi-alert'"
          /> {{ whatsapp.id ? 'Editar' :
              'Adicionar'
            }}
          Canal
        </div>
      </q-card-section>
      <q-card-section>
        <div class="row">
          <div class="col-12 q-my-sm">
            <q-select
              :disable="!!whatsapp.id"
              v-model="whatsapp.type"
              :options="optionsWhatsappsTypes"
              label="Tipo"
              emit-value
              map-options
              outlined
              rounded
              dense
            />
          </div>
          <div class="col-12">
            <c-input
              outlined
              rounded
              label="Nome"
              dense
              v-model="whatsapp.name"
              :validator="$v.whatsapp.name"
              @blur="$v.whatsapp.name.$touch"
            />
          </div>

          <div class="col-12 q-my-sm" v-if="whatsapp.type === 'hub'">
            <q-select v-model="selectedHubOption"
              rounded
              outlined
              dense
              :options="hubOptions"
              label="Selecione um Hub"
              filled />
          </div>

          <div
            class="col-12 q-mt-md"
            v-if="whatsapp.type === 'telegram'"
          >
            <c-input
              outlined
              dense
              label="Token Telegram"
              v-model="whatsapp.tokenTelegram"
            />
          </div>
          <div
            class="q-mt-md row full-width justify-center"
            v-if="whatsapp.type === 'instagram'"
          >
            <div class="col">
              <fieldset class="full-width q-pa-md rounded-all">
                <legend>Dados da conta do Instagram</legend>
                <div
                  class="col-12 q-mb-md"
                  v-if="whatsapp.type === 'instagram'"
                >
                  <c-input
                    outlined
                    dense
                    label="Usuário"
                    v-model="whatsapp.instagramUser"
                    hint="Seu usuário do Instagram (sem @)"
                  />
                </div>
                <div
                  v-if="whatsapp.type === 'instagram' && !isEdit"
                  class="text-center"
                >
                  <q-btn
                    color="positive"
                    icon="edit"
                    label="Nova senha"
                    @click="isEdit = !isEdit"
                  >
                    <q-tooltip>
                      Alterar senha
                    </q-tooltip>
                  </q-btn>
                </div>
                <div
                  class="col-12"
                  v-if="whatsapp.type === 'instagram' && isEdit"
                >
                  <c-input
                    outlined
                    rounded
                    label="Senha"
                    :type="isPwd ? 'password' : 'text'"
                    v-model="whatsapp.instagramKey"
                    hint="Senha utilizada para logar no Instagram"
                    placeholder="*************"
                    :disable="!isEdit"
                  >
                    <template v-slot:after>
                      <q-btn
                        class="bg-padrao"
                        round
                        flat
                        color="negative"
                        icon="mdi-close"
                        @click="isEdit = !isEdit"
                      >
                        <q-tooltip>
                          Cancelar alteração de senha
                        </q-tooltip>

                      </q-btn>
                    </template>
                    <template v-slot:append>
                      <q-icon
                        :name="isPwd ? 'visibility_off' : 'visibility'"
                        class="cursor-pointer"
                        @click="isPwd = !isPwd"
                      />
                    </template>
                  </c-input>
                </div>
              </fieldset>

            </div>
          </div>
        </div>

        <div class="row q-my-md" v-if="!whatsapp?.type?.includes('hub')">
          <div class="col-12 relative-position">
            <label class="text-caption">Mensagem Despedida:
            </label>
            <textarea
              ref="inputFarewellMessage"
              style="min-height: 15vh; max-height: 15vh;"
              class="q-pa-sm rounded-all bg-white full-width"
              placeholder="Digite a mensagem"
              autogrow
              dense
              outlined
              v-model="whatsapp.farewellMessage"
            >
            </textarea>
            <div class="absolute-top-right">
              <q-btn
                rounded
                dense
                color="black"
                style="margin-bottom: -40px; margin-right: -10px"
              >
                <q-icon
                  size="1.5em"
                  name="mdi-variable"
                />
                <q-tooltip>
                  Variáveis
                </q-tooltip>
                <q-menu touch-position>
                  <q-list
                    dense
                    style="min-width: 100px"
                  >
                    <q-item
                      v-for="variavel in variaveis"
                      :key="variavel.label"
                      clickable
                      @click="onInsertSelectVariable(variavel.value)"
                      v-close-popup
                    >
                      <q-item-section>{{ variavel.label }}</q-item-section>
                    </q-item>
                  </q-list>
                </q-menu>
              </q-btn>
            </div>
          </div>
        </div>
      </q-card-section>
      <q-card-actions
        align="center"
        class="q-mt-lg"
      >
        <q-btn
          rounded
          label="Sair"
          class="q-px-md q-mr-lg"
          color="negative"
          v-close-popup
        />
        <q-btn
          label="Salvar"
          color="positive"
          rounded
          class="q-px-md"
          @click="handleSaveWhatsApp(whatsapp)"
        />
      </q-card-actions>
    </q-card>
  </q-dialog>
</template>

<script type="rocketlazyloadscript">
import { required, minLength, maxLength } from 'vuelidate/lib/validators'
import { UpdateWhatsapp, CriarWhatsapp } from 'src/service/sessoesWhatsapp'
import { ListarHub, AdicionarHub } from 'src/service/hub'
import cInput from 'src/components/cInput.vue'
import { copyToClipboard, Notify } from 'quasar'

export default {
  components: { cInput },
  name: 'ModalWhatsapp',
  props: {
    modalWhatsapp: {
      type: Boolean,
      default: false
    },
    whatsAppId: {
      type: Number,
      default: null
    },
    whatsAppEdit: {
      type: Object,
      default: () => { }
    }
  },
  data () {
    return {
      hubOptions: [],
      selectedHubOption: null,
      isPwd: true,
      isEdit: false,
      whatsapp: {
        name: '',
        isDefault: false,
        tokenTelegram: '',
        instagramUser: '',
        instagramKey: '',
        tokenAPI: '',
        type: 'whatsapp',
        farewellMessage: ''
      },
      optionsWhatsappsTypes: [
        { label: 'Whatsapp', value: 'whatsapp' },
        { label: 'Telegram', value: 'telegram' },
        { label: 'Instagram', value: 'instagram' },
        { label: 'Hub Notificame', value: 'hub' }
      ],
      variaveis: [
        { label: 'Nome', value: '{{name}}' },
        { label: 'Saudação', value: '{{greeting}}' },
        { label: 'Protocolo', value: '{{protocol}}' }
      ]
    }
  },
  validations: {
    whatsapp: {
      name: { required, minLength: minLength(3), maxLength: maxLength(50) },
      isDefault: {}
    }
  },
  computed: {
    cBaseUrlIntegração () {
      return this.whatsapp.UrlMessengerWebHook
    }
  },
  watch: {
    'whatsapp.type' (newType) {
      if (newType === 'hub') {
        this.listarHubOptions()
      }
    }
  },
  methods: {
    async listarHubOptions () {
      try {
        const response = await ListarHub()
        this.hubOptions = response.data
          .filter(hub => hub.channel === 'facebook' || hub.channel === 'instagram')
          .map(hub => ({
            label: hub.name,
            value: hub
          }))
      } catch (error) {
        console.error('Erro ao listar Hubs:', error)
      }
    },
    copy (text) {
      copyToClipboard(text)
        .then(this.$notificarSucesso('URL de integração copiada!'))
        .catch()
    },

    onInsertSelectVariable (variable) {
      const self = this
      var tArea = this.$refs.inputFarewellMessage
      // get cursor's position:
      var startPos = tArea.selectionStart,
        endPos = tArea.selectionEnd,
        cursorPos = startPos,
        tmpStr = tArea.value
      // filter:
      if (!variable) {
        return
      }
      // insert:
      self.txtContent = this.whatsapp.farewellMessage
      self.txtContent = tmpStr.substring(0, startPos) + variable + tmpStr.substring(endPos, tmpStr.length)
      this.whatsapp.farewellMessage = self.txtContent
      // move cursor:
      setTimeout(() => {
        tArea.selectionStart = tArea.selectionEnd = cursorPos + 1
      }, 10)
    },

    fecharModal () {
      this.whatsapp = {
        name: '',
        isDefault: false
      }
      this.$emit('update:whatsAppEdit', {})
      this.$emit('update:modalWhatsapp', false)
    },
    abrirModal () {
      if (this.whatsAppEdit.id) {
        this.whatsapp = { ...this.whatsAppEdit }
      }
    },
    async handleSaveWhatsApp (whatsapp) {
      this.$v.whatsapp.$touch()
      if (whatsapp.type !== 'hub') {
        if (this.$v.whatsapp.$error) {
          return this.$q.notify({
            type: 'warning',
            progress: true,
            position: 'top',
            message: 'Ops! Verifique os erros...',
            actions: [{
              icon: 'close',
              round: true,
              color: 'white'
            }]
          })
        }
        try {
          if (this.whatsAppEdit.id) {
            await UpdateWhatsapp(this.whatsAppEdit.id, whatsapp)
          } else {
            await CriarWhatsapp(whatsapp)
          }
          this.$q.notify({
            type: 'positive',
            progress: true,
            position: 'top',
            message: `Whatsapp ${this.whatsAppEdit.id ? 'editado' : 'criado'} com sucesso!`,
            actions: [{
              icon: 'close',
              round: true,
              color: 'white'
            }]
          })
          this.$emit('recarregar-lista')
          this.fecharModal()
        } catch (error) {
          console.error(error, error.data.error === 'ERR_NO_PERMISSION_CONNECTIONS_LIMIT')
          if (error.data.error === 'ERR_NO_PERMISSION_CONNECTIONS_LIMIT') {
            Notify.create({
              type: 'negative',
              message: 'Limite de conexões atingida.',
              caption: 'ERR_NO_PERMISSION_CONNECTIONS_LIMIT',
              position: 'top',
              progress: true
            })
          } else {
            console.error(error)
            return this.$q.notify({
              type: 'error',
              progress: true,
              position: 'top',
              message: 'Ops! Verifique os erros... O nome da conexão não pode existir na plataforma, é um identificador único.',
              actions: [{
                icon: 'close',
                round: true,
                color: 'white'
              }]
            })
          }
        }
      } else if (whatsapp.type === 'hub') {
        if (this.$v.whatsapp.$error) {
          return this.$q.notify({
            type: 'warning',
            progress: true,
            position: 'top',
            message: 'Ops! Verifique os erros...',
            actions: [{
              icon: 'close',
              round: true,
              color: 'white'
            }]
          })
        }
        if (!this.selectedHubOption) {
          return this.$q.notify({
            type: 'warning',
            message: 'Por favor, selecione um Hub antes de continuar.',
            position: 'top',
            actions: [{ icon: 'close', round: true, color: 'white' }]
          })
        }
        const selectedHub = this.selectedHubOption.value
        const data = {
          name: this.whatsapp.name,
          status: 'CONNECTED',
          isDefault: false,
          type: 'hub_' + selectedHub.channel,
          wabaId: selectedHub.id,
          number: selectedHub.id,
          profilePic: selectedHub.profile_pic,
          phone: selectedHub
        }

        const payload = {
          channels: [data]
        }
        try {
          const response = await AdicionarHub(payload)
          console.log(response)
          this.$q.notify({
            type: 'positive',
            message: 'Hub adicionado com sucesso!',
            position: 'top'
          })
          this.$emit('recarregar-lista')
          this.fecharModal()
        } catch (error) {
          console.error('Erro ao adicionar o Hub:', error)
          this.$q.notify({
            type: 'negative',
            message: 'Erro ao adicionar o Hub. Por favor, tente novamente.',
            position: 'top'
          })
        }
      }
    }
  },
  destroyed () {
    this.$v.whatsapp.$reset()
  }
}
</script>

<style lang="scss" scoped>
</style>

				
			

SERVICES

frontend\src\service\hub.js

				
					import request from 'src/service/request'

export function AdicionarHub (data) {
  return request({
    url: '/hub-channel/',
    method: 'post',
    data
  })
}

export function ListarHub () {
  return request({
    url: '/hub-channel/',
    method: 'get'
  })
}

export function EnviarMensagemHub (ticketId, data) {
  return request({
    url: `/hub-message/${ticketId}`,
    method: 'post',
    data
  })
}

				
			

Obtenha suporte para instalação na comunidade ZDG

 

🔗 Notificame

🧩 Facebook

🧩 Instagram

🧩 Izing.Io

Conheça alguns dos alunos da melhor comunidade de marketing de conversa do Brasil

E entenda porque tanta gente está economizando tempo e ganhando dinheiro explorando robôs e automações, mesmo sem nunca antes ter tido contato com uma API.

"O Pedrinho pega na nossa mão. Se eu consegui, você também consegue."

"Eu sou desenvolvedor de sistemas, e venho utilizando as soluções do Pedrinho para integrar nos meus sistemas, e o ganho de tempo é excepcional."

"O Pedrinho tem uma didática excelente e com o curso dele, consegui colocar minha API para rodar 24 horas e estou fazendo vendas todos os dias."

"A estratégia mais eficiente e totalmente escalável."

Comunidade ZDG © 2023 | CNPJ: 35.617.749/0001-67 | Razão Social: BIANCA SANT ANA PEREIRA 10398514607
Rua Alaor Ferreira da Fonseca, 295, CEP 37.136-132, Alfenas – MG | Tel: (35) 9 8875-4197 | E-mail: contato@comunidadezdg.com.br | Política de Privacidade | Termos de Uso
Art. 49 do Código de Defesa do Consumidor | GARANTIA TOTAL DE 7 DIAS | Este produto não garante a obtenção de resultados. Qualquer referência ao desempenho de uma estratégia não deve ser interpretada como uma garantia de resultados.