From c8cd4ff3767eadcc9773c2eb20bfa9c9881ebdd2 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Tue, 5 Nov 2024 08:48:22 -0300 Subject: [PATCH 01/24] Add scripts para sequelize --- backend/package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/package.json b/backend/package.json index 76706942..763bef01 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,11 +5,13 @@ "main": "index.js", "scripts": { "build": "tsc", + "create": "npx sequelize 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" @@ -101,4 +103,4 @@ "engines": { "node": ">=18.0.0" } -} +} \ No newline at end of file From 6706ef2a091163be1226355d08efa47681c6f74b Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Tue, 12 Nov 2024 09:04:56 -0300 Subject: [PATCH 02/24] =?UTF-8?q?Corre=C3=A7=C3=A3o=20na=20migration=20alt?= =?UTF-8?q?er-queueId-foreign-key-on-tickets.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...22-alter-queueId-foreign-key-on-tickets.ts | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) 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", From b04fe8e060c9c9166add955c09e33471236d7323 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Thu, 14 Nov 2024 08:15:07 -0300 Subject: [PATCH 03/24] =?UTF-8?q?Remo=C3=A7=C3=A3o=20do=20config.json?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- UPDATE.sh | 22 +++++++-------- docs/INSTALL_VPS.md | 14 +++------- frontend/.gitignore | 5 ++-- frontend/src/assets/Logo_circle.png | Bin 86453 -> 0 bytes frontend/src/assets/logo-dash.png | Bin 1837 -> 0 bytes frontend/src/assets/logo.png | Bin 2557 -> 0 bytes frontend/src/config.json.example | 42 ---------------------------- 7 files changed, 17 insertions(+), 66 deletions(-) delete mode 100644 frontend/src/assets/Logo_circle.png delete mode 100644 frontend/src/assets/logo-dash.png delete mode 100644 frontend/src/assets/logo.png delete mode 100644 frontend/src/config.json.example diff --git a/UPDATE.sh b/UPDATE.sh index 6ac95454..6efe3a3f 100644 --- a/UPDATE.sh +++ b/UPDATE.sh @@ -151,20 +151,20 @@ sleep 2 cd ../frontend -sleep 2 +# sleep 2 -echo " " | tee -a "$LOG_FILE" -echo "VERIFICANDO O CONFIG.JSON" | tee -a "$LOG_FILE" -echo " " | tee -a "$LOG_FILE" +# echo " " | tee -a "$LOG_FILE" +# echo "VERIFICANDO O CONFIG.JSON" | tee -a "$LOG_FILE" +# echo " " | tee -a "$LOG_FILE" -sleep 2 +# 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" -else - echo "O arquivo config.json já existe" | tee -a "$LOG_FILE" -fi +# 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" +# else +# echo "O arquivo config.json já existe" | tee -a "$LOG_FILE" +# fi sleep 2 diff --git a/docs/INSTALL_VPS.md b/docs/INSTALL_VPS.md index 18abd057..ec3b14f7 100644 --- a/docs/INSTALL_VPS.md +++ b/docs/INSTALL_VPS.md @@ -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 diff --git a/frontend/.gitignore b/frontend/.gitignore index 48df9d1c..cb4d0956 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -5,14 +5,13 @@ /.pnp .pnp.js - # testing /coverage # production /build /dist -/src/config.json +/public/assets # misc .DS_Store @@ -27,4 +26,4 @@ yarn-debug.log* yarn-error.log* package-lock.json yarn.lock -build \ No newline at end of file +build diff --git a/frontend/src/assets/Logo_circle.png b/frontend/src/assets/Logo_circle.png deleted file mode 100644 index 7e9e439671d80721cd56ff13766c5ef366242c6e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 86453 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 ZckRv&FD{`bzqg>Y_!Bf(128jA{}=O-{6GKz diff --git a/frontend/src/assets/logo-dash.png b/frontend/src/assets/logo-dash.png deleted file mode 100644 index 580abb13bd2024fd4d4d75b203c2b1ebbfee5ca3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1837 zcmaJ?X;c$e6pjig9H1)7R)siLRBU#LBoc%qganOZL_pjK$q+`&%rF^Aj9M_Rq;4Qe zaTf$c3k4CXh+=vwLIo}2f`E#rYDKZ+xZz>569x1~>7043psNQB&m>3{|H_2{wu3KUas!6$v0tjs;Z^%mX?vRsm#4$pa#NrF1DO z0OKLC9s?Ka7lbSH2?~x9m>WR$*Kr8}HHgc}I`tY@!`1PCPr6)UY+0rO49K3rcmmpPj}@BoB0Oc}?O z2}J|8h#3!v$8nTPqiMBTs+L7XFcpo#;czS(OeTduP&DgcT&|HkC3>LIiSmw{jW{ZC;{ z_&OA%$v_RV7E=&`i<@dOg>nTLD8~^j96{C$-eOohf+L!E1SJa=v&l0eAy|oMH8Vfq zrBbd0*5Gnj0ZN2CfKZ@9kdo^=o5P{=IsBj?K9j)+7Vz10mLFf}$MO{sI|9}qSBNOq zszDeZD-j|?ZxirEVnkBGb z1$uCa$R5jZC~b+!P~w9yk?k0f){h+z77^8QLLv+b*EM%6Z`jpBw|_TrXy#{Vx#*+^{pI@c6_^Lx}hYpTE>&+N`sGHMOr#NfTcCbHXuknlTZ%_tJON zN|#k_BgfjO#E`5;kzGk9(iodSx}}i}BgWeMyH1D9J@KMX zJ-^@N7~5<+8aJ#q7f%~h|0q;!W1ls)ew^+vCTriPlVTUbEurXX=yMnPanwxWYEuv~!>2;rVfvVs&ZEfW{3(7~^ z9<5pP>W5`}`wX*IyP0yg*RfGr2&#J8+LsYn>{a1D=dRcI`7VDoswm!jbK@2 z*4d<1#{- z#@*1BLxGDDr@7qAmDG0bON92kc=3%Gby~FLT|zzM+~~iZN<6A@lljYCR&p*a!cK_CUQnh%zPnst$*yY#u_~6VE zE0+<;S!K@Z9Whh;dY`0LJalg@e7@x;?^5U0DLFzjMNqcq^8ILay4M}d?zK2OAj!Q% z6#w{iO5wb|GLC6m$B_c7*=dw&58JyVP2iQgx&8W3tC!;RXTa^kj#!abi`=s{q^T;e zDxxr3k$v7AzWr9C{agCOiA#D@9aaW88j2{$u7gIr+4S=h_v=ekMzwo#+nyyGJgN+Ejt!=L-?!G=kVi^ zwi}Zc_}JcgAF=ho()QLE?Rv9&`>nbSN!=j_Dyol+ty8;sbo?@-@^La=$u_sm8LE$g hviYiXipNx0yOP50+LtKt~z$C#7ghl)q!@pV#-lqTn diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png deleted file mode 100644 index c693f9022481f0c2dbd68847ad1431d2533a80be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2557 zcmaJ@dsI^C7N=WHjiya`OUpEdUei)RR4_%m<2@=47`-%b~~@m`*-GL`$)&srTTc>5tnz>#TG3_np0edw;+E`@VHf z@AuwqVzk}}0)d!#?jia@Ad9G=%~<{o_-v}C0N`t#z=I+nb0P#H8V`WDF*xA>)RRSv z1bhJ+BR=lmfC~h&7{>IY2q>g|SUQIVrx|?UVip&ShCp0g#atRa1`t5Qfk-AB2h&y6 zz@SV94i<4ZX~k+x6=d!z%x9=y?ZNGB`` zjdgH_&R;Mv8;=oz^(F3_&jpTfuqc6mi$x$rA`x8V2D>JZ(2Yg0(?4;$rUg;Y^VW|7S0h0a4@jaxh1f;pJCbjd741N5Mmk^frQ%|mbBnV zBK?0?7VEP&U*HS;UGM({=KIBS0faBW=LmUpuyGNa45_$S0uP`GI6OZNCw76w{ZSkN zhabh^LJ4Fv^oKwuo52zBx6k^MNLWucUqEBi0Z$?h1|qO|zQY|wv?sc`xjP_{ z9)uleXAg|MqZ855&B@Wj!DAto$e{~a09&vS%lJFi|L3ECWd^Wo-#7NsO~y$The)05^9fEf;chJ1ajd6RJ>>jBDrhnkHi01PKA77)*6Ge z-$^0h?5tVQ+BG$?#a1<4()e*Iue>MnNwe%GwY|CWXnJ(FzO+!8?op?qs_AUsc7<>Q zrTi~UtgTvqG1IK!?6gmg*>-Ja_%zVhSgw2A^p@VO$yKI@Jkf(;Z%Rxe`dU`n&w77Hgt=08PXEVxpPZ{Tel`z}{yFi1JSCdEe0E^t+o>!; z)eLoO^mFt*<%@zvJ(2--m{swE__LUokw;KdKUZz9YVOa=4$gDrSGRNe?uQMp$qA~R z)J_)LWIa48v4E$A;Ou!m3D4H}-PD-eGtxQ3s6}!aOLaf8FyBmiV1Nv zSB+ooH0L$A$}RZbqw7KmeJ>J}27_ zR4U(eA6Bi6R_XT^l0#xR2_4ejLsK8GVeT306r%F)XW)q#*cvrTl@Q`ks|> znN9#WOy5y8KO3!xcSW4dS6XCk2Kmiy!`C)r`@X1VOPLLNQw zLumm2{F z{LbwaaD<;gO-MMbu1-jnXwLX+3>#a05 z)T^ZaR&P?wZD&~nRm{qo7-GaZ_W9k#(O-F0x3^GORRIWgJ}Wp0efJqb#7}Zq7 zE2w<-P(&FjWT>sO>&>2=hLX<1nOLz8Ez9QMts;@_5$mE?8mfDc$>0rqIBPuk9fcd4 zjSn*oj)u(c=j19QB}es`z?V*qv>;z&Nt2@7gHKiIhjg!9%wts}?yh`QQ(9VuYIuE7 zXvi5K>D*qhPs8AqQyp}EHz_pBva<2*&OB|y@Ckcun-=zI8rLNmU3IA;(SN41R0ovz zc|OzV0Mm*3cCz$_qS7(B+dm}r-1MDW8G64ORc~fXJ~;+>)9fqiXy9Mw1y+S>uf%qGp~)2mE+uXXRW zF4BWypCboXT*WR%MSrm3}!R_jugOaMasRX3|aBqkWEx3**=N z4V`}4x}l$}qipowCMIj}E5XK;W}Ws{W)A4bm|??1&GLe}G1-g0b+TGn{fB1X#gD32 z&9UC(b|`gFX~#4z8=K5kj}a!KT>r#Zn`_SL#kamUEWRsM{AsgRd^N12jTOuyOu;ojK!SfeSR8WfOX4xZ@qM1zOiEF%lUFTQk@Ozuuz1WEJh V38;O$&&KdW=IP;0EOHM^`7a@Re^vkh diff --git a/frontend/src/config.json.example b/frontend/src/config.json.example deleted file mode 100644 index 1099813c..00000000 --- a/frontend/src/config.json.example +++ /dev/null @@ -1,42 +0,0 @@ -{ - "system": { - "name": "Press Ticket", - "url": "https://pressticket.com.br", - "color": { - "lightTheme": { - "palette": { - "primary": "#6B62FE", - "secondary": "#F50057" - }, - "toolbar": { - "background": "linear-gradient(to right, #6B62FE, #f50057)" - }, - "menuItens": "#ffffff", - "sub": "#ffffff", - "toolbarIcon": "#ffffff", - "divide": "#E0E0E0" - }, - "darkTheme": { - "palette": { - "primary": "#52d869", - "secondary": "#ff9100", - "background": { - "default": "#080d14", - "paper": "#181d22" - }, - "text": { - "primary": "#52d869", - "secondary": "#ffffff" - } - }, - "toolbar": { - "background": "linear-gradient(to right, #52d869, #ff9100)" - }, - "menuItens": "#181d22", - "sub": "#181d22", - "toolbarIcon": "#181d22", - "divide": "#080d14" - } - } - } -} \ No newline at end of file From 723639f9b08f2bc7011a91a5e4e5b1b7d953b96d Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Fri, 15 Nov 2024 00:32:16 -0300 Subject: [PATCH 04/24] =?UTF-8?q?Add=20Personaliza=C3=A7=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/api.rest | 42 +- backend/package.json | 4 +- backend/src/app.ts | 19 +- backend/src/config/auth.ts | 2 +- backend/src/config/uploadConfig.ts | 90 +++ .../controllers/PersonalizationController.ts | 89 +++ backend/src/database/index.ts | 4 +- .../20241106170332-create-personalizations.ts | 68 +++ .../20200904070004-create-default-settings.ts | 6 - backend/src/models/Personalization.ts | 58 ++ backend/src/routes/index.ts | 2 + backend/src/routes/personalizationRoutes.ts | 25 + .../CreateOrUpdatePersonalizationService.ts | 74 +++ .../DeletePersonalizationService.ts | 13 + .../ListPersonalizationsService.ts | 8 + frontend/public/index.html | 41 +- frontend/src/App.js | 178 +++--- ...background-dark.jpg => backgroundDark.jpg} | Bin ...ckground-light.png => backgroundLight.png} | Bin frontend/src/assets/logo.jpg | Bin 0 -> 42586 bytes frontend/src/assets/logoTicket.jpg | Bin 0 -> 67524 bytes .../src/components/ErrorBoundary/index.js | 26 + .../src/components/ThemeSelector/index.js | 15 + .../src/components/TicketListItem/index.js | 4 +- frontend/src/layout/index.js | 117 +++- frontend/src/pages/Login/index.js | 104 +++- .../src/pages/Settings/ComponentSettings.js | 101 ++++ frontend/src/pages/Settings/Personalize.js | 539 ++++++++++++++++++ frontend/src/pages/Settings/index.js | 370 +++--------- frontend/src/pages/Signup/index.js | 110 +++- frontend/src/pages/Tickets/index.js | 47 +- frontend/src/routes/index.js | 6 +- frontend/src/themes/darkTheme.js | 48 ++ frontend/src/themes/lightTheme.js | 39 ++ frontend/src/themes/themeConfig.js | 8 + 35 files changed, 1740 insertions(+), 517 deletions(-) create mode 100644 backend/src/config/uploadConfig.ts create mode 100644 backend/src/controllers/PersonalizationController.ts create mode 100644 backend/src/database/migrations/20241106170332-create-personalizations.ts create mode 100644 backend/src/models/Personalization.ts create mode 100644 backend/src/routes/personalizationRoutes.ts create mode 100644 backend/src/services/PersonalizationServices/CreateOrUpdatePersonalizationService.ts create mode 100644 backend/src/services/PersonalizationServices/DeletePersonalizationService.ts create mode 100644 backend/src/services/PersonalizationServices/ListPersonalizationsService.ts rename frontend/src/assets/{wa-background-dark.jpg => backgroundDark.jpg} (100%) rename frontend/src/assets/{wa-background-light.png => backgroundLight.png} (100%) create mode 100644 frontend/src/assets/logo.jpg create mode 100644 frontend/src/assets/logoTicket.jpg create mode 100644 frontend/src/components/ErrorBoundary/index.js create mode 100644 frontend/src/components/ThemeSelector/index.js create mode 100644 frontend/src/pages/Settings/ComponentSettings.js create mode 100644 frontend/src/pages/Settings/Personalize.js create mode 100644 frontend/src/themes/darkTheme.js create mode 100644 frontend/src/themes/lightTheme.js create mode 100644 frontend/src/themes/themeConfig.js diff --git a/backend/api.rest b/backend/api.rest index 9e6852e2..049e2c2d 100644 --- a/backend/api.rest +++ b/backend/api.rest @@ -1,10 +1,20 @@ # 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 +@token = 6175c0d0-acd5-4776-95a9-592c795da986 +@token2 = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlByZXNzLVRpY2tldCIsInByb2ZpbGUiOiJhZG1pbiIsImlkIjoxLCJpYXQiOjE3MzEzNzIwNDksImV4cCI6MTczMTM3NTY0OX0.QwidYXoxONiHC7fsTF87IxLJ-nJ-aNrMFCK4habN2yQ -# (Enviar Mensagem) Teste da Rota POST /api/messages/send +### (Login) Teste da Rota POST /auth/login +POST {{baseUrl}}/auth/login +Content-Type: application/json + +{ + "email": "admin@pressticket.com.br", + "password": "admin" +} + +### (Enviar Mensagem) Teste da Rota POST /api/messages/send POST {{baseUrl}}/api/messages/send Authorization: Bearer {{token}} Content-Type: application/json @@ -17,4 +27,28 @@ Content-Type: application/json "whatsappId": "1" } -### +### (Listar Personalizações) Teste da Rota GET /personalizations +GET {{baseUrl}}/personalizations +Content-Type: application/json + +### (Criar ou Atualizar Personalização) Teste da Rota PUT /personalizations/:theme +PUT {{baseUrl}}/personalizations/light +Authorization: Bearer {{token2}} +Content-Type: application/json + +{ + "company": "Press Ticket", + "url": "https://pressticket.com.br", + "primaryColor": "#ffffff", + "secondaryColor": "#0000ff", + "backgroundDefault": "#ff00ff", + "backgroundPaper": "#00ff00", + "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 763bef01..53fb5c01 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,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", @@ -103,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/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/PersonalizationController.ts b/backend/src/controllers/PersonalizationController.ts new file mode 100644 index 00000000..d605f20a --- /dev/null +++ b/backend/src/controllers/PersonalizationController.ts @@ -0,0 +1,89 @@ +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 createOrUpdate = async ( + req: Request, + res: Response +): Promise => { + try { + const personalizationData: PersonalizationData = req.body; + const { theme } = req.params; + + 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 + ); + } + } + + 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/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/20241106170332-create-personalizations.ts b/backend/src/database/migrations/20241106170332-create-personalizations.ts new file mode 100644 index 00000000..a15f8880 --- /dev/null +++ b/backend/src/database/migrations/20241106170332-create-personalizations.ts @@ -0,0 +1,68 @@ +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: false + }, + company: { + type: DataTypes.STRING, + allowNull: false + }, + url: { + type: DataTypes.STRING, + allowNull: false + }, + primaryColor: { + type: DataTypes.STRING, + allowNull: false + }, + secondaryColor: { + type: DataTypes.STRING, + allowNull: false + }, + backgroundDefault: { + type: DataTypes.STRING, + allowNull: false + }, + backgroundPaper: { + type: DataTypes.STRING, + allowNull: false + }, + favico: { + type: DataTypes.TEXT, + allowNull: true + }, + logo: { + type: DataTypes.TEXT, + allowNull: true + }, + logoTicket: { + type: DataTypes.TEXT, + 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/models/Personalization.ts b/backend/src/models/Personalization.ts new file mode 100644 index 00000000..6b081de6 --- /dev/null +++ b/backend/src/models/Personalization.ts @@ -0,0 +1,58 @@ +import { + AutoIncrement, + Column, + CreatedAt, + DataType, + Model, + PrimaryKey, + Table, + UpdatedAt +} from "sequelize-typescript"; + +@Table +class Personalization extends Model { + @PrimaryKey + @AutoIncrement + @Column + id: number; + + @Column(DataType.STRING) + theme: string; + + @Column(DataType.STRING) + company: string; + + @Column(DataType.STRING) + url: string; + + @Column(DataType.STRING) + primaryColor: string; + + @Column(DataType.STRING) + secondaryColor: string; + + @Column(DataType.STRING) + backgroundDefault: string; + + @Column(DataType.STRING) + backgroundPaper: string; + + @Column(DataType.TEXT) + favico: string | null; + + @Column(DataType.TEXT) + logo: string | null; + + @Column(DataType.TEXT) + logoTicket: 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..1be6ddd3 --- /dev/null +++ b/backend/src/routes/personalizationRoutes.ts @@ -0,0 +1,25 @@ +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.put( + "/personalizations/:theme", + isAuth, + uploadConfig.fields([ + { name: "favico", maxCount: 1 }, + { name: "logo", maxCount: 1 }, + { name: "logoTicket", maxCount: 1 } + ]), + PersonalizationController.createOrUpdate +); +personalizationRoutes.delete( + "/personalizations/:theme", + isAuth, + PersonalizationController.remove +); + +export default personalizationRoutes; 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/frontend/public/index.html b/frontend/public/index.html index 60518374..ce920d7b 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 + + %REACT_APP_PAGE_TITLE% + + + + + + + + + +
+ + diff --git a/frontend/src/App.js b/frontend/src/App.js index 238aa99c..cf4703cf 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,135 +1,101 @@ -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, { 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 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 [locale, setLocale] = useState(ptBR); const [theme, setTheme] = useState("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 = (themeType, updatedConfig) => { + if (themeType === "light") { + setLightThemeConfig((prevConfig) => ({ ...prevConfig, ...updatedConfig })); + } else if (themeType === "dark") { + setDarkThemeConfig((prevConfig) => ({ ...prevConfig, ...updatedConfig })); + } + }; useEffect(() => { - - const fetchDarkMode = async () => { + const fetchThemeConfig = 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); + } + if (darkConfig) { + setDarkThemeConfig(darkConfig); + } + } } catch (err) { - setTheme("light") toastError(err); } }; - fetchDarkMode(); - + fetchThemeConfig(); }, []); useEffect(() => { - const i18nlocale = localStorage.getItem("i18nextLng"); - const browserLocale = i18nlocale.substring(0, 2) + i18nlocale.substring(3, 5); + const savedTheme = localStorage.getItem("theme") || "light"; + setTheme(savedTheme); + }, [theme]); - if (browserLocale === "ptBR") { - setLocale(ptBR); + useEffect(() => { + const i18nlocale = localStorage.getItem("i18nextLng"); + 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"; + fetch(faviconPath, { method: "HEAD" }) + .then(response => { + if (response.ok) { + favicon.href = faviconPath; + } else { + favicon.href = "/favicon.ico"; + } + }) + .catch(() => { + favicon.href = "/favicon.ico"; + }); + + 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/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.jpg b/frontend/src/assets/logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..de9153f74aa844fd334dbcc3fa27879c9dfa34f5 GIT binary patch literal 42586 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

=K!$ciYGsF>&l(F!q~n3Y(G*q%6sxQ6%@ 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 deu errado. Tente novamente mais tarde.

; + } + + return this.props.children; + } +} + +export default ErrorBoundary; diff --git a/frontend/src/components/ThemeSelector/index.js b/frontend/src/components/ThemeSelector/index.js new file mode 100644 index 00000000..4a56024e --- /dev/null +++ b/frontend/src/components/ThemeSelector/index.js @@ -0,0 +1,15 @@ +import { IconButton } from '@material-ui/core'; +import { Brightness4, Brightness7 } from '@material-ui/icons'; +import React from 'react'; + +function ThemeSelector({ toggleTheme }) { + const themeStorage = localStorage.getItem('theme'); + + return ( + + {themeStorage === 'light' ? : } + + ); +} + +export default ThemeSelector; \ No newline at end of file diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 7324e6f4..f6ace5bd 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -47,13 +47,11 @@ import MarkdownWrapper from "../MarkdownWrapper"; import clsx from "clsx"; import receiveIcon from "../../assets/receive.png"; import sendIcon from "../../assets/send.png"; -import { system } from "../../config.json"; 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 => ({ ticket: { position: "relative", @@ -523,7 +521,7 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { ({ toolbar: { paddingRight: 24, color: "#ffffff", - background: theme.palette.toolbar.main + background: theme.palette?.toolbar?.main || "#6B62FE", }, toolbarIcon: { display: "flex", @@ -55,7 +54,7 @@ const useStyles = makeStyles((theme) => ({ justifyContent: "flex-end", padding: "0 8px", minHeight: "48px", - backgroundColor: theme.palette.toolbarIcon.main + backgroundColor: theme.palette?.toolbarIcon?.main || "#ffffff", }, appBar: { zIndex: theme.zIndex.drawer + 1, @@ -121,11 +120,11 @@ const useStyles = makeStyles((theme) => ({ systemCss: { display: "flex", justifyContent: "center", - fontSize: 12 - } + fontSize: 12, + }, })); -const LoggedInLayout = ({ children }) => { +const LoggedInLayout = ({ children, toggleTheme, onThemeConfigUpdate }) => { const classes = useStyles(); const [userModalOpen, setUserModalOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); @@ -135,6 +134,83 @@ const LoggedInLayout = ({ children }) => { const [drawerVariant, setDrawerVariant] = useState("permanent"); const { user } = useContext(AuthContext); const [latestVersion, setLatestVersion] = useState(""); + const themeStorage = localStorage.getItem("theme"); + const [companyData, setCompanyData] = useState({ + logo: defaultLogo, + name: "Press Ticket" + }); + + useEffect(() => { + const fetchCompanyData = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + + if (lightConfig) { + setCompanyData(prevData => ({ + ...prevData, + name: lightConfig.company || "Press Ticket" + })); + } + } + } catch (err) { + toastError(err); + } + }; + + const socket = openSocket(); + socket.on("personalization", data => { + if (data.action === "update") { + fetchCompanyData(); + } + }); + + fetchCompanyData(); + + return () => { + socket.disconnect(); + }; + }, []); + + useEffect(() => { + const fetchLogo = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + const darkConfig = data.find(themeConfig => themeConfig.theme === "dark"); + + if (themeStorage === "light" && lightConfig && lightConfig.logo) { + setCompanyData(prevData => ({ + ...prevData, + logo: PUBLIC_ASSET_PATH + lightConfig.logo + })); + onThemeConfigUpdate("light", { logo: PUBLIC_ASSET_PATH + lightConfig.logo }); + } else if (themeStorage === "dark" && darkConfig && darkConfig.logo) { + setCompanyData(prevData => ({ + ...prevData, + logo: PUBLIC_ASSET_PATH + darkConfig.logo + })); + onThemeConfigUpdate("dark", { logo: PUBLIC_ASSET_PATH + darkConfig.logo }); + } else { + setCompanyData(prevData => ({ + ...prevData, + logo: defaultLogo + })); + } + } + + } catch (err) { + toastError(err); + } + }; + + fetchLogo(); + }, [themeStorage, onThemeConfigUpdate]); useEffect(() => { const compareVersions = async () => { @@ -234,7 +310,7 @@ const LoggedInLayout = ({ children }) => { open={drawerOpen} >
- logo + logo setDrawerOpen(!drawerOpen)}> @@ -275,8 +351,11 @@ const LoggedInLayout = ({ children }) => { noWrap className={classes.title} > - {i18n.t("mainDrawer.appBar.message.hi")} {user.name}, {i18n.t("mainDrawer.appBar.message.text")} {system.name || "Press Ticket"} + {i18n.t("mainDrawer.appBar.message.hi")} {user.name}, {i18n.t("mainDrawer.appBar.message.text")} {companyData.name || "Press Ticket"} + + + {user.id && }
@@ -314,7 +393,7 @@ const LoggedInLayout = ({ children }) => { {latestVersion && latestVersion > systemVersion ? ( diff --git a/frontend/src/pages/Login/index.js b/frontend/src/pages/Login/index.js index 23dc57a6..3a0e7864 100644 --- a/frontend/src/pages/Login/index.js +++ b/frontend/src/pages/Login/index.js @@ -1,34 +1,36 @@ -import React, { useState, useContext } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import { + Box, Button, + Container, CssBaseline, - TextField, Grid, - Box, - Typography, - Container, - InputAdornment, IconButton, - Link + InputAdornment, + Link, + TextField, + Typography } from '@material-ui/core'; -import { Visibility, VisibilityOff } from '@material-ui/icons'; import { makeStyles } from "@material-ui/core/styles"; +import { Visibility, VisibilityOff } from '@material-ui/icons'; +import defaultLogo from '../../assets/logo.jpg'; +import { AuthContext } from "../../context/Auth/AuthContext"; +import toastError from "../../errors/toastError"; +import api from "../../services/api"; import { i18n } from "../../translate/i18n"; -import { AuthContext } from "../../context/Auth/AuthContext"; -import { system } from "../../config.json"; -import logo from '../../assets/logo.png'; +const PUBLIC_ASSET_PATH = '/assets/'; -const Copyright = () => { +const Copyright = ({ companyName, companyUrl }) => { return ( © {new Date().getFullYear()} {" - "} - - {system.name} + + {companyName || "Press Ticket"} {"."} @@ -56,8 +58,76 @@ const Login = () => { const [user, setUser] = useState({ email: "", password: "" }); const [showPassword, setShowPassword] = useState(false); - const { handleLogin } = useContext(AuthContext); + const [theme, setTheme] = useState("light"); + const [companyData, setCompanyData] = useState({ + logo: defaultLogo, + name: "Press Ticket", + url: "https://github.com/rtenorioh/Press-Ticket" + }); + + useEffect(() => { + const fetchCompanyData = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + + if (lightConfig) { + setCompanyData(prevData => ({ + ...prevData, + name: lightConfig.company || "Press Ticket", + url: lightConfig.url || "https://github.com/rtenorioh/Press-Ticket" + })); + } + } + } catch (err) { + toastError(err); + } + }; + + const savedTheme = localStorage.getItem("theme") || "light"; + setTheme(savedTheme); + + fetchCompanyData(); + }, []); + + useEffect(() => { + const fetchLogo = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + const darkConfig = data.find(themeConfig => themeConfig.theme === "dark"); + + if (theme === "light" && lightConfig && lightConfig.logo) { + setCompanyData(prevData => ({ + ...prevData, + logo: PUBLIC_ASSET_PATH + lightConfig.logo + })); + } else if (theme === "dark" && darkConfig && darkConfig.logo) { + setCompanyData(prevData => ({ + ...prevData, + logo: PUBLIC_ASSET_PATH + darkConfig.logo + })); + } else { + setCompanyData(prevData => ({ + ...prevData, + logo: defaultLogo + })); + } + } + + } catch (err) { + toastError(err); + } + }; + + fetchLogo(); + }, [theme]); const handleChangeInput = (e) => { setUser({ ...user, [e.target.name]: e.target.value }); @@ -72,7 +142,7 @@ const Login = () => {
- logo + logo {i18n.t("login.title")} @@ -138,7 +208,7 @@ const Login = () => {
- +
); }; diff --git a/frontend/src/pages/Settings/ComponentSettings.js b/frontend/src/pages/Settings/ComponentSettings.js new file mode 100644 index 00000000..c4acc27d --- /dev/null +++ b/frontend/src/pages/Settings/ComponentSettings.js @@ -0,0 +1,101 @@ +import { Checkbox, FormControlLabel, Grid, Paper, Select, Tooltip, Typography } from "@material-ui/core"; +import { makeStyles } from "@material-ui/core/styles"; +import React from "react"; +import { i18n } from "../../translate/i18n.js"; + +const useStyles = makeStyles(theme => ({ + paper: { + padding: theme.spacing(0, 1), + display: "flex", + alignItems: "center", + marginBottom: theme.spacing(2), + }, + settingOption: { + marginLeft: "auto", + }, + gridContainer: { + padding: theme.spacing(2), + }, +})); + +const ComponentSettings = ({ settings, getSettingValue, handleChangeBooleanSetting, handleChangeSetting }) => { + const classes = useStyles(); + + const booleanSettings = [ + { key: "userCreation", label: i18n.t("settings.settings.userCreation.name"), note: i18n.t("settings.settings.userCreation.note") }, + { key: "allTicket", label: i18n.t("settings.settings.allTicket.name"), note: i18n.t("settings.settings.allTicket.note") }, + { key: "CheckMsgIsGroup", label: i18n.t("settings.settings.CheckMsgIsGroup.name"), note: i18n.t("settings.settings.CheckMsgIsGroup.note") }, + { key: "call", label: i18n.t("settings.settings.call.name"), note: i18n.t("settings.settings.call.note") }, + { key: "sideMenu", label: i18n.t("settings.settings.sideMenu.name"), note: i18n.t("settings.settings.sideMenu.note") }, + { key: "quickAnswer", label: i18n.t("settings.settings.quickAnswer.name"), note: i18n.t("settings.settings.quickAnswer.note") }, + { key: "closeTicketApi", label: i18n.t("settings.settings.closeTicketApi.name"), note: i18n.t("settings.settings.closeTicketApi.note") }, + { key: "ASC", label: i18n.t("settings.settings.ASC.name"), note: i18n.t("settings.settings.ASC.note") }, + { key: "created", label: i18n.t("settings.settings.created.name"), note: i18n.t("settings.settings.created.note") }, + ]; + + const settingsChunks = []; + for (let i = 0; i < booleanSettings.length; i += 8) { + settingsChunks.push(booleanSettings.slice(i, i + 8)); + } + + return ( + + {settingsChunks.map((chunk, index) => ( + + {chunk.map(setting => ( + + + 0 && getSettingValue(setting.key) === "enabled"} + onChange={handleChangeBooleanSetting} + name={setting.key} + color="primary" + /> + } + label={setting.label} + /> + + + ))} + + ))} + + + + + {i18n.t("settings.settings.timeCreateNewTicket.name")} + + + + + + + ); +}; + +export default ComponentSettings; diff --git a/frontend/src/pages/Settings/Personalize.js b/frontend/src/pages/Settings/Personalize.js new file mode 100644 index 00000000..c8715c4c --- /dev/null +++ b/frontend/src/pages/Settings/Personalize.js @@ -0,0 +1,539 @@ +import { AppBar, Box, Button, Card, CardMedia, Grid, IconButton, makeStyles, Tab, Tabs, TextField, Typography } from "@material-ui/core"; +import DeleteIcon from "@material-ui/icons/Delete"; +import PropTypes from "prop-types"; +import React, { useEffect, useState } from "react"; +import { toast } from "react-toastify"; +import api from "../../services/api"; +import openSocket from "../../services/socket-io"; + +const PUBLIC_ASSET_PATH = '/assets/'; + +const useStyles = makeStyles((theme) => ({ + root: { + backgroundColor: theme.palette.background.paper, + width: "100%", + }, + fullWidthContainer: { + width: "100%", + padding: theme.spacing(5), + boxSizing: "border-box", + }, + cardContainer: { + display: "flex", + alignItems: "center", + marginBottom: theme.spacing(1), + }, + card: { + width: theme.spacing(15), + height: theme.spacing(15), + marginRight: theme.spacing(1), + display: "flex", + flexDirection: "column", + alignItems: "center", + justifyContent: "center", + position: "relative", + border: "1px solid #ddd", + cursor: "pointer", + }, + cardMedia: { + width: "100%", + height: "100%", + objectFit: "contain", + }, + deleteIcon: { + position: "absolute", + top: 0, + right: 0, + color: theme.palette.error.main, + }, + textField: { + marginBottom: theme.spacing(2), + }, + title: { + marginBottom: theme.spacing(1), + }, + titleContainer: { + display: "flex", + justifyContent: "space-between", + alignItems: "center", + marginBottom: theme.spacing(2), + }, + button: { + marginLeft: theme.spacing(2), + }, +})); + +const TabPanel = (props) => { + const { children, value, index, ...other } = props; + + return ( + + ); +}; + +TabPanel.propTypes = { + children: PropTypes.node, + index: PropTypes.any.isRequired, + value: PropTypes.any.isRequired, +}; + +const PersonalizeSettings = ({ onThemeConfigUpdate }) => { + const classes = useStyles(); + const [tabValue, setTabValue] = useState(0); + const [data, setData] = useState({ company: "", url: "" }); + const [logos, setLogos] = useState({ + themeLight: { favico: null, logo: null, logoTicket: null }, + themeDark: { favico: null, logo: null, logoTicket: null } + }); + const [colors, setColors] = useState({ + themeLight: { primaryColor: "#ffffff", secondaryColor: "#ffffff", backgroundDefault: "#ffffff", backgroundPaper: "#ffffff" }, + themeDark: { primaryColor: "#000000", secondaryColor: "#000000", backgroundDefault: "#000000", backgroundPaper: "#000000" }, + }); + + useEffect(() => { + const fetchPersonalizations = async () => { + try { + const { data } = await api.get("/personalizations"); + if (data && data.length > 0) { + let lightTheme = {}; + let darkTheme = {}; + + data.forEach(personalization => { + if (personalization.theme === "light") { + lightTheme = personalization; + } else if (personalization.theme === "dark") { + darkTheme = personalization; + } + }); + + setData({ + company: lightTheme.company || "", + url: lightTheme.url || "" + }); + + setLogos({ + themeLight: { + favico: lightTheme.favico ? `${PUBLIC_ASSET_PATH}${lightTheme.favico}` : null, + logo: lightTheme.logo ? `${PUBLIC_ASSET_PATH}${lightTheme.logo}` : null, + logoTicket: lightTheme.logoTicket ? `${PUBLIC_ASSET_PATH}${lightTheme.logoTicket} ` : null, + }, + themeDark: { + favico: darkTheme.favico ? `${PUBLIC_ASSET_PATH}${darkTheme.favico}` : null, + logo: darkTheme.logo ? `${PUBLIC_ASSET_PATH}${darkTheme.logo}` : null, + logoTicket: darkTheme.logoTicket ? `${PUBLIC_ASSET_PATH}${darkTheme.logoTicket}` : null, + } + }); + + setColors({ + themeLight: { + primaryColor: lightTheme.primaryColor || "#ffffff", + secondaryColor: lightTheme.secondaryColor || "#ffffff", + backgroundDefault: lightTheme.backgroundDefault || "#ffffff", + backgroundPaper: lightTheme.backgroundPaper || "#ffffff", + }, + themeDark: { + primaryColor: darkTheme.primaryColor || "#000000", + secondaryColor: darkTheme.secondaryColor || "#000000", + backgroundDefault: darkTheme.backgroundDefault || "#000000", + backgroundPaper: darkTheme.backgroundPaper || "#000000", + }, + }); + } + } catch (err) { + toast.error("Erro ao buscar personalizações"); + } + }; + + fetchPersonalizations(); + + const socket = openSocket(); + socket.on("personalization", data => { + if (data.action === "update") { + fetchPersonalizations(); + } + }); + + return () => { + socket.disconnect(); + }; + }, []); + + + const handleTabChange = (event, newValue) => { + setTabValue(newValue); + }; + + const handleCardClick = (theme, key) => { + document.getElementById(`${theme}-${key}-input`).click(); + }; + + const handleCompanyChange = (event) => { + const value = event.target.value || ""; + setData((prevState) => ({ ...prevState, company: value })); + }; + + const handleUrlChange = (event) => { + const value = event.target.value || ""; + setData((prevState) => ({ ...prevState, url: value })); + }; + + const handleSaveCompanyData = async () => { + const payload = { + company: data.company, + url: data.url, + }; + try { + await api.put("/personalizations/light", payload); + toast.success("Dados da empresa salvos com sucesso!"); + } catch (err) { + toast.error("Erro ao salvar dados da empresa"); + } + }; + + const handleLogoChange = async (event, theme, type) => { + const file = event.target.files[0]; + if (file) { + const formData = new FormData(); + formData.append("theme", theme); + formData.append(type, file); + + try { + const response = await api.put(`/personalizations/${theme}`, formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + + const updatedLogoName = response.data[type]; + const updatedLogoUrl = `${PUBLIC_ASSET_PATH}${updatedLogoName}`; + + setLogos((prevState) => ({ + ...prevState, + [theme === "light" ? "themeLight" : "themeDark"]: { + ...prevState[theme === "light" ? "themeLight" : "themeDark"], + [type]: updatedLogoUrl, + }, + })); + + if (response.status === 200) { + toast.success(`Imagem ${type} atualizada com sucesso!`); + + onThemeConfigUpdate(theme, { + [type]: updatedLogoUrl + }); + } else { + throw new Error(`Erro ao salvar a imagem ${type} para o tema ${theme}`); + } + } catch (error) { + console.error("Erro ao enviar a imagem:", error); + toast.error("Erro ao atualizar a imagem"); + } + } + }; + + const handleDeleteImage = (theme, type) => { + setLogos((prevState) => ({ + ...prevState, + [theme === "light" ? "themeLight" : "themeDark"]: { + ...prevState[theme === "light" ? "themeLight" : "themeDark"], + [type]: null + } + })); + }; + + + const handleColorChange = (event, theme, field) => { + const newColor = event.target.value; + setColors((prevState) => ({ + ...prevState, + [theme]: { + ...prevState[theme], + [field]: newColor, + }, + })); + }; + + const handleSaveColors = async (theme) => { + const colorsToSave = colors[theme === "light" ? "themeLight" : "themeDark"]; + const payload = { + primaryColor: colorsToSave.primaryColor, + secondaryColor: colorsToSave.secondaryColor, + backgroundDefault: colorsToSave.backgroundDefault, + backgroundPaper: colorsToSave.backgroundPaper, + }; + + try { + const response = await api.put(`/personalizations/${theme}`, payload); + + if (response.status === 200) { + toast.success(`Cores do tema ${theme} salvas com sucesso!`); + onThemeConfigUpdate(theme, colorsToSave); + } else { + throw new Error(`Erro ao salvar cores do tema ${theme}`); + } + } catch (err) { + console.error("Erro ao salvar as cores:", err); + toast.error(`Erro ao salvar cores do tema ${theme}`); + } + }; + + return ( +
+ + + + + + + + +
+ + + +
+
+ +
+ + + Theme Light + + {Object.keys(logos.themeLight).map((key) => ( + + + !logos.themeLight[key] && handleCardClick("light", key)} + /> + + {key.charAt(0).toUpperCase() + key.slice(1)} + + {logos.themeLight[key] && ( + { + e.stopPropagation(); + handleDeleteImage("light", key); + }} + > + + + )} + + handleLogoChange(e, "light", key)} + /> + + ))} + + + + + Theme Dark + + {Object.keys(logos.themeDark).map((key) => ( + + + !logos.themeDark[key] && handleCardClick("dark", key)} + /> + + {key.charAt(0).toUpperCase() + key.slice(1)} + + {logos.themeDark[key] && ( + { + e.stopPropagation(); + handleDeleteImage("dark", key); + }} + > + + + )} + + handleLogoChange(e, "dark", key)} + /> + + ))} + + + + +
+
+ +
+
+ Theme Light + +
+ + + handleColorChange(e, "themeLight", "primaryColor")} + /> + + + handleColorChange(e, "themeLight", "secondaryColor")} + /> + + + handleColorChange(e, "themeLight", "backgroundDefault")} + /> + + + handleColorChange(e, "themeLight", "backgroundPaper")} + /> + + +
+ Theme Dark + +
+ + + handleColorChange(e, "themeDark", "primaryColor")} + /> + + + handleColorChange(e, "themeDark", "secondaryColor")} + /> + + + handleColorChange(e, "themeDark", "backgroundDefault")} + /> + + + handleColorChange(e, "themeDark", "backgroundPaper")} + /> + + +
+
+
+ ); +}; + +export default PersonalizeSettings; \ No newline at end of file diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 523435c1..9f7e77a3 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -2,131 +2,80 @@ import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import openSocket from "socket.io-client"; -import Container from "@material-ui/core/Container"; -import Paper from "@material-ui/core/Paper"; -import Select from "@material-ui/core/Select"; -import { makeStyles, withStyles } from "@material-ui/core/styles"; -import Typography from "@material-ui/core/Typography"; +import { Box, Container, makeStyles, Tab, Tabs } from "@material-ui/core"; import { toast } from "react-toastify"; -import Tooltip from "@material-ui/core/Tooltip"; - -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Switch from '@material-ui/core/Switch'; import toastError from "../../errors/toastError"; import api from "../../services/api"; import { i18n } from "../../translate/i18n.js"; +import ErrorBoundary from "../../components/ErrorBoundary"; +import ComponentSettings from "./ComponentSettings"; +import Personalize from "./Personalize.js"; + const useStyles = makeStyles(theme => ({ root: { - backgroundColor: theme.palette.background.default, - display: "flex", - alignItems: "center", - padding: theme.spacing(4), - }, - - paper: { - padding: theme.spacing(2), + flexGrow: 1, display: "flex", - alignItems: "center", + height: "92%", + backgroundColor: theme.palette.background.paper, + padding: theme.spacing(1) }, - - settingOption: { - marginLeft: "auto", + tabs: { + borderRight: `1px solid ${theme.palette.divider}`, + minWidth: 200, }, - margin: { - margin: theme.spacing(1), + container: { + padding: theme.spacing(0), }, - })); -const IOSSwitch = withStyles((theme) => ({ - root: { - width: 42, - height: 26, - padding: 0, - margin: theme.spacing(1), - }, - switchBase: { - padding: 1, - '&$checked': { - transform: 'translateX(16px)', - color: theme.palette.common.white, - '& + $track': { - backgroundColor: '#52d869', - opacity: 1, - border: 'none', - }, - }, - '&$focusVisible $thumb': { - color: '#52d869', - border: '6px solid #fff', - }, - }, - thumb: { - width: 24, - height: 24, - }, - track: { - borderRadius: 26 / 2, - border: `1px solid ${theme.palette.grey[400]}`, - backgroundColor: theme.palette.grey[50], - opacity: 1, - transition: theme.transitions.create(['background-color', 'border']), - }, - checked: {}, - focusVisible: {}, -})) - (({ classes, ...props }) => { - return ( - - ); - }); - -const Settings = () => { +const Settings = ({ onThemeConfigUpdate }) => { const classes = useStyles(); const history = useHistory(); - const [settings, setSettings] = useState([]); + const [tabValue, setTabValue] = useState(0); useEffect(() => { + let isMounted = true; + const fetchSession = async () => { try { const { data } = await api.get("/settings"); - setSettings(data); + if (isMounted) { + setSettings(data); + } } catch (err) { toastError(err); } }; + fetchSession(); + + return () => { + isMounted = false; + }; }, []); useEffect(() => { + let isMounted = true; const socket = openSocket(process.env.REACT_APP_BACKEND_URL); socket.on("settings", data => { - if (data.action === "update") { + if (isMounted && data.action === "update") { setSettings(prevState => { const aux = [...prevState]; const settingIndex = aux.findIndex(s => s.key === data.setting.key); - aux[settingIndex].value = data.setting.value; + if (settingIndex !== -1) { + aux[settingIndex].value = data.setting.value; + } return aux; }); } }); return () => { + isMounted = false; socket.disconnect(); }; }, []); @@ -139,12 +88,13 @@ const Settings = () => { await api.put(`/settings/${settingKey}`, { value: selectedValue, }); - toast.success(i18n.t("settings.success")); + toast.success("Configuração atualizada com sucesso"); history.go(0); } catch (err) { toastError(err); } }; + const handleChangeSetting = async e => { const selectedValue = e.target.value; const settingKey = e.target.name; @@ -160,223 +110,49 @@ const Settings = () => { }; const getSettingValue = key => { - const { value } = settings.find(s => s.key === key); - return value; + const setting = settings.find(s => s.key === key); + return setting ? setting.value : ""; + }; + + const handleTabChange = (event, newValue) => { + setTabValue(newValue); }; return (
- - - {i18n.t("settings.title")} - - - - - 0 && getSettingValue("userCreation") === "enabled"} - onChange={handleChangeBooleanSetting} name="userCreation" - /> - } - label={i18n.t("settings.settings.userCreation.name")} - /> - - - - - - - - 0 && getSettingValue("allTicket") === "enabled"} - onChange={handleChangeBooleanSetting} name="allTicket" - />} - label={i18n.t("settings.settings.allTicket.name")} - /> - - - - - - - - 0 && getSettingValue("CheckMsgIsGroup") === "enabled"} - onChange={handleChangeBooleanSetting} name="CheckMsgIsGroup" - /> - } label={i18n.t("settings.settings.CheckMsgIsGroup.name")} - /> - - - - - - - 0 && getSettingValue("call") === "enabled"} - onChange={handleChangeBooleanSetting} name="call" - />} - label={i18n.t("settings.settings.call.name")} - /> - - - - - - - 0 && getSettingValue("sideMenu") === "enabled"} - onChange={handleChangeBooleanSetting} name="sideMenu" - />} - label={i18n.t("settings.settings.sideMenu.name")} - /> - - - - - - - - - - 0 && getSettingValue("quickAnswer") === "enabled"} - onChange={handleChangeBooleanSetting} name="quickAnswer" - />} - label={i18n.t("settings.settings.quickAnswer.name")} - /> - - - - - - - 0 && getSettingValue("closeTicketApi") === "enabled"} - onChange={handleChangeBooleanSetting} name="closeTicketApi" - />} - label={i18n.t("settings.settings.closeTicketApi.name")} - /> - - - - - - - 0 && getSettingValue("darkMode") === "enabled"} - onChange={handleChangeBooleanSetting} name="darkMode" - />} - label={i18n.t("settings.settings.darkMode.name")} - /> - - - - - - - 0 && getSettingValue("ASC") === "enabled"} - onChange={handleChangeBooleanSetting} name="ASC" - />} - label={i18n.t("settings.settings.ASC.name")} - /> - - - - - - - 0 && getSettingValue("created") === "enabled"} - onChange={handleChangeBooleanSetting} name="created" - />} - label={i18n.t("settings.settings.created.name")} - /> - - - - - - - - {i18n.t("settings.settings.timeCreateNewTicket.name")} - - - - - + + + + + + {tabValue === 0 && ( + + + + + + )} + {tabValue === 1 && ( + + + + + + )} +
); }; diff --git a/frontend/src/pages/Signup/index.js b/frontend/src/pages/Signup/index.js index c501c063..5acee9e3 100644 --- a/frontend/src/pages/Signup/index.js +++ b/frontend/src/pages/Signup/index.js @@ -1,22 +1,21 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; -import * as Yup from "yup"; -import { useHistory } from "react-router-dom"; -import { Link as RouterLink } from "react-router-dom"; +import { Field, Form, Formik } from "formik"; +import { Link as RouterLink, useHistory } from "react-router-dom"; import { toast } from "react-toastify"; -import { Formik, Form, Field } from "formik"; +import * as Yup from "yup"; import { + Box, Button, + Container, CssBaseline, - TextField, Grid, - Box, - Typography, - Container, - InputAdornment, IconButton, - Link + InputAdornment, + Link, + TextField, + Typography } from '@material-ui/core'; import { Visibility, VisibilityOff } from '@material-ui/icons'; @@ -25,19 +24,19 @@ import { makeStyles } from "@material-ui/core/styles"; import { i18n } from "../../translate/i18n"; -import api from "../../services/api"; +import defaultLogo from '../../assets/logo.jpg'; import toastError from "../../errors/toastError"; +import api from "../../services/api"; -import { system } from "../../config.json"; -import logo from '../../assets/logo.png'; +const PUBLIC_ASSET_PATH = '/assets/'; -const Copyright = () => { +const Copyright = ({ companyName, companyUrl }) => { return ( © {new Date().getFullYear()} {" - "} - - {system.name || "Press Ticket"} + + {companyName || "Press Ticket"} {"."} @@ -72,10 +71,78 @@ const UserSchema = Yup.object().shape({ const SignUp = () => { const classes = useStyles(); const history = useHistory(); - const initialState = { name: "", email: "", password: "" }; const [showPassword, setShowPassword] = useState(false); const [user] = useState(initialState); + const [theme, setTheme] = useState("light"); + const [companyData, setCompanyData] = useState({ + logo: defaultLogo, + name: "Press Ticket", + url: "https://github.com/rtenorioh/Press-Ticket" + }); + + useEffect(() => { + const fetchCompanyData = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + + if (lightConfig) { + setCompanyData(prevData => ({ + ...prevData, + name: lightConfig.company || "Press Ticket", + url: lightConfig.url || "https://github.com/rtenorioh/Press-Ticket" + })); + } + } + } catch (err) { + toastError(err); + } + }; + + const savedTheme = localStorage.getItem("theme") || "light"; + setTheme(savedTheme); + + fetchCompanyData(); + }, []); + + useEffect(() => { + const fetchLogo = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + const darkConfig = data.find(themeConfig => themeConfig.theme === "dark"); + + if (theme === "light" && lightConfig && lightConfig.logo) { + setCompanyData(prevData => ({ + ...prevData, + logo: PUBLIC_ASSET_PATH + lightConfig.logo + })); + } else if (theme === "dark" && darkConfig && darkConfig.logo) { + setCompanyData(prevData => ({ + ...prevData, + logo: PUBLIC_ASSET_PATH + darkConfig.logo + })); + } else { + setCompanyData(prevData => ({ + ...prevData, + logo: defaultLogo + })); + } + } + + } catch (err) { + toastError(err); + } + }; + + fetchLogo(); + }, [theme]); const handleSignUp = async values => { try { @@ -91,11 +158,10 @@ const SignUp = () => {
- logo + logo {i18n.t("signup.title")} - {/*
*/} { > {i18n.t("signup.buttons.submit")} - + { )}
- +
); }; diff --git a/frontend/src/pages/Tickets/index.js b/frontend/src/pages/Tickets/index.js index e66e1491..14aaa097 100644 --- a/frontend/src/pages/Tickets/index.js +++ b/frontend/src/pages/Tickets/index.js @@ -1,16 +1,20 @@ -import React from "react"; -import { useParams } from "react-router-dom"; import Grid from "@material-ui/core/Grid"; import Paper from "@material-ui/core/Paper"; import { makeStyles } from "@material-ui/core/styles"; +import React, { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; -import TicketsManager from "../../components/TicketsManager/"; import Ticket from "../../components/Ticket/"; +import TicketsManager from "../../components/TicketsManager/"; -import { i18n } from "../../translate/i18n"; import Hidden from "@material-ui/core/Hidden"; +import { i18n } from "../../translate/i18n"; + +import defaultLogoTicket from '../../assets/logoTicket.jpg'; +import toastError from "../../errors/toastError"; +import api from "../../services/api"; -import logo from "../../assets/Logo_circle.png"; +const PUBLIC_ASSET_PATH = '/assets/'; const useStyles = makeStyles((theme) => ({ chatContainer: { @@ -67,12 +71,37 @@ const useStyles = makeStyles((theme) => ({ const Chat = () => { const classes = useStyles(); const { ticketId } = useParams(); + const [logo, setLogo] = useState(defaultLogoTicket); + const themeStorage = localStorage.getItem("theme"); + + useEffect(() => { + const fetchLogo = async () => { + try { + const { data } = await api.get("/personalizations"); + if (data && data.length > 0) { + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + const darkConfig = data.find(themeConfig => themeConfig.theme === "dark"); + + if (themeStorage === "light" && lightConfig?.logoTicket) { + setLogo(PUBLIC_ASSET_PATH + lightConfig.logoTicket); + } else if (themeStorage === "dark" && darkConfig?.logoTicket) { + setLogo(PUBLIC_ASSET_PATH + darkConfig.logoTicket); + } else { + setLogo(defaultLogoTicket); + } + } + } catch (err) { + toastError(err); + } + }; + + fetchLogo(); + }, [themeStorage]); return (
- {/* */} { - {/* */} {ticketId ? ( <> @@ -92,13 +120,12 @@ const Chat = () => { ) : ( - {/* */}
- +
{i18n.t("chat.noTicketMessage")} -
+
)} diff --git a/frontend/src/routes/index.js b/frontend/src/routes/index.js index ab7ba1b6..96154e45 100644 --- a/frontend/src/routes/index.js +++ b/frontend/src/routes/index.js @@ -23,7 +23,7 @@ import { AuthProvider } from "../context/Auth/AuthContext"; import { WhatsAppsProvider } from "../context/WhatsApp/WhatsAppsContext"; import Route from "./Route"; -const Routes = () => { +const Routes = ({ toggleTheme, onThemeConfigUpdate }) => { return ( @@ -31,14 +31,14 @@ const Routes = () => { - + - + } isPrivate /> diff --git a/frontend/src/themes/darkTheme.js b/frontend/src/themes/darkTheme.js new file mode 100644 index 00000000..ccc48024 --- /dev/null +++ b/frontend/src/themes/darkTheme.js @@ -0,0 +1,48 @@ +import { createTheme } from '@material-ui/core/styles'; +import backgroundImageDark from "../assets/backgroundDark.jpg"; + +const getDarkTheme = (config, locale) => + createTheme( + { + overrides: { + MuiCssBaseline: { + "@global": { + body: { + backgroundColor: config?.backgroundDefault || "#2E2E3A", + }, + }, + }, + }, + scrollbarStyles: { + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-thumb": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", + backgroundColor: config?.scrollbarThumb || "#8A7DCC", + }, + }, + palette: { + primary: { main: config?.primaryColor || "#8A7DCC" }, + secondary: { main: config?.secondaryColor || "#CCCCCC" }, + toolbar: { main: config?.toolbarColor || "#8A7DCC" }, + menuItens: { main: config?.menuItens || "#181D22" }, + sub: { main: config?.sub || "#383850" }, + toolbarIcon: { main: config?.toolbarIconColor || "#FFFFFF" }, + divide: { main: config?.divide || "#383850" }, + background: { + default: config?.backgroundDefault || "#2E2E3A", + paper: config?.backgroundPaper || "#383850", + }, + text: { + primary: config?.textPrimary || "#FFFFFF", + secondary: config?.textSecondary || "#CCCCCC", + }, + }, + backgroundImage: `url(${config?.backgroundImage || backgroundImageDark})`, + }, + locale + ); + +export default getDarkTheme; diff --git a/frontend/src/themes/lightTheme.js b/frontend/src/themes/lightTheme.js new file mode 100644 index 00000000..72b0351b --- /dev/null +++ b/frontend/src/themes/lightTheme.js @@ -0,0 +1,39 @@ +import { createTheme } from '@material-ui/core/styles'; +import backgroundImageLight from "../assets/backgroundLight.png"; + +const getLightTheme = (config, locale) => + createTheme( + { + scrollbarStyles: { + "&::-webkit-scrollbar": { + width: "8px", + height: "8px", + }, + "&::-webkit-scrollbar-thumb": { + boxShadow: "inset 0 0 6px rgba(0, 0, 0, 0.3)", + backgroundColor: config?.scrollbarThumb || "#E5E5E5", + }, + }, + palette: { + primary: { main: config?.primaryColor || "#5C4B9B" }, + secondary: { main: config?.secondaryColor || "#D5C6F0" }, + toolbar: { main: config?.toolbarColor || "#5C4B9B" }, + menuItens: { main: config?.menuItens || "#FFFFFF" }, + sub: { main: config?.sub || "#F7F7F7" }, + toolbarIcon: { main: config?.toolbarIconColor || "#FFFFFF" }, + divide: { main: config?.divide || "#E0E0E0" }, + background: { + default: config?.backgroundDefault || "#FFFFFF", + paper: config?.backgroundPaper || "#F7F7F7", + }, + text: { + primary: config?.textPrimary || "#000000", + secondary: config?.textSecondary || "#333333", + }, + }, + backgroundImage: `url(${config?.backgroundImage || backgroundImageLight})`, + }, + locale + ); + +export default getLightTheme; diff --git a/frontend/src/themes/themeConfig.js b/frontend/src/themes/themeConfig.js new file mode 100644 index 00000000..d38ea6ad --- /dev/null +++ b/frontend/src/themes/themeConfig.js @@ -0,0 +1,8 @@ +import getDarkTheme from './darkTheme'; +import getLightTheme from './lightTheme'; + +const loadThemeConfig = (theme, config, locale) => { + return theme === 'light' ? getLightTheme(config, locale) : getDarkTheme(config, locale); +}; + +export default loadThemeConfig; From ee573f0371eb560b94737c8440e0afa7eb9e5fb2 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Tue, 19 Nov 2024 15:14:47 -0300 Subject: [PATCH 05/24] Add MasterAdmin --- .../src/controllers/IntegrationController.ts | 6 +-- backend/src/controllers/SettingController.ts | 6 +-- backend/src/controllers/UserController.ts | 8 ++-- .../20241118200400-create-masteradmin-user.ts | 26 ++++++++++++ .../services/UserServices/ListUsersService.ts | 21 ++++++---- docs/INSTALL_VPS.md | 24 ++++++++--- docs/INSTALL_localhost.md | 1 - frontend/public/index.html | 8 ++-- frontend/src/App.js | 36 ++++++++++++++++ frontend/src/pages/Settings/index.js | 42 +++++++++---------- frontend/src/rules.js | 13 ++++++ 11 files changed, 142 insertions(+), 49 deletions(-) create mode 100644 backend/src/database/seeds/20241118200400-create-masteradmin-user.ts 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/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/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/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/docs/INSTALL_VPS.md b/docs/INSTALL_VPS.md index ec3b14f7..d3571e67 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 @@ -464,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 @@ -522,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..eb7ea6fa 100644 --- a/docs/INSTALL_localhost.md +++ b/docs/INSTALL_localhost.md @@ -155,7 +155,6 @@ Crie ou edite o arquivo `.env` no diretório `frontend` com as seguintes informa ```bash REACT_APP_BACKEND_URL=http://localhost:8080 REACT_APP_HOURS_CLOSE_TICKETS_AUTO= -REACT_APP_PAGE_TITLE=PressTicket PORT=3333 ``` diff --git a/frontend/public/index.html b/frontend/public/index.html index ce920d7b..1d6009c8 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -1,7 +1,7 @@ - %REACT_APP_PAGE_TITLE% + Carregando... - +
diff --git a/frontend/src/App.js b/frontend/src/App.js index cf4703cf..4314e21b 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -6,6 +6,7 @@ import "react-toastify/dist/ReactToastify.css"; import toastError from "./errors/toastError"; import Routes from "./routes"; import api from "./services/api"; +import openSocket from "./services/socket-io"; import loadThemeConfig from './themes/themeConfig'; const App = () => { @@ -88,6 +89,41 @@ const App = () => { document.head.appendChild(favicon); }, [theme]); + useEffect(() => { + const fetchPageTitle = async () => { + try { + const { data } = await api.get("/personalizations"); + + if (data && data.length > 0) { + + const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); + + if (lightConfig) { + document.title = lightConfig.company; + } else { + document.title = "Press Ticket"; + } + } + } catch (err) { + toastError(err); + document.title = "Erro ao carregar título"; + } + }; + + const socket = openSocket(); + socket.on("personalization", data => { + if (data.action === "update") { + fetchPageTitle(); + } + }); + + fetchPageTitle(); + + return () => { + socket.disconnect(); + }; + }, []); + const selectedTheme = loadThemeConfig(theme, theme === "light" ? lightThemeConfig : darkThemeConfig, locale); return ( diff --git a/frontend/src/pages/Settings/index.js b/frontend/src/pages/Settings/index.js index 9f7e77a3..2f7d3b43 100644 --- a/frontend/src/pages/Settings/index.js +++ b/frontend/src/pages/Settings/index.js @@ -1,15 +1,13 @@ -import React, { useEffect, useState } from "react"; -import { useHistory } from "react-router-dom"; -import openSocket from "socket.io-client"; - import { Box, Container, makeStyles, Tab, Tabs } from "@material-ui/core"; +import React, { useContext, useEffect, useState } from "react"; +import { useHistory } from "react-router-dom"; import { toast } from "react-toastify"; - +import ErrorBoundary from "../../components/ErrorBoundary"; +import { AuthContext } from "../../context/Auth/AuthContext"; import toastError from "../../errors/toastError"; import api from "../../services/api"; +import openSocket from "../../services/socket-io.js"; import { i18n } from "../../translate/i18n.js"; - -import ErrorBoundary from "../../components/ErrorBoundary"; import ComponentSettings from "./ComponentSettings"; import Personalize from "./Personalize.js"; @@ -19,7 +17,7 @@ const useStyles = makeStyles(theme => ({ display: "flex", height: "92%", backgroundColor: theme.palette.background.paper, - padding: theme.spacing(1) + padding: theme.spacing(1), }, tabs: { borderRight: `1px solid ${theme.palette.divider}`, @@ -35,6 +33,8 @@ const Settings = ({ onThemeConfigUpdate }) => { const history = useHistory(); const [settings, setSettings] = useState([]); const [tabValue, setTabValue] = useState(0); + const { user } = useContext(AuthContext); + const isMasterAdminEnabled = process.env.REACT_APP_MASTERADMIN === "ON"; useEffect(() => { let isMounted = true; @@ -59,13 +59,13 @@ const Settings = ({ onThemeConfigUpdate }) => { useEffect(() => { let isMounted = true; - const socket = openSocket(process.env.REACT_APP_BACKEND_URL); + const socket = openSocket(); - socket.on("settings", data => { + socket.on("settings", (data) => { if (isMounted && data.action === "update") { - setSettings(prevState => { + setSettings((prevState) => { const aux = [...prevState]; - const settingIndex = aux.findIndex(s => s.key === data.setting.key); + const settingIndex = aux.findIndex((s) => s.key === data.setting.key); if (settingIndex !== -1) { aux[settingIndex].value = data.setting.value; } @@ -80,7 +80,7 @@ const Settings = ({ onThemeConfigUpdate }) => { }; }, []); - const handleChangeBooleanSetting = async e => { + const handleChangeBooleanSetting = async (e) => { const selectedValue = e.target.checked ? "enabled" : "disabled"; const settingKey = e.target.name; @@ -95,7 +95,7 @@ const Settings = ({ onThemeConfigUpdate }) => { } }; - const handleChangeSetting = async e => { + const handleChangeSetting = async (e) => { const selectedValue = e.target.value; const settingKey = e.target.name; @@ -109,8 +109,8 @@ const Settings = ({ onThemeConfigUpdate }) => { } }; - const getSettingValue = key => { - const setting = settings.find(s => s.key === key); + const getSettingValue = (key) => { + const setting = settings.find((s) => s.key === key); return setting ? setting.value : ""; }; @@ -128,7 +128,9 @@ const Settings = ({ onThemeConfigUpdate }) => { className={classes.tabs} > - + {(!isMasterAdminEnabled || user.profile === "masteradmin") && ( + + )} {tabValue === 0 && ( @@ -143,12 +145,10 @@ const Settings = ({ onThemeConfigUpdate }) => { )} - {tabValue === 1 && ( + {tabValue === 1 && (!isMasterAdminEnabled || user.profile === "masteradmin") && ( - + )} diff --git a/frontend/src/rules.js b/frontend/src/rules.js index 4ef9b4ea..b0aa2a9f 100644 --- a/frontend/src/rules.js +++ b/frontend/src/rules.js @@ -14,6 +14,19 @@ const rules = { "contacts-page:deleteContact", ], }, + masteradmin: { + static: [ + "drawer-admin-items:view", + "tickets-manager:showall", + "user-modal:editProfile", + "user-modal:editQueues", + "user-table:editTricked", + "ticket-options:deleteTicket", + "ticket-options:transferWhatsapp", + "contacts-page:deleteContact", + "settings:personalize", + ], + } }; export default rules; \ No newline at end of file From dcb3faedb19460449a8b405262d81c7892e65768 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Tue, 19 Nov 2024 21:40:43 -0300 Subject: [PATCH 06/24] =?UTF-8?q?Corre=C3=A7=C3=A3o=20de=20componente=20n?= =?UTF-8?q?=C3=A3o=20desmontado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/TicketListItem/index.js | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index f6ace5bd..1673f679 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -205,10 +205,8 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { useEffect(() => { isMounted.current = true; - const delayDebounceFn = setTimeout(() => { + setTimeout(() => { const fetchTicket = async () => { - if (!isMounted.current) return; - try { const { data } = await api.get("/tickets/" + ticket.id); if (isMounted.current) { @@ -224,7 +222,6 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { }, 500); return () => { - clearTimeout(delayDebounceFn); isMounted.current = false; }; }, [ticket.id, user, history]); @@ -253,16 +250,19 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { status: "open", userId: user?.id, }); + history.push(`/tickets/${id}`); } catch (err) { - setLoading(false); - toastError(err); - } - if (isMounted.current) { - setLoading(false); + if (isMounted.current) { + toastError(err); + } + } finally { + if (isMounted.current) { + setLoading(false); + } } - history.push(`/tickets/${id}`); }; + const queueName = selectedTicket => { let name = null; let color = null; @@ -340,12 +340,16 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { } else { const fetchUserName = async () => { + if (!isMounted.current) return; try { - const { data } = await api.get("/users/" + ticket.userId, { - }); - setUserName(data['name']); + const { data } = await api.get("/users/" + ticket.userId, {}); + if (isMounted.current) { + setUserName(data['name']); + } } catch (err) { - toastError(err); + if (isMounted.current) { + toastError(err); + } } }; fetchUserName(); @@ -441,6 +445,7 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { {ticket.status === "closed" && ( From fcb6f24b08fd11eafcddf737e398a2fb311a7db1 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Tue, 19 Nov 2024 23:01:25 -0300 Subject: [PATCH 07/24] =?UTF-8?q?Novos=20comandos=20em=20conex=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/WhatsAppController.ts | 75 ++++++++++++++++++- backend/src/libs/wbot.ts | 28 +++++++ backend/src/routes/whatsappRoutes.ts | 15 +++- .../WhatsappService/RestartWhatsAppService.ts | 23 ++++++ frontend/src/pages/Connections/index.js | 57 ++++++++++++++ 5 files changed, 195 insertions(+), 3 deletions(-) create mode 100644 backend/src/services/WhatsappService/RestartWhatsAppService.ts diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index 26727add..bba5673c 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -1,12 +1,14 @@ 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 { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession"; +import Whatsapp from "../models/Whatsapp"; 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 +126,74 @@ 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; + + try { + await shutdownWbot(whatsappId); + const io = getIO(); + io.emit("whatsapp", { + action: "update", + whatsappId + }); + return res + .status(200) + .json({ message: "WhatsApp session shutdown successfully." }); + } catch (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/libs/wbot.ts b/backend/src/libs/wbot.ts index 0663364e..37902070 100644 --- a/backend/src/libs/wbot.ts +++ b/backend/src/libs/wbot.ts @@ -285,3 +285,31 @@ 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); + + const sessionIndex = sessions.findIndex(s => s.id === whatsappIDNumber); + if (sessionIndex !== -1) { + await sessions[sessionIndex].destroy(); + sessions.splice(sessionIndex, 1); + } else { + throw new AppError("WhatsApp session not initialized."); + } +}; 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/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/frontend/src/pages/Connections/index.js b/frontend/src/pages/Connections/index.js index 95937090..b44f21b7 100644 --- a/frontend/src/pages/Connections/index.js +++ b/frontend/src/pages/Connections/index.js @@ -221,9 +221,56 @@ const Connections = () => { setConfirmModalInfo(confirmationModalInitialState); }; + const handleRestartSession = async (whatsAppId) => { + try { + await api.post(`/whatsapp/${whatsAppId}/restart`); + toast.success(i18n.t("connections.toasts.sessionRestarted")); + } catch (err) { + toastError(err); + } + }; + + const handleStartSession = async (whatsAppId) => { + try { + await api.post(`/whatsapp/${whatsAppId}/start`); + toast.success(i18n.t("connections.toasts.sessionStarted")); + } catch (err) { + toastError(err); + } + }; + + const handleShutdownSession = async (whatsAppId) => { + try { + await api.post(`/whatsapp/${whatsAppId}/shutdown`); + toast.success(i18n.t("connections.toasts.sessionShutdown")); + } catch (err) { + toastError(err); + } + }; + const renderActionButtons = whatsApp => { return ( <> + {whatsApp.status === "DISCONNECTED" && ( + + )} + {whatsApp.status === "qrcode" && ( + + )} {whatsApp.status === "qrcode" && ( )} + {whatsApp.status && ( + + )} ); }; From 59461e0b2e55802c800744e4abc1a99d283c37d6 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Fri, 29 Nov 2024 20:24:19 -0300 Subject: [PATCH 08/24] Ajuste no comando de criar BD --- backend/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/package.json b/backend/package.json index 53fb5c01..0479bd09 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "main": "index.js", "scripts": { "build": "tsc", - "create": "npx sequelize db:create", + "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", @@ -105,4 +105,4 @@ "engines": { "node": ">=18.0.0" } -} +} \ No newline at end of file From 433d778a12dbb5bba86c96e5e6bcc14143b565f0 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Fri, 29 Nov 2024 20:56:48 -0300 Subject: [PATCH 09/24] =?UTF-8?q?Ajuste=20nas=20fun=C3=A7=C3=B5es=20de=20c?= =?UTF-8?q?onex=C3=B5es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/src/controllers/WhatsAppController.ts | 23 ++++++-- .../controllers/WhatsAppSessionController.ts | 27 ++++++--- backend/src/libs/wbot.ts | 59 +++++++++++++------ frontend/src/layout/index.js | 2 +- frontend/src/translate/languages/pt.js | 3 +- 5 files changed, 82 insertions(+), 32 deletions(-) diff --git a/backend/src/controllers/WhatsAppController.ts b/backend/src/controllers/WhatsAppController.ts index bba5673c..e3f67838 100644 --- a/backend/src/controllers/WhatsAppController.ts +++ b/backend/src/controllers/WhatsAppController.ts @@ -2,9 +2,8 @@ import { Request, Response } from "express"; import AppError from "../errors/AppError"; import { getIO } from "../libs/socket"; import { initWbot, removeWbot, shutdownWbot } from "../libs/wbot"; -import { StartWhatsAppSession } from "../services/WbotServices/StartWhatsAppSession"; - 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"; @@ -157,17 +156,31 @@ export const shutdown = async ( ): 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 }); - return res - .status(200) - .json({ message: "WhatsApp session shutdown successfully." }); + 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 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/libs/wbot.ts b/backend/src/libs/wbot.ts index 37902070..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); @@ -305,11 +302,39 @@ export const restartWbot = async (whatsappId: number): Promise => { 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) { + 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); - } else { - throw new AppError("WhatsApp session not initialized."); + 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/frontend/src/layout/index.js b/frontend/src/layout/index.js index c8a177a1..03bba997 100644 --- a/frontend/src/layout/index.js +++ b/frontend/src/layout/index.js @@ -351,7 +351,7 @@ const LoggedInLayout = ({ children, toggleTheme, onThemeConfigUpdate }) => { noWrap className={classes.title} > - {i18n.t("mainDrawer.appBar.message.hi")} {user.name}, {i18n.t("mainDrawer.appBar.message.text")} {companyData.name || "Press Ticket"} + {i18n.t("mainDrawer.appBar.message.hi")}, {user.name}! {i18n.t("mainDrawer.appBar.message.text")} {companyData.name || "Press Ticket"}. diff --git a/frontend/src/translate/languages/pt.js b/frontend/src/translate/languages/pt.js index 183feb37..a8657745 100644 --- a/frontend/src/translate/languages/pt.js +++ b/frontend/src/translate/languages/pt.js @@ -125,6 +125,7 @@ const messages = { }, buttons: { add: "Adicionar WhatsApp", + shutdown: "Excluir", restart: "Restart", disconnect: "desconectar", tryAgain: "Tentar novamente", @@ -457,7 +458,7 @@ const messages = { appBar: { message: { hi: "Olá", - text: "seja bem vindo ao Sistema" + text: "Seja bem-vindo ao Sistema" }, user: { profile: "Perfil", From c1d0fb5c9390a47adbf724525f8af804352beed6 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Fri, 29 Nov 2024 21:01:49 -0300 Subject: [PATCH 10/24] =?UTF-8?q?Corre=C3=A7=C3=A3o=20do=20isGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 0479bd09..18ab2192 100644 --- a/backend/package.json +++ b/backend/package.json @@ -58,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": { From 1b2d352b0739a77e28fbafc7952eb33746b47d07 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Fri, 29 Nov 2024 21:08:03 -0300 Subject: [PATCH 11/24] Melhoria no carregamento dos Tickets --- .../TicketServices/ListTicketsService.ts | 11 +++- .../src/components/TicketListItem/index.js | 62 +++++-------------- 2 files changed, 24 insertions(+), 49 deletions(-) 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/frontend/src/components/TicketListItem/index.js b/frontend/src/components/TicketListItem/index.js index 1673f679..ecc09ca1 100644 --- a/frontend/src/components/TicketListItem/index.js +++ b/frontend/src/components/TicketListItem/index.js @@ -1,16 +1,3 @@ -import React, { useContext, useEffect, useRef, useState } from "react"; - -import { - useHistory, - useParams -} from "react-router-dom"; - -import { - format, - isSameDay, - parseISO -} from "date-fns"; - import { Avatar, Badge, @@ -24,7 +11,7 @@ import { Tooltip, Typography } from "@material-ui/core"; - +import { green } from "@material-ui/core/colors"; import { ClearOutlined, Done, @@ -37,20 +24,26 @@ import { Visibility, WhatsApp } from "@material-ui/icons"; - -import { green } from "@material-ui/core/colors"; - -import AcceptTicketWithouSelectQueue from "../AcceptTicketWithoutQueueModal"; -import ContactTag from "../ContactTag"; -import MarkdownWrapper from "../MarkdownWrapper"; - import clsx from "clsx"; +import { + format, + isSameDay, + parseISO +} from "date-fns"; +import React, { useContext, useEffect, useRef, useState } from "react"; +import { + useHistory, + useParams +} from "react-router-dom"; import receiveIcon from "../../assets/receive.png"; import sendIcon from "../../assets/send.png"; import { AuthContext } from "../../context/Auth/AuthContext"; import toastError from "../../errors/toastError"; import api from "../../services/api"; import { i18n } from "../../translate/i18n"; +import AcceptTicketWithouSelectQueue from "../AcceptTicketWithoutQueueModal"; +import ContactTag from "../ContactTag"; +import MarkdownWrapper from "../MarkdownWrapper"; const useStyles = makeStyles(theme => ({ ticket: { @@ -176,7 +169,6 @@ const useStyles = makeStyles(theme => ({ secondaryContentSecond: { display: 'flex', marginTop: 2, - //marginLeft: "5px", alignItems: "flex-start", flexWrap: "wrap", flexDirection: "row", @@ -200,7 +192,6 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { const { user } = useContext(AuthContext); const [acceptTicketWithouSelectQueueOpen, setAcceptTicketWithouSelectQueueOpen] = useState(false); const [tag, setTag] = useState([]); - const [uName, setUserName] = useState(null); useEffect(() => { isMounted.current = true; @@ -262,7 +253,6 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { } }; - const queueName = selectedTicket => { let name = null; let color = null; @@ -336,25 +326,6 @@ const TicketListItem = ({ ticket, userId, filteredTags }) => { history.push(`/tickets/${id}`); }; - if (ticket.status === "pending") { - - } else { - const fetchUserName = async () => { - if (!isMounted.current) return; - try { - const { data } = await api.get("/users/" + ticket.userId, {}); - if (isMounted.current) { - setUserName(data['name']); - } - } catch (err) { - if (isMounted.current) { - toastError(err); - } - } - }; - fetchUserName(); - } - return ( { /> )} - - {uName && ( + {ticket.status !== "pending" && ticket?.user?.name && ( { marginRight: "5px", marginBottom: "3px", }} - label={uName.toUpperCase()} + label={ticket?.user?.name.toUpperCase()} /> )} From 3e071d75fa0478d44c80bc12dff866c83268b882 Mon Sep 17 00:00:00 2001 From: Robson Tenorio Henriques Date: Sun, 1 Dec 2024 23:18:37 -0300 Subject: [PATCH 12/24] Diversos Ajustes --- backend/api.rest | 13 +- backend/src/config/database.ts | 8 +- backend/src/models/OldMessage.ts | 2 +- frontend/package.json | 5 +- frontend/server.js | 33 +- frontend/src/App.js | 90 ++--- .../AcceptTicketWithoutQueueModal/index.js | 115 +++--- frontend/src/components/Audio/index.jsx | 158 ++++---- .../src/components/BackdropLoading/index.js | 36 +- .../src/components/ButtonWithSpinner/index.js | 42 +- frontend/src/components/Can/index.js | 15 +- .../components/CodeSnippetGenerator/index.js | 51 ++- frontend/src/components/ColorPicker/index.js | 199 +++------- .../src/components/ConfirmationModal/index.js | 43 +- .../src/components/ContactChannels/index.js | 125 +++--- frontend/src/components/ContactModal/index.js | 366 ++++++++---------- frontend/src/components/ContactTag/index.js | 30 +- .../src/components/CopyToClipboard/index.js | 37 +- .../src/components/LocationPreview/index.js | 125 ++++-- frontend/src/components/MainHeader/index.js | 11 +- .../MainHeaderButtonsWrapper/index.js | 13 +- .../components/MessageHistoryModal/index.js | 34 +- frontend/src/components/MessageInput/index.js | 19 +- .../MessageVariablesPicker/index.js | 57 +-- .../components/NotificationsPopOver/index.js | 179 ++++----- frontend/src/components/QueueModal/index.js | 363 ++++++++--------- frontend/src/components/QueueSelect/index.js | 18 +- .../src/components/QuickAnswersModal/index.js | 89 +++-- frontend/src/components/TabPanel/index.js | 2 +- .../src/components/ThemeSelector/index.js | 26 +- frontend/src/components/Ticket/index.js | 12 +- .../src/components/TicketListItem/index.js | 18 +- frontend/src/config.js | 27 +- frontend/src/config/messageVariables.js | 38 ++ frontend/src/errors/toastError.js | 36 +- .../src/hooks/useMessageVariables/index.js | 10 + frontend/src/layout/MainListItems.js | 84 ++-- frontend/src/layout/index.js | 14 +- frontend/src/pages/ApiDocs/index.js | 25 +- frontend/src/pages/ApiKey/index.js | 12 +- frontend/src/pages/Connections/index.js | 156 ++++---- frontend/src/pages/Contacts/index.js | 21 +- frontend/src/pages/Integrations/index.js | 131 ++++--- frontend/src/pages/Queues/index.js | 48 ++- frontend/src/pages/Tags/index.js | 24 +- frontend/src/themes/darkTheme.js | 2 +- frontend/src/translate/languages/pt.js | 5 +- 47 files changed, 1547 insertions(+), 1420 deletions(-) create mode 100644 frontend/src/config/messageVariables.js create mode 100644 frontend/src/hooks/useMessageVariables/index.js diff --git a/backend/api.rest b/backend/api.rest index 049e2c2d..e9cd7dc7 100644 --- a/backend/api.rest +++ b/backend/api.rest @@ -3,7 +3,9 @@ #Variaveis @baseUrl = http://localhost:8080 @token = 6175c0d0-acd5-4776-95a9-592c795da986 -@token2 = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlByZXNzLVRpY2tldCIsInByb2ZpbGUiOiJhZG1pbiIsImlkIjoxLCJpYXQiOjE3MzEzNzIwNDksImV4cCI6MTczMTM3NTY0OX0.QwidYXoxONiHC7fsTF87IxLJ-nJ-aNrMFCK4habN2yQ +@token2 = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IlByZXNzIFRpY2tldCIsInByb2ZpbGUiOiJhZG1pbiIsImlkIjoxLCJpYXQiOjE3MzI4ODUzMzQsImV4cCI6MTczMjg4ODkzNH0.4MSoKumH6IT2loUzdlm9OwKzy8BOohcAkMBv6j6Ho4o + +@refreshToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidG9rZW5WZXJzaW9uIjowLCJpYXQiOjE3MzI4ODUzMzQsImV4cCI6MTczMzQ5MDEzNH0.RwuRIufYUB9dQZQg0aeaVtnOryn-sESNL1mFlShmBM4 ### (Login) Teste da Rota POST /auth/login POST {{baseUrl}}/auth/login @@ -14,13 +16,18 @@ Content-Type: application/json "password": "admin" } +### 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", @@ -32,7 +39,7 @@ GET {{baseUrl}}/personalizations Content-Type: application/json ### (Criar ou Atualizar Personalização) Teste da Rota PUT /personalizations/:theme -PUT {{baseUrl}}/personalizations/light +PUT {{baseUrl}}/personalizations/dark Authorization: Bearer {{token2}} Content-Type: application/json 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/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/frontend/package.json b/frontend/package.json index 0bae6f60..566580cb 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-beta", "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", 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 4314e21b..31331fde 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -1,17 +1,17 @@ import { CssBaseline } from "@material-ui/core"; import { ptBR } from "@material-ui/core/locale"; import { ThemeProvider } from "@material-ui/core/styles"; -import React, { useEffect, useState } from 'react'; +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 openSocket from "./services/socket-io"; -import loadThemeConfig from './themes/themeConfig'; +import loadThemeConfig from "./themes/themeConfig"; const App = () => { const [locale, setLocale] = useState(ptBR); - const [theme, setTheme] = useState("light"); + const [theme, setTheme] = useState(localStorage.getItem("theme") || "light"); const [lightThemeConfig, setLightThemeConfig] = useState({}); const [darkThemeConfig, setDarkThemeConfig] = useState({}); @@ -21,42 +21,50 @@ const App = () => { localStorage.setItem("theme", newTheme); }; - const onThemeConfigUpdate = (themeType, updatedConfig) => { + const onThemeConfigUpdate = useCallback((themeType, updatedConfig) => { if (themeType === "light") { setLightThemeConfig((prevConfig) => ({ ...prevConfig, ...updatedConfig })); } else if (themeType === "dark") { setDarkThemeConfig((prevConfig) => ({ ...prevConfig, ...updatedConfig })); } - }; + }, []); useEffect(() => { - const fetchThemeConfig = async () => { + const fetchPersonalizations = async () => { try { const { data } = await api.get("/personalizations"); if (data && data.length > 0) { - const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); - const darkConfig = data.find(themeConfig => themeConfig.theme === "dark"); + 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) { toastError(err); + document.title = "Erro ao carregar título"; } }; - fetchThemeConfig(); - }, []); + const socket = openSocket(); + socket.on("personalization", () => { + fetchPersonalizations(); + }); - useEffect(() => { - const savedTheme = localStorage.getItem("theme") || "light"; - setTheme(savedTheme); - }, [theme]); + fetchPersonalizations(); + + return () => { + socket.off("personalization"); + socket.disconnect(); + }; + }, []); useEffect(() => { const i18nlocale = localStorage.getItem("i18nextLng"); @@ -74,57 +82,15 @@ const App = () => { favicon.rel = "shortcut icon"; const faviconPath = theme === "dark" ? "/assets/favicoDark.ico" : "/assets/favico.ico"; - fetch(faviconPath, { method: "HEAD" }) - .then(response => { - if (response.ok) { - favicon.href = faviconPath; - } else { - favicon.href = "/favicon.ico"; - } - }) - .catch(() => { - favicon.href = "/favicon.ico"; - }); - + favicon.href = faviconPath; document.head.appendChild(favicon); }, [theme]); - useEffect(() => { - const fetchPageTitle = async () => { - try { - const { data } = await api.get("/personalizations"); - - if (data && data.length > 0) { - - const lightConfig = data.find(themeConfig => themeConfig.theme === "light"); - - if (lightConfig) { - document.title = lightConfig.company; - } else { - document.title = "Press Ticket"; - } - } - } catch (err) { - toastError(err); - document.title = "Erro ao carregar título"; - } - }; - - const socket = openSocket(); - socket.on("personalization", data => { - if (data.action === "update") { - fetchPageTitle(); - } - }); - - fetchPageTitle(); - - return () => { - socket.disconnect(); - }; - }, []); - - const selectedTheme = loadThemeConfig(theme, theme === "light" ? lightThemeConfig : darkThemeConfig, locale); + const selectedTheme = loadThemeConfig( + theme, + theme === "light" ? lightThemeConfig : darkThemeConfig, + locale + ); return ( diff --git a/frontend/src/components/AcceptTicketWithoutQueueModal/index.js b/frontend/src/components/AcceptTicketWithoutQueueModal/index.js index 73770a56..414fb3b2 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 { 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 { i18n } from "../../translate/i18n"; +import ButtonWithSpinner from "../ButtonWithSpinner"; const useStyles = makeStyles((theme) => ({ - autoComplete: { + autoComplete: { width: 300, - // marginBottom: 20 }, maxWidth: { width: "100%", @@ -39,40 +32,48 @@ 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 handleClose = () => { - onClose(); - setSelectedQueue(""); -}; + const userId = user?.id; -const handleUpdateTicketStatus = async (queueId) => { - setLoading(true); - try { - await api.put(`/tickets/${ticketId}`, { - status: "open", - userId: user?.id || null, - queueId: queueId - }); + const handleClose = useCallback(() => { + onClose(); + setSelectedQueue(INITIAL_QUEUE_VALUE); + }, [onClose]); - setLoading(false); - history.push(`/tickets/${ticketId}`); - handleClose(); - } catch (err) { - setLoading(false); - toastError(err); - } -}; + const handleUpdateTicketStatus = useCallback(async (queueId) => { + setLoading(true); + try { + await api.put(`/tickets/${ticketId}`, { + status: "open", + userId: userId || null, + queueId, + }); -return ( - <> - - + setLoading(false); + history.push(`/tickets/${ticketId}`); + handleClose(); + } catch (err) { + setLoading(false); + toastError(err); + } + }, [ticketId, userId, history, handleClose]); + + return ( + + {i18n.t("ticketsList.acceptModal.title")} @@ -103,7 +104,7 @@ return ( handleUpdateTicketStatus(selectedQueue)} color="primary" loading={loading} @@ -112,8 +113,14 @@ return ( - -); + ); +}; + + +AcceptTicketWithouSelectQueue.propTypes = { + modalOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + ticketId: PropTypes.number.isRequired, }; -export default AcceptTicketWithouSelectQueue; \ No newline at end of file +export default AcceptTicketWithouSelectQueue; diff --git a/frontend/src/components/Audio/index.jsx b/frontend/src/components/Audio/index.jsx index 2dcdefbb..236a5575 100644 --- a/frontend/src/components/Audio/index.jsx +++ b/frontend/src/components/Audio/index.jsx @@ -1,82 +1,88 @@ import { Button } from "@material-ui/core"; -import React, { useRef, useEffect, useState } from "react"; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; -const LS_NAME = 'audioMessageRate'; +const LS_NAME = "audioMessageRate"; -const Audio = ({url}) => { - const audioRef = useRef(null); - const [audioRate, setAudioRate] = useState(parseFloat(localStorage.getItem(LS_NAME) || "1")); - const [showButtonRate, setShowButtonRate] = useState(false); - const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; - - useEffect(() => { +const Audio = ({ url }) => { + const audioRef = useRef(null); + const [audioRate, setAudioRate] = useState(() => + parseFloat(localStorage.getItem(LS_NAME) || "1") + ); + const [showButtonRate, setShowButtonRate] = useState(false); + const isIOS = useMemo( + () => /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream, + [] + ); + + useEffect(() => { + if (audioRef.current) { audioRef.current.playbackRate = audioRate; - localStorage.setItem(LS_NAME, audioRate); - }, [audioRate]); - - useEffect(() => { - audioRef.current.onplaying = () => { - setShowButtonRate(true); - }; - audioRef.current.onpause = () => { - setShowButtonRate(false); - }; - audioRef.current.onended = () => { - setShowButtonRate(false); - }; - }, []); - - const toggleRate = () => { - let newRate = null; - - switch (audioRate) { - case 0.5: - newRate = 1; - break; - case 1: - newRate = 1.5; - break; - case 1.5: - newRate = 2; - break; - case 2: - newRate = 0.5; - break; - default: - newRate = 1; - break; - } - - setAudioRate(newRate); - }; - - const getAudioSource = () => { - let sourceUrl = url; - - if (isIOS) { - sourceUrl = sourceUrl.replace(".ogg", ".mp3"); - } - - return ( - - ); + } + localStorage.setItem(LS_NAME, audioRate); + }, [audioRate]); + + useEffect(() => { + if (!audioRef.current) return; + + const handlePlaying = () => setShowButtonRate(true); + const handlePause = () => setShowButtonRate(false); + const handleEnded = () => setShowButtonRate(false); + + const audio = audioRef.current; + audio.addEventListener("playing", handlePlaying); + audio.addEventListener("pause", handlePause); + audio.addEventListener("ended", handleEnded); + + return () => { + audio.removeEventListener("playing", handlePlaying); + audio.removeEventListener("pause", handlePause); + audio.removeEventListener("ended", handleEnded); }; - - return ( - <> - - {showButtonRate && ( - - )} - - ); -} + }, []); + + const toggleRate = useCallback(() => { + const newRate = + { + 0.5: 1, + 1: 1.5, + 1.5: 2, + 2: 0.5, + }[audioRate] || 1; + + setAudioRate(newRate); + }, [audioRate]); + + const getAudioSource = useCallback(() => { + const sourceUrl = isIOS ? url.replace(".ogg", ".mp3") : url; + return ; + }, [url, isIOS]); + + const buttonStyle = useMemo( + () => ({ + marginLeft: "5px", + marginTop: "-45px", + }), + [] + ); + + return ( + <> + + {showButtonRate && ( + + )} + + ); +}; -export default Audio; \ No newline at end of file +export default Audio; diff --git a/frontend/src/components/BackdropLoading/index.js b/frontend/src/components/BackdropLoading/index.js index 183b02b0..aebcd2dd 100644 --- a/frontend/src/components/BackdropLoading/index.js +++ b/frontend/src/components/BackdropLoading/index.js @@ -1,23 +1,35 @@ +import PropTypes from "prop-types"; import React from "react"; -import Backdrop from "@material-ui/core/Backdrop"; import CircularProgress from "@material-ui/core/CircularProgress"; -import { makeStyles } from "@material-ui/core/styles"; +import { styled } from "@material-ui/core/styles"; -const useStyles = makeStyles(theme => ({ - backdrop: { - zIndex: theme.zIndex.drawer + 1, - color: "#fff", - }, +const BackdropStyled = styled('div')(({ theme, open }) => ({ + zIndex: theme.zIndex.drawer + 1, + color: "#fff", + display: open ? "flex" : "none", + position: "fixed", + top: 0, + left: 0, + right: 0, + bottom: 0, + alignItems: "center", + justifyContent: "center", + backgroundColor: "rgba(0, 0, 0, 0.5)" })); -const BackdropLoading = () => { - const classes = useStyles(); +const BackdropLoading = ({ open = true, color = "inherit", ariaLabel = "Loading..." }) => { return ( - - - + + + ); }; +BackdropLoading.propTypes = { + open: PropTypes.bool, + color: PropTypes.oneOf(["primary", "secondary", "inherit"]), + ariaLabel: PropTypes.string, +}; + export default BackdropLoading; diff --git a/frontend/src/components/ButtonWithSpinner/index.js b/frontend/src/components/ButtonWithSpinner/index.js index 542c39c9..d9431a5d 100644 --- a/frontend/src/components/ButtonWithSpinner/index.js +++ b/frontend/src/components/ButtonWithSpinner/index.js @@ -1,16 +1,14 @@ -import React from "react"; - +import { Button, CircularProgress } from "@material-ui/core"; import { makeStyles } from "@material-ui/core/styles"; -import { green } from "@material-ui/core/colors"; -import { CircularProgress, Button } from "@material-ui/core"; +import PropTypes from "prop-types"; +import React from "react"; const useStyles = makeStyles(theme => ({ button: { position: "relative", }, - buttonProgress: { - color: green[500], + color: props => props.spinnerColor || theme.palette.primary.main, position: "absolute", top: "50%", left: "50%", @@ -19,17 +17,41 @@ const useStyles = makeStyles(theme => ({ }, })); -const ButtonWithSpinner = ({ loading, children, ...rest }) => { - const classes = useStyles(); +const ButtonWithSpinner = ({ + loading, + children, + spinnerSize = 24, + spinnerColor = null, + ariaLabel = "Loading...", + ...rest +}) => { + const classes = useStyles({ spinnerColor }); return ( - ); }; +ButtonWithSpinner.propTypes = { + loading: PropTypes.bool.isRequired, + children: PropTypes.node.isRequired, + spinnerSize: PropTypes.number, + spinnerColor: PropTypes.string, + ariaLabel: PropTypes.string, +}; + export default ButtonWithSpinner; diff --git a/frontend/src/components/Can/index.js b/frontend/src/components/Can/index.js index 3d802150..12e494cc 100644 --- a/frontend/src/components/Can/index.js +++ b/frontend/src/components/Can/index.js @@ -1,16 +1,16 @@ +import PropTypes from "prop-types"; import rules from "../../rules"; const check = (role, action, data) => { const permissions = rules[role]; if (!permissions) { - // role is not present in the rules + console.warn(`Role "${role}" não encontrada nas regras.`); return false; } const staticPermissions = permissions.static; if (staticPermissions && staticPermissions.includes(action)) { - // static rule not provided for action return true; } @@ -19,12 +19,12 @@ const check = (role, action, data) => { if (dynamicPermissions) { const permissionCondition = dynamicPermissions[action]; if (!permissionCondition) { - // dynamic rule not provided for action return false; } return permissionCondition(data); } + return false; }; @@ -36,4 +36,13 @@ Can.defaultProps = { no: () => null, }; +Can.propTypes = { + role: PropTypes.string.isRequired, + perform: PropTypes.string.isRequired, + data: PropTypes.object, + yes: PropTypes.func, + no: PropTypes.func, +}; + export { Can }; + diff --git a/frontend/src/components/CodeSnippetGenerator/index.js b/frontend/src/components/CodeSnippetGenerator/index.js index bb16204c..30d99a3a 100644 --- a/frontend/src/components/CodeSnippetGenerator/index.js +++ b/frontend/src/components/CodeSnippetGenerator/index.js @@ -2,7 +2,6 @@ import { Button, FormControl, IconButton, - InputLabel, MenuItem, Modal, Select, @@ -10,13 +9,13 @@ import { } from "@material-ui/core"; import { makeStyles } from "@material-ui/core/styles"; import CloseIcon from "@material-ui/icons/Close"; -import React, { useState } from "react"; -import codeSnippets from './codeSnippets.js'; - -const useStyles = makeStyles(theme => ({ +import React, { useCallback, useState } from "react"; +import codeSnippets from "./codeSnippets.js"; +const useStyles = makeStyles((theme) => ({ root: { display: "flex", flexDirection: "column", + alignItems: "center", }, modalContent: { padding: theme.spacing(4), @@ -26,9 +25,12 @@ const useStyles = makeStyles(theme => ({ boxShadow: "0 4px 8px rgba(0, 0, 0, 0.1)", outline: "none", position: "relative", + textAlign: "center", }, selectContainer: { + marginTop: theme.spacing(1), marginBottom: theme.spacing(2), + width: "100%", }, snippetBox: { marginTop: theme.spacing(2), @@ -47,6 +49,9 @@ const useStyles = makeStyles(theme => ({ top: theme.spacing(1), color: theme.palette.secondary.main, }, + copyButton: { + marginTop: theme.spacing(2), + }, })); const CodeSnippetGenerator = ({ number, body, userId, queueId, whatsappId, token }) => { @@ -54,32 +59,44 @@ const CodeSnippetGenerator = ({ number, body, userId, queueId, whatsappId, token const [selectedLanguage, setSelectedLanguage] = useState(""); const [open, setOpen] = useState(false); const [snippet, setSnippet] = useState(""); + const [copied, setCopied] = useState(false); - const handleOpen = () => { + const handleOpen = useCallback(() => { const generateSnippet = codeSnippets[selectedLanguage]; if (generateSnippet) { - setSnippet(generateSnippet(number, body, userId, queueId, whatsappId, token)); + setSnippet( + generateSnippet(number, body, userId, queueId, whatsappId, token) + ); setOpen(true); } - }; + }, [selectedLanguage, number, body, userId, queueId, whatsappId, token]); - const handleClose = () => { + const handleClose = useCallback(() => { setOpen(false); - }; + setCopied(false); + }, []); const handleChange = (e) => { setSelectedLanguage(e.target.value); }; + const handleCopy = () => { + navigator.clipboard.writeText(snippet); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + return (
- Selecione uma linguagem (Apenas para envio de texto) setSelectedQueue(e.target.value)} - label={i18n.t("ticketsList.acceptModal.queue")} + label={t("ticketsList.acceptModal.queue")} >   {user.queues.map((queue) => ( @@ -99,7 +99,7 @@ const AcceptTicketWithouSelectQueue = ({ modalOpen, onClose, ticketId }) => { disabled={loading} variant="outlined" > - {i18n.t("ticketsList.buttons.cancel")} + {t("ticketsList.buttons.cancel")} { color="primary" loading={loading} > - {i18n.t("ticketsList.buttons.start")} + {t("ticketsList.buttons.start")}
diff --git a/frontend/src/components/ConfirmationModal/index.js b/frontend/src/components/ConfirmationModal/index.js index 93d238cb..5329bfd2 100644 --- a/frontend/src/components/ConfirmationModal/index.js +++ b/frontend/src/components/ConfirmationModal/index.js @@ -7,8 +7,7 @@ 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 { i18n } from "../../translate/i18n"; +import { useTranslation } from "react-i18next"; const useStyles = makeStyles((theme) => ({ dialogTitle: { @@ -33,6 +32,7 @@ const useStyles = makeStyles((theme) => ({ const ConfirmationModal = ({ title, children, open, onClose, onConfirm }) => { const classes = useStyles(); + const { t } = useTranslation(); return ( { className={classes.cancelButton} startIcon={} > - {i18n.t("confirmationModal.buttons.cancel")} + {t("confirmationModal.buttons.cancel")} 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 2b5b6788..325fc54c 100644 --- a/frontend/src/components/ContactModal/index.js +++ b/frontend/src/components/ContactModal/index.js @@ -19,12 +19,12 @@ import { 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) => ({ root: { @@ -68,6 +68,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { const classes = useStyles(); const { user } = useContext(AuthContext); const [contact, setContact] = useState(initialState); + const { t } = useTranslation(); useEffect(() => { const abortController = new AbortController(); @@ -108,7 +109,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { const { data } = await api.post("/contacts", values); if (onSave) onSave(data); } - toast.success(i18n.t("contactModal.success")); + toast.success(t("contactModal.success")); handleClose(); } catch (err) { toastError(err); @@ -119,8 +120,8 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { {contactId - ? i18n.t("contactModal.title.edit") - : i18n.t("contactModal.title.add")} + ? t("contactModal.title.edit") + : t("contactModal.title.add")} { - {i18n.t("contactModal.form.mainInfo")} + {t("contactModal.form.mainInfo")} { {user.isTricked === "enabled" && ( { )} { style={{ marginBottom: 8, marginTop: 12 }} variant="subtitle1" > - {i18n.t("contactModal.form.extraInfo")} + {t("contactModal.form.extraInfo")} {({ push, remove }) => ( @@ -188,7 +189,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { > { /> { color="primary" onClick={() => push({ name: "", value: "" })} > - + {i18n.t("contactModal.buttons.addExtraInfo")} + + {t("contactModal.buttons.addExtraInfo")}
@@ -231,7 +232,7 @@ const ContactModal = ({ open, onClose, contactId, initialValues, onSave }) => { disabled={isSubmitting} variant="outlined" > - {i18n.t("contactModal.buttons.cancel")} + {t("contactModal.buttons.cancel")} diff --git a/frontend/src/components/MessageInput/index.js b/frontend/src/components/MessageInput/index.js index b0e44f5c..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(); @@ -510,7 +511,7 @@ const MessageInput = ({ ticketStatus }) => { ) : ( - {i18n.t("uploads.titles.titleFileList")} ({medias.length}) + {t("uploads.titles.titleFileList")} ({medias.length}) {medias.map((value, index) => { @@ -563,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))}
@@ -582,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} 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 0d1bb61f..4d465dab 100644 --- a/frontend/src/components/MessageVariablesPicker/index.js +++ b/frontend/src/components/MessageVariablesPicker/index.js @@ -1,7 +1,7 @@ import { Chip, makeStyles } from "@material-ui/core"; import React from "react"; +import { useTranslation } from "react-i18next"; import useMessageVariables from "../../hooks/useMessageVariables"; -import { i18n } from "../../translate/i18n"; import OutlinedDiv from "../OutlinedDiv"; const useStyles = makeStyles((theme) => ({ @@ -14,6 +14,7 @@ const useStyles = makeStyles((theme) => ({ const MessageVariablesPicker = ({ onClick, disabled, customVariables = [] }) => { const classes = useStyles(); const msgVars = useMessageVariables(customVariables); + const { t } = useTranslation(); const handleClick = (e, value) => { e.preventDefault(); @@ -25,7 +26,7 @@ const MessageVariablesPicker = ({ onClick, disabled, customVariables = [] }) => {msgVars.map((msgVar) => ( diff --git a/frontend/src/components/MessagesList/index.js b/frontend/src/components/MessagesList/index.js index 19e0a1f8..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); @@ -779,7 +749,7 @@ const MessagesList = ({ ticketId, isGroup }) => { : {message.body} )} - {message.isEdited && {i18n.t("message.edited")} } + {message.isEdited && {t("message.edited")} } {format(parseISO(message.createdAt), "HH:mm")}
@@ -831,7 +801,7 @@ const MessagesList = ({ ticketId, isGroup }) => { : {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")} ({ root: { @@ -55,7 +52,7 @@ const QuickAnswersModal = ({ shortcut: "", message: "", }; - + const { t } = useTranslation(); const isMounted = useRef(true); const messageInputRef = useRef(); const [loading, setLoading] = useState(false); @@ -111,7 +108,7 @@ const QuickAnswersModal = ({ } onClose(); } - toast.success(i18n.t("quickAnswersModal.success")); + toast.success(t("quickAnswersModal.success")); } catch (err) { toastError(err); } @@ -142,8 +139,8 @@ const QuickAnswersModal = ({ > {quickAnswerId - ? i18n.t("quickAnswersModal.title.edit") - : i18n.t("quickAnswersModal.title.add")} + ? t("quickAnswersModal.title.edit") + : t("quickAnswersModal.title.add")} - {i18n.t("quickAnswersModal.buttons.cancel")} + {t("quickAnswersModal.buttons.cancel")} {quickAnswerId - ? i18n.t("quickAnswersModal.buttons.okEdit") - : i18n.t("quickAnswersModal.buttons.okAdd")} + ? t("quickAnswersModal.buttons.okEdit") + : t("quickAnswersModal.buttons.okAdd")} 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")} ( { + 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")} - + - + { perform="drawer-admin-items:view" yes={() => ( <> - +
- {i18n.t("dashboard.messages.closed.title")} + {t("dashboard.messages.closed.title")} diff --git a/frontend/src/pages/Integrations/index.js b/frontend/src/pages/Integrations/index.js index 3a3ca67d..4ee34b10 100644 --- a/frontend/src/pages/Integrations/index.js +++ b/frontend/src/pages/Integrations/index.js @@ -9,11 +9,11 @@ import { } from "@material-ui/core"; import { Visibility, VisibilityOff } from "@material-ui/icons"; import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import Title from "../../components/Title"; import toastError from "../../errors/toastError"; import api from "../../services/api"; -import { i18n } from "../../translate/i18n.js"; const useStyles = makeStyles((theme) => ({ root: { @@ -48,6 +48,7 @@ const useStyles = makeStyles((theme) => ({ const Integrations = () => { const classes = useStyles(); + const { t } = useTranslation(); const [integrations, setIntegrations] = useState([]); const [showKeys, setShowKeys] = useState({ @@ -91,7 +92,7 @@ const Integrations = () => { await api.put(`/integrations/${integrationKey}`, { value: selectedValue, }); - toast.success(i18n.t("integrations.success")); + toast.success(t("integrations.success")); setMaskedValues((prevState) => ({ ...prevState, [integrationKey]: maskValue(selectedValue), @@ -120,11 +121,11 @@ const Integrations = () => { return (
- {i18n.t("integrations.title")} + {t("integrations.title")}
- {i18n.t("integrations.integrations.openai.title")} + {t("integrations.integrations.openai.title")}
@@ -133,7 +134,7 @@ const Integrations = () => { id="organization" name="organization" margin="dense" - label={i18n.t("integrations.integrations.openai.organization")} + label={t("integrations.integrations.openai.organization")} variant="outlined" value={ showKeys["organization"] @@ -157,7 +158,7 @@ const Integrations = () => { className={classes.textField} id="apikey" name="apikey" - label={i18n.t("integrations.integrations.openai.apikey")} + label={t("integrations.integrations.openai.apikey")} margin="dense" variant="outlined" onChange={handleChangeIntegration} @@ -184,7 +185,7 @@ const Integrations = () => {
- {i18n.t("integrations.integrations.n8n.title")} + {t("integrations.integrations.n8n.title")}
@@ -193,7 +194,7 @@ const Integrations = () => { id="urlApiN8N" name="urlApiN8N" margin="dense" - label={i18n.t("integrations.integrations.n8n.urlApiN8N")} + label={t("integrations.integrations.n8n.urlApiN8N")} variant="outlined" onChange={handleChangeIntegration} fullWidth @@ -217,7 +218,7 @@ const Integrations = () => {
- {i18n.t("integrations.integrations.hub.title")} + {t("integrations.integrations.hub.title")}
@@ -226,7 +227,7 @@ const Integrations = () => { id="hubToken" name="hubToken" margin="dense" - label={i18n.t("integrations.integrations.hub.hubToken")} + label={t("integrations.integrations.hub.hubToken")} variant="outlined" onChange={handleChangeIntegration} fullWidth @@ -254,7 +255,7 @@ const Integrations = () => {
- {i18n.t("integrations.integrations.maps.title")} + {t("integrations.integrations.maps.title")}
@@ -263,7 +264,7 @@ const Integrations = () => { id="apiMaps" name="apiMaps" margin="dense" - label={i18n.t("integrations.integrations.maps.apiMaps")} + label={t("integrations.integrations.maps.apiMaps")} variant="outlined" onChange={handleChangeIntegration} fullWidth diff --git a/frontend/src/pages/Login/index.js b/frontend/src/pages/Login/index.js index 3a0e7864..8b9be1bf 100644 --- a/frontend/src/pages/Login/index.js +++ b/frontend/src/pages/Login/index.js @@ -16,11 +16,11 @@ import { import { makeStyles } from "@material-ui/core/styles"; import { Visibility, VisibilityOff } from '@material-ui/icons'; +import { useTranslation } from "react-i18next"; import defaultLogo from '../../assets/logo.jpg'; import { AuthContext } from "../../context/Auth/AuthContext"; import toastError from "../../errors/toastError"; import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; const PUBLIC_ASSET_PATH = '/assets/'; @@ -55,7 +55,7 @@ const useStyles = makeStyles((theme) => ({ const Login = () => { const classes = useStyles(); - + const { t } = useTranslation(); const [user, setUser] = useState({ email: "", password: "" }); const [showPassword, setShowPassword] = useState(false); const { handleLogin } = useContext(AuthContext); @@ -144,7 +144,7 @@ const Login = () => {
logo - {i18n.t("login.title")} + {t("login.title")} { required fullWidth id="email" - label={i18n.t("login.form.email")} + label={t("login.form.email")} name="email" value={user.email} onChange={handleChangeInput} @@ -166,7 +166,7 @@ const Login = () => { required fullWidth name="password" - label={i18n.t("login.form.password")} + label={t("login.form.password")} id="password" value={user.password} onChange={handleChangeInput} @@ -192,7 +192,7 @@ const Login = () => { color="primary" className={classes.submit} > - {i18n.t("login.buttons.submit")} + {t("login.buttons.submit")} @@ -202,7 +202,7 @@ const Login = () => { component={RouterLink} to="/signup" > - {i18n.t("login.buttons.register")} + {t("login.buttons.register")} diff --git a/frontend/src/pages/Queues/index.js b/frontend/src/pages/Queues/index.js index b1145a7c..2773a71b 100644 --- a/frontend/src/pages/Queues/index.js +++ b/frontend/src/pages/Queues/index.js @@ -17,6 +17,7 @@ import { Edit } from "@material-ui/icons"; import React, { useEffect, useReducer, useState } from "react"; +import { useTranslation } from "react-i18next"; import { toast } from "react-toastify"; import ConfirmationModal from "../../components/ConfirmationModal"; import MainContainer from "../../components/MainContainer"; @@ -28,7 +29,6 @@ import Title from "../../components/Title"; import toastError from "../../errors/toastError"; import api from "../../services/api"; import openSocket from "../../services/socket-io"; -import { i18n } from "../../translate/i18n"; const useStyles = makeStyles((theme) => ({ mainPaper: { @@ -90,7 +90,7 @@ const reducer = (state, action) => { const Queues = () => { const classes = useStyles(); - + const { t } = useTranslation(); const [queues, dispatch] = useReducer(reducer, []); const [loading, setLoading] = useState(false); @@ -154,7 +154,7 @@ const Queues = () => { const handleDeleteQueue = async (queueId) => { try { await api.delete(`/queue/${queueId}`); - toast.success(i18n.t("queues.notifications.queueDeleted")); + toast.success(t("queues.notifications.queueDeleted")); } catch (err) { toastError(err); } @@ -166,14 +166,14 @@ const Queues = () => { handleDeleteQueue(selectedQueue.id)} > - {i18n.t("queues.confirmationModal.deleteMessage")} + {t("queues.confirmationModal.deleteMessage")} { queueId={selectedQueue?.id} /> - {i18n.t("queues.title")} ({queues.length}) + {t("queues.title")} ({queues.length}) - +
@@ -334,7 +336,7 @@ const PersonalizeSettings = ({ onThemeConfigUpdate }) => {
- Theme Light + {t("settings.personalize.tabpanel.light")} {Object.keys(logos.themeLight).map((key) => ( @@ -373,7 +375,7 @@ const PersonalizeSettings = ({ onThemeConfigUpdate }) => { - Theme Dark + {t("settings.personalize.tabpanel.dark")} {Object.keys(logos.themeDark).map((key) => ( @@ -417,20 +419,20 @@ const PersonalizeSettings = ({ onThemeConfigUpdate }) => {
- Theme Light + {t("settings.personalize.tabpanel.light")}
{ { { {
- Theme Dark + {t("settings.personalize.tabpanel.dark")}
{ { { ({ const Settings = ({ onThemeConfigUpdate }) => { const classes = useStyles(); - const history = useHistory(); + const { t } = useTranslation(); const [settings, setSettings] = useState([]); const [tabValue, setTabValue] = useState(0); const { user } = useContext(AuthContext); @@ -88,8 +87,7 @@ const Settings = ({ onThemeConfigUpdate }) => { await api.put(`/settings/${settingKey}`, { value: selectedValue, }); - toast.success("Configuração atualizada com sucesso"); - history.go(0); + toast.success(t("settings.success")); } catch (err) { toastError(err); } @@ -103,7 +101,7 @@ const Settings = ({ onThemeConfigUpdate }) => { await api.put(`/settings/${settingKey}`, { value: selectedValue, }); - toast.success(i18n.t("settings.success")); + toast.success(t("settings.success")); } catch (err) { toastError(err); } @@ -128,9 +126,9 @@ const Settings = ({ onThemeConfigUpdate }) => { className={classes.tabs} > {(!isMasterAdminEnabled || user.profile === "masteradmin") && ( - + )} - + {tabValue === 0 && (!isMasterAdminEnabled || user.profile === "masteradmin") && ( diff --git a/frontend/src/pages/Signup/index.js b/frontend/src/pages/Signup/index.js index 5acee9e3..28e38531 100644 --- a/frontend/src/pages/Signup/index.js +++ b/frontend/src/pages/Signup/index.js @@ -1,10 +1,3 @@ -import React, { useEffect, useState } from "react"; - -import { Field, Form, Formik } from "formik"; -import { Link as RouterLink, useHistory } from "react-router-dom"; -import { toast } from "react-toastify"; -import * as Yup from "yup"; - import { Box, Button, @@ -17,13 +10,14 @@ import { TextField, Typography } from '@material-ui/core'; - -import { Visibility, VisibilityOff } from '@material-ui/icons'; - import { makeStyles } from "@material-ui/core/styles"; - -import { i18n } from "../../translate/i18n"; - +import { Visibility, VisibilityOff } from '@material-ui/icons'; +import { Field, Form, Formik } from "formik"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link as RouterLink, useHistory } from "react-router-dom"; +import { toast } from "react-toastify"; +import * as Yup from "yup"; import defaultLogo from '../../assets/logo.jpg'; import toastError from "../../errors/toastError"; import api from "../../services/api"; @@ -70,6 +64,7 @@ const UserSchema = Yup.object().shape({ const SignUp = () => { const classes = useStyles(); + const { t } = useTranslation(); const history = useHistory(); const initialState = { name: "", email: "", password: "" }; const [showPassword, setShowPassword] = useState(false); @@ -147,7 +142,7 @@ const SignUp = () => { const handleSignUp = async values => { try { await api.post("/auth/signup", values); - toast.success(i18n.t("signup.toasts.success")); + toast.success(t("signup.toasts.success")); history.push("/login"); } catch (err) { toastError(err); @@ -160,7 +155,7 @@ const SignUp = () => {
logo - {i18n.t("signup.title")} + {t("signup.title")} { variant="outlined" fullWidth id="name" - label={i18n.t("signup.form.name")} + label={t("signup.form.name")} autoFocus /> @@ -197,7 +192,7 @@ const SignUp = () => { variant="outlined" fullWidth id="email" - label={i18n.t("signup.form.email")} + label={t("signup.form.email")} name="email" error={touched.email && Boolean(errors.email)} helperText={touched.email && errors.email} @@ -214,7 +209,7 @@ const SignUp = () => { autoComplete="current-password" error={touched.password && Boolean(errors.password)} helperText={touched.password && errors.password} - label={i18n.t("signup.form.password")} + label={t("signup.form.password")} type={showPassword ? 'text' : 'password'} InputProps={{ endAdornment: ( @@ -238,7 +233,7 @@ const SignUp = () => { color="primary" className={classes.submit} > - {i18n.t("signup.buttons.submit")} + {t("signup.buttons.submit")} @@ -248,7 +243,7 @@ const SignUp = () => { component={RouterLink} to="/login" > - {i18n.t("signup.buttons.login")} + {t("signup.buttons.login")} diff --git a/frontend/src/pages/Tags/index.js b/frontend/src/pages/Tags/index.js index 458a0e2b..ec28404b 100644 --- a/frontend/src/pages/Tags/index.js +++ b/frontend/src/pages/Tags/index.js @@ -1,7 +1,3 @@ -import React, { useCallback, useEffect, useReducer, useState } from "react"; -import { toast } from "react-toastify"; -import openSocket from "../../services/socket-io"; - import { Button, IconButton, @@ -16,7 +12,6 @@ import { Tooltip } from "@material-ui/core"; import { makeStyles } from "@material-ui/core/styles"; - import { AddCircleOutline, DeleteForever, @@ -24,18 +19,19 @@ import { Edit, Search } from "@material-ui/icons"; - +import React, { useCallback, useEffect, useReducer, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "react-toastify"; +import ConfirmationModal from "../../components/ConfirmationModal"; import MainContainer from "../../components/MainContainer"; import MainHeader from "../../components/MainHeader"; import MainHeaderButtonsWrapper from "../../components/MainHeaderButtonsWrapper"; -import Title from "../../components/Title"; - -import ConfirmationModal from "../../components/ConfirmationModal"; import TableRowSkeleton from "../../components/TableRowSkeleton"; import TagModal from "../../components/TagModal"; +import Title from "../../components/Title"; import toastError from "../../errors/toastError"; import api from "../../services/api"; -import { i18n } from "../../translate/i18n"; +import openSocket from "../../services/socket-io"; const reducer = (state, action) => { if (action.type === "LOAD_TAGS") { @@ -98,7 +94,7 @@ const useStyles = makeStyles((theme) => ({ const Tags = () => { const classes = useStyles(); - + const { t } = useTranslation(); const [loading, setLoading] = useState(false); const [pageNumber, setPageNumber] = useState(1); const [hasMore, setHasMore] = useState(false); @@ -176,7 +172,7 @@ const Tags = () => { const handleDeleteTag = async (tagId) => { try { await api.delete(`/tags/${tagId}`); - toast.success(i18n.t("tags.toasts.deleted")); + toast.success(t("tags.toasts.deleted")); } catch (err) { toastError(err); } @@ -192,7 +188,7 @@ const Tags = () => { const handleDeleteAllTags = async () => { try { await api.delete(`/tags`); - toast.success(i18n.t("tags.toasts.deletedAll")); + toast.success(t("tags.toasts.deletedAll")); } catch (err) { toastError(err); } @@ -221,8 +217,8 @@ const Tags = () => { { } > { - deletingTag ? `${i18n.t("tags.confirmationModal.deleteMessage")}` - : `${i18n.t("tags.confirmationModal.deleteAllMessage")}` + deletingTag ? `${t("tags.confirmationModal.deleteMessage")}` + : `${t("tags.confirmationModal.deleteAllMessage")}` } { tagId={selectedTag && selectedTag.id} /> - {i18n.t("tags.title")} ({tags.length}) + {t("tags.title")} ({tags.length}) { ), }} /> - + - +