diff --git a/.gitignore b/.gitignore index 0ff50c0b..5ae1892f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ ssl/ .vscode estrutura_projeto.txt +log/ diff --git a/UPDATE.sh b/UPDATE.sh index 6ac95454..23867df2 100644 --- a/UPDATE.sh +++ b/UPDATE.sh @@ -1,200 +1,306 @@ #!/bin/bash -VERSION="v1.9.0" +VERSION="v1.10.0" -SCRIPT_DIR=$(dirname "$0") +# Define o diretório base absoluto +SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) +# Define diretórios de logs usando caminhos absolutos LOG_DIR="$SCRIPT_DIR/log" CURRENT_LOG_DIR="$LOG_DIR/atual" ARCHIVED_LOG_DIR="$LOG_DIR/arquivos" -mkdir -p "$CURRENT_LOG_DIR" -mkdir -p "$ARCHIVED_LOG_DIR" - -LOG_FILE="$CURRENT_LOG_DIR/update_$(date +"%Y-%m-%d_%H-%M-%S").log" +# Cria os diretórios de log +if ! mkdir -p "$CURRENT_LOG_DIR" "$ARCHIVED_LOG_DIR"; then + echo "Erro: Não foi possível criar os diretórios de log. Verifique as permissões." + exit 1 +fi COLOR="\e[38;5;92m" RESET="\e[0m" +BOLD="\e[1m" + +echo -e " " +echo -e "${COLOR}██████╗ ██████╗ ███████╗███████╗███████╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗${RESET}" +echo -e "${COLOR}██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝${RESET}" +echo -e "${COLOR}██████╔╝██████╔╝█████╗ ███████╗███████╗ ██║ ██║██║ █████╔╝ █████╗ ██║ ${RESET}" +echo -e "${COLOR}██╔═══╝ ██╔══██╗██╔══╝ ╚════██║╚════██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ${RESET}" +echo -e "${COLOR}██║ ██║ ██║███████╗███████║███████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ${RESET}" +echo -e "${COLOR}╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ${RESET}" +echo -e "${COLOR}ATUALIZANDO PARA A VERSÃO:${RESET} ${BOLD}$VERSION${RESET}" +echo -e " " +sleep 2 +# Lista de fusos horários +declare -a TIMEZONES=( + "UTC" + "America/Sao_Paulo" + "America/New_York" + "Europe/London" + "Europe/Paris" + "Asia/Tokyo" + "Asia/Shanghai" + "Australia/Sydney" +) + +# Exibir a lista de fusos horários para o usuário echo " " -echo "${COLOR}██████╗ ██████╗ ███████╗███████╗███████╗ ████████╗██╗ ██████╗██╗ ██╗███████╗████████╗${RESET}" -echo "${COLOR}██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝ ╚══██╔══╝██║██╔════╝██║ ██╔╝██╔════╝╚══██╔══╝${RESET}" -echo "${COLOR}██████╔╝██████╔╝█████╗ ███████╗███████╗ ██║ ██║██║ █████╔╝ █████╗ ██║ ${RESET}" -echo "${COLOR}██╔═══╝ ██╔══██╗██╔══╝ ╚════██║╚════██║ ██║ ██║██║ ██╔═██╗ ██╔══╝ ██║ ${RESET}" -echo "${COLOR}██║ ██║ ██║███████╗███████║███████║ ██║ ██║╚██████╗██║ ██╗███████╗ ██║ ${RESET}" -echo "${COLOR}╚═╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ${RESET}" -echo "\e[92mATUALIZANDO PARA A VERSÃO:\e[0m \e[1m$VERSION\e[0m" | tee -a "$LOG_FILE" +echo "Por favor, escolha seu fuso horário:" echo " " +for i in "${!TIMEZONES[@]}"; do + echo "[$i] ${TIMEZONES[$i]}" +done -# sleep 2 - -# echo "PATH: $PATH" | tee -a "$LOG_FILE" - -# # Função para verificar se comandos necessários estão instalados -# check_dependency() { -# if ! command -v "$1" &>/dev/null; then -# echo "$1 não está instalado. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi -# } - -# # Verificar se as dependências estão instaladas -# check_dependency node -# check_dependency npm -# check_dependency pm2 - -# # Gerenciar logs antigos: compactar e mover para a pasta de arquivos (logs mais antigos que 30 dias) -# find "$CURRENT_LOG_DIR" -type f -mtime +30 -exec gzip {} \; -exec mv {}.gz "$ARCHIVED_LOG_DIR" \; +# Capturar a escolha do usuário +echo " " +read -p "Digite o número correspondente ao fuso horário: " TZ_INDEX +echo " " -# sleep 2 +# Validar entrada +if [[ ! "$TZ_INDEX" =~ ^[0-9]+$ ]] || [[ "$TZ_INDEX" -ge "${#TIMEZONES[@]}" ]]; then + echo " " + echo "Escolha inválida. Usando o fuso horário padrão: UTC." + echo " " + SELECTED_TZ="UTC" +else + SELECTED_TZ="${TIMEZONES[$TZ_INDEX]}" + echo " " + echo "Fuso horário escolhido: $SELECTED_TZ" + echo " " +fi -# echo " " | tee -a "$LOG_FILE" -# echo "VERIFICANDO A VERSÃO DO NODE JS" | tee -a "$LOG_FILE" -# echo " " | tee -a "$LOG_FILE" +# Configuração do arquivo de log (ajustado para usar o fuso horário) +LOG_FILE="$CURRENT_LOG_DIR/update_${VERSION}_$(TZ=$SELECTED_TZ date +"%Y-%m-%d_%H-%M-%S").log" + +# Adicionar informações iniciais ao log +{ + echo " " + echo "**************************************************************" + echo "* PRESS TICKET - LOG DE ATUALIZAÇÃO *" + echo "**************************************************************" + echo " Versão Atualizada: $VERSION " + echo " Fuso Horário: $SELECTED_TZ " + echo " Hora Local: $(TZ=$SELECTED_TZ date) " + echo "**************************************************************" + echo " " +} | tee -a "$LOG_FILE" -# sleep 2 +echo " " +echo "Arquivo de de log criado com sucesso: $LOG_FILE" +echo " " +# Exibir a hora ajustada e salvar no log +echo "Fuso horário ajustado para: $SELECTED_TZ" | tee -a "$LOG_FILE" +echo "Hora ajustada para o log: $(TZ=$SELECTED_TZ date)" | tee -a "$LOG_FILE" + +# Verifica se o arquivo de log pode ser criado +if ! touch "$LOG_FILE"; then + echo "Erro: Não foi possível criar o arquivo de log $LOG_FILE. Verifique as permissões." + exit 1 +fi -# NODE_PATH="/usr/bin/node" +# Compactação de logs antigos +find "$CURRENT_LOG_DIR" -type f -mtime +30 -exec gzip {} \; -exec mv {}.gz "$ARCHIVED_LOG_DIR" \; -# if [ ! -x "$NODE_PATH" ]; then -# echo "Node.js não está instalado corretamente ou não foi encontrado. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi +# Verificação de versão do Node.js +{ + echo " " + echo "VERIFICANDO A VERSÃO DO NODE.JS" + echo " " +} | tee -a "$LOG_FILE" -# CURRENT_NODE_VERSION=$($NODE_PATH -v | cut -d'v' -f2) +sleep 2 -# # Função para comparar versões utilizando dpkg -# compare_versions() { -# dpkg --compare-versions "$1" "lt" "$2" -# } +NODE_PATH="/usr/bin/node" + +# Verifica se o Node.js está instalado +if [ ! -x "$NODE_PATH" ]; then + { + echo "Node.js não está instalado corretamente ou não foi encontrado. Instalando a versão 20.x..." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + sudo npm install -g npm + if [ $? -ne 0 ]; then + echo "Erro ao instalar o Node.js ou o npm. Saindo..." + exit 1 + fi + echo "Node.js instalado com sucesso." + exit 0 + } | tee -a "$LOG_FILE" +fi -# # Comparação de versões do Node.js -# if compare_versions "$CURRENT_NODE_VERSION" "18"; then -# echo "Versão do Node.js atual é inferior a 18. Atualizando para a 20.x..." | tee -a "$LOG_FILE" -# sudo apt-get remove -y nodejs | tee -a "$LOG_FILE" -# curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - | tee -a "$LOG_FILE" -# sudo apt-get install -y nodejs | tee -a "$LOG_FILE" -# sudo npm install -g npm | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao atualizar o Node.js ou o npm. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi -# else -# echo "Versão do Node.js é 18 ou superior. Prosseguindo com a atualização..." | tee -a "$LOG_FILE" -# fi +CURRENT_NODE_VERSION=$($NODE_PATH -v | cut -d'v' -f2) + +# Função para comparação de versões +version_less_than() { + [ "$(printf '%s\n' "$1" "$2" | sort -V | head -n1)" = "$1" ] +} + +# Atualização do Node.js, se necessário +if version_less_than "$CURRENT_NODE_VERSION" "18.0.0"; then + { + echo "Versão do Node.js atual ($CURRENT_NODE_VERSION) é inferior a 18. Atualizando para a versão 20.x..." + curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - + sudo apt-get install -y nodejs + sudo npm install -g npm + if [ $? -ne 0 ]; then + echo "Erro ao atualizar o Node.js ou o npm. Saindo..." + exit 1 + fi + echo "Node.js atualizado com sucesso para a versão 20.x." + } | tee -a "$LOG_FILE" +else + echo "A versão do Node.js instalada ($CURRENT_NODE_VERSION) é igual ou superior a 18. Prosseguindo..." | tee -a "$LOG_FILE" +fi sleep 2 -echo " " | tee -a "$LOG_FILE" -echo "BAIXANDO AS ATUALIZAÇÕES" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "BAIXANDO AS ATUALIZAÇÕES" + echo " " +} | tee -a "$LOG_FILE" sleep 2 git reset --hard | tee -a "$LOG_FILE" git pull | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao realizar o git pull. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi -echo " " | tee -a "$LOG_FILE" -echo "ACESSANDO O BACKEND" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "ACESSANDO O BACKEND" + echo " " +} | tee -a "$LOG_FILE" sleep 2 -cd backend +cd backend || { + echo "Erro ao acessar o diretório do backend." | tee -a "$LOG_FILE" + exit 1 +} -echo " " | tee -a "$LOG_FILE" -echo "ATUALIZANDO OS ARQUIVOS DO BACKEND" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "ATUALIZANDO OS ARQUIVOS DO BACKEND" + echo " " +} | tee -a "$LOG_FILE" sleep 2 sudo rm -rf node_modules | tee -a "$LOG_FILE" npm install | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao instalar dependências do backend. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi + sudo rm -rf dist | tee -a "$LOG_FILE" npm run build | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" -echo "EXECUTANDO O DB:MIGRATE" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "EXECUTANDO O DB:MIGRATE" + echo " " +} | tee -a "$LOG_FILE" sleep 2 npx sequelize db:migrate | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao executar as migrações do banco de dados. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi -echo " " | tee -a "$LOG_FILE" -echo "EXECUTANDO O DB:SEED:ALL" | tee -a "$LOG_FILE" +{ + echo " " + echo "EXECUTANDO O DB:SEED:ALL" + echo " " +} | tee -a "$LOG_FILE" sleep 2 npx sequelize db:seed:all | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao rodar seeds no banco de dados. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi -echo " " | tee -a "$LOG_FILE" -echo "ACESSANDO O FRONTEND" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "ACESSANDO O FRONTEND" + echo " " +} | tee -a "$LOG_FILE" sleep 2 -cd ../frontend - -sleep 2 +cd ../frontend || { + echo "Erro ao acessar o diretório do frontend." | tee -a "$LOG_FILE" + exit 1 +} + +# Verifica o arquivo .env e adiciona REACT_APP_MASTERADMIN se necessário +if [ -f .env ]; then + if grep -q "^REACT_APP_MASTERADMIN=" .env; then + echo "A variável REACT_APP_MASTERADMIN já está presente no arquivo .env." | tee -a "$LOG_FILE" + else + echo "Adicionando a variável REACT_APP_MASTERADMIN ao final do arquivo .env." | tee -a "$LOG_FILE" + echo "" >>.env + echo "# Para permitir acesso apenas do MasterAdmin (sempre ON)" >>.env + echo "REACT_APP_MASTERADMIN=ON" >>.env + echo "A variável REACT_APP_MASTERADMIN foi adicionada ao final do arquivo .env." | tee -a "$LOG_FILE" + fi +else + echo "O arquivo .env não foi encontrado. O processo de instalação precisa ser finalizado antes de prosseguir." | tee -a "$LOG_FILE" + exit 1 +fi -echo " " | tee -a "$LOG_FILE" -echo "VERIFICANDO O CONFIG.JSON" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "VERIFICANDO O CONFIG.JSON" + echo " " +} | tee -a "$LOG_FILE" sleep 2 -if [ ! -e src/config.json ]; then - echo "Criando o arquivo config.json" | tee -a "$LOG_FILE" - cp src/config.json.example src/config.json | tee -a "$LOG_FILE" +if [ -e src/config.json ]; then + echo "O arquivo config.json existe. Excluindo o arquivo..." | tee -a "$LOG_FILE" + rm src/config.json + echo "Arquivo config.json excluído com sucesso." | tee -a "$LOG_FILE" else - echo "O arquivo config.json já existe" | tee -a "$LOG_FILE" + echo "O arquivo config.json não existe. Tudo certo, prosseguindo com a atualização." | tee -a "$LOG_FILE" fi sleep 2 -echo " " | tee -a "$LOG_FILE" -echo "ATUALIZANDO OS ARQUIVOS DO FRONTEND" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "ATUALIZANDO OS ARQUIVOS DO FRONTEND" + echo " " +} | tee -a "$LOG_FILE" sleep 2 sudo rm -rf node_modules | tee -a "$LOG_FILE" npm install | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao instalar dependências do frontend. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi + sudo rm -rf build | tee -a "$LOG_FILE" npm run build | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" -echo "RESTART PM2" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +{ + echo " " + echo "RESTART PM2" + echo " " +} | tee -a "$LOG_FILE" sleep 2 -pm2 restart all | tee -a "$LOG_FILE" -# if [ $? -ne 0 ]; then -# echo "Erro ao reiniciar o PM2. Saindo..." | tee -a "$LOG_FILE" -# exit 1 -# fi +ENV_FILE="../backend/.env" + +if [ -f "$ENV_FILE" ]; then + PM2_FRONTEND=$(grep "^PM2_FRONTEND=" "$ENV_FILE" | cut -d '=' -f2) + PM2_BACKEND=$(grep "^PM2_BACKEND=" "$ENV_FILE" | cut -d '=' -f2) + + if [ -n "$PM2_FRONTEND" ] && [ -n "$PM2_BACKEND" ]; then + echo "Reiniciando PM2 com os IDs especificados..." | tee -a "$LOG_FILE" + pm2 restart "$PM2_FRONTEND" | tee -a "$LOG_FILE" + pm2 restart "$PM2_BACKEND" | tee -a "$LOG_FILE" + else + echo "Erro: IDs PM2_FRONTEND ou PM2_BACKEND não encontrados." | tee -a "$LOG_FILE" + exit 1 + fi +else + echo "Erro: Arquivo .env não encontrado no backend." | tee -a "$LOG_FILE" + exit 1 +fi -echo " " | tee -a "$LOG_FILE" -echo "PRESS TICKET ATUALIZADO COM SUCESSO!!!" | tee -a "$LOG_FILE" -echo "Log de atualização salvo em: $LOG_FILE" +{ + echo " " + echo "PRESS TICKET ATUALIZADO COM SUCESSO!!!" + echo "Log de atualização salvo em: $LOG_FILE" +} | tee -a "$LOG_FILE" diff --git a/backend/api.rest b/backend/api.rest index 9e6852e2..a32f1121 100644 --- a/backend/api.rest +++ b/backend/api.rest @@ -1,20 +1,78 @@ # PARA USAR PRECISA DA EXTENSÃO DO VS CODE "REST Client" #Variaveis -@baseUrl = http://localhost:4000 -@token = a3031d64-7423-4bfc-b43e-6b6f2ab24160 +@baseUrl = http://localhost:8080 +# @baseUrl = https://apidev.pressticket.com.br +@token = 6175c0d0-acd5-4776-95a9-592c795da986 +@token2 = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6Ik1hc3RlckFkbWluIiwicHJvZmlsZSI6Im1hc3RlcmFkbWluIiwiaWQiOjIsImlhdCI6MTczMzY4OTk2NiwiZXhwIjoxNzMzNjkzNTY2fQ.g_jJs3vovlU5i9sS0IUoJ3Ew_m7W8GzqjWd-kieFFpc -# (Enviar Mensagem) Teste da Rota POST /api/messages/send +@refreshToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidG9rZW5WZXJzaW9uIjowLCJpYXQiOjE3MzI4ODUzMzQsImV4cCI6MTczMzQ5MDEzNH0.RwuRIufYUB9dQZQg0aeaVtnOryn-sESNL1mFlShmBM4 + +### (Login) Teste da Rota POST /auth/login +POST {{baseUrl}}/auth/login +Content-Type: application/json + +{ + "email": "masteradmin@pressticket.com.br", + "password": "masteradmin" +} + +### Teste da Rota POST /auth/refresh_token +POST {{baseUrl}}/auth/refresh_token +Authorization: Bearer {{refreshToken}} +Content-Type: application/json + +### (Enviar Mensagem) Teste da Rota POST /api/messages/send POST {{baseUrl}}/api/messages/send Authorization: Bearer {{token}} Content-Type: application/json { - "number": "5522999999999", + "number": "5522992463080", "body": "Mensagem de Teste da API com user e queue atualizado", "userId": "1", "queueId": "2", "whatsappId": "1" } -### +### (Listar Personalizações) Teste da Rota GET /personalizations +GET {{baseUrl}}/personalizations +Content-Type: application/json + +### (Criar ou Atualizar Dados da Empresa) Teste da Rota PUT /personalizations/:theme/company +PUT {{baseUrl}}/personalizations/light/company +Authorization: Bearer {{token2}} +Content-Type: application/json + +{ + "company": "Press Ticket", + "url": "https://pressticket.com.br" +} + +### (Criar ou Atualizar Cores) Teste da Rota PUT /personalizations/:theme/colors +PUT {{baseUrl}}/personalizations/light/colorS +Authorization: Bearer {{token2}} +Content-Type: application/json + +{ + "primaryColor": "#ffffff", + "secondaryColor": "#0000ff", + "backgroundDefault": "#ff00ff", + "backgroundPaper": "#00ff00" +} + +### (Criar ou Atualizar Logos) Teste da Rota PUT /personalizations/:theme/logos +PUT {{baseUrl}}/personalizations/light/logos +Authorization: Bearer {{token2}} +Content-Type: application/json + +{ + "favico": "teste.ico", + "logo": null, + "logoTicket": null +} + +### (Remover Personalização) Teste da Rota DELETE /personalizations/:theme +DELETE {{baseUrl}}/personalizations/light +Authorization: Bearer {{token2}} +Content-Type: application/json diff --git a/backend/package.json b/backend/package.json index 76706942..18ab2192 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,11 +5,13 @@ "main": "index.js", "scripts": { "build": "tsc", + "create": "npx sequelize-cli db:create", "migrate": "tsc && npx sequelize-cli db:migrate", "migrate:undo": "tsc && npx sequelize-cli db:migrate:undo", + "sequelize": "npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all", "watch": "tsc -w", "start": "nodemon dist/server.js", - "dev:server": "ts-node-dev --respawn --transpile-only --ignore node_modules src/server.ts", + "dev": "ts-node-dev --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" @@ -19,10 +21,12 @@ "dependencies": { "@ffmpeg-installer/ffmpeg": "^1.1.0", "@sentry/node": "^5.29.2", + "@types/body-parser": "^1.19.5", "@types/mime-types": "^2.1.4", "@types/pino": "^6.3.4", "axios": "^1.7.7", "bcryptjs": "^2.4.3", + "body-parser": "^1.20.3", "cookie-parser": "^1.4.5", "cors": "^2.8.5", "date-fns": "^2.16.1", @@ -54,7 +58,7 @@ "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^4.6.3", "uuid": "^8.3.2", - "whatsapp-web.js": "^1.26.0", + "whatsapp-web.js": "^v1.26.1-alpha.3", "yup": "^0.32.8" }, "devDependencies": { @@ -101,4 +105,4 @@ "engines": { "node": ">=18.0.0" } -} +} \ No newline at end of file diff --git a/backend/src/app.ts b/backend/src/app.ts index dd8155bc..4e265f29 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -1,13 +1,13 @@ -import "./bootstrap"; -import "reflect-metadata"; -import "express-async-errors"; -import express, { Request, Response, NextFunction } from "express"; -import cors from "cors"; -import cookieParser from "cookie-parser"; import * as Sentry from "@sentry/node"; - -import "./database"; +import bodyParser from "body-parser"; +import cookieParser from "cookie-parser"; +import cors from "cors"; +import express, { NextFunction, Request, Response } from "express"; +import "express-async-errors"; +import "reflect-metadata"; +import "./bootstrap"; import uploadConfig from "./config/upload"; +import "./database"; import AppError from "./errors/AppError"; import routes from "./routes"; import { logger } from "./utils/logger"; @@ -28,6 +28,9 @@ app.use(Sentry.Handlers.requestHandler()); app.use("/public", express.static(uploadConfig.directory)); app.use(routes); +app.use(bodyParser.json({ limit: "10mb" })); +app.use(bodyParser.urlencoded({ limit: "10mb", extended: true })); + app.use(Sentry.Handlers.errorHandler()); app.use(async (err: Error, req: Request, res: Response, _: NextFunction) => { diff --git a/backend/src/config/auth.ts b/backend/src/config/auth.ts index 6f8c5fd9..eba1b5f9 100644 --- a/backend/src/config/auth.ts +++ b/backend/src/config/auth.ts @@ -1,6 +1,6 @@ export default { secret: process.env.JWT_SECRET || "mysecret", - expiresIn: "15m", + expiresIn: "1h", refreshSecret: process.env.JWT_REFRESH_SECRET || "myanothersecret", refreshExpiresIn: "7d" }; diff --git a/backend/src/config/database.ts b/backend/src/config/database.ts index 290aab6b..e113c690 100644 --- a/backend/src/config/database.ts +++ b/backend/src/config/database.ts @@ -3,13 +3,13 @@ require("../bootstrap"); module.exports = { define: { charset: "utf8mb4", - collate: "utf8mb4_bin" + collate: "utf8mb4_general_ci" }, dialect: process.env.DB_DIALECT || "mysql", timezone: process.env.DB_TIMEZONE || "-03:00", - host: process.env.DB_HOST || 'localhost', - database: process.env.DB_NAME || 'press-ticket', - username: process.env.DB_USER || 'root', + host: process.env.DB_HOST || "localhost", + database: process.env.DB_NAME || "press-ticket", + username: process.env.DB_USER || "root", password: process.env.DB_PASS, port: process.env.DB_PORT || 3306, logging: false, diff --git a/backend/src/config/uploadConfig.ts b/backend/src/config/uploadConfig.ts new file mode 100644 index 00000000..fc389c14 --- /dev/null +++ b/backend/src/config/uploadConfig.ts @@ -0,0 +1,90 @@ +import { Request } from "express"; +import fs from "fs"; +import multer, { FileFilterCallback } from "multer"; +import path from "path"; + +const deleteIfExists = (filePath: string) => { + if (fs.existsSync(filePath)) { + fs.unlinkSync(filePath); + console.log(`Arquivo ${filePath} deletado com sucesso.`); + } else { + console.log(`Arquivo ${filePath} não encontrado para exclusão.`); + } +}; + +const storage = multer.diskStorage({ + destination: (req, file, cb) => { + const dest = path.resolve( + __dirname, + "..", + "..", + "..", + "frontend", + "public", + "assets" + ); + console.log(`Destino do upload: ${dest}`); + cb(null, dest); + }, + filename: (req: Request, file, cb) => { + const { theme } = req.params; + console.log(`Tema recebido: ${theme}`); + let fileName = ""; + + if (theme === "light") { + if (file.fieldname === "favico") { + fileName = "favico.ico"; + } else if (file.fieldname === "logo") { + fileName = "logo.jpg"; + } else if (file.fieldname === "logoTicket") { + fileName = "logoTicket.jpg"; + } + } else if (theme === "dark") { + if (file.fieldname === "favico") { + fileName = "favicoDark.ico"; + } else if (file.fieldname === "logo") { + fileName = "logoDark.jpg"; + } else if (file.fieldname === "logoTicket") { + fileName = "logoTicketDark.jpg"; + } + } + + const filePath = path.resolve( + __dirname, + "..", + "..", + "..", + "frontend", + "public", + "assets", + fileName + ); + console.log(`Nome do arquivo gerado: ${fileName}`); + deleteIfExists(filePath); + cb(null, fileName); + } +}); + +const fileFilter = ( + req: Request, + file: Express.Multer.File, + cb: FileFilterCallback +) => { + const allowedMimeTypes = ["image/jpeg", "image/png", "image/x-icon"]; + if (allowedMimeTypes.includes(file.mimetype)) { + cb(null, true); + } else { + cb( + new Error( + "Formato de arquivo inválido. Apenas .jpg, .png e .ico são permitidos." + ) + ); + } +}; + +const uploadConfig = multer({ + storage, + fileFilter +}); + +export default uploadConfig; diff --git a/backend/src/controllers/IntegrationController.ts b/backend/src/controllers/IntegrationController.ts index f03e04f7..be1e3b45 100644 --- a/backend/src/controllers/IntegrationController.ts +++ b/backend/src/controllers/IntegrationController.ts @@ -1,10 +1,10 @@ import { Request, Response } from "express"; -import { getIO } from "../libs/socket"; import AppError from "../errors/AppError"; +import { getIO } from "../libs/socket"; -import UpdateIntegrationService from "../services/IntegrationServices/UpdateIntegrationService"; import ListIntegrationsService from "../services/IntegrationServices/ListIntegrationsService"; +import UpdateIntegrationService from "../services/IntegrationServices/UpdateIntegrationService"; export const index = async (req: Request, res: Response): Promise => { if (req.user.profile === "") { @@ -20,7 +20,7 @@ export const update = async ( req: Request, res: Response ): Promise => { - if (req.user.profile !== "admin") { + if (req.user.profile !== "admin" && req.user.profile !== "masteradmin") { throw new AppError("ERR_NO_PERMISSION", 403); } const { integrationKey: key } = req.params; diff --git a/backend/src/controllers/PersonalizationController.ts b/backend/src/controllers/PersonalizationController.ts new file mode 100644 index 00000000..5a4639f8 --- /dev/null +++ b/backend/src/controllers/PersonalizationController.ts @@ -0,0 +1,152 @@ +import { Request, Response } from "express"; +import path from "path"; +import { getIO } from "../libs/socket"; +import createOrUpdatePersonalization from "../services/PersonalizationServices/CreateOrUpdatePersonalizationService"; +import deletePersonalization from "../services/PersonalizationServices/DeletePersonalizationService"; +import listPersonalizations from "../services/PersonalizationServices/ListPersonalizationsService"; + +interface PersonalizationData { + theme: string; + company?: string; + url?: string; + primaryColor?: string; + secondaryColor?: string; + backgroundDefault?: string; + backgroundPaper?: string; + favico?: string | null; + logo?: string | null; + logoTicket?: string | null; +} + +export const createOrUpdateCompany = async ( + req: Request, + res: Response +): Promise => { + try { + const { theme } = req.params; + const { company, url } = req.body; + + const personalizationData = { theme, company, url }; + + const personalization = await createOrUpdatePersonalization({ + personalizationData, + theme + }); + + const io = getIO(); + io.emit("personalization", { + action: personalization.isNew ? "create" : "update", + personalization: personalization.data + }); + + return res.status(200).json(personalization.data); + } catch (error) { + return res.status(500).json({ message: error.message }); + } +}; + +export const createOrUpdateLogos = async ( + req: Request, + res: Response +): Promise => { + try { + const { theme } = req.params; + const personalizationData: PersonalizationData = { + theme + }; + + if (req.files) { + const files = req.files as { [fieldname: string]: Express.Multer.File[] }; + + if (files.favico && files.favico.length > 0) { + personalizationData.favico = path.basename(files.favico[0].path); + } + if (files.logo && files.logo.length > 0) { + personalizationData.logo = path.basename(files.logo[0].path); + } + if (files.logoTicket && files.logoTicket.length > 0) { + personalizationData.logoTicket = path.basename( + files.logoTicket[0].path + ); + } + } + + personalizationData.theme = theme; + + const personalization = await createOrUpdatePersonalization({ + personalizationData, + theme + }); + + const io = getIO(); + io.emit("personalization", { + action: personalization.isNew ? "create" : "update", + personalization: personalization.data + }); + + return res.status(200).json(personalization.data); + } catch (error) { + return res.status(500).json({ message: error.message }); + } +}; + +export const createOrUpdateColors = async ( + req: Request, + res: Response +): Promise => { + try { + const { theme } = req.params; + const { primaryColor, secondaryColor, backgroundDefault, backgroundPaper } = + req.body; + + const personalizationData: PersonalizationData = { + theme, + primaryColor, + secondaryColor, + backgroundDefault, + backgroundPaper + }; + + const personalization = await createOrUpdatePersonalization({ + personalizationData, + theme + }); + + const io = getIO(); + io.emit("personalization", { + action: personalization.isNew ? "create" : "update", + personalization: personalization.data + }); + + return res.status(200).json(personalization.data); + } catch (error) { + return res.status(500).json({ message: error.message }); + } +}; + +export const list = async (_req: Request, res: Response): Promise => { + try { + const personalizations = await listPersonalizations(); + return res.status(200).json(personalizations); + } catch (error) { + return res.status(500).json({ message: error.message }); + } +}; + +export const remove = async ( + req: Request, + res: Response +): Promise => { + try { + const { theme } = req.params; + await deletePersonalization(theme); + const io = getIO(); + io.emit("personalization", { + action: "delete", + theme + }); + return res.status(204).send(); + } catch (error) { + return res.status(404).json({ message: error.message }); + } +}; diff --git a/backend/src/controllers/SettingController.ts b/backend/src/controllers/SettingController.ts index 99197b7a..3ed97553 100644 --- a/backend/src/controllers/SettingController.ts +++ b/backend/src/controllers/SettingController.ts @@ -1,10 +1,10 @@ import { Request, Response } from "express"; -import { getIO } from "../libs/socket"; import AppError from "../errors/AppError"; +import { getIO } from "../libs/socket"; -import UpdateSettingService from "../services/SettingServices/UpdateSettingService"; import ListSettingsService from "../services/SettingServices/ListSettingsService"; +import UpdateSettingService from "../services/SettingServices/UpdateSettingService"; export const index = async (req: Request, res: Response): Promise => { if (req.user.profile === "") { @@ -20,7 +20,7 @@ export const update = async ( req: Request, res: Response ): Promise => { - if (req.user.profile !== "admin") { + if (req.user.profile !== "admin" && req.user.profile !== "masteradmin") { throw new AppError("ERR_NO_PERMISSION", 403); } const { settingKey: key } = req.params; diff --git a/backend/src/controllers/UserController.ts b/backend/src/controllers/UserController.ts index 9de1bd9f..a7b71867 100644 --- a/backend/src/controllers/UserController.ts +++ b/backend/src/controllers/UserController.ts @@ -1,14 +1,14 @@ import { Request, Response } from "express"; import { getIO } from "../libs/socket"; -import CheckSettingsHelper from "../helpers/CheckSettings"; import AppError from "../errors/AppError"; +import CheckSettingsHelper from "../helpers/CheckSettings"; import CreateUserService from "../services/UserServices/CreateUserService"; +import DeleteUserService from "../services/UserServices/DeleteUserService"; import ListUsersService from "../services/UserServices/ListUsersService"; -import UpdateUserService from "../services/UserServices/UpdateUserService"; import ShowUserService from "../services/UserServices/ShowUserService"; -import DeleteUserService from "../services/UserServices/DeleteUserService"; +import UpdateUserService from "../services/UserServices/UpdateUserService"; type IndexQuery = { searchParam: string; @@ -119,7 +119,7 @@ export const remove = async ( ): Promise => { const { userId } = req.params; - if (req.user.profile !== "admin") { + if (req.user.profile !== "admin" && req.user.profile !== "masteradmin") { throw new AppError("ERR_NO_PERMISSION", 403); } diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index 26727add..e3f67838 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -1,12 +1,13 @@ import { Request, Response } from "express"; import AppError from "../errors/AppError"; import { getIO } from "../libs/socket"; -import { removeWbot } from "../libs/wbot"; +import { initWbot, removeWbot, shutdownWbot } from "../libs/wbot"; +import Whatsapp from "../models/Whatsapp"; import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession"; - import CreateWhatsAppService from "../services/WhatsappService/CreateWhatsAppService"; import DeleteWhatsAppService from "../services/WhatsappService/DeleteWhatsAppService"; import ListWhatsAppsService from "../services/WhatsappService/ListWhatsAppsService"; +import RestartWhatsAppService from "../services/WhatsappService/RestartWhatsAppService"; import ShowWhatsAppService from "../services/WhatsappService/ShowWhatsAppService"; import UpdateWhatsAppService from "../services/WhatsappService/UpdateWhatsAppService"; @@ -124,3 +125,88 @@ export const remove = async ( return res.status(200).json({ message: "Whatsapp deleted." }); }; + +export const restart = async ( + req: Request, + res: Response +): Promise => { + const { whatsappId } = req.params; + + try { + await RestartWhatsAppService(whatsappId); + const io = getIO(); + io.emit("whatsapp", { + action: "update", + whatsappId + }); + return res + .status(200) + .json({ message: "WhatsApp session restarted successfully." }); + } catch (error) { + return res.status(500).json({ + message: "Failed to restart WhatsApp session.", + error: (error as Error).message + }); + } +}; + +export const shutdown = async ( + req: Request, + res: Response +): Promise => { + const { whatsappId } = req.params; + + if (!whatsappId) { + return res.status(400).json({ message: "WhatsApp ID is required." }); + } + + try { + console.log(`Iniciando shutdown para WhatsApp ID: ${whatsappId}`); + + await shutdownWbot(whatsappId); + console.log( + `Shutdown realizado com sucesso para WhatsApp ID: ${whatsappId}` + ); + + const io = getIO(); + io.emit("whatsapp", { + action: "update", + whatsappId + }); + console.log("Evento emitido com sucesso via WebSocket."); + + return res.status(200).json({ + message: "WhatsApp session shutdown successfully." + }); + } catch (error) { + console.error("Erro ao desligar o WhatsApp:", error); + + return res.status(500).json({ + message: "Failed to shutdown WhatsApp session.", + error: (error as Error).message + }); + } +}; + +export const start = async (req: Request, res: Response): Promise => { + const { whatsappId } = req.params; + const whatsapp = await Whatsapp.findByPk(whatsappId); + if (!whatsapp) throw Error("no se encontro el whatsapp"); + + try { + await initWbot(whatsapp); + const io = getIO(); + io.emit("whatsapp", { + action: "update", + whatsappId + }); + return res + .status(200) + .json({ message: "WhatsApp session started successfully." }); + } catch (error) { + return res.status(500).json({ + message: "Failed to start WhatsApp session.", + error: (error as Error).message + }); + } +}; diff --git a/backend/src/controllers/WhatsAppSessionController.ts b/backend/src/controllers/WhatsAppSessionController.ts index 38cf9295..1333abac 100644 --- a/backend/src/controllers/WhatsAppSessionController.ts +++ b/backend/src/controllers/WhatsAppSessionController.ts @@ -27,14 +27,25 @@ const update = async (req: Request, res: Response): Promise => { }; const remove = async (req: Request, res: Response): Promise => { - const { whatsappId } = req.params; - const whatsapp = await ShowWhatsAppService(whatsappId); - - const wbot = getWbot(whatsapp.id); - - wbot.logout(); - - return res.status(200).json({ message: "Session disconnected." }); + try { + console.log("Recebendo solicitação de desconexão..."); + const { whatsappId } = req.params; + const whatsapp = await ShowWhatsAppService(whatsappId); + + console.log("Obtendo instância do WhatsApp..."); + const wbot = getWbot(whatsapp.id); + + console.log("Executando logout..."); + if (wbot && typeof wbot.logout === "function") { + await wbot.logout(); + } + + console.log("Logout concluído. Respondendo ao cliente..."); + return res.status(200).json({ message: "Session disconnected." }); + } catch (error) { + console.error("Erro ao desconectar:", error); + return res.status(500).json({ error: "Failed to disconnect session." }); + } }; export default { store, remove, update }; diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 38a78f81..04cc54aa 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -5,6 +5,7 @@ import ContactTag from "../models/ContactTag"; import Integration from "../models/Integration"; import Message from "../models/Message"; import OldMessage from "../models/OldMessage"; +import Personalization from "../models/Personalization"; import Queue from "../models/Queue"; import QuickAnswer from "../models/QuickAnswer"; import Setting from "../models/Setting"; @@ -36,7 +37,8 @@ const models = [ Tag, ContactTag, Integration, - OldMessage + OldMessage, + Personalization ]; sequelize.addModels(models); diff --git a/backend/src/database/migrations/20240921153422-alter-queueId-foreign-key-on-tickets.ts b/backend/src/database/migrations/20240921153422-alter-queueId-foreign-key-on-tickets.ts index 045ac75c..61119e99 100644 --- a/backend/src/database/migrations/20240921153422-alter-queueId-foreign-key-on-tickets.ts +++ b/backend/src/database/migrations/20240921153422-alter-queueId-foreign-key-on-tickets.ts @@ -2,17 +2,18 @@ import { QueryInterface } from "sequelize"; export default { up: async (queryInterface: QueryInterface) => { - const [results]: any = await queryInterface.sequelize.query(` - SELECT CONSTRAINT_NAME - FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_NAME = 'Tickets' AND COLUMN_NAME = 'queueId'; - `); + const [results]: any = await queryInterface.sequelize.query( + "SHOW CREATE TABLE Tickets;" + ); + const createTableSql = results[0]["Create Table"]; - if (results.length > 0) { - const constraintName = results[0].CONSTRAINT_NAME; - if (constraintName) { - await queryInterface.removeConstraint("Tickets", constraintName); - } + if ( + createTableSql.includes("CONSTRAINT `Tickets_queueId_custom_foreign`") + ) { + await queryInterface.removeConstraint( + "Tickets", + "Tickets_queueId_custom_foreign" + ); } await queryInterface.addConstraint("Tickets", ["queueId"], { @@ -28,10 +29,19 @@ export default { }, down: async (queryInterface: QueryInterface) => { - await queryInterface.removeConstraint( - "Tickets", - "Tickets_queueId_custom_foreign" + const [results]: any = await queryInterface.sequelize.query( + "SHOW CREATE TABLE Tickets;" ); + const createTableSql = results[0]["Create Table"]; + + if ( + createTableSql.includes("CONSTRAINT `Tickets_queueId_custom_foreign`") + ) { + await queryInterface.removeConstraint( + "Tickets", + "Tickets_queueId_custom_foreign" + ); + } await queryInterface.addConstraint("Tickets", ["queueId"], { type: "foreign key", diff --git a/backend/src/database/migrations/20241106170332-create-personalizations.ts b/backend/src/database/migrations/20241106170332-create-personalizations.ts new file mode 100644 index 00000000..df2a42e2 --- /dev/null +++ b/backend/src/database/migrations/20241106170332-create-personalizations.ts @@ -0,0 +1,96 @@ +import { DataTypes, QueryInterface } from "sequelize"; + +module.exports = { + up: async (queryInterface: QueryInterface) => { + await queryInterface.createTable("Personalizations", { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true, + allowNull: false + }, + theme: { + type: DataTypes.STRING, + allowNull: true + }, + company: { + type: DataTypes.STRING, + allowNull: true + }, + url: { + type: DataTypes.STRING, + allowNull: true + }, + primaryColor: { + type: DataTypes.STRING, + allowNull: true + }, + secondaryColor: { + type: DataTypes.STRING, + allowNull: true + }, + backgroundDefault: { + type: DataTypes.STRING, + allowNull: true + }, + backgroundPaper: { + type: DataTypes.STRING, + allowNull: true + }, + favico: { + type: DataTypes.TEXT, + allowNull: true + }, + logo: { + type: DataTypes.TEXT, + allowNull: true + }, + logoTicket: { + type: DataTypes.TEXT, + allowNull: true + }, + toolbarColor: { + type: DataTypes.STRING, + allowNull: true + }, + toolbarIconColor: { + type: DataTypes.STRING, + allowNull: true + }, + menuItens: { + type: DataTypes.STRING, + allowNull: true + }, + sub: { + type: DataTypes.STRING, + allowNull: true + }, + textPrimary: { + type: DataTypes.STRING, + allowNull: true + }, + textSecondary: { + type: DataTypes.STRING, + allowNull: true + }, + divide: { + type: DataTypes.STRING, + allowNull: true + }, + createdAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + }, + updatedAt: { + type: DataTypes.DATE, + allowNull: false, + defaultValue: DataTypes.NOW + } + }); + }, + + down: async (queryInterface: QueryInterface) => { + await queryInterface.dropTable("Personalizations"); + } +}; diff --git a/backend/src/database/seeds/20200904070004-create-default-settings.ts b/backend/src/database/seeds/20200904070004-create-default-settings.ts index e7fa676d..b385284f 100644 --- a/backend/src/database/seeds/20200904070004-create-default-settings.ts +++ b/backend/src/database/seeds/20200904070004-create-default-settings.ts @@ -40,12 +40,6 @@ module.exports = { value: "disabled", createdAt: new Date(), updatedAt: new Date() - }, - { - key: "darkMode", - value: "disabled", - createdAt: new Date(), - updatedAt: new Date() } ], {} diff --git a/backend/src/database/seeds/20241118200400-create-masteradmin-user.ts b/backend/src/database/seeds/20241118200400-create-masteradmin-user.ts new file mode 100644 index 00000000..2c4b60f4 --- /dev/null +++ b/backend/src/database/seeds/20241118200400-create-masteradmin-user.ts @@ -0,0 +1,26 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: async (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Users", + [ + { + name: "MasterAdmin", + email: "masteradmin@pressticket.com.br", + passwordHash: + "$2a$08$nLlBSlHj.6XJNFLq.FSjVOjp4rSFHtFYHSUewBIQhceOv4gXU3yLC", + profile: "masteradmin", + tokenVersion: 0, + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: async (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Users", {}); + } +}; diff --git a/backend/src/database/seeds/20241208171800-create-themes-personalizations.ts b/backend/src/database/seeds/20241208171800-create-themes-personalizations.ts new file mode 100644 index 00000000..3bf77afe --- /dev/null +++ b/backend/src/database/seeds/20241208171800-create-themes-personalizations.ts @@ -0,0 +1,56 @@ +import { QueryInterface } from "sequelize"; + +module.exports = { + up: (queryInterface: QueryInterface) => { + return queryInterface.bulkInsert( + "Personalizations", + [ + { + theme: "light", + company: "Press Ticket", + url: "https://pressticket.com.br", + primaryColor: "#5C4B9B", + secondaryColor: "#D5C6F0", + backgroundDefault: "#FFFFFF", + backgroundPaper: "#F7F7F7", + toolbarColor: "#5C4B9B", + toolbarIconColor: "#FFFFFF", + menuItens: "#FFFFFF", + sub: "#F7F7F7", + textPrimary: "#000000", + textSecondary: "#333333", + divide: "#E0E0E0", + favico: null, + logo: null, + logoTicket: null, + createdAt: new Date(), + updatedAt: new Date() + }, + { + theme: "dark", + primaryColor: "#8A7DCC", + secondaryColor: "#CCCCCC", + backgroundDefault: "#2E2E3A", + backgroundPaper: "#383850", + toolbarColor: "#8A7DCC", + toolbarIconColor: "#FFFFFF", + menuItens: "#181D22", + sub: "#383850", + textPrimary: "#FFFFFF", + textSecondary: "#CCCCCC", + divide: "#2E2E3A", + favico: null, + logo: null, + logoTicket: null, + createdAt: new Date(), + updatedAt: new Date() + } + ], + {} + ); + }, + + down: (queryInterface: QueryInterface) => { + return queryInterface.bulkDelete("Personalizations", {}); + } +}; diff --git a/backend/src/libs/wbot.ts b/backend/src/libs/wbot.ts index 0663364e..1974b11c 100644 --- a/backend/src/libs/wbot.ts +++ b/backend/src/libs/wbot.ts @@ -1,4 +1,6 @@ +import fs from "fs/promises"; import { Configuration, CreateImageRequestSizeEnum, OpenAIApi } from "openai"; +import path from "path"; import qrCode from "qrcode-terminal"; import { Client, LocalAuth, MessageMedia } from "whatsapp-web.js"; import AppError from "../errors/AppError"; @@ -20,17 +22,14 @@ interface CreateImageRequest { } async function findIntegrationValue(key: string): Promise { - // Encontre a instância de integração com base na chave fornecida const integration = await Integration.findOne({ where: { key } }); - // Se a instância for encontrada, retorne o valor if (integration) { return integration.value; } - // Caso contrário, retorne null return null as string | null; } @@ -50,13 +49,12 @@ let openai: OpenAIApi; openai = new OpenAIApi(configuration); })(); -// gera resposta em texto const getDavinciResponse = async (clientText: string): Promise => { const options = { - model: "text-davinci-003", // Modelo GPT a ser usado - prompt: clientText, // Texto enviado pelo usuário - temperature: 1, // Nível de variação das respostas geradas, 1 é o máximo - max_tokens: 4000 // Quantidade de tokens (palavras) a serem retornadas pelo bot, 4000 é o máximo + model: "text-davinci-003", + prompt: clientText, + temperature: 1, + max_tokens: 4000 }; try { @@ -71,15 +69,14 @@ const getDavinciResponse = async (clientText: string): Promise => { } }; -// gera a url da imagem const getDalleResponse = async ( clientText: string ): Promise => { const options: CreateImageRequest = { - prompt: clientText, // Descrição da imagem - n: 1, // Número de imagens a serem geradas + prompt: clientText, + n: 1, // eslint-disable-next-line no-underscore-dangle - size: CreateImageRequestSizeEnum._1024x1024 // Tamanho da imagem + size: CreateImageRequestSizeEnum._1024x1024 }; try { @@ -238,7 +235,7 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { wbot.on("message", async msg => { const msgChatGPT: string = msg.body; - // mensagem de texto + if (msgChatGPT.includes("/gpt ")) { const index = msgChatGPT.indexOf(" "); const question = msgChatGPT.substring(index + 1); @@ -246,7 +243,7 @@ export const initWbot = async (whatsapp: Whatsapp): Promise => { wbot.sendMessage(msg.from, response); }); } - // imagem + if (msgChatGPT.includes("/gptM ")) { const index = msgChatGPT.indexOf(" "); const imgDescription = msgChatGPT.substring(index + 1); @@ -285,3 +282,59 @@ export const removeWbot = (whatsappId: number): void => { logger.error(err); } }; + +export const restartWbot = async (whatsappId: number): Promise => { + const sessionIndex = sessions.findIndex(s => s.id === whatsappId); + if (sessionIndex !== -1) { + const whatsapp = await Whatsapp.findByPk(whatsappId); + if (!whatsapp) { + throw new AppError("WhatsApp not found."); + } + sessions[sessionIndex].destroy(); + sessions.splice(sessionIndex, 1); + + const newSession = await initWbot(whatsapp); + return newSession; + } + throw new AppError("WhatsApp session not initialized."); +}; + +export const shutdownWbot = async (whatsappId: string): Promise => { + const whatsappIDNumber: number = parseInt(whatsappId, 10); + + if (Number.isNaN(whatsappIDNumber)) { + throw new AppError("Invalid WhatsApp ID format."); + } + + const sessionIndex = sessions.findIndex(s => s.id === whatsappIDNumber); + if (sessionIndex === -1) { + console.warn(`Sessão com ID ${whatsappIDNumber} não foi encontrada.`); + throw new AppError("WhatsApp session not initialized."); + } + + const sessionPath = path.resolve( + __dirname, + `../../.wwebjs_auth/session-bd_${whatsappIDNumber}` + ); + + try { + console.log(`Desligando sessão para WhatsApp ID: ${whatsappIDNumber}`); + await sessions[sessionIndex].destroy(); + console.log(`Sessão com ID ${whatsappIDNumber} desligada com sucesso.`); + + console.log(`Removendo arquivos da sessão: ${sessionPath}`); + await fs.rm(sessionPath, { recursive: true, force: true }); + console.log(`Arquivos da sessão removidos com sucesso: ${sessionPath}`); + + sessions.splice(sessionIndex, 1); + console.log( + `Sessão com ID ${whatsappIDNumber} removida da lista de sessões.` + ); + } catch (error) { + console.error( + `Erro ao desligar ou limpar a sessão com ID ${whatsappIDNumber}:`, + error + ); + throw new AppError("Failed to destroy WhatsApp session."); + } +}; diff --git a/backend/src/models/OldMessage.ts b/backend/src/models/OldMessage.ts index 5ebdca05..bf1949c6 100644 --- a/backend/src/models/OldMessage.ts +++ b/backend/src/models/OldMessage.ts @@ -32,7 +32,7 @@ class OldMessage extends Model { updatedAt: Date; @ForeignKey(() => Message) - @Column + @Column(DataType.STRING) messageId: string; @BelongsTo(() => Message, "messageId") diff --git a/backend/src/models/Personalization.ts b/backend/src/models/Personalization.ts new file mode 100644 index 00000000..5d18d0fe --- /dev/null +++ b/backend/src/models/Personalization.ts @@ -0,0 +1,79 @@ +import { + AutoIncrement, + Column, + CreatedAt, + DataType, + Model, + PrimaryKey, + Table, + UpdatedAt +} from "sequelize-typescript"; + +@Table +class Personalization extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column({ type: DataType.STRING, allowNull: true }) + theme: string; + + @Column({ type: DataType.STRING, allowNull: true }) + company: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + url: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + primaryColor: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + secondaryColor: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + backgroundDefault: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + backgroundPaper: string | null; + + @Column({ type: DataType.TEXT, allowNull: true }) + favico: string | null; + + @Column({ type: DataType.TEXT, allowNull: true }) + logo: string | null; + + @Column({ type: DataType.TEXT, allowNull: true }) + logoTicket: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + toolbarColor: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + toolbarIconColor: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + menuItens: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + sub: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + textPrimary: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + textSecondary: string | null; + + @Column({ type: DataType.STRING, allowNull: true }) + divide: string | null; + + @CreatedAt + @Column(DataType.DATE(6)) + createdAt: Date; + + @UpdatedAt + @Column(DataType.DATE(6)) + updatedAt: Date; +} + +export default Personalization; diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 931fd100..82ae299f 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -8,6 +8,7 @@ import hubMessageRoutes from "./hubMessageRoutes"; import hubWebhookRoutes from "./hubWebhookRoutes"; import integrationRoutes from "./integrationRoutes"; import messageRoutes from "./messageRoutes"; +import personalizationRoutes from "./personalizationRoutes"; import queueRoutes from "./queueRoutes"; import quickAnswerRoutes from "./quickAnswerRoutes"; import settingRoutes from "./settingRoutes"; @@ -37,5 +38,6 @@ routes.use(hubChannelRoutes); routes.use(hubMessageRoutes); routes.use(hubWebhookRoutes); routes.use(systemRoutes); +routes.use(personalizationRoutes); export default routes; diff --git a/backend/src/routes/personalizationRoutes.ts b/backend/src/routes/personalizationRoutes.ts new file mode 100644 index 00000000..dea2454f --- /dev/null +++ b/backend/src/routes/personalizationRoutes.ts @@ -0,0 +1,37 @@ +import { Router } from "express"; +import uploadConfig from "../config/uploadConfig"; +import * as PersonalizationController from "../controllers/PersonalizationController"; +import isAuth from "../middleware/isAuth"; + +const personalizationRoutes = Router(); + +personalizationRoutes.get("/personalizations", PersonalizationController.list); +personalizationRoutes.delete( + "/personalizations/:theme", + isAuth, + PersonalizationController.remove +); +personalizationRoutes.put( + "/personalizations/:theme/company", + isAuth, + PersonalizationController.createOrUpdateCompany +); + +personalizationRoutes.put( + "/personalizations/:theme/logos", + isAuth, + uploadConfig.fields([ + { name: "favico", maxCount: 1 }, + { name: "logo", maxCount: 1 }, + { name: "logoTicket", maxCount: 1 } + ]), + PersonalizationController.createOrUpdateLogos +); + +personalizationRoutes.put( + "/personalizations/:theme/colors", + isAuth, + PersonalizationController.createOrUpdateColors +); + +export default personalizationRoutes; diff --git a/backend/src/routes/whatsappRoutes.ts b/backend/src/routes/whatsappRoutes.ts index dc187a70..8fc40023 100644 --- a/backend/src/routes/whatsappRoutes.ts +++ b/backend/src/routes/whatsappRoutes.ts @@ -1,7 +1,6 @@ import express from "express"; -import isAuth from "../middleware/isAuth"; - import * as WhatsAppController from "../controllers/WhatsAppController"; +import isAuth from "../middleware/isAuth"; const whatsappRoutes = express.Router(); @@ -19,4 +18,16 @@ whatsappRoutes.delete( WhatsAppController.remove ); +whatsappRoutes.post( + "/whatsapp/:whatsappId/restart", + isAuth, + WhatsAppController.restart +); + +whatsappRoutes.post( + "/whatsapp/:whatsappId/shutdown", + isAuth, + WhatsAppController.shutdown +); + export default whatsappRoutes; diff --git a/backend/src/services/PersonalizationServices/CreateOrUpdatePersonalizationService.ts b/backend/src/services/PersonalizationServices/CreateOrUpdatePersonalizationService.ts new file mode 100644 index 00000000..5e0883e7 --- /dev/null +++ b/backend/src/services/PersonalizationServices/CreateOrUpdatePersonalizationService.ts @@ -0,0 +1,74 @@ +import Personalization from "../../models/Personalization"; + +interface PersonalizationData { + theme: string; + company?: string; + url?: string; + primaryColor?: string; + secondaryColor?: string; + backgroundDefault?: string; + backgroundPaper?: string; + favico?: string | null; + logo?: string | null; + logoTicket?: string | null; +} + +interface Response { + isNew: boolean; + data: Personalization; +} + +const createOrUpdatePersonalization = async ({ + personalizationData, + theme +}: { + personalizationData: PersonalizationData; + theme: string; +}): Promise => { + const { + company, + url, + primaryColor, + secondaryColor, + backgroundDefault, + backgroundPaper, + favico, + logo, + logoTicket + } = personalizationData; + + let personalization = await Personalization.findOne({ where: { theme } }); + + if (personalization) { + await personalization.update({ + company, + url, + primaryColor, + secondaryColor, + backgroundDefault, + backgroundPaper, + favico, + logo, + logoTicket + }); + + await personalization.reload(); + return { isNew: false, data: personalization }; + } + personalization = await Personalization.create({ + theme, + company, + url, + primaryColor, + secondaryColor, + backgroundDefault, + backgroundPaper, + favico, + logo, + logoTicket + }); + + return { isNew: true, data: personalization }; +}; + +export default createOrUpdatePersonalization; diff --git a/backend/src/services/PersonalizationServices/DeletePersonalizationService.ts b/backend/src/services/PersonalizationServices/DeletePersonalizationService.ts new file mode 100644 index 00000000..5e4bf7b3 --- /dev/null +++ b/backend/src/services/PersonalizationServices/DeletePersonalizationService.ts @@ -0,0 +1,13 @@ +import AppError from "../../errors/AppError"; +import Personalization from "../../models/Personalization"; + +const deletePersonalization = async (theme: string): Promise => { + const personalization = await Personalization.findOne({ where: { theme } }); + + if (!personalization) { + throw new AppError("ERR_NO_PERSONALIZATION_FOUND", 404); + } + await personalization.destroy(); +}; + +export default deletePersonalization; diff --git a/backend/src/services/PersonalizationServices/ListPersonalizationsService.ts b/backend/src/services/PersonalizationServices/ListPersonalizationsService.ts new file mode 100644 index 00000000..89dd4079 --- /dev/null +++ b/backend/src/services/PersonalizationServices/ListPersonalizationsService.ts @@ -0,0 +1,8 @@ +import Personalization from "../../models/Personalization"; + +const listPersonalizations = async (): Promise => { + const personalizations = await Personalization.findAll(); + return personalizations; +}; + +export default listPersonalizations; diff --git a/backend/src/services/TicketServices/ListTicketsService.ts b/backend/src/services/TicketServices/ListTicketsService.ts index e31e43f3..a1bac484 100644 --- a/backend/src/services/TicketServices/ListTicketsService.ts +++ b/backend/src/services/TicketServices/ListTicketsService.ts @@ -1,10 +1,10 @@ import { endOfDay, parseISO, startOfDay } from "date-fns"; import { col, Filterable, fn, Includeable, Op, where } from "sequelize"; - import Contact from "../../models/Contact"; import Message from "../../models/Message"; import Queue from "../../models/Queue"; import Ticket from "../../models/Ticket"; +import User from "../../models/User"; import Whatsapp from "../../models/Whatsapp"; import ListSettingsServiceOne from "../SettingServices/ListSettingsServiceOne"; import ShowUserService from "../UserServices/ShowUserService"; @@ -67,7 +67,12 @@ const ListTicketsService = async ({ { model: Whatsapp, as: "whatsapp", - attributes: ["name", "type", "color"] + attributes: ["id", "name", "type", "color"] + }, + { + model: User, + as: "user", + attributes: ["id", "name"] } ]; @@ -144,7 +149,7 @@ const ListTicketsService = async ({ }; } - const limit = 100; + const limit = 20; const offset = limit * (+pageNumber - 1); const listSettingsService = await ListSettingsServiceOne({ key: "ASC" }); diff --git a/backend/src/services/UserServices/ListUsersService.ts b/backend/src/services/UserServices/ListUsersService.ts index 60d6baf7..055bc6cb 100644 --- a/backend/src/services/UserServices/ListUsersService.ts +++ b/backend/src/services/UserServices/ListUsersService.ts @@ -19,15 +19,20 @@ const ListUsersService = async ({ pageNumber = "1" }: Request): Promise => { const whereCondition = { - [Op.or]: [ + [Op.and]: [ + { profile: { [Op.ne]: "masteradmin" } }, { - "$User.name$": Sequelize.where( - Sequelize.fn("LOWER", Sequelize.col("User.name")), - "LIKE", - `%${searchParam.toLowerCase()}%` - ) - }, - { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } } + [Op.or]: [ + { + "$User.name$": Sequelize.where( + Sequelize.fn("LOWER", Sequelize.col("User.name")), + "LIKE", + `%${searchParam.toLowerCase()}%` + ) + }, + { email: { [Op.like]: `%${searchParam.toLowerCase()}%` } } + ] + } ] }; const limit = 20; diff --git a/backend/src/services/WbotServices/wbotMessageListener.ts b/backend/src/services/WbotServices/wbotMessageListener.ts index ab1c13f5..a3ae458a 100644 --- a/backend/src/services/WbotServices/wbotMessageListener.ts +++ b/backend/src/services/WbotServices/wbotMessageListener.ts @@ -43,7 +43,6 @@ interface Session extends Client { const writeFileAsync = promisify(writeFile); const verifyContact = async (msgContact: WbotContact): Promise => { - const contactData = { name: msgContact.name || msgContact.pushname || msgContact.id.user, number: msgContact.id.user, @@ -236,7 +235,7 @@ const verifyMediaMessage = async ( id: msg.id.id, ticketId: ticket.id, contactId: msg.fromMe ? undefined : contact.id, - body: $strBody, + body: $strBody || media.filename, fromMe: msg.fromMe, read: msg.fromMe, mediaUrl: media.filename, diff --git a/backend/src/services/WhatsappService/RestartWhatsAppService.ts b/backend/src/services/WhatsappService/RestartWhatsAppService.ts new file mode 100644 index 00000000..f806506a --- /dev/null +++ b/backend/src/services/WhatsappService/RestartWhatsAppService.ts @@ -0,0 +1,23 @@ +// src/services/WhatsappService/RestartWhatsAppService.ts +import { getWbot, restartWbot } from "../../libs/wbot"; +import { logger } from "../../utils/logger"; + +const RestartWhatsAppService = async (whatsappId: string): Promise => { + const whatsappIDNumber: number = parseInt(whatsappId, 10); + + try { + const wbot = getWbot(whatsappIDNumber); + if (!wbot) { + throw new Error("No active session found for this ID."); + } + + await restartWbot(whatsappIDNumber); + logger.info(`WhatsApp session for ID ${whatsappId} has been restarted.`); + } catch (error) { + logger.error( + `Failed to restart WhatsApp session: ${(error as Error).message}` + ); + } +}; + +export default RestartWhatsAppService; diff --git a/docs/INSTALL_VPS.md b/docs/INSTALL_VPS.md index 18abd057..915414cd 100644 --- a/docs/INSTALL_VPS.md +++ b/docs/INSTALL_VPS.md @@ -307,14 +307,14 @@ npm install #URL BACKEND REACT_APP_BACKEND_URL=https://back.pressticket.com.br -#Tempo de encerramento automático dos tickets +#Tempo de encerramento automático dos tickets em horas REACT_APP_HOURS_CLOSE_TICKETS_AUTO= -#Nome da Guia do navegador -REACT_APP_PAGE_TITLE=PressTicket - #PORTA do frontend PORT=3333 + +#Para permitir acesso apenas do MasterAdmin (sempre ON) +REACT_APP_MASTERADMIN=OFF ``` ### 8.4 Editando o arquivo .env do frontend usando os dados do item 8.3 @@ -323,31 +323,25 @@ PORT=3333 nano .env ``` -### 8.5 Criando o arquivo config.json baseado no exemplo - -``` -cp src/config.json.example src/config.json -``` - -### 8.6 Compilando o frontend +### 8.5 Compilando o frontend ``` npm run build ``` -### 8.7 Iniciando o frontend com PM2 +### 8.6 Iniciando o frontend com PM2 ``` pm2 start server.js --name Press-Ticket-frontend ``` -### 8.8 Salvando os serviços iniciados pelo PM2 +### 8.7 Salvando os serviços iniciados pelo PM2 ``` pm2 save ``` -### 8.9 Listar os serviços iniciados pelo PM2 +### 8.8 Listar os serviços iniciados pelo PM2 ``` pm2 list @@ -367,7 +361,7 @@ sudo apt install nginx sudo nano /etc/nginx/sites-available/Press-Ticket-frontend ``` -Preencha com as informações abaixo: +Preencha com as informações abaixo, atualizando as informações de acordo com o seu domínio: ``` server { @@ -392,7 +386,7 @@ server { sudo nano /etc/nginx/sites-available/Press-Ticket-backend ``` -Preencha com as informações abaixo: +Preencha com as informações abaixo atualizando as informações de acordo com o seu domínio: ``` server { @@ -470,7 +464,7 @@ sudo nano /etc/nginx/nginx.conf ### 9.12 Incluir no arquivos de configuração do nginx dentro do http no item 9.11 ``` -client_max_body_size 20M; +client_max_body_size 50M; ``` ### 9.13 Testando o Nginx @@ -528,3 +522,17 @@ Senha: ``` admin ``` + +# Seção 12: Usuário Master para Acesso + +Usuário: + +``` +masteradmin@pressticket.com.br +``` + +Senha: + +``` +masteradmin +``` diff --git a/docs/INSTALL_localhost.md b/docs/INSTALL_localhost.md index 6586334c..929d3552 100644 --- a/docs/INSTALL_localhost.md +++ b/docs/INSTALL_localhost.md @@ -18,7 +18,7 @@ Execute o seguinte comando no seu terminal para criar o banco de dados: ```bash -CREATE DATABASE press-ticket CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +CREATE DATABASE press_ticket CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ``` #### 1.2. Se estiver usando XAMPP ou WAMPP, poderá criar o banco de dados via phpMyAdmin: @@ -59,25 +59,35 @@ Crie ou edite o arquivo `.env` no diretório `backend` com as seguintes informa ```bash NODE_ENV= -WEBHOOK=https://true-melons-travel.loca.lt + +#URLs e Portas +WEBHOOK=https://ninety-yaks-trade.loca.lt BACKEND_URL=http://localhost FRONTEND_URL=http://localhost:3333 PORT=8080 PROXY_PORT=8080 + +#Caminho do Chrome CHROME_BIN=C:\Program Files\Google\Chrome\Application\chrome.exe +#Dados de acesso ao Banco de dados DB_DIALECT=mysql DB_HOST=localhost DB_TIMEZONE=-03:00 DB_USER=root DB_PASS= -DB_NAME=press-ticket +DB_NAME=press_ticket +#Limitar Usuários e Conexões USER_LIMIT=3 -CONNECTIONS_LIMIT=1 +CONNECTIONS_LIMIT=5 -JWT_SECRET=5g1yk7pD9q3YL0iBEuUlPwOiWLj3I5tK+/rhHm+jgdE= -JWT_REFRESH_SECRET=F2c8gag5nvqQkBOmOu5dWkK+gqZnjPUzHmx7S2tWkvs= +#Modo DEMO que evita alterar algumas funções, para ativar: ON +DEMO=OFF + +#Permitir a rotação de tokens +JWT_SECRET=JYszCWFNE0kmbbb0w/dvMl66zDd1GZozzaC27dKOCDY= +JWT_REFRESH_SECRET=FwJXkGgXv7ARfxPRb7/6RdNmtXJlR4PsQvvw8VIbOho= ``` --- @@ -153,10 +163,18 @@ cd Press-Ticket/frontend Crie ou edite o arquivo `.env` no diretório `frontend` com as seguintes informações: ```bash +#URL BACKEND REACT_APP_BACKEND_URL=http://localhost:8080 + +#Tempo de encerramento automático dos tickets em horas REACT_APP_HOURS_CLOSE_TICKETS_AUTO= -REACT_APP_PAGE_TITLE=PressTicket + +#PORTA do frontend PORT=3333 + +# Para permitir acesso apenas do MasterAdmin (sempre ON) +REACT_APP_MASTERADMIN=ON + ``` --- @@ -204,3 +222,17 @@ admin ``` --- + +# Usuário Master para Acesso + +Usuário: + +``` +masteradmin@pressticket.com.br +``` + +Senha: + +``` +masteradmin +``` diff --git a/docs/UPDATE_VPS.md b/docs/UPDATE_VPS.md index 38894b14..0ec1bf71 100644 --- a/docs/UPDATE_VPS.md +++ b/docs/UPDATE_VPS.md @@ -25,7 +25,7 @@ cd Press-Ticket/ Com o diretório correto acessado, execute o script de atualização utilizando o comando abaixo: ```bash -sh UPDATE.sh +chmod +x UPDATE.sh && ./UPDATE.sh ``` > Nota: O script `UPDATE.sh` será responsável por realizar o processo de atualização automaticamente. diff --git a/frontend/.gitignore b/frontend/.gitignore index 48df9d1c..457d0bf9 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -5,14 +5,12 @@ /.pnp .pnp.js - # testing /coverage # production /build /dist -/src/config.json # misc .DS_Store @@ -27,4 +25,4 @@ yarn-debug.log* yarn-error.log* package-lock.json yarn.lock -build \ No newline at end of file +build diff --git a/frontend/package.json b/frontend/package.json index 0bae6f60..fb06b9dd 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "frontend", "version": "1.0.0", - "systemVersion": "v1.9.0", + "systemVersion": "v1.10.0", "private": true, "scripts": { "start": "react-scripts --openssl-legacy-provider start", @@ -10,7 +10,7 @@ "eject": "react-scripts eject" }, "dependencies": { - "@material-ui/core": "^4.11.0", + "@material-ui/core": "^4.12.4", "@material-ui/icons": "^4.11.3", "@material-ui/lab": "^4.0.0-alpha.56", "@testing-library/jest-dom": "^5.11.4", @@ -20,6 +20,7 @@ "date-fns": "^2.16.1", "emoji-mart": "^3.0.1", "formik": "^2.2.0", + "helmet": "^8.0.0", "i18next": "^19.8.2", "i18next-browser-languagedetector": "^6.0.1", "markdown-to-jsx": "^7.1.0", @@ -33,11 +34,13 @@ "react-copy-to-clipboard": "^5.1.0", "react-csv": "^2.2.2", "react-dom": "^16.13.1", + "react-i18next": "^10.11.0", "react-modal-image": "^2.5.0", "react-router-dom": "^5.2.0", "react-scripts": "3.4.3", "react-tagcloud": "^2.3.3", "react-toastify": "^6.0.9", + "react-world-flags": "^1.6.0", "recharts": "^2.13.0", "socket.io-client": "^3.0.5", "use-sound": "^2.0.1", diff --git a/frontend/public/assets/favicon.ico b/frontend/public/assets/favicon.ico new file mode 100644 index 00000000..c4c8a3d2 Binary files /dev/null and b/frontend/public/assets/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html index 60518374..1d6009c8 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,21 +1,24 @@ - - %REACT_APP_PAGE_TITLE% - - - - - - - - - - - -
- - \ No newline at end of file + + Carregando... + + + + + + + + + +
+ + diff --git a/frontend/server.js b/frontend/server.js index 743541a4..f1590beb 100644 --- a/frontend/server.js +++ b/frontend/server.js @@ -1,12 +1,37 @@ -//simple express server to run frontend production build; const express = require("express"); const path = require("path"); +const helmet = require("helmet"); const app = express(); require('dotenv').config(); -app.use(express.static(path.join(__dirname, "build"))); -app.get("/*", function (req, res) { +const PORT = process.env.PORT || 3333; + +if (!process.env.PORT) { + console.warn( + "⚠️ PORT não definida no arquivo .env. Usando porta padrão: 3333. " + + "Você pode definir isso no arquivo .env para evitar este aviso." + ); +} + +app.use( + helmet({ + contentSecurityPolicy: false, // Desativa CSP (útil para evitar problemas com bibliotecas de terceiros) + crossOriginEmbedderPolicy: false, // Desativa política de incorporação para permitir imagens e mídias de terceiros + }) +); + +const oneDay = 24 * 60 * 60 * 1000; // Cache de 1 dia em milissegundos +app.use(express.static(path.join(__dirname, "build"), { maxAge: oneDay })); + +app.get("/*", (req, res) => { res.sendFile(path.join(__dirname, "build", "index.html")); }); -app.listen(process.env.PORT || 3333); \ No newline at end of file +app.use((err, req, res, next) => { + console.error("Erro interno:", err.stack); + res.status(500).send("Algo deu errado! Verifique os logs do servidor."); +}); + +app.listen(PORT, () => { + console.log(`Servidor rodando na porta ${PORT}`); +}); diff --git a/frontend/src/App.js b/frontend/src/App.js index 238aa99c..31331fde 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,135 +1,103 @@ -import React, { useEffect, useState } from "react"; -import "react-toastify/dist/ReactToastify.css"; -import Routes from "./routes"; - -import { ptBR } from "@material-ui/core/locale"; -import { createTheme, ThemeProvider } from "@material-ui/core/styles"; - import { CssBaseline } from "@material-ui/core"; - +import { ptBR } from "@material-ui/core/locale"; +import { ThemeProvider } from "@material-ui/core/styles"; +import React, { useCallback, useEffect, useState } from "react"; +import "react-toastify/dist/ReactToastify.css"; import toastError from "./errors/toastError"; +import Routes from "./routes"; import api from "./services/api"; - -import darkBackground from "./assets/wa-background-dark.jpg"; -import lightBackground from "./assets/wa-background-light.png"; -import config from "./config.json"; - -const { system } = config; +import openSocket from "./services/socket-io"; +import loadThemeConfig from "./themes/themeConfig"; const App = () => { - const [locale, setLocale] = useState(); - - const lightTheme = createTheme( - { - scrollbarStyles: { - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#e8e8e8", - }, - }, - palette: { - primary: { main: system?.color?.lightTheme?.palette?.primary || "#6B62FE" }, - secondary: { main: system?.color?.lightTheme?.palette?.secondary || "#F50057" }, - toolbar: { main: system?.color?.lightTheme?.toolbar?.background || "#6B62FE" }, - menuItens: { main: system?.color?.lightTheme?.menuItens || "#ffffff" }, - sub: { main: system?.color?.lightTheme?.sub || "#ffffff" }, - toolbarIcon: { main: system?.color?.lightTheme?.toolbarIcon || "#ffffff" }, - divide: { main: system?.color?.lightTheme?.divide || "#E0E0E0" }, - background: { - default: system?.color?.lightTheme?.palette?.background?.default || "#eeeeee", - paper: system?.color?.lightTheme?.palette?.background?.paper || "#ffffff", - }, - }, - backgroundImage: `url(${lightBackground})`, - }, - locale - ); - - const darkTheme = createTheme( - { - overrides: { - MuiCssBaseline: { - '@global': { - body: { - backgroundColor: "#080d14", - } - } - } - }, - scrollbarStyles: { - "&::-webkit-scrollbar": { - width: "8px", - height: "8px", - }, - "&::-webkit-scrollbar-thumb": { - boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", - backgroundColor: "#ffffff", - }, - }, - palette: { - primary: { main: system.color.darkTheme.palette.primary || "#52d869" }, - secondary: { main: system.color.darkTheme.palette.secondary || "#ff9100" }, - toolbar: { main: system.color.darkTheme.toolbar.background || "#52d869" }, - menuItens: { main: system.color.darkTheme.menuItens || "#181d22" }, - sub: { main: system.color.darkTheme.sub || "#181d22" }, - toolbarIcon: { main: system.color.darkTheme.toolbarIcon || "#181d22" }, - divide: { main: system.color.darkTheme.divide || "#080d14" }, - background: { - default: system.color.darkTheme.palette.background.default || "#080d14", - paper: system.color.darkTheme.palette.background.paper || "#181d22", - }, - text: { - primary: system.color.darkTheme.palette.text.primary || "#52d869", - secondary: system.color.darkTheme.palette.text.secondary || "#ffffff", - }, - }, - backgroundImage: `url(${darkBackground})`, - }, - locale - ); - - const [theme, setTheme] = useState("light"); + const [locale, setLocale] = useState(ptBR); + const [theme, setTheme] = useState(localStorage.getItem("theme") || "light"); + const [lightThemeConfig, setLightThemeConfig] = useState({}); + const [darkThemeConfig, setDarkThemeConfig] = useState({}); + + const toggleTheme = () => { + const newTheme = theme === "light" ? "dark" : "light"; + setTheme(newTheme); + localStorage.setItem("theme", newTheme); + }; + + const onThemeConfigUpdate = useCallback((themeType, updatedConfig) => { + if (themeType === "light") { + setLightThemeConfig((prevConfig) => ({ ...prevConfig, ...updatedConfig })); + } else if (themeType === "dark") { + setDarkThemeConfig((prevConfig) => ({ ...prevConfig, ...updatedConfig })); + } + }, []); useEffect(() => { - - const fetchDarkMode = async () => { + const fetchPersonalizations = async () => { try { - const { data } = await api.get("/settings"); - const settingIndex = data.filter(s => s.key === 'darkMode'); + const { data } = await api.get("/personalizations"); - if (settingIndex[0].value === "enabled") { - setTheme("dark") - } + if (data && data.length > 0) { + const lightConfig = data.find((themeConfig) => themeConfig.theme === "light"); + const darkConfig = data.find((themeConfig) => themeConfig.theme === "dark"); + if (lightConfig) { + setLightThemeConfig(lightConfig); + document.title = lightConfig.company || "Press Ticket"; + } + + if (darkConfig) { + setDarkThemeConfig(darkConfig); + } + } } catch (err) { - setTheme("light") toastError(err); + document.title = "Erro ao carregar título"; } }; - fetchDarkMode(); + const socket = openSocket(); + socket.on("personalization", () => { + fetchPersonalizations(); + }); + fetchPersonalizations(); + + return () => { + socket.off("personalization"); + socket.disconnect(); + }; }, []); useEffect(() => { const i18nlocale = localStorage.getItem("i18nextLng"); - const browserLocale = i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); - - if (browserLocale === "ptBR") { - setLocale(ptBR); + if (i18nlocale) { + const browserLocale = i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); + if (browserLocale === "ptBR") { + setLocale(ptBR); + } } }, []); + useEffect(() => { + const favicon = document.querySelector("link[rel*='icon']") || document.createElement("link"); + favicon.type = "image/x-icon"; + favicon.rel = "shortcut icon"; + + const faviconPath = theme === "dark" ? "/assets/favicoDark.ico" : "/assets/favico.ico"; + favicon.href = faviconPath; + document.head.appendChild(favicon); + }, [theme]); + + const selectedTheme = loadThemeConfig( + theme, + theme === "light" ? lightThemeConfig : darkThemeConfig, + locale + ); + return ( - - + + ); }; -export default App; \ No newline at end of file +export default App; diff --git a/frontend/src/assets/Logo_circle.png b/frontend/src/assets/Logo_circle.png deleted file mode 100644 index 7e9e4396..00000000 Binary files a/frontend/src/assets/Logo_circle.png and /dev/null differ diff --git a/frontend/src/assets/wa-background-dark.jpg b/frontend/src/assets/backgroundDark.jpg similarity index 100% rename from frontend/src/assets/wa-background-dark.jpg rename to frontend/src/assets/backgroundDark.jpg diff --git a/frontend/src/assets/wa-background-light.png b/frontend/src/assets/backgroundLight.png similarity index 100% rename from frontend/src/assets/wa-background-light.png rename to frontend/src/assets/backgroundLight.png diff --git a/frontend/src/assets/logo-dash.png b/frontend/src/assets/logo-dash.png deleted file mode 100644 index 580abb13..00000000 Binary files a/frontend/src/assets/logo-dash.png and /dev/null differ diff --git a/frontend/src/assets/logo.jpg b/frontend/src/assets/logo.jpg new file mode 100644 index 00000000..de9153f7 Binary files /dev/null and b/frontend/src/assets/logo.jpg differ diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png deleted file mode 100644 index c693f902..00000000 Binary files a/frontend/src/assets/logo.png and /dev/null differ diff --git a/frontend/src/assets/logoTicket.jpg b/frontend/src/assets/logoTicket.jpg new file mode 100644 index 00000000..66f1f853 Binary files /dev/null and b/frontend/src/assets/logoTicket.jpg differ diff --git a/frontend/src/components/AcceptTicketWithoutQueueModal/index.js b/frontend/src/components/AcceptTicketWithoutQueueModal/index.js index 73770a56..b8d0fe71 100644 --- a/frontend/src/components/AcceptTicketWithoutQueueModal/index.js +++ b/frontend/src/components/AcceptTicketWithoutQueueModal/index.js @@ -1,34 +1,27 @@ -import React, { useState, useContext } from "react"; -import { useHistory } from "react-router-dom"; - import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - FormControl, - InputLabel, - makeStyles, - MenuItem, - Select - } from "@material-ui/core"; - - -import api from "../../services/api"; + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + InputLabel, + makeStyles, + MenuItem, + Select, +} from "@material-ui/core"; +import PropTypes from "prop-types"; +import React, { useCallback, useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; import { AuthContext } from "../../context/Auth/AuthContext"; -import ButtonWithSpinner from "../ButtonWithSpinner"; -import { i18n } from "../../translate/i18n"; import toastError from "../../errors/toastError"; - -// const filter = createFilterOptions({ -// trim: true, -// }); +import api from "../../services/api"; +import ButtonWithSpinner from "../ButtonWithSpinner"; const useStyles = makeStyles((theme) => ({ - autoComplete: { + autoComplete: { width: 300, - // marginBottom: 20 }, maxWidth: { width: "100%", @@ -39,50 +32,58 @@ const useStyles = makeStyles((theme) => ({ }, })); +const INITIAL_QUEUE_VALUE = ""; + const AcceptTicketWithouSelectQueue = ({ modalOpen, onClose, ticketId }) => { const history = useHistory(); const classes = useStyles(); - const [selectedQueue, setSelectedQueue] = useState(''); + const [selectedQueue, setSelectedQueue] = useState(INITIAL_QUEUE_VALUE); const [loading, setLoading] = useState(false); const { user } = useContext(AuthContext); + const { t } = useTranslation(); + const userId = user?.id; -const handleClose = () => { - onClose(); - setSelectedQueue(""); -}; + const handleClose = useCallback(() => { + onClose(); + setSelectedQueue(INITIAL_QUEUE_VALUE); + }, [onClose]); -const handleUpdateTicketStatus = async (queueId) => { - setLoading(true); - try { - await api.put(`/tickets/${ticketId}`, { - status: "open", - userId: user?.id || null, - queueId: queueId - }); + const handleUpdateTicketStatus = useCallback(async (queueId) => { + setLoading(true); + try { + await api.put(`/tickets/${ticketId}`, { + status: "open", + userId: userId || null, + queueId, + }); - setLoading(false); - history.push(`/tickets/${ticketId}`); - handleClose(); - } catch (err) { - setLoading(false); - toastError(err); - } -}; + setLoading(false); + history.push(`/tickets/${ticketId}`); + handleClose(); + } catch (err) { + setLoading(false); + toastError(err); + } + }, [ticketId, userId, history, handleClose]); -return ( - <> - - - {i18n.t("ticketsList.acceptModal.title")} + return ( + + + {t("ticketsList.acceptModal.title")} - {i18n.t("ticketsList.acceptModal.queue")} + {t("ticketsList.acceptModal.queue")} + + Selecione uma linguagem + {Object.keys(codeSnippets).map((lang) => ( {lang} @@ -99,8 +116,11 @@ const CodeSnippetGenerator = ({ number, body, userId, queueId, whatsappId, token
- {/* Botão de fechar */} - + @@ -118,9 +138,10 @@ const CodeSnippetGenerator = ({ number, body, userId, queueId, whatsappId, token
diff --git a/frontend/src/components/ColorPicker/index.js b/frontend/src/components/ColorPicker/index.js index b7c57d1a..3a860e40 100644 --- a/frontend/src/components/ColorPicker/index.js +++ b/frontend/src/components/ColorPicker/index.js @@ -1,156 +1,87 @@ import { Dialog } from "@material-ui/core"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import { ChromePicker } from "react-color"; -import { GithubPicker } from "react-color"; - -const ColorPicker = ({ onChange, currentColor, handleClose, open }) => { +const ColorPicker = ({ onChange, currentColor, handleClose, open, theme }) => { const [selectedColor, setSelectedColor] = useState(currentColor); - const colors = [ - "#DCDCDC", - "#FFFAF0", - "#FDF5E6", - "#FAF0E6", - "#FAEBD7", - "#FFEFD5", - "#FFEBCD", - "#FFE4C4", - "#FFDAB9", - "#FFDEAD", - "#FFE4B5", - "#FFF8DC", - "#FFFFF0", - "#FFFACD", - "#E6E6FA", - "#FFE4E1", - "#FFFFFF", - "#000000", - "#2F4F4F", - "#696969", - "#708090", - "#778899", - "#BEBEBE", - "#D3D3D3", - "#191970", - "#000080", - "#6495ED", - "#483D8B", - "#6A5ACD", - "#7B68EE", - "#8470FF", - "#0000CD", - "#4169E1", - "#0000FF", - "#1E90FF", - "#00BFFF", - "#87CEEB", - "#87CEFA", - "#4682B4", - "#B0C4DE", - "#ADD8E6", - "#B0E0E6", - "#AFEEEE", - "#00CED1", - "#48D1CC", - "#40E0D0", - "#00FFFF", - "#E0FFFF", - "#5F9EA0", - "#66CDAA", - "#7FFFD4", - "#006400", - "#556B2F", - "#8FBC8F", - "#2E8B57", - "#3CB371", - "#20B2AA", - "#98FB98", - "#00FF7F", - "#7CFC00", - "#00FF00", - "#7FFF00", - "#00FA9A", - "#ADFF2F", - "#32CD32", - "#9ACD32", - "#228B22", - "#6B8E23", - "#BDB76B", - "#EEE8AA", - "#FAFAD2", - "#FFFFE0", - "#FFFF00", - "#FFD700", - "#EEDD82", - "#DAA520", - "#B8860B", - "#BC8F8F", - "#CD5C5C", - "#8B4513", - "#A0522D", - "#CD853F", - "#DEB887", - "#F5F5DC", - "#F5DEB3", - "#F4A460", - "#D2B48C", - "#D2691E", - "#B22222", - "#A52A2A", - "#E9967A", - "#FA8072", - "#FFA07A", - "#FFA500", - "#FF8C00", - "#FF7F50", - "#F08080", - "#FF6347", - "#FF4500", - "#FF0000", - "#FF69B4", - "#FF1493", - "#FFC0CB", - "#FFB6C1", - "#DB7093", - "#B03060", - "#C71585", - "#D02090", - "#FF00FF", - "#EE82EE", - "#DDA0DD", - "#DA70D6", - "#BA55D3", - "#9932CC", - "#9400D3", - "#8A2BE2", - "#A020F0", - "#9370DB", - "#D8BFD8", - "#FFFAFA", - ]; + + useEffect(() => { + setSelectedColor(currentColor); + }, [currentColor]); const handleChange = (color) => { setSelectedColor(color.hex); + onChange(color.hex); + }; + + const handleSave = () => { handleClose(); }; return ( - onChange(color.hex)} - /> +
+
+ +
+
+ +
+
); }; -export default ColorPicker; +export default ColorPicker; \ No newline at end of file diff --git a/frontend/src/components/ConfirmationModal/index.js b/frontend/src/components/ConfirmationModal/index.js index ce340f2c..5329bfd2 100644 --- a/frontend/src/components/ConfirmationModal/index.js +++ b/frontend/src/components/ConfirmationModal/index.js @@ -1,31 +1,61 @@ -import React from "react"; import Button from "@material-ui/core/Button"; import Dialog from "@material-ui/core/Dialog"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; import Typography from "@material-ui/core/Typography"; +import { makeStyles } from "@material-ui/core/styles"; +import { Cancel, CheckCircle } from "@material-ui/icons"; +import React from "react"; +import { useTranslation } from "react-i18next"; -import { i18n } from "../../translate/i18n"; +const useStyles = makeStyles((theme) => ({ + dialogTitle: { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + dialogActions: { + justifyContent: "space-between", + padding: theme.spacing(2), + }, + cancelButton: { + color: theme.palette.error.main, + }, + confirmButton: { + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastText, + "&:hover": { + backgroundColor: theme.palette.primary.dark, + }, + }, +})); const ConfirmationModal = ({ title, children, open, onClose, onConfirm }) => { + const classes = useStyles(); + const { t } = useTranslation(); + return ( onClose(false)} aria-labelledby="confirm-dialog" + maxWidth="sm" + fullWidth > - {title} + + {title} + {children} - + diff --git a/frontend/src/components/ContactChannels/index.js b/frontend/src/components/ContactChannels/index.js index c006ea24..6bf02471 100644 --- a/frontend/src/components/ContactChannels/index.js +++ b/frontend/src/components/ContactChannels/index.js @@ -3,74 +3,65 @@ import { Email, Facebook, Instagram, Sms, Telegram } from "@material-ui/icons"; import React from "react"; const ContactChannels = ({ contact, handleSaveTicket, setContactTicket, setNewTicketModalOpen }) => { - const channels = []; + const channels = [ + { + id: contact.telegramId, + label: "Telegram", + color: "#0088cc", + Icon: Telegram, + action: () => handleSaveTicket(contact.id), + }, + { + id: contact.messengerId, + label: "Facebook", + color: "#3b5998", + Icon: Facebook, + action: () => handleSaveTicket(contact.id), + }, + { + id: contact.instagramId, + label: "Instagram", + color: "#cd486b", + Icon: Instagram, + action: () => { + setContactTicket(contact); + setNewTicketModalOpen(true); + }, + }, + { + id: contact.email, + label: "Email", + color: "#004f9f", + Icon: Email, + action: () => handleSaveTicket(contact.id), + }, + { + id: contact.webchatId, + label: "WebChat", + color: "#EB6D58", + Icon: Sms, + action: () => handleSaveTicket(contact.id), + }, + ]; - if (contact.telegramId) { - channels.push( - <> - handleSaveTicket(contact.id)}> - - - - - {contact.telegramId} - - ); - } - - if (contact.messengerId) { - channels.push( - <> - handleSaveTicket(contact.id)}> - - - - - {contact.messengerId} - - ); - } - - if (contact.instagramId) { - channels.push( - <> - setContactTicket(contact) && setNewTicketModalOpen(true)}> - - - - - {contact.instagramId} - - ); - } - - if (contact.email) { - channels.push( - <> - handleSaveTicket(contact.id)}> - - - - - {contact.email} - - ); - } - - if (contact.webchatId) { - channels.push( - <> - handleSaveTicket(contact.id)}> - - - - - {contact.webchatId} - - ); - } - - return channels.length > 0 ? channels.map((channel, index) => {channel}) : "-"; + return ( + <> + {channels.filter(channel => channel.id).map((channel, index) => ( + + + + + + + {channel.id} + + ))} + + ); }; export default ContactChannels; diff --git a/frontend/src/components/ContactDrawer/index.js b/frontend/src/components/ContactDrawer/index.js index d8da29ef..81eca610 100644 --- a/frontend/src/components/ContactDrawer/index.js +++ b/frontend/src/components/ContactDrawer/index.js @@ -1,25 +1,22 @@ -import React, { useState, useContext } from "react"; - +import Drawer from "@material-ui/core/Drawer"; +import IconButton from "@material-ui/core/IconButton"; +import InputLabel from "@material-ui/core/InputLabel"; +import Link from "@material-ui/core/Link"; import { makeStyles } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; -import IconButton from "@material-ui/core/IconButton"; import CloseIcon from "@material-ui/icons/Close"; -import Drawer from "@material-ui/core/Drawer"; -import Link from "@material-ui/core/Link"; -import InputLabel from "@material-ui/core/InputLabel"; +import React, { useContext, useState } from "react"; //import Avatar from "@material-ui/core/Avatar"; import Button from "@material-ui/core/Button"; import Paper from "@material-ui/core/Paper"; - -import { i18n } from "../../translate/i18n"; - -import ContactModal from "../ContactModal"; +import { useTranslation } from "react-i18next"; +import { AuthContext } from "../../context/Auth/AuthContext"; import ContactDrawerSkeleton from "../ContactDrawerSkeleton"; +import ContactModal from "../ContactModal"; +import CopyToClipboard from "../CopyToClipboard"; import MarkdownWrapper from "../MarkdownWrapper"; import { TagsContainer } from "../TagsContainer"; import ModalImageContatc from "./ModalImage"; -import CopyToClipboard from "../CopyToClipboard"; -import { AuthContext } from "../../context/Auth/AuthContext"; const drawerWidth = 320; @@ -55,14 +52,12 @@ const useStyles = makeStyles(theme => ({ overflowY: "scroll", ...theme.scrollbarStyles, }, - contactAvatar: { margin: 15, width: 160, height: 160, borderRadius: 10, }, - contactHeader: { display: "flex", padding: 8, @@ -73,7 +68,6 @@ const useStyles = makeStyles(theme => ({ margin: 4, }, }, - contactDetails: { marginTop: 8, padding: 8, @@ -90,6 +84,7 @@ const ContactDrawer = ({ open, handleDrawerClose, contact, loading }) => { const classes = useStyles(); const { user } = useContext(AuthContext); const [modalOpen, setModalOpen] = useState(false); + const { t } = useTranslation(); return ( { - {i18n.t("contactDrawer.header")} + {t("contactDrawer.header")} {loading ? ( @@ -126,10 +121,10 @@ const ContactDrawer = ({ open, handleDrawerClose, contact, loading }) => { {contact.name} - + - {user.isTricked === "enabled" ? contact.number : contact.number.slice(0,-4) + "****"} - + {user.isTricked === "enabled" ? contact.number : contact.number.slice(0, -4) + "****"} + {contact.email && ( @@ -142,7 +137,7 @@ const ContactDrawer = ({ open, handleDrawerClose, contact, loading }) => { color="primary" onClick={() => setModalOpen(true)} > - {i18n.t("contactDrawer.buttons.edit")} + {t("contactDrawer.buttons.edit")} @@ -153,7 +148,7 @@ const ContactDrawer = ({ open, handleDrawerClose, contact, loading }) => { contactId={contact.id} > - {i18n.t("contactDrawer.extraInfo")} + {t("contactDrawer.extraInfo")} {contact?.extraInfo?.map(info => ( { + const { t } = useTranslation(); + return (
@@ -21,7 +23,7 @@ const ContactDrawerSkeleton = ({ classes }) => { - {i18n.t("contactDrawer.extraInfo")} + {t("contactDrawer.extraInfo")} diff --git a/frontend/src/components/ContactModal/index.js b/frontend/src/components/ContactModal/index.js index 1c6bafa4..325fc54c 100644 --- a/frontend/src/components/ContactModal/index.js +++ b/frontend/src/components/ContactModal/index.js @@ -1,17 +1,3 @@ -import React, { - useContext, - useEffect, - useRef, - useState -} from "react"; - -import { - Field, - FieldArray, - Form, - Formik -} from "formik"; - import { Button, CircularProgress, @@ -22,19 +8,25 @@ import { IconButton, makeStyles, TextField, - Typography + Typography, } from "@material-ui/core"; import { green } from "@material-ui/core/colors"; import DeleteOutlineIcon from "@material-ui/icons/DeleteOutline"; - +import { + Field, + FieldArray, + Form, + Formik, +} from "formik"; +import React, { useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import * as Yup from "yup"; import { AuthContext } from "../../context/Auth/AuthContext"; import toastError from "../../errors/toastError"; import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ root: { display: "flex", flexWrap: "wrap", @@ -45,7 +37,6 @@ const useStyles = makeStyles(theme => ({ }, extraAttr: { display: "flex", - justifyContent: "center", alignItems: "center", }, btnWrapper: { @@ -58,231 +49,213 @@ const useStyles = makeStyles(theme => ({ left: "50%", marginTop: -12, marginLeft: -12, - } + }, })); +const initialState = { + name: "", + number: "", + email: "", + extraInfo: [], +}; + const ContactSchema = Yup.object().shape({ - name: Yup.string() - .min(2, "Too Short!") - .max(50, "Too Long!") - .required("Required"), - // number: Yup.string().min(8, "Too Short!").max(50, "Too Long!"), + name: Yup.string().min(2, "Too Short!").max(50, "Too Long!").required("Required"), email: Yup.string().email("Invalid email"), }); const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { - const initialState = { - name: "", - number: "", - email: "", - }; - const classes = useStyles(); - const isMounted = useRef(true); const { user } = useContext(AuthContext); const [contact, setContact] = useState(initialState); + const { t } = useTranslation(); useEffect(() => { - return () => { - isMounted.current = false; - }; - }, []); - - useEffect(() => { + const abortController = new AbortController(); const fetchContact = async () => { - if (initialValues) { - setContact(prevState => { - return { ...prevState, ...initialValues }; - }); - } - - if (!contactId) return; - try { - const { data } = await api.get(`/contacts/${contactId}`); - if (isMounted.current) { + if (initialValues) { + setContact((prev) => ({ ...prev, ...initialValues })); + } + + if (contactId) { + const { data } = await api.get(`/contacts/${contactId}`, { + signal: abortController.signal, + }); setContact(data); } } catch (err) { - toastError(err); + if (!abortController.signal.aborted) { + toastError(err); + } } }; fetchContact(); - }, [contactId, open, initialValues]); + + return () => abortController.abort(); + }, [contactId, initialValues]); const handleClose = () => { onClose(); setContact(initialState); }; - const handleSaveContact = async values => { + const handleSaveContact = async (values) => { try { if (contactId) { await api.put(`/contacts/${contactId}`, values); - handleClose(); } else { const { data } = await api.post("/contacts", values); - if (onSave) { - onSave(data); - } - handleClose(); + if (onSave) onSave(data); } - toast.success(i18n.t("contactModal.success")); + toast.success(t("contactModal.success")); + handleClose(); } catch (err) { toastError(err); } }; return ( -
- - - {contactId - ? `${i18n.t("contactModal.title.edit")}` - : `${i18n.t("contactModal.title.add")}`} - - { - setTimeout(() => { - handleSaveContact(values); - actions.setSubmitting(false); - }, 400); - }} - > - {({ values, errors, touched, isSubmitting }) => ( -
- - - {i18n.t("contactModal.form.mainInfo")} - + + + {contactId + ? t("contactModal.title.edit") + : t("contactModal.title.add")} + + { + await handleSaveContact(values); + actions.setSubmitting(false); + }} + > + {({ values, errors, touched, isSubmitting }) => ( + + + + {t("contactModal.form.mainInfo")} + + + {user.isTricked === "enabled" && ( - {user.isTricked === "enabled" ? - - : "" - } -
- -
- - {i18n.t("contactModal.form.extraInfo")} - - - - {({ push, remove }) => ( - <> - {values.extraInfo && - values.extraInfo.length > 0 && - values.extraInfo.map((info, index) => ( -
- - - remove(index)} - > - - -
- ))} -
- -
- - )} -
-
- - - - - - )} -
-
-
+ + + remove(index)} + > + + +
+ ))} +
+ +
+ + )} + +
+ + + + + + )} + +
); }; diff --git a/frontend/src/components/ContactTag/index.js b/frontend/src/components/ContactTag/index.js index c08b80bb..184cdc43 100644 --- a/frontend/src/components/ContactTag/index.js +++ b/frontend/src/components/ContactTag/index.js @@ -1,7 +1,8 @@ -import React from "react"; import { makeStyles } from "@material-ui/core"; +import PropTypes from "prop-types"; +import React from "react"; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ tag: { padding: "1px 5px", borderRadius: "3px", @@ -11,17 +12,28 @@ const useStyles = makeStyles(theme => ({ marginRight: "5px", marginBottom: "3px", whiteSpace: "nowrap", - } + backgroundColor: (props) => props.backgroundColor || "#000", + }, })); const ContactTag = ({ tag }) => { - const classes = useStyles(); + const classes = useStyles({ backgroundColor: tag.color }); return ( -
- {tag.name.toUpperCase()} +
+ {tag.name?.toUpperCase() || "UNKNOWN"}
- ) -} + ); +}; + +ContactTag.propTypes = { + tag: PropTypes.shape({ + name: PropTypes.string.isRequired, + color: PropTypes.string, + }).isRequired, +}; -export default ContactTag; \ No newline at end of file +export default ContactTag; diff --git a/frontend/src/components/CopyToClipboard/index.js b/frontend/src/components/CopyToClipboard/index.js index 61dce1c7..39ae266c 100644 --- a/frontend/src/components/CopyToClipboard/index.js +++ b/frontend/src/components/CopyToClipboard/index.js @@ -1,23 +1,30 @@ -import React, { useState } from "react"; -import { - IconButton, - Tooltip -} from "@material-ui/core"; +import { IconButton, Tooltip } from "@material-ui/core"; import { FileCopyOutlined } from "@material-ui/icons"; -import { i18n } from "../../translate/i18n"; +import PropTypes from "prop-types"; +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; -const CopyToClipboard = ({ content, color }) => { +const CopyToClipboard = ({ content, color = "inherit" }) => { + const { t } = useTranslation(); const [tooltipMessage, setTooltipMessage] = useState( - i18n.t("copyToClipboard.copy") + t("copyToClipboard.copy") ); - const handleCopyToClipboard = () => { - navigator.clipboard.writeText(content); - setTooltipMessage(i18n.t("copyToClipboard.copied")); + const handleCopyToClipboard = async () => { + if (navigator.clipboard) { + try { + await navigator.clipboard.writeText(content); + setTooltipMessage(t("copyToClipboard.copied")); + } catch (err) { + setTooltipMessage(t("copyToClipboard.failed")); + } + } else { + setTooltipMessage(t("copyToClipboard.notSupported")); + } }; const handleCloseTooltip = () => { - setTooltipMessage(i18n.t("copyToClipboard.copy")); + setTooltipMessage(t("copyToClipboard.copy")); }; return ( @@ -27,11 +34,20 @@ const CopyToClipboard = ({ content, color }) => { placement="top" title={tooltipMessage} > - + ); }; -export default CopyToClipboard; \ No newline at end of file +CopyToClipboard.propTypes = { + content: PropTypes.string.isRequired, + color: PropTypes.string, +}; + +export default CopyToClipboard; diff --git a/frontend/src/components/ErrorBoundary/index.js b/frontend/src/components/ErrorBoundary/index.js new file mode 100644 index 00000000..05d8bcb1 --- /dev/null +++ b/frontend/src/components/ErrorBoundary/index.js @@ -0,0 +1,26 @@ +import React from "react"; + +class ErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = { hasError: false }; + } + + static getDerivedStateFromError(error) { + return { hasError: true }; + } + + componentDidCatch(error, errorInfo) { + console.error("Uncaught error:", error, errorInfo); + } + + render() { + if (this.state.hasError) { + return

Algo deu errado. Tente novamente mais tarde.

; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/LanguageSelector/index.js b/frontend/src/components/LanguageSelector/index.js new file mode 100644 index 00000000..159aa549 --- /dev/null +++ b/frontend/src/components/LanguageSelector/index.js @@ -0,0 +1,92 @@ +import { IconButton, Menu, MenuItem, Tooltip } from "@material-ui/core"; +import React, { useState } from "react"; +import Flag from "react-world-flags"; +import i18n from "../../translate/i18n"; + +const LanguageSelector = () => { + const [anchorEl, setAnchorEl] = useState(null); + const currentLanguage = i18n.language; + + const handleOpenMenu = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleCloseMenu = () => { + setAnchorEl(null); + }; + + const handleLanguageChange = (language) => { + console.log("Idioma selecionado:", language); + i18n.changeLanguage(language) + .then(() => console.log("Idioma alterado com sucesso:", language)) + .catch((err) => console.error("Erro ao alterar idioma:", err)); + localStorage.setItem("i18nextLng", language); + handleCloseMenu(); + }; + + const languages = [ + { code: "pt", label: "Português", flag: "BR" }, + { code: "en", label: "English", flag: "US" }, + { code: "es", label: "Español", flag: "ES" }, + { code: "fr", label: "Français", flag: "FR" }, + { code: "de", label: "Deutsch", flag: "DE" }, + { code: "it", label: "Italiano", flag: "IT" }, + { code: "zh", label: "中文", flag: "CN" }, + { code: "ja", label: "日本語", flag: "JP" }, + { code: "ru", label: "Русский", flag: "RU" }, + { code: "ar", label: "العربية", flag: "SA" }, + { code: "hi", label: "हिन्दी", flag: "IN" }, + { code: "id", label: "Bahasa Indonesia", flag: "ID" }, + ]; + + const disabledLanguages = ["fr", "de", "it", "zh", "ja", "ru", "ar", "hi", "id"]; + + return ( + <> + + + lang.code === currentLanguage)?.flag || "BR"} + style={{ width: 24, height: 24, borderRadius: "50%" }} + /> + + + + {languages.map((language) => ( + handleLanguageChange(language.code)} + disabled={disabledLanguages.includes(language.code)} + > + + {language.label} + + ))} + + + ); +}; + +export default LanguageSelector; diff --git a/frontend/src/components/LocationPreview/index.js b/frontend/src/components/LocationPreview/index.js index f6a3907f..292c84bd 100644 --- a/frontend/src/components/LocationPreview/index.js +++ b/frontend/src/components/LocationPreview/index.js @@ -1,52 +1,95 @@ -import React, { useEffect } from 'react'; +import { Button, Divider, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import PropTypes from "prop-types"; +import React from "react"; import toastError from "../../errors/toastError"; -import Typography from "@material-ui/core/Typography"; - -import { Button, Divider, } from "@material-ui/core"; +const useStyles = makeStyles((theme) => ({ + container: { + minWidth: "250px", + display: "flex", + flexDirection: "column", + }, + imageContainer: { + display: "flex", + justifyContent: "center", + alignItems: "center", + marginBottom: theme.spacing(2), + }, + image: { + width: "100px", + cursor: "pointer", + borderRadius: theme.shape.borderRadius, + boxShadow: theme.shadows[1], + }, + description: { + margin: theme.spacing(1, 2), + color: theme.palette.text.primary, + wordBreak: "break-word", + }, + button: { + marginTop: theme.spacing(1), + }, +})); const LocationPreview = ({ image, link, description }) => { - useEffect(() => {}, [image, link, description]); + const classes = useStyles(); - const handleLocation = async() => { - try { - window.open(link); - } catch (err) { - toastError(err); - } - } + const handleLocation = async () => { + try { + if (link) { + window.open(link, "_blank", "noopener, noreferrer"); + } + } catch (err) { + toastError(err); + } + }; - return ( - <> -
-
-
- -
- { description && ( -
- -
') }}>
-
-
- )} -
-
- - -
-
+ return ( +
+
+ {description
- + {description && ( + + {description.split("\\n").map((line, index) => ( + + {line} +
+
+ ))} +
+ )} + + +
); +}; + +LocationPreview.propTypes = { + image: PropTypes.string, + link: PropTypes.string, + description: PropTypes.string, +}; +LocationPreview.defaultProps = { + image: null, + link: null, + description: "", }; -export default LocationPreview; \ No newline at end of file +export default LocationPreview; diff --git a/frontend/src/components/MainHeader/index.js b/frontend/src/components/MainHeader/index.js index 46fa8abc..4624c785 100644 --- a/frontend/src/components/MainHeader/index.js +++ b/frontend/src/components/MainHeader/index.js @@ -1,19 +1,18 @@ -import React from "react"; - import { makeStyles } from "@material-ui/core/styles"; +import React from "react"; -const useStyles = makeStyles(theme => ({ - contactsHeader: { +const useStyles = makeStyles((theme) => ({ + headerContainer: { display: "flex", alignItems: "center", - padding: "0px 6px 6px 6px", + padding: theme.spacing(0, 1, 1, 1), }, })); const MainHeader = ({ children }) => { const classes = useStyles(); - return
{children}
; + return
{children}
; }; export default MainHeader; diff --git a/frontend/src/components/MainHeaderButtonsWrapper/index.js b/frontend/src/components/MainHeaderButtonsWrapper/index.js index 47b35767..e869995a 100644 --- a/frontend/src/components/MainHeaderButtonsWrapper/index.js +++ b/frontend/src/components/MainHeaderButtonsWrapper/index.js @@ -1,9 +1,8 @@ -import React from "react"; - import { makeStyles } from "@material-ui/core/styles"; +import React from "react"; -const useStyles = makeStyles(theme => ({ - MainHeaderButtonsWrapper: { +const useStyles = makeStyles((theme) => ({ + headerButtonsWrapper: { display: "flex", marginLeft: "auto", "& > *": { @@ -12,10 +11,10 @@ const useStyles = makeStyles(theme => ({ }, })); -const MainHeaderButtonsWrapper = ({ children }) => { +const HeaderButtonsWrapper = ({ children }) => { const classes = useStyles(); - return
{children}
; + return
{children}
; }; -export default MainHeaderButtonsWrapper; +export default HeaderButtonsWrapper; diff --git a/frontend/src/components/MessageHistoryModal/index.js b/frontend/src/components/MessageHistoryModal/index.js index 9ed94e84..b73e642c 100644 --- a/frontend/src/components/MessageHistoryModal/index.js +++ b/frontend/src/components/MessageHistoryModal/index.js @@ -1,25 +1,26 @@ -import PropTypes from "prop-types"; -import React from "react"; - import { Table, TableBody, TableCell, TableContainer, TableRow, makeStyles } from "@material-ui/core"; import Button from "@material-ui/core/Button"; import Dialog from "@material-ui/core/Dialog"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; - import { format, parseISO } from "date-fns"; - -import { i18n } from "../../translate/i18n"; +import PropTypes from "prop-types"; +import React from "react"; +import { useTranslation } from "react-i18next"; const useStyles = makeStyles((theme) => ({ timestamp: { - minWidth: 250 - } + minWidth: 250, + [theme.breakpoints.down("sm")]: { + minWidth: 150, + }, + }, })); const MessageHistoryModal = ({ open, onClose, oldMessages }) => { const classes = useStyles(); + const { t } = useTranslation(); return ( { onClose={() => onClose(false)} aria-labelledby="dialog-title" > - {i18n.t("messageHistoryModal.title")} + + {t("messageHistoryModal.title")} + {oldMessages?.map((oldMessage) => ( - + {oldMessage.body} - + {format(parseISO(oldMessage.createdAt), "dd/MM HH:mm")} @@ -52,11 +57,11 @@ const MessageHistoryModal = ({ open, onClose, oldMessages }) => { - ); }; @@ -64,7 +69,7 @@ const MessageHistoryModal = ({ open, onClose, oldMessages }) => { MessageHistoryModal.propTypes = { open: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, - oldMessages: PropTypes.array + oldMessages: PropTypes.array, }; -export default MessageHistoryModal; \ No newline at end of file +export default MessageHistoryModal; diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index 994d8015..ef6d6692 100644 --- a/frontend/src/components/MessageInput/index.js +++ b/frontend/src/components/MessageInput/index.js @@ -41,6 +41,7 @@ import React, { useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { AuthContext } from "../../context/Auth/AuthContext"; import { EditMessageContext } from "../../context/EditingMessage/EditingMessageContext"; @@ -48,7 +49,6 @@ import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessa import toastError from "../../errors/toastError"; import { useLocalStorage } from "../../hooks/useLocalStorage"; import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; import RecordingTimer from "./RecordingTimer"; const Mp3Recorder = new MicRecorder({ bitRate: 128 }); @@ -240,6 +240,7 @@ const MessageInput = ({ ticketStatus }) => { const { user } = useContext(AuthContext); const [signMessage, setSignMessage] = useLocalStorage("signOption", true); const [channelType, setChannelType] = useState(null); + const { t } = useTranslation(); useEffect(() => { inputRef.current.focus(); @@ -319,6 +320,10 @@ const MessageInput = ({ ticketStatus }) => { }; const handleUploadMedia = async (e) => { + if (!e || !e.preventDefault) { + console.error("Evento inválido ou não fornecido!"); + return; + } setLoading(true); e.preventDefault(); const formData = new FormData(); @@ -327,6 +332,7 @@ const MessageInput = ({ ticketStatus }) => { formData.append("medias", media); formData.append("body", media.name); }); + try { if (channelType !== null) { await api.post(`/hub-message/${ticketId}`, formData); @@ -336,7 +342,6 @@ const MessageInput = ({ ticketStatus }) => { } catch (err) { toastError(err); } - setLoading(false); setMedias([]); }; @@ -360,7 +365,6 @@ const MessageInput = ({ ticketStatus }) => { await api.post(`/messages/edit/${editingMessage.id}`, message); } else { await api.post(`/messages/${ticketId}`, message); - } } catch (err) { toastError(err); @@ -507,7 +511,7 @@ const MessageInput = ({ ticketStatus }) => { ) : ( - {i18n.t("uploads.titles.titleFileList")} ({medias.length}) + {t("uploads.titles.titleFileList")} ({medias.length}) {medias.map((value, index) => { @@ -526,14 +530,14 @@ const MessageInput = ({ ticketStatus }) => { { + if (input !== null) { input.focus(); } }} - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === "Enter") { - handleUploadMedia(); + handleUploadMedia(e); } }} defaultValue={medias[0].name} @@ -560,7 +564,7 @@ const MessageInput = ({ ticketStatus }) => { onDrop={(e) => handleInputDrop(e)} >
- {i18n.t("uploads.titles.titleUploadMsgDragDrop")} + {t("uploads.titles.titleUploadMsgDragDrop")}
{(replyingMessage && renderReplyingMessage(replyingMessage)) || (editingMessage && renderReplyingMessage(editingMessage))}
@@ -579,7 +583,7 @@ const MessageInput = ({ ticketStatus }) => { { { { className={classes.messageInput} placeholder={ ticketStatus === "open" - ? i18n.t("messagesInput.placeholderOpen") - : i18n.t("messagesInput.placeholderClosed") + ? t("messagesInput.placeholderOpen") + : t("messagesInput.placeholderClosed") } multiline maxRows={5} @@ -707,10 +711,10 @@ const MessageInput = ({ ticketStatus }) => { onPaste={(e) => { ticketStatus === "open" && handleInputPaste(e); }} - onKeyPress={(e) => { + onKeyDown={(e) => { if (loading || e.shiftKey) return; else if (e.key === "Enter") { - handleSendMessage(); + handleSendMessage(e); } }} /> diff --git a/frontend/src/components/MessageOptionsMenu/index.js b/frontend/src/components/MessageOptionsMenu/index.js index bcab4201..581d49ff 100644 --- a/frontend/src/components/MessageOptionsMenu/index.js +++ b/frontend/src/components/MessageOptionsMenu/index.js @@ -1,14 +1,12 @@ -import React, { useContext, useState } from "react"; - -import MenuItem from "@material-ui/core/MenuItem"; - import { Menu } from "@material-ui/core"; +import MenuItem from "@material-ui/core/MenuItem"; import PropTypes from "prop-types"; +import React, { useContext, useState } from "react"; +import { useTranslation } from "react-i18next"; import { EditMessageContext } from "../../context/EditingMessage/EditingMessageContext"; import { ReplyMessageContext } from "../../context/ReplyingMessage/ReplyingMessageContext"; import toastError from "../../errors/toastError"; import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; import ConfirmationModal from "../ConfirmationModal"; import MessageHistoryModal from "../MessageHistoryModal"; @@ -17,6 +15,7 @@ const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => { const { setEditingMessage } = useContext(EditMessageContext); const [confirmationOpen, setConfirmationOpen] = useState(false); const [messageHistoryOpen, setMessageHistoryOpen] = useState(false); + const { t } = useTranslation(); const canEditMessage = () => { const timeDiff = new Date() - new Date(message.updatedAt); @@ -45,7 +44,7 @@ const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => { if (canEditMessage()) { setEditingMessage(message); } else { - toastError(new Error(i18n.t("messageOptionsMenu.edit.error.timeExceeded"))); + toastError(new Error(t("messageOptionsMenu.edit.error.timeExceeded"))); } handleClose(); }; @@ -58,12 +57,12 @@ const MessageOptionsMenu = ({ message, menuOpen, handleClose, anchorEl }) => { return ( <> - {i18n.t("messageOptionsMenu.confirmationModal.message")} + {t("messageOptionsMenu.confirmationModal.message")} { > {message.fromMe && [ - {i18n.t("messageOptionsMenu.delete")} + {t("messageOptionsMenu.delete")} , canEditMessage() && ( - {i18n.t("messageOptionsMenu.edit")} + {t("messageOptionsMenu.edit")} ) ]} {message.oldMessages?.length > 0 && ( - {i18n.t("messageOptionsMenu.history")} + {t("messageOptionsMenu.history")} )} - {i18n.t("messageOptionsMenu.reply")} + {t("messageOptionsMenu.reply")} diff --git a/frontend/src/components/MessageVariablesPicker/index.js b/frontend/src/components/MessageVariablesPicker/index.js index 4f52c01f..4d465dab 100644 --- a/frontend/src/components/MessageVariablesPicker/index.js +++ b/frontend/src/components/MessageVariablesPicker/index.js @@ -1,17 +1,20 @@ -import React from "react"; import { Chip, makeStyles } from "@material-ui/core"; -import { i18n } from "../../translate/i18n"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import useMessageVariables from "../../hooks/useMessageVariables"; import OutlinedDiv from "../OutlinedDiv"; -const useStyles = makeStyles(theme => ({ +const useStyles = makeStyles((theme) => ({ chip: { margin: theme.spacing(0.5), - cursor: "pointer" - } + cursor: "pointer", + }, })); -const MessageVariablesPicker = ({ onClick, disabled }) => { +const MessageVariablesPicker = ({ onClick, disabled, customVariables = [] }) => { const classes = useStyles(); + const msgVars = useMessageVariables(customVariables); + const { t } = useTranslation(); const handleClick = (e, value) => { e.preventDefault(); @@ -19,56 +22,17 @@ const MessageVariablesPicker = ({ onClick, disabled }) => { onClick(value); }; - const msgVars = [ - { - name: i18n.t("messageVariablesPicker.vars.contactName"), - value: "{{name}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.user"), - value: "{{user.name}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.greeting"), - value: "{{ms}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.protocolNumber"), - value: "{{protocol}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.date"), - value: "{{date}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.hour"), - value: "{{hour}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.ticket_id"), - value: "{{ticket_id}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.queue"), - value: "{{queue}} " - }, - { - name: i18n.t("messageVariablesPicker.vars.connection"), - value: "{{connection}} " - } - ]; - return ( - {msgVars.map(msgVar => ( + {msgVars.map((msgVar) => ( handleClick(e, msgVar.value)} + onMouseDown={(e) => handleClick(e, msgVar.value)} label={msgVar.name} size="small" className={classes.chip} @@ -79,4 +43,4 @@ const MessageVariablesPicker = ({ onClick, disabled }) => { ); }; -export default MessageVariablesPicker; \ No newline at end of file +export default MessageVariablesPicker; diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 0716dcbf..bf2c5f51 100644 --- a/frontend/src/components/MessagesList/index.js +++ b/frontend/src/components/MessagesList/index.js @@ -24,8 +24,11 @@ import { parseISO } from "date-fns"; import React, { useEffect, useReducer, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "react-toastify"; +import toastError from "../../errors/toastError"; +import api from "../../services/api"; import openSocket from "../../services/socket-io"; - import Audio from "../Audio"; import LocationPreview from "../LocationPreview"; import MarkdownWrapper from "../MarkdownWrapper"; @@ -33,11 +36,6 @@ import MessageOptionsMenu from "../MessageOptionsMenu"; import ModalImageCors from "../ModalImageCors"; import VcardPreview from "../VcardPreview"; -import { toast } from "react-toastify"; -import toastError from "../../errors/toastError"; -import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; - const useStyles = makeStyles((theme) => ({ messagesListWrapper: { overflow: "hidden", @@ -46,12 +44,10 @@ const useStyles = makeStyles((theme) => ({ flexDirection: "column", flexGrow: 1, }, - ticketNumber: { color: theme.palette.secondary.main, padding: 8, }, - messagesList: { backgroundImage: theme.backgroundImage, display: "flex", @@ -64,7 +60,6 @@ const useStyles = makeStyles((theme) => ({ }, ...theme.scrollbarStyles, }, - circleLoading: { color: blue[500], position: "absolute", @@ -73,7 +68,6 @@ const useStyles = makeStyles((theme) => ({ left: "50%", marginTop: 12, }, - messageLeft: { marginRight: 20, marginTop: 2, @@ -88,7 +82,6 @@ const useStyles = makeStyles((theme) => ({ top: 0, right: 0, }, - whiteSpace: "pre-wrap", backgroundColor: "#ffffff", color: "#303030", @@ -103,7 +96,6 @@ const useStyles = makeStyles((theme) => ({ paddingBottom: 0, boxShadow: "0 1px 1px #b3b3b3", }, - quotedContainerLeft: { margin: "-3px -80px 6px -6px", overflow: "hidden", @@ -112,7 +104,6 @@ const useStyles = makeStyles((theme) => ({ display: "flex", position: "relative", }, - quotedMsg: { padding: 10, maxWidth: 300, @@ -121,13 +112,11 @@ const useStyles = makeStyles((theme) => ({ whiteSpace: "pre-wrap", overflow: "hidden", }, - quotedSideColorLeft: { flex: "none", width: "4px", backgroundColor: "#6bcbef", }, - messageRight: { marginLeft: 20, marginTop: 2, @@ -142,7 +131,6 @@ const useStyles = makeStyles((theme) => ({ top: 0, right: 0, }, - whiteSpace: "pre-wrap", backgroundColor: "#dcf8c6", color: "#303030", @@ -157,7 +145,6 @@ const useStyles = makeStyles((theme) => ({ paddingBottom: 0, boxShadow: "0 1px 1px #b3b3b3", }, - quotedContainerRight: { margin: "-3px -80px 6px -6px", overflowY: "hidden", @@ -166,20 +153,17 @@ const useStyles = makeStyles((theme) => ({ display: "flex", position: "relative", }, - quotedMsgRight: { padding: 10, maxWidth: 300, height: "auto", whiteSpace: "pre-wrap", }, - quotedSideColorRight: { flex: "none", width: "4px", backgroundColor: "#35cd96", }, - messageActionsButton: { display: "none", position: "relative", @@ -189,30 +173,25 @@ const useStyles = makeStyles((theme) => ({ opacity: "90%", "&:hover, &.Mui-focusVisible": { backgroundColor: "inherit" }, }, - messageContactName: { display: "flex", color: "#6bcbef", fontWeight: 500, }, - textContentItem: { overflowWrap: "break-word", padding: "3px 80px 6px 6px", }, - textContentItemDeleted: { fontStyle: "italic", color: "rgba(0, 0, 0, 0.36)", overflowWrap: "break-word", padding: "3px 80px 6px 6px", }, - textContentItemEdited: { overflowWrap: "break-word", padding: "3px 120px 6px 6px", }, - messageMedia: { objectFit: "cover", width: 250, @@ -222,7 +201,6 @@ const useStyles = makeStyles((theme) => ({ borderBottomLeftRadius: 8, borderBottomRightRadius: 8, }, - timestamp: { fontSize: 11, position: "absolute", @@ -230,7 +208,6 @@ const useStyles = makeStyles((theme) => ({ right: 5, color: "#999", }, - dailyTimestamp: { alignItems: "center", textAlign: "center", @@ -241,38 +218,32 @@ const useStyles = makeStyles((theme) => ({ borderRadius: "10px", boxShadow: "0 1px 1px #b3b3b3", }, - dailyTimestampText: { color: "#808888", padding: 8, alignSelf: "center", marginLeft: "0px", }, - ackIcons: { fontSize: 18, verticalAlign: "middle", marginLeft: 4, }, - deletedIcon: { fontSize: 18, verticalAlign: "middle", marginRight: 4, color: red[200] }, - deletedMsg: { color: red[200] }, - ackDoneAllIcon: { color: blue[500], fontSize: 18, verticalAlign: "middle", marginLeft: 4, }, - downloadMedia: { display: "flex", alignItems: "center", @@ -280,7 +251,6 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: "inherit", padding: 10, }, - messageCenter: { marginTop: 5, alignItems: "center", @@ -369,7 +339,7 @@ const MessagesList = ({ ticketId, isGroup }) => { const [hasMore, setHasMore] = useState(false); const [loading, setLoading] = useState(false); const lastMessageRef = useRef(); - + const { t } = useTranslation(); const [selectedMessage, setSelectedMessage] = useState({}); const [anchorEl, setAnchorEl] = useState(null); const messageOptionsMenuOpen = Boolean(anchorEl); @@ -774,9 +744,12 @@ const MessagesList = ({ ticketId, isGroup }) => { })} > {message.quotedMsg && renderQuotedMessage(message)} - {message.body} + {(message.mediaType === "image" + ? '' + : {message.body} + )} - {message.isEdited && {i18n.t("message.edited")} } + {message.isEdited && {t("message.edited")} } {format(parseISO(message.createdAt), "HH:mm")}
@@ -823,9 +796,12 @@ const MessagesList = ({ ticketId, isGroup }) => { )} {message.quotedMsg && renderQuotedMessage(message)} - {message.body} + {(message.mediaType === "image" + ? '' + : {message.body} + )} - {message.isEdited && {i18n.t("message.edited")} } + {message.isEdited && {t("message.edited")} } {format(parseISO(message.createdAt), "HH:mm")} {renderMessageAck(message)} diff --git a/frontend/src/components/NewTicketModal/index.js b/frontend/src/components/NewTicketModal/index.js index b4a20975..533a686b 100644 --- a/frontend/src/components/NewTicketModal/index.js +++ b/frontend/src/components/NewTicketModal/index.js @@ -1,34 +1,28 @@ -import React, { useState, useEffect, useContext } from "react"; -import { useHistory } from "react-router-dom"; - +import { + FormControl, + InputLabel, + makeStyles, + MenuItem, + Select +} from "@material-ui/core"; import Button from "@material-ui/core/Button"; -import TextField from "@material-ui/core/TextField"; +import CircularProgress from "@material-ui/core/CircularProgress"; import Dialog from "@material-ui/core/Dialog"; - import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; +import TextField from "@material-ui/core/TextField"; import Autocomplete, { createFilterOptions, } from "@material-ui/lab/Autocomplete"; -import CircularProgress from "@material-ui/core/CircularProgress"; - -import { i18n } from "../../translate/i18n"; +import React, { useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; +import { AuthContext } from "../../context/Auth/AuthContext"; +import toastError from "../../errors/toastError"; import api from "../../services/api"; import ButtonWithSpinner from "../ButtonWithSpinner"; import ContactModal from "../ContactModal"; -import toastError from "../../errors/toastError"; -import { AuthContext } from "../../context/Auth/AuthContext"; - -import { - FormControl, - InputLabel, - makeStyles, - MenuItem, - Select -} from "@material-ui/core"; - - const useStyles = makeStyles((theme) => ({ autoComplete: { @@ -50,7 +44,7 @@ const filter = createFilterOptions({ const NewTicketModal = ({ modalOpen, onClose }) => { const history = useHistory(); - + const { t } = useTranslation(); const [options, setOptions] = useState([]); const [loading, setLoading] = useState(false); const [searchParam, setSearchParam] = useState(""); @@ -143,7 +137,7 @@ const NewTicketModal = ({ modalOpen, onClose }) => { if (option.number) { return `${option.name} - ${option.number}`; } else { - return `${i18n.t("newTicketModal.add")} ${option.name}`; + return `${t("newTicketModal.add")} ${option.name}`; } }; @@ -165,7 +159,7 @@ const NewTicketModal = ({ modalOpen, onClose }) => { > - {i18n.t("newTicketModal.title")} + {t("newTicketModal.title")} @@ -188,7 +182,7 @@ const NewTicketModal = ({ modalOpen, onClose }) => { renderInput={params => ( { - {i18n.t("ticketsList.acceptModal.queue")} + {t("ticketsList.acceptModal.queue")} { return queue ? ( ({ root: { @@ -55,7 +52,7 @@ const QuickAnswersModal = ({ shortcut: "", message: "", }; - + const { t } = useTranslation(); const isMounted = useRef(true); const messageInputRef = useRef(); const [loading, setLoading] = useState(false); @@ -63,38 +60,41 @@ const QuickAnswersModal = ({ useEffect(() => { return () => { - isMounted.current = false; + isMounted.current = false; }; }, []); useEffect(() => { - if (initialValues && isMounted.current) { - setQuickAnswer(prevState => { - return { ...prevState, ...initialValues }; - }); + if (initialValues) { + setQuickAnswer({ ...initialState, ...initialValues }); + } else if (quickAnswerId) { + const fetchQuickAnswer = async () => { + setLoading(true); + try { + const { data } = await api.get(`/quickAnswers/${quickAnswerId}`); + if (isMounted.current) { + setQuickAnswer(data); + } + setLoading(false); + } catch (err) { + setLoading(false); + toastError(err); + } + }; + fetchQuickAnswer(); } + }, [quickAnswerId, initialValues, initialState]); -(async () => { - if (!quickAnswerId) return ; - - setLoading(true); - try { - const { data } = await api.get(`/quickAnswers/${quickAnswerId}`); - if (!isMounted.current) return; + useEffect(() => { + if (open && messageInputRef.current) { + messageInputRef.current.focus(); + } + }, [open]); - setQuickAnswer(prevState => { - return { ...prevState, ...data }; - }); - - setLoading(false); - } catch (err) { - setLoading(false); - toastError(err); - } - })(); + const handleClose = () => { + onClose(); setQuickAnswer(initialState); - // eslint-disable-next-line - }, [quickAnswerId, open, initialValues]); + }; const handleSaveQuickAnswer = async values => { try { @@ -108,22 +108,24 @@ const QuickAnswersModal = ({ } onClose(); } - toast.success(i18n.t("quickAnswersModal.success")); + toast.success(t("quickAnswersModal.success")); } catch (err) { toastError(err); } }; - const handleClickMsgVar = async (msgVar, setValueFunc) => { + const handleClickMsgVar = (msgVar, setValueFunc) => { const el = messageInputRef.current; const firstHalfText = el.value.substring(0, el.selectionStart); const secondHalfText = el.value.substring(el.selectionEnd); - const newCursorPos = el.selectionStart + msgVar.length; setValueFunc("message", `${firstHalfText}${msgVar}${secondHalfText}`); - await new Promise(r => setTimeout(r, 100)); - messageInputRef.current.setSelectionRange(newCursorPos, newCursorPos); + const newCursorPos = el.selectionStart + msgVar.length; + setTimeout(() => { + el.setSelectionRange(newCursorPos, newCursorPos); + el.focus(); + }, 100); }; return ( @@ -132,13 +134,13 @@ const QuickAnswersModal = ({ maxWidth="sm" fullWidth open={open} - onClose={onClose} + onClose={handleClose} scroll="paper" > {quickAnswerId - ? i18n.t("quickAnswersModal.title.edit") - : i18n.t("quickAnswersModal.title.add")} + ? t("quickAnswersModal.title.edit") + : t("quickAnswersModal.title.add")} {quickAnswerId - ? i18n.t("quickAnswersModal.buttons.okEdit") - : i18n.t("quickAnswersModal.buttons.okAdd")} + ? t("quickAnswersModal.buttons.okEdit") + : t("quickAnswersModal.buttons.okAdd")} @@ -210,4 +212,4 @@ const QuickAnswersModal = ({ ); }; -export default QuickAnswersModal; \ No newline at end of file +export default QuickAnswersModal; diff --git a/frontend/src/components/TabPanel/index.js b/frontend/src/components/TabPanel/index.js index 15402537..a7fcb991 100644 --- a/frontend/src/components/TabPanel/index.js +++ b/frontend/src/components/TabPanel/index.js @@ -9,7 +9,7 @@ const TabPanel = ({ children, value, name, ...rest }) => { aria-labelledby={`simple-tab-${name}`} {...rest} > - <>{children} + {children} ); } else return null; diff --git a/frontend/src/components/TagModal/index.js b/frontend/src/components/TagModal/index.js index 407a7485..5438fba2 100644 --- a/frontend/src/components/TagModal/index.js +++ b/frontend/src/components/TagModal/index.js @@ -1,13 +1,3 @@ -import React, { useContext, useEffect, useState } from "react"; - -import { - Field, - Form, - Formik -} from "formik"; -import { toast } from "react-toastify"; -import * as Yup from "yup"; - import { Button, CircularProgress, @@ -20,13 +10,18 @@ import { makeStyles, TextField } from "@material-ui/core"; - import { green } from "@material-ui/core/colors"; import { Colorize } from "@material-ui/icons"; +import { + Field, + Form, + Formik +} from "formik"; import { ColorBox } from 'material-ui-color'; - -import { i18n } from "../../translate/i18n"; - +import React, { useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; import { AuthContext } from "../../context/Auth/AuthContext"; import toastError from "../../errors/toastError"; import api from "../../services/api"; @@ -71,6 +66,7 @@ const TagSchema = Yup.object().shape({ const TagModal = ({ open, onClose, tagId, reload }) => { const classes = useStyles(); + const { t } = useTranslation(); const { user } = useContext(AuthContext); const [colorPickerModalOpen, setColorPickerModalOpen] = useState(false); const initialState = { @@ -108,7 +104,7 @@ const TagModal = ({ open, onClose, tagId, reload }) => { } else { await api.post("/tags", tagData); } - toast.success(i18n.t("tagModal.success")); + toast.success(t("tagModal.success")); if (typeof reload == 'function') { reload(); } @@ -128,7 +124,7 @@ const TagModal = ({ open, onClose, tagId, reload }) => { scroll="paper" > - {(tagId ? `${i18n.t("tagModal.title.edit")}` : `${i18n.t("tagModal.title.add")}`)} + {(tagId ? `${t("tagModal.title.edit")}` : `${t("tagModal.title.add")}`)} {
{ { disabled={isSubmitting} variant="outlined" > - {i18n.t("tagModal.buttons.cancel")} + {t("tagModal.buttons.cancel")}
- {ticket.status === "closed" && ( - - )} {ticket.contact.telegramId && ( @@ -483,6 +450,14 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { > {ticket.contact.name} + {ticket.status === "closed" && ( + + )} } secondary={ @@ -519,11 +494,11 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => {

{ticket.whatsappId && ( - + { marginRight: "5px", marginBottom: "3px", }} - label={(ticket.whatsapp?.name || i18n.t("ticketsList.items.user")).toUpperCase()} + label={(ticket.whatsapp?.name || t("ticketsList.items.user")).toUpperCase()} /> )} - - {uName && ( - + {ticket.status !== "pending" && ticket?.user?.name && ( + { marginRight: "5px", marginBottom: "3px", }} - label={uName.toUpperCase()} + label={ticket?.user?.name.toUpperCase()} /> )}

- + { tag?.map((tag) => { @@ -577,7 +551,7 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { />
{(ticket.status === "pending" && (ticket.queue === null || ticket.queue === undefined)) && ( - + { )} {ticket.status === "pending" && ticket.queue !== null && ( - + { )} {ticket.status === "pending" && ( - + { )} {ticket.status === "pending" && ( - + { )} {ticket.status === "open" && ( - + { )} {ticket.status === "open" && ( - + { + const { t } = useTranslation(); const [confirmationOpen, setConfirmationOpen] = useState(false); const [transferTicketModalOpen, setTransferTicketModalOpen] = useState(false); const isMounted = useRef(true); @@ -66,29 +65,27 @@ const TicketOptionsMenu = ({ ticket, menuOpen, handleClose, anchorEl }) => { onClose={handleClose} > - {i18n.t("ticketOptionsMenu.transfer")} + {t("ticketOptionsMenu.transfer")} ( - {i18n.t("ticketOptionsMenu.delete")} + {t("ticketOptionsMenu.delete")} )} /> - {i18n.t("ticketOptionsMenu.confirmationModal.message")} + {t("ticketOptionsMenu.confirmationModal.message")} ({ ticketsListWrapper: { @@ -28,14 +24,12 @@ const useStyles = makeStyles((theme) => ({ borderTopRightRadius: 0, borderBottomRightRadius: 0, }, - ticketsList: { flex: 1, overflowY: "scroll", ...theme.scrollbarStyles, borderTop: "2px solid rgba(0, 0, 0, 0.12)", }, - ticketsListHeader: { color: "rgb(67, 83, 105)", zIndex: 2, @@ -45,28 +39,24 @@ const useStyles = makeStyles((theme) => ({ alignItems: "center", justifyContent: "space-between", }, - ticketsCount: { fontWeight: "normal", color: "rgb(104, 121, 146)", marginLeft: "8px", fontSize: "14px", }, - noTicketsText: { textAlign: "center", color: "rgb(104, 121, 146)", fontSize: "14px", lineHeight: "1.4", }, - noTicketsTitle: { textAlign: "center", fontSize: "16px", fontWeight: "600", margin: "0px", }, - noTicketsDiv: { display: "flex", height: "100px", @@ -169,6 +159,7 @@ const TicketsList = (props) => { tags, } = props; const classes = useStyles(); + const { t } = useTranslation(); const [pageNumber, setPageNumber] = useState(1); const [ticketsList, dispatch] = useReducer(reducer, []); const { user } = useContext(AuthContext); @@ -338,10 +329,10 @@ const TicketsList = (props) => { {ticketsList.length === 0 && !loading ? (
- {i18n.t("ticketsList.noTicketsTitle")} + {t("ticketsList.noTicketsTitle")}

- {i18n.t("ticketsList.noTicketsMessage")} + {t("ticketsList.noTicketsMessage")}

) : ( diff --git a/frontend/src/components/TicketsManager/index.js b/frontend/src/components/TicketsManager/index.js index 63320a3f..960e9777 100644 --- a/frontend/src/components/TicketsManager/index.js +++ b/frontend/src/components/TicketsManager/index.js @@ -1,5 +1,3 @@ -import React, { useContext, useEffect, useState } from "react"; - import { Badge, Button, @@ -10,23 +8,21 @@ import { Tab, Tabs } from "@material-ui/core"; - import { AllInboxRounded, HourglassEmptyRounded, MoveToInbox, Search } from "@material-ui/icons"; - +import React, { useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { AuthContext } from "../../context/Auth/AuthContext"; import { Can } from "../Can"; import NewTicketModal from "../NewTicketModal"; import TabPanel from "../TabPanel"; import TicketsList from "../TicketsList"; import TicketsQueueSelect from "../TicketsQueueSelect"; -import { AuthContext } from "../../context/Auth/AuthContext"; -import { i18n } from "../../translate/i18n"; - const useStyles = makeStyles((theme) => ({ ticketsWrapper: { position: "relative", @@ -37,23 +33,19 @@ const useStyles = makeStyles((theme) => ({ borderTopRightRadius: 0, borderBottomRightRadius: 0, }, - tabsHeader: { flex: "none", backgroundColor: theme.palette.background.default, }, - settingsIcon: { alignSelf: "center", marginLeft: "auto", padding: 8, }, - tab: { minWidth: 120, width: 120, }, - ticketOptionsBox: { display: "flex", justifyContent: "space-between", @@ -61,7 +53,6 @@ const useStyles = makeStyles((theme) => ({ backgroundColor: theme.palette.background.paper, padding: theme.spacing(1), }, - serachInputWrapper: { flex: 1, backgroundColor: theme.palette.background.default, @@ -70,14 +61,12 @@ const useStyles = makeStyles((theme) => ({ padding: 4, marginRight: theme.spacing(1), }, - searchIcon: { color: theme.palette.primary.main, marginLeft: 6, marginRight: 6, alignSelf: "center", }, - searchInput: { flex: 1, border: "none", @@ -85,7 +74,6 @@ const useStyles = makeStyles((theme) => ({ padding: "10px", outline: "none", }, - badge: { right: 0, }, @@ -104,17 +92,15 @@ const useStyles = makeStyles((theme) => ({ const TicketsManager = () => { const classes = useStyles(); - + const { t } = useTranslation(); const [searchParam, setSearchParam] = useState(""); const [tab, setTab] = useState("open"); const [tabOpen] = useState("open"); const [newTicketModalOpen, setNewTicketModalOpen] = useState(false); const [showAllTickets, setShowAllTickets] = useState(false); const { user } = useContext(AuthContext); - const [openCount, setOpenCount] = useState(0); const [pendingCount, setPendingCount] = useState(0); - const userQueueIds = user.queues.map((q) => q.id); const [selectedQueueIds, setSelectedQueueIds] = useState(userQueueIds || []); @@ -158,7 +144,7 @@ const TicketsManager = () => { { max={9999} color="secondary" > - {i18n.t("tickets.tabs.open.title")} + {t("tickets.tabs.open.title")} } classes={{ root: classes.tab }} @@ -200,7 +186,7 @@ const TicketsManager = () => { max={9999} color="secondary" > - {i18n.t("ticketsList.pendingHeader")} + {t("ticketsList.pendingHeader")} } classes={{ root: classes.tab }} @@ -208,7 +194,7 @@ const TicketsManager = () => { } - label={i18n.t("tickets.tabs.closed.title")} + label={t("tickets.tabs.closed.title")} classes={{ root: classes.tab }} /> @@ -219,14 +205,14 @@ const TicketsManager = () => { color="primary" onClick={() => setNewTicketModalOpen(true)} > - {i18n.t("ticketsManager.buttons.newTicket")} + {t("ticketsManager.buttons.newTicket")} ( { + const { t } = useTranslation(); + const handleChange = e => { onChange(e.target.value); }; @@ -35,7 +36,7 @@ const TicketsQueueSelect = ({ }, getContentAnchorEl: null, }} - renderValue={() => i18n.t("ticketsQueueSelect.placeholder")} + renderValue={() => t("ticketsQueueSelect.placeholder")} > {userQueues?.length > 0 && userQueues.map(queue => ( diff --git a/frontend/src/components/TransferTicketModal/index.js b/frontend/src/components/TransferTicketModal/index.js index cc6e80a5..eee26e0d 100644 --- a/frontend/src/components/TransferTicketModal/index.js +++ b/frontend/src/components/TransferTicketModal/index.js @@ -1,36 +1,33 @@ -import React, { useState, useEffect, useContext } from "react"; -import { useHistory } from "react-router-dom"; - +import { makeStyles } from "@material-ui/core"; import Button from "@material-ui/core/Button"; -import TextField from "@material-ui/core/TextField"; +import CircularProgress from "@material-ui/core/CircularProgress"; import Dialog from "@material-ui/core/Dialog"; -import Select from "@material-ui/core/Select"; -import FormControl from "@material-ui/core/FormControl"; -import InputLabel from "@material-ui/core/InputLabel"; -import MenuItem from "@material-ui/core/MenuItem"; -import { makeStyles } from "@material-ui/core"; - import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; +import FormControl from "@material-ui/core/FormControl"; +import InputLabel from "@material-ui/core/InputLabel"; +import MenuItem from "@material-ui/core/MenuItem"; +import Select from "@material-ui/core/Select"; +import TextField from "@material-ui/core/TextField"; import Autocomplete, { createFilterOptions, } from "@material-ui/lab/Autocomplete"; -import CircularProgress from "@material-ui/core/CircularProgress"; - -import { i18n } from "../../translate/i18n"; -import api from "../../services/api"; -import ButtonWithSpinner from "../ButtonWithSpinner"; +import React, { useContext, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; +import { AuthContext } from "../../context/Auth/AuthContext"; import toastError from "../../errors/toastError"; import useQueues from "../../hooks/useQueues"; import useWhatsApps from "../../hooks/useWhatsApps"; -import { AuthContext } from "../../context/Auth/AuthContext"; +import api from "../../services/api"; +import ButtonWithSpinner from "../ButtonWithSpinner"; import { Can } from "../Can"; const useStyles = makeStyles((theme) => ({ - maxWidth: { - width: "100%", - }, + maxWidth: { + width: "100%", + }, })); const filterOptions = createFilterOptions({ @@ -39,6 +36,7 @@ const filterOptions = createFilterOptions({ const TransferTicketModal = ({ modalOpen, onClose, ticketid, ticketWhatsappId }) => { const history = useHistory(); + const { t } = useTranslation(); const [options, setOptions] = useState([]); const [queues, setQueues] = useState([]); const [allQueues, setAllQueues] = useState([]); @@ -115,7 +113,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid, ticketWhatsappId }) } } - if(selectedWhatsapp) { + if (selectedWhatsapp) { data.whatsappId = selectedWhatsapp; } @@ -133,7 +131,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid, ticketWhatsappId })
- {i18n.t("transferTicketModal.title")} + {t("transferTicketModal.title")} ( - {i18n.t("transferTicketModal.fieldQueueLabel")} + {t("transferTicketModal.fieldQueueLabel")} setSelectedWhatsapp(e.target.value)} - label={i18n.t("transferTicketModal.fieldConnectionPlaceholder")} + label={t("transferTicketModal.fieldConnectionPlaceholder")} > {whatsApps.map((whasapp) => ( {whasapp.name} @@ -215,7 +213,7 @@ const TransferTicketModal = ({ modalOpen, onClose, ticketid, ticketWhatsappId }) disabled={loading} variant="outlined" > - {i18n.t("transferTicketModal.buttons.cancel")} + {t("transferTicketModal.buttons.cancel")} - {i18n.t("transferTicketModal.buttons.ok")} + {t("transferTicketModal.buttons.ok")} diff --git a/frontend/src/components/UserModal/index.js b/frontend/src/components/UserModal/index.js index ec5e6ed4..1a6968ed 100644 --- a/frontend/src/components/UserModal/index.js +++ b/frontend/src/components/UserModal/index.js @@ -1,45 +1,40 @@ -import React, { useState, useEffect, useContext, useRef } from "react"; -import { useHistory } from "react-router-dom"; -import * as Yup from "yup"; -import { - Formik, - Form, - Field -} from "formik"; -import { toast } from "react-toastify"; - import { Button, + CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, - CircularProgress, - Select, + FormControl, + IconButton, + InputAdornment, InputLabel, makeStyles, MenuItem, - FormControl, - TextField, - InputAdornment, - IconButton + Select, + TextField } from '@material-ui/core'; - +import { green } from "@material-ui/core/colors"; import { Visibility, VisibilityOff } from '@material-ui/icons'; - -import { green } from "@material-ui/core/colors"; - -import { i18n } from "../../translate/i18n"; - -import api from "../../services/api"; -import toastError from "../../errors/toastError"; -import QueueSelect from "../QueueSelect"; +import { + Field, + Form, + Formik +} from "formik"; +import React, { useContext, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useHistory } from "react-router-dom"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; import { AuthContext } from "../../context/Auth/AuthContext"; -import { Can } from "../Can"; +import toastError from "../../errors/toastError"; import useWhatsApps from "../../hooks/useWhatsApps"; +import api from "../../services/api"; +import { Can } from "../Can"; +import QueueSelect from "../QueueSelect"; const useStyles = makeStyles(theme => ({ root: { @@ -89,7 +84,7 @@ const UserSchema = Yup.object().shape({ const UserModal = ({ open, onClose, userId }) => { const classes = useStyles(); - + const { t } = useTranslation(); const initialState = { name: "", email: "", @@ -143,7 +138,7 @@ const UserModal = ({ open, onClose, userId }) => { } else { await api.post("/users", userData); } - toast.success(i18n.t("userModal.success")); + toast.success(t("userModal.success")); history.go(0); } catch (err) { toastError(err); @@ -162,8 +157,8 @@ const UserModal = ({ open, onClose, userId }) => { > {userId - ? `${i18n.t("userModal.title.edit")}` - : `${i18n.t("userModal.title.add")}`} + ? `${t("userModal.title.edit")}` + : `${t("userModal.title.add")}`} {
{ name="password" variant="outlined" margin="dense" - label={i18n.t("userModal.form.password")} + label={t("userModal.form.password")} error={touched.password && Boolean(errors.password)} helperText={touched.password && errors.password} type={showPassword ? 'text' : 'password'} @@ -218,7 +213,7 @@ const UserModal = ({ open, onClose, userId }) => {
{ yes={() => ( <> - {i18n.t("userModal.form.profile")} + {t("userModal.form.profile")} - {i18n.t("userModal.form.admin")} - {i18n.t("userModal.form.user")} + {t("userModal.form.admin")} + {t("userModal.form.user")} )} @@ -271,12 +266,12 @@ const UserModal = ({ open, onClose, userId }) => { perform="user-modal:editQueues" yes={() => (!loading && - {i18n.t("userModal.form.whatsapp")} + {t("userModal.form.whatsapp")} setWhatsappId(e.target.value)} - label={i18n.t("userModal.form.whatsapp")} + label={t("userModal.form.whatsapp")} >   {whatsApps.map((whatsapp) => ( @@ -293,7 +288,7 @@ const UserModal = ({ open, onClose, userId }) => {
{ /> { yes={() => ( <> - {i18n.t("userModal.form.isTricked")} + {t("userModal.form.isTricked")} - {i18n.t("userModal.form.enabled")} - {i18n.t("userModal.form.disabled")} + {t("userModal.form.enabled")} + {t("userModal.form.disabled")} )} @@ -380,7 +375,7 @@ const UserModal = ({ open, onClose, userId }) => { disabled={isSubmitting} variant="outlined" > - {i18n.t("userModal.buttons.cancel")} + {t("userModal.buttons.cancel")}