muita coisa

This commit is contained in:
Gabriel Pastori 2023-11-05 00:41:04 -03:00
parent e0f6d28a88
commit 558545e469
23 changed files with 3005 additions and 2824 deletions

View File

@ -5,7 +5,7 @@
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to Vuetify 3</title>
<title>Evolution Manager</title>
</head>
<body>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -5,3 +5,18 @@
<script setup>
//
</script>
<style lang="scss">
@for $i from 0 through 8 {
.gap-x-#{$i} {
column-gap: #{$i * 0.25}rem;
}
.gap-y-#{$i} {
row-gap: #{$i * 0.25}rem;
}
.gap-#{$i} {
gap: #{$i * 0.25}rem;
}
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,6 +0,0 @@
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M261.126 140.65L164.624 307.732L256.001 466L377.028 256.5L498.001 47H315.192L261.126 140.65Z" fill="#1697F6"/>
<path d="M135.027 256.5L141.365 267.518L231.64 111.178L268.731 47H256H14L135.027 256.5Z" fill="#AEDDFF"/>
<path d="M315.191 47C360.935 197.446 256 466 256 466L164.624 307.732L315.191 47Z" fill="#1867C0"/>
<path d="M268.731 47C76.0026 47 141.366 267.518 141.366 267.518L268.731 47Z" fill="#7BC6FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

View File

@ -0,0 +1,59 @@
<template>
<v-tabs>
<v-tab v-for="tab in tabs" :key="tab.id" :value="tab.id">
<v-icon start>{{ tab.icon }}</v-icon>
{{ tab.title }}
</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item v-for="tab in tabs" :key="tab.id" :value="tab.id" >
<div class="d-flex flex-column gap-8">
<component
v-for="component in tab.components"
:key="component"
:is="component"
:instance="instance"
/>
</div>
</v-window-item>
</v-window>
</template>
<script>
import Webhook from "./settings/webhook.vue";
import Websocket from "./settings/Websocket.vue";
import Rabbitmq from "./settings/Rabbitmq.vue";
import Chatwoot from "./settings/Chatwoot.vue";
import Typebot from "./settings/Typebot.vue";
export default {
components: {
Webhook,
Websocket,
Rabbitmq,
Chatwoot,
Typebot,
},
data: () => ({
tab: "settings",
tabs: [
{
id: "settings",
icon: "mdi-cog",
title: "Configurações",
components: ["Webhook", "Websocket", "Rabbitmq", "Chatwoot", "Typebot"],
},
],
}),
props: {
instance: {
type: Object,
required: true,
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,99 @@
<template>
<v-card
variant="outlined"
class="d-flex align-center gap-4 pa-2"
rounded="xl"
>
<v-avatar size="100" rounded="xl">
<v-icon
v-if="
instance.instance.status != 'open' &&
statusMapper[instance.instance.status].icon
"
size="70"
>
{{ statusMapper[instance.instance.status].icon }}
</v-icon>
<v-img
v-else
:src="
instance.instance.profilePictureUrl ||
'https://cdn.vuetifyjs.com/images/lists/1.jpg'
"
/>
</v-avatar>
<div class="d-flex flex-column">
<span class="text-overline" style="line-height: 1em">
{{ owner }}
</span>
<h2 class="mb-0">{{ instance.instance.instanceName }}</h2>
<small>{{ instance.instance.profileStatus }}</small>
</div>
<v-spacer></v-spacer>
<v-btn
@click="disconnectInstance"
:disabled="instance.instance.status === 'close'"
:loading="disconnect.loading"
variant="tonal"
color="error"
size="small"
>
<v-icon start>mdi-cellphone-nfc-off</v-icon>
{{ disconnect.confirm ? "Tem Certeza?" : "Desconectar" }}
</v-btn>
</v-card>
</template>
<script>
import { useAppStore } from "@/store/app";
import statusMapper from "@/helpers/mappers/status";
import instanceController from "@/services/instanceController";
export default {
name: "InstanceHeader",
data: () => ({
disconnect: {
confirm: false,
loading: false,
},
statusMapper: statusMapper,
AppStore: useAppStore(),
}),
methods: {
async disconnectInstance() {
if (!this.disconnect.confirm) return (this.disconnect.confirm = true);
this.disconnect.loading = true;
try {
this.disconnect.confirm = false;
await instanceController.logout(this.instance.instance.instanceName);
await this.AppStore.reconnect();
} catch (e) {
console.log(e);
alert(e.message || e.error || "Erro desconhecido");
} finally {
this.disconnect.loading = false;
}
},
},
computed: {
owner() {
if (!this.instance?.instance?.owner)
return (
this.statusMapper[this.instance.instance.status]?.text ||
"Desconhecido"
);
return (this.instance?.instance?.owner || "").split("@")[0];
},
},
props: {
instance: {
type: Object,
required: true,
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,220 @@
<template>
<v-card variant="outlined" :loading="loading">
<v-card-title class="d-flex align-center">
<v-icon start>mdi-chat</v-icon>
Chatwoot
<v-spacer></v-spacer>
<v-btn
size="small"
icon
:disabled="loading"
variant="tonal"
@click="expanded = !expanded"
:style="{ transform: expanded ? 'rotate(180deg)' : '' }"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="expanded">
<v-alert v-if="error" type="error" class="mb-3">
{{ error }}
</v-alert>
<v-form v-model="valid">
<v-text-field
v-model="chatwootData.url"
label="URL"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(url) => {
if (!url) return 'URL é obrigatório';
if (!url.startsWith('http'))
return 'URL deve começar com http ou https';
return true;
},
]"
/>
<div class="d-flex gap-4 flex-wrap">
<div class="flex-grow-1">
<v-text-field
v-model="chatwootData.account_id"
label="ID da conta"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(account_id) => {
if (!account_id) return 'ID da conta é obrigatório';
return true;
},
]"
/>
</div>
<div class="flex-grow-1">
<v-text-field
v-model="chatwootData.token"
label="Token da conta"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(token) => {
if (!token) return 'Token é obrigatório';
return true;
},
]"
/>
</div>
</div>
<div class="d-flex gap-x-4 flex-wrap">
<div>
<v-checkbox
v-model="chatwootData.sign_msg"
label="Assinar mensagens"
:disabled="loading"
hide-details
class="mb-3"
density="compact"
/>
</div>
<div>
<v-checkbox
v-model="chatwootData.reopen_conversation"
label="Reabrir conversa"
:disabled="loading"
hide-details
class="mb-3"
density="compact"
/>
</div>
<div>
<v-checkbox
v-model="chatwootData.conversation_pending"
label="Conversa pendente"
:disabled="loading"
hide-details
class="mb-3"
density="compact"
/>
</div>
</div>
</v-form>
</v-card-text>
<v-card-actions v-if="expanded">
<v-switch
v-model="chatwootData.enabled"
label="Habilitado"
color="primary"
:disabled="loading"
hide-details
></v-switch>
<v-spacer></v-spacer>
<v-btn
:disabled="
!valid ||
JSON.stringify(chatwootData) === JSON.stringify(defaultChatwootData)
"
:loading="loading"
color="primary"
@click="saveChatwoot"
variant="tonal"
>
Salvar
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import instanceController from "@/services/instanceController";
export default {
name: "InstanceChatwoot",
props: {
instance: {
type: Object,
required: true,
},
},
data: () => ({
expanded: false,
loading: false,
error: false,
valid: false,
chatwootData: {
enabled: false,
url: "",
account_id: "",
token: "",
sign_msg: true,
reopen_conversation: true,
conversation_pending: false,
},
defaultChatwootData: {
enabled: false,
url: "",
account_id: "",
token: "",
sign_msg: true,
reopen_conversation: true,
conversation_pending: false,
},
}),
methods: {
async saveChatwoot() {
try {
this.loading = true;
this.error = false;
await instanceController.chatwoot.set(
this.instance.instance.instanceName,
{
...this.chatwootData,
url: this.chatwootData.url.trim().replace(/\/$/, ""),
}
);
this.defaultChatwootData = Object.assign({}, this.chatwootData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
async loadChatwoot() {
try {
this.loading = true;
this.error = false;
const chatwootData = await instanceController.chatwoot.get(
this.instance.instance.instanceName
);
this.chatwootData = Object.assign({}, chatwootData);
this.defaultChatwootData = Object.assign({}, chatwootData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
},
watch: {
expanded(expanded) {
if (expanded) this.loadChatwoot();
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,154 @@
<template>
<v-card variant="outlined" :loading="loading">
<v-card-title class="d-flex align-center">
<v-icon start>mdi-rabbit</v-icon>
RabbitMQ
<v-spacer></v-spacer>
<v-btn
size="small"
icon
:disabled="loading"
variant="tonal"
@click="expanded = !expanded"
:style="{ transform: expanded ? 'rotate(180deg)' : '' }"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="expanded">
<v-alert v-if="error" type="error" class="mb-3">
{{ error }}
</v-alert>
<v-form v-model="valid">
<v-select
:items="rabbitmqEventsType"
v-model="rabbitmqData.events"
:disabled="loading"
label="Eventos"
hide-details
class="mb-3"
multiple
outlined
dense
chips
/>
</v-form>
</v-card-text>
<v-card-actions v-if="expanded">
<v-switch
v-model="rabbitmqData.enabled"
label="Habilitado"
color="primary"
:disabled="loading"
hide-details
></v-switch>
<v-spacer></v-spacer>
<v-btn
:disabled="
!valid ||
JSON.stringify(rabbitmqData) === JSON.stringify(defaultRabbitmqData)
"
:loading="loading"
color="primary"
@click="saveRabbitmq"
variant="tonal"
>
Salvar
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import instanceController from "@/services/instanceController";
export default {
name: "InstanceRabbitmq",
props: {
instance: {
type: Object,
required: true,
},
},
data: () => ({
expanded: false,
loading: false,
error: false,
valid: false,
rabbitmqData: {
enabled: false,
events: [],
},
defaultRabbitmqData: {
enabled: false,
events: [],
},
rabbitmqEventsType: [
"APPLICATION_STARTUP",
"QRCODE_UPDATED",
"MESSAGES_SET",
"MESSAGES_UPSERT",
"MESSAGES_UPDATE",
"MESSAGES_DELETE",
"SEND_MESSAGE",
"CONTACTS_SET",
"CONTACTS_UPSERT",
"CONTACTS_UPDATE",
"PRESENCE_UPDATE",
"CHATS_SET",
"CHATS_UPSERT",
"CHATS_UPDATE",
"CHATS_DELETE",
"GROUPS_UPSERT",
"GROUP_UPDATE",
"GROUP_PARTICIPANTS_UPDATE",
"CONNECTION_UPDATE",
"CALL",
"NEW_JWT_TOKEN",
],
}),
methods: {
async saveRabbitmq() {
try {
this.loading = true;
this.error = false;
await instanceController.rabbitmq.set(
this.instance.instance.instanceName,
this.rabbitmqData
);
this.defaultRabbitmqData = Object.assign({}, this.rabbitmqData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
async loadRabbitmq() {
try {
this.loading = true;
this.error = false;
const rabbitmqData = await instanceController.rabbitmq.get(
this.instance.instance.instanceName
);
this.rabbitmqData = Object.assign({}, rabbitmqData);
this.defaultRabbitmqData = Object.assign({}, rabbitmqData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
},
mounted() {
this.loadRabbitmq();
},
};
</script>
<style></style>

View File

@ -0,0 +1,252 @@
<template>
<v-card variant="outlined" :loading="loading">
<v-card-title class="d-flex align-center">
<v-icon start>mdi-robot-happy</v-icon>
Typebot
<v-spacer></v-spacer>
<v-btn
size="small"
icon
:disabled="loading"
variant="tonal"
@click="expanded = !expanded"
:style="{ transform: expanded ? 'rotate(180deg)' : '' }"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="expanded">
<v-alert v-if="error" type="error" class="mb-3">
{{ error }}
</v-alert>
<v-form v-model="valid">
<v-text-field
v-model="typebotData.url"
label="URL"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(url) => {
if (!url) return 'URL é obrigatório';
if (!url.startsWith('http'))
return 'URL deve começar com http ou https';
return true;
},
]"
/>
<div class="d-flex gap-4 flex-wrap">
<div class="flex-grow-1">
<v-text-field
v-model="typebotData.typebot"
label="Typebot"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(account_id) => {
if (!account_id) return 'Typebot é obrigatório';
return true;
},
]"
/>
</div>
<div class="flex-grow-1">
<v-text-field
v-model="typebotData.keyword_finish"
label="Palavra-chave de finalização"
placeholder="#SAIR"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(token) => {
if (!token)
return 'Palavra-chave de finalização é obrigatório';
return true;
},
]"
/>
</div>
</div>
<div class="d-flex gap-4 flex-wrap">
<div class="flex-grow-1">
<v-text-field
v-model="typebotData.expire"
label="Tempo de expiração (em minutos)"
:disabled="loading"
type="number"
min="0"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(v) => {
if (!v) return 'Tempo de expiração é obrigatório';
return true;
},
]"
/>
</div>
<div class="flex-grow-1">
<v-text-field
v-model="typebotData.delay_message"
label="Tempo de atraso da mensagem (em milisegundos)"
type="number"
min="0"
:hint="`${typebotData.delay_message}ms = ${(
typebotData.delay_message / 1000
)
.toFixed(1)
.replace('.0', '')}s`"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(token) => {
if (token == null || token < 0)
return 'Palavra-chave de finalização é obrigatório';
return true;
},
]"
/>
</div>
</div>
<v-text-field
v-model="typebotData.unknown_message"
label="Mensagem de desconhecido"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(token) => {
if (!token) return 'Mensagem de desconhecido é obrigatório';
return true;
},
]"
/>
</v-form>
</v-card-text>
<v-card-actions v-if="expanded">
<v-switch
v-model="typebotData.enabled"
label="Habilitado"
color="primary"
:disabled="loading"
hide-details
></v-switch>
<v-spacer></v-spacer>
<v-btn
:disabled="
!valid ||
JSON.stringify(typebotData) === JSON.stringify(defaultTypebotData)
"
:loading="loading"
color="primary"
@click="saveTypebot"
variant="tonal"
>
Salvar
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import instanceController from "@/services/instanceController";
export default {
name: "InstanceTypebot",
props: {
instance: {
type: Object,
required: true,
},
},
data: () => ({
expanded: false,
loading: false,
error: false,
valid: false,
typebotData: {
enabled: false,
expire: 0,
sessions: [],
typebot: "",
url: "",
keyword_finish: "",
unknown_message: "",
},
defaultTypebotData: {
enabled: false,
expire: 0,
sessions: [],
typebot: "",
url: "",
keyword_finish: "",
unknown_message: "",
},
}),
methods: {
async saveTypebot() {
try {
this.loading = true;
this.error = false;
await instanceController.typebot.set(
this.instance.instance.instanceName,
{
...this.typebotData,
url: this.typebotData.url.trim().replace(/\/$/, ""),
}
);
this.defaultTypebotData = Object.assign({}, this.typebotData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
async loadTypebot() {
try {
this.loading = true;
this.error = false;
const typebotData = await instanceController.typebot.get(
this.instance.instance.instanceName
);
this.typebotData = Object.assign({}, typebotData);
this.defaultTypebotData = Object.assign({}, typebotData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
},
watch: {
expanded(expanded) {
if (expanded) this.loadTypebot();
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,204 @@
<template>
<v-card variant="outlined" :loading="loading">
<v-card-title class="d-flex align-center">
<v-icon start>mdi-webhook</v-icon>
Webhook
<v-spacer></v-spacer>
<v-btn
size="small"
icon
:disabled="loading"
variant="tonal"
@click="expanded = !expanded"
:style="{ transform: expanded ? 'rotate(180deg)' : '' }"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="expanded">
<v-alert v-if="error" type="error" class="mb-3">
{{ error }}
</v-alert>
<v-form v-model="valid">
<v-text-field
v-model="webhookData.url"
label="URL"
:disabled="loading"
outlined
dense
hide-details="auto"
class="mb-3"
:rules="[
(url) => {
if (!url) return 'URL é obrigatório';
if (!url.startsWith('http'))
return 'URL deve começar com http ou https';
return true;
},
]"
/>
<v-select
:items="webhookEventsType"
v-model="webhookData.events"
:disabled="loading"
label="Eventos"
hide-details
class="mb-3"
multiple
outlined
dense
chips
/>
<div class="d-flex gap-x-4 flex-wrap align-center">
<div>
<v-checkbox
v-model="webhookData.webhook_base64"
:disabled="loading"
label="Webhook base64"
hide-details
class="mb-3"
density="compact"
/>
</div>
<div>
<v-checkbox
v-model="webhookData.webhook_by_events"
:disabled="loading"
label="Webhook por eventos"
hide-details
class="mb-3"
density="compact"
/>
</div>
</div>
</v-form>
</v-card-text>
<v-card-actions v-if="expanded">
<v-switch
v-model="webhookData.enabled"
label="Habilitado"
color="primary"
:disabled="loading"
hide-details
></v-switch>
<v-spacer></v-spacer>
<v-btn
:disabled="
!valid ||
JSON.stringify(webhookData) === JSON.stringify(defaultWebhookData)
"
:loading="loading"
color="primary"
@click="saveWebhook"
variant="tonal"
>
Salvar
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import instanceController from "@/services/instanceController";
export default {
name: "InstanceWebhook",
props: {
instance: {
type: Object,
required: true,
},
},
data: () => ({
expanded: false,
loading: false,
error: false,
valid: false,
webhookData: {
enabled: false,
events: [],
url: "",
webhook_base64: false,
webhook_by_events: false,
},
defaultWebhookData: {
enabled: false,
events: [],
url: "",
webhook_base64: false,
webhook_by_events: false,
},
webhookEventsType: [
"APPLICATION_STARTUP",
"QRCODE_UPDATED",
"MESSAGES_SET",
"MESSAGES_UPSERT",
"MESSAGES_UPDATE",
"MESSAGES_DELETE",
"SEND_MESSAGE",
"CONTACTS_SET",
"CONTACTS_UPSERT",
"CONTACTS_UPDATE",
"PRESENCE_UPDATE",
"CHATS_SET",
"CHATS_UPSERT",
"CHATS_UPDATE",
"CHATS_DELETE",
"GROUPS_UPSERT",
"GROUP_UPDATE",
"GROUP_PARTICIPANTS_UPDATE",
"CONNECTION_UPDATE",
"CALL",
"NEW_JWT_TOKEN",
],
}),
methods: {
async saveWebhook() {
try {
this.loading = true;
this.error = false;
await instanceController.webhook.set(
this.instance.instance.instanceName,
{
...this.webhookData,
url: this.webhookData.url.trim().replace(/\/$/, ""),
}
);
this.defaultWebhookData = Object.assign({}, this.webhookData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
async loadWebhook() {
try {
this.loading = true;
this.error = false;
const webhookData = await instanceController.webhook.get(
this.instance.instance.instanceName
);
this.webhookData = Object.assign({}, webhookData);
this.defaultWebhookData = Object.assign({}, webhookData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
},
watch: {
expanded(expanded) {
if (expanded) this.loadWebhook();
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,157 @@
<template>
<v-card variant="outlined" :loading="loading">
<v-card-title class="d-flex align-center">
<v-icon start>mdi-email-fast</v-icon>
Websocket
<v-spacer></v-spacer>
<v-btn
size="small"
icon
:disabled="loading"
variant="tonal"
@click="expanded = !expanded"
:style="{ transform: expanded ? 'rotate(180deg)' : '' }"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</v-card-title>
<v-card-text v-if="expanded">
<v-alert v-if="error" type="error" class="mb-3">
{{ error }}
</v-alert>
<v-form v-model="valid">
<v-select
:items="websocketEventsType"
v-model="websocketData.events"
:disabled="loading"
label="Eventos"
hide-details
class="mb-3"
multiple
outlined
dense
chips
/>
</v-form>
</v-card-text>
<v-card-actions v-if="expanded">
<v-switch
v-model="websocketData.enabled"
label="Habilitado"
color="primary"
:disabled="loading"
hide-details
></v-switch>
<v-spacer></v-spacer>
<v-btn
:disabled="
!valid ||
JSON.stringify(websocketData) === JSON.stringify(defaultWebsocketData)
"
:loading="loading"
color="primary"
@click="saveWebsocket"
variant="tonal"
>
Salvar
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
import instanceController from "@/services/instanceController";
export default {
name: "InstanceWebsocket",
props: {
instance: {
type: Object,
required: true,
},
},
data: () => ({
expanded: false,
loading: false,
error: false,
valid: false,
websocketData: {
enabled: false,
events: [],
},
defaultWebsocketData: {
enabled: false,
events: [],
},
websocketEventsType: [
"APPLICATION_STARTUP",
"QRCODE_UPDATED",
"MESSAGES_SET",
"MESSAGES_UPSERT",
"MESSAGES_UPDATE",
"MESSAGES_DELETE",
"SEND_MESSAGE",
"CONTACTS_SET",
"CONTACTS_UPSERT",
"CONTACTS_UPDATE",
"PRESENCE_UPDATE",
"CHATS_SET",
"CHATS_UPSERT",
"CHATS_UPDATE",
"CHATS_DELETE",
"GROUPS_UPSERT",
"GROUP_UPDATE",
"GROUP_PARTICIPANTS_UPDATE",
"CONNECTION_UPDATE",
"CALL",
"NEW_JWT_TOKEN",
],
}),
methods: {
async saveWebsocket() {
try {
this.loading = true;
this.error = false;
await instanceController.websocket.set(
this.instance.instance.instanceName,
this.websocketData
);
this.defaultWebsocketData = Object.assign({}, this.websocketData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
async loadWebsocket() {
try {
this.loading = true;
this.error = false;
const websocketData = await instanceController.websocket.get(
this.instance.instance.instanceName
);
this.websocketData = Object.assign({}, websocketData);
this.defaultWebsocketData = Object.assign({}, websocketData);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
},
watch: {
expanded: {
handler() {
if (this.expanded) this.loadWebsocket();
},
},
},
};
</script>
<style></style>

View File

@ -0,0 +1,99 @@
<template>
<v-alert
icon="mdi-qrcode-scan"
v-if="instance.instance?.status != 'open'"
type="warning"
>
<div class="d-flex justify-space-between align-center flex-wrap">
<span>Telefone não conectado</span>
<v-btn @click="startConnect" size="small"> Conectar </v-btn>
</div>
</v-alert>
<v-dialog v-model="dialog" max-width="350px">
<v-card :loading="loading && qrCode">
<v-card-text>
<v-img v-if="qrCode" :src="qrCode" width="300px" height="300px" />
<v-card
v-else
width="300px"
height="300px"
variant="outlined"
elevation="0"
>
<v-card-text class="d-flex justify-center align-center h-100">
<v-progress-circular indeterminate color="primary" />
</v-card-text>
</v-card>
<v-alert type="error" v-if="error">
{{ Array.isArray(error) ? error.join(", ") : error }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="dialog = false" :disabled="loading"> Cancel </v-btn>
<v-spacer></v-spacer>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { useAppStore } from "@/store/app";
import instanceController from "@/services/instanceController";
export default {
name: "SettingsModal",
data: () => ({
dialog: false,
error: false,
loading: false,
qrCode: null,
success: false,
timeout: null,
AppStore: useAppStore(),
}),
methods: {
async loadQr() {
try {
this.loading = true;
this.error = false;
const response = await instanceController.connect(
this.instance.instance.instanceName
);
if (response.base64) this.qrCode = response.base64;
else {
this.dialog = false;
return;
}
this.timeout = setTimeout(this.loadQr, 40000);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
},
async startConnect() {
clearTimeout(this.timeout);
this.dialog = true;
this.error = false;
await this.loadQr();
await this.AppStore.reconnect();
},
},
props: {
instance: {
type: Object,
required: true,
},
},
emits: ["close"],
};
</script>

View File

@ -5,7 +5,7 @@
:persistent="!AppStore.validConnection"
>
<v-card>
<v-card-text>
<v-card-text class="d-flex flex-column gap-4">
<v-form v-model="valid">
<h3 class="mb-4">Criar instancia</h3>
<v-text-field
@ -21,7 +21,7 @@
]"
/>
<v-text-field
v-model="instance.apiKey"
v-model="instance.token"
label="API Key"
required
outlined
@ -62,7 +62,7 @@
:disabled="!valid"
:loading="loading"
>
Conectar
Criar
</v-btn>
</v-card-actions>
</v-card>
@ -80,7 +80,7 @@ export default {
valid: false,
instance: {
instanceName: "",
apiKey: "",
token: "",
},
loading: false,
error: false,
@ -88,7 +88,7 @@ export default {
}),
methods: {
generateApiKey() {
this.instance.apiKey =
this.instance.token =
Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
},

View File

@ -1,25 +1,28 @@
<template>
<v-dialog
v-model="dialog"
max-width="500px"
:persistent="!AppStore.validConnection"
>
<v-dialog v-model="dialog" max-width="500px" :persistent="!AppStore.validConnection">
<v-card>
<v-card-text>
<v-form v-model="valid">
<h3 class="mb-4">Configurar conexão</h3>
<v-text-field
v-model="apiKey"
label="Global API Key"
required
outlined
:type="revelPassword ? 'text' : 'password'"
:append-inner-icon="revelPassword ? 'mdi-eye' : 'mdi-eye-off'"
@click:append-inner="revelPassword = !revelPassword"
/>
</v-form>
<v-alert type="error" v-if="error">
{{ Array.isArray(error) ? error.join(", ") : error }}
</v-alert>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
v-if="AppStore.validConnection"
text
@click="dialog = false"
:disabled="loading"
>
Cancel
</v-btn>
<v-btn text to="/" :disabled="loading">Cancel</v-btn>
<v-btn
color="success"
variant="tonal"
@ -35,13 +38,18 @@
</template>
<script>
import instanceController from "@/services/instanceController";
import { useAppStore } from "@/store/app";
export default {
name: "SettingsModal",
data: () => ({
dialog: false,
valid: false,
revelPassword: false,
connection: {
host: "",
globalApiKey: "",
},
loading: false,
error: false,
AppStore: useAppStore(),
@ -52,13 +60,8 @@ export default {
this.loading = true;
this.error = false;
const instance = await instanceController.create(this.instance);
await this.AppStore.reconnect();
this.$router.push({
name: "instance",
params: { id: instance.instance.instanceName },
});
await this.AppStore.setConnection(this.connection);
this.dialog = false;
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
@ -67,9 +70,6 @@ export default {
},
open() {
this.dialog = true;
this.error = false;
this.instance.instanceName = "";
this.generateApiKey();
},
},

View File

@ -2,11 +2,16 @@ export default {
close: {
color: "red",
text: "Desconectado",
icon: "mdi-close-circle",
icon: "mdi-cellphone-off",
},
connecting: {
color: "warning",
text: "Conectando",
icon: "mdi-cellphone-settings",
},
open: {
color: "green",
text: "Conectado",
icon: "mdi-check-circle",
icon: "mdi-cellphone-nfc",
},
}

View File

@ -15,6 +15,27 @@ http.interceptors.request.use(
config.baseURL = appStore.connection.host;
config.headers["apikey"] = appStore.connection.globalApiKey;
// find all uri variables and replace them with the value from the params object
// e.g. /instance/connect/:instance -> /instance/connect/instance1
const params = Object.entries(config.params || {});
if (params.length > 0) {
config.url = config.url.replace(/:(\w+)/g, (_, key) => {
const value = params.find(([k]) => k === key)?.[1];
if (value) {
delete config.params[key];
return value;
}
return _;
});
if (params.instance) {
const apikey = appStore.getInstanceApiKey(params.instance);
if (apikey) config.headers["apikey"] = apikey;
}
}
return config;
},
error => Promise.reject(error)

View File

@ -1,8 +1,10 @@
<template>
<v-app-bar flat>
<v-app-bar-title>
<v-icon icon="mdi-whatsapp" left />
<v-btn variant="text" @click="$router.push('/')">
<v-img src="@/assets/logo.png" height="24" width="24" class="mr-2" />
Evolution Manager
</v-btn>
</v-app-bar-title>
<v-icon v-if="AppStore.validConnection" color="success">
@ -33,8 +35,9 @@ export default {
this.$refs.settings.open();
},
},
mounted() {
if (!this.AppValidConnection) this.openSettings();
async mounted() {
await this.AppStore.loadConnection();
if (!this.AppStore.validConnection) this.openSettings();
},
};
</script>

View File

@ -19,8 +19,40 @@ const create = async (data) => {
});
}
const connect = async (instanceName) => {
return await http
.get("/instance/connect/:instance", {
params: {
instance: instanceName
}
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const logout = async (instanceName) => {
return await http
.delete("/instance/logout/:instance", {
params: {
instance: instanceName
}
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
import settings from "./instanceSettingsController.js";
export default {
fetchAll,
create
create,
connect,
logout,
...settings
};

View File

@ -0,0 +1,139 @@
import http from "../http-common";
const findWebhook = async (instanceName) => {
return await http
.get("/webhook/find/:instance", {
params: {
instance: instanceName
}
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const setWebhook = async (instanceName, data) => {
return await http
.post("/webhook/set/:instance", data, {
params: {
instance: instanceName
}
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const findWebsocket = async (instanceName) => {
return await http
.get("/websocket/find/:instance", {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const setWebsocket = async (instanceName, data) => {
return await http
.post("/websocket/set/:instance", data, {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const findRabbitmq = async (instanceName) => {
return await http
.get("/rabbitmq/find/:instance", {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const setRabbitmq = async (instanceName, data) => {
return await http
.post("/rabbitmq/set/:instance", data, {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const findChatwoot = async (instanceName) => {
return await http
.get("/chatwoot/find/:instance", {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const setChatwoot = async (instanceName, data) => {
return await http
.post("/chatwoot/set/:instance", data, {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const findTypebot = async (instanceName) => {
return await http
.get("/typebot/find/:instance", {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
const setTypebot = async (instanceName, data) => {
return await http
.post("/typebot/set/:instance", data, {
params: { instance: instanceName }
})
.then((r) => r.data)
.catch((error) => {
throw error.response?.data || error.response || error;
});
}
export default {
webhook: {
get: findWebhook,
set: setWebhook,
},
websocket: {
get: findWebsocket,
set: setWebsocket,
},
rabbitmq: {
get: findRabbitmq,
set: setRabbitmq,
},
chatwoot: {
get: findChatwoot,
set: setChatwoot,
},
typebot: {
get: findTypebot,
set: setTypebot,
}
}

View File

@ -2,10 +2,18 @@
import axios from 'axios'
import { defineStore } from 'pinia'
export const useAppStore = defineStore('app', {
getters: {
validConnection: (state) => state.connection.valid,
instances: (state) => state.instancesList,
getInstance: (state) => (instanceName) => state.instancesList.find(
(instance) => instance.instance.instanceName === instanceName
),
getInstanceApiKey: (state) => (instance) => {
return state.getInstance(instance).instance.apiKey ||
state.instancesKeys[instance]
},
},
state: () => ({
connection: {
@ -13,6 +21,7 @@ export const useAppStore = defineStore('app', {
host: null,
globalApiKey: null,
},
instancesKeys: {},
instancesList: [],
}),
@ -29,9 +38,8 @@ export const useAppStore = defineStore('app', {
url: '/instance/fetchInstances'
})
this.connection.valid = true
this.connection.host = host
this.connection.globalApiKey = globalApiKey
this.saveConnection({ host, globalApiKey })
this.instancesList = responde.data
} catch (e) {
this.connection.valid = false
@ -39,10 +47,31 @@ export const useAppStore = defineStore('app', {
}
},
async loadInstance(instanceName) {
try {
const { host, globalApiKey } = this.connection;
const response = await axios({
method: 'GET',
baseURL: host,
headers: {
'Content-Type': 'application/json',
'apikey': globalApiKey
},
url: `/instance/fetchInstances?instanceName=${instanceName}`
})
} catch (e) {
this.connection.valid = false
throw e.response?.data?.response?.message || e.response || e
}
},
async reconnect() {
try {
const { host, globalApiKey } = this.connection
const responde = await axios({
const response = await axios({
method: 'GET',
baseURL: host,
headers: {
@ -52,14 +81,48 @@ export const useAppStore = defineStore('app', {
url: '/instance/fetchInstances'
})
this.connection.valid = true
this.connection.host = host
this.connection.globalApiKey = globalApiKey
this.instancesList = responde.data
this.saveConnection({ host, globalApiKey })
this.instancesList = response.data
} catch (e) {
this.connection.valid = false
throw e.response?.data?.response?.message || e.response || e
}
},
setInstanceStatus(instance, status) {
const index = this.instancesList.findIndex(
(instance) => instance.instance.instanceName === instance
)
this.instancesList[index].instance.status = status
},
addInstanceKey({ instance, key }) {
this.instancesKeys[instance] = key
},
saveConnection({ host, globalApiKey }) {
this.connection = {
valid: true,
host,
globalApiKey,
}
if (typeof window !== 'undefined') {
window.localStorage.setItem('connection', JSON.stringify({
host,
globalApiKey,
}))
}
},
async loadConnection() {
if (typeof window !== 'undefined') {
const connection = window.localStorage.getItem('connection')
if (connection) {
this.connection = JSON.parse(connection)
return this.reconnect()
}
}
}
}
})

View File

@ -3,33 +3,21 @@
{{ error }}
</v-alert>
<div v-else-if="instance" class="d-flex flex-column" style="gap: 1.5rem">
;
<v-card variant="outlined" class="d-flex align-center">
<v-avatar size="100">
<v-icon v-if="statusMapper[instance.instance.status].icon" size="70">
{{ statusMapper[instance.instance.status].icon }}
</v-icon>
</v-avatar>
<div>
<h2>{{ instance.instance.instanceName }}</h2>
</div>
</v-card>
<v-alert
icon="mdi-qrcode-scan"
v-if="instance.status != 'connected'"
type="warning"
>
<div class="d-flex justify-space-between align-center flex-wrap">
<span>Telefone não conectado</span>
<v-btn @click="connectPhone" size="small"> Conectar </v-btn>
</div>
</v-alert>
<InstanceHeader :instance="instance" />
<ConnectPhone :instance="instance" />
<InstanceBody :instance="instance" />
<InstanceApiKey :instance="instance" ref="apiKeyModal" />
</div>
</template>
<script>
import { useAppStore } from "@/store/app";
import statusMapper from "@/helpers/mappers/status";
import InstanceApiKey from "@/components/modal/InstanceApiKey.vue";
import ConnectPhone from "@/components/modal/ConnectPhone.vue";
import InstanceHeader from "@/components/instance/InstanceHeader.vue";
import InstanceBody from "@/components/instance/InstanceBody.vue";
export default {
name: "HomeInstance",
@ -37,27 +25,32 @@ export default {
AppStore: useAppStore(),
loading: true,
error: false,
instance: null,
statusMapper: statusMapper,
}),
methods: {
async loadInstance() {
if (!this.AppStore.instances) await this.AppStore.reconnect();
const instances = this.AppStore.instances;
const instance = instances.find(
(instance) => instance.instance.instanceName === this.$route.params.id
);
if (!instance) {
this.error = "Instância não encontrada";
return;
try {
this.loading = true;
this.error = false;
await this.AppStore.loadInstance(this.$route.params.id);
} catch (e) {
this.error = e.message?.message || e.message || e;
} finally {
this.loading = false;
}
this.instance = instance;
},
},
watch: {},
computed: {
instance() {
return this.AppStore.getInstance(this.$route.params.id);
},
},
async mounted() {
if (this.AppStore.validConnection) this.loadInstance();
else this.$router.push({ name: "instances" });
},
components: { InstanceApiKey, ConnectPhone, InstanceHeader, InstanceBody },
};
</script>

4148
yarn.lock

File diff suppressed because it is too large Load Diff