├── .gitignore ├── backend ├── certs │ ├── coloque_seus_certificado_aqui │ ├── producaoActom.p12 │ ├── homologacaoActom.p12 │ └── producao-448607-Admin.p12 ├── src │ ├── waversion.json │ ├── @types │ │ ├── qrcode-terminal.d.ts │ │ └── express.d.ts │ ├── helpers │ │ ├── parseToMilliseconds.ts │ │ ├── randomValue.ts │ │ ├── GetWhatsappWbot.ts │ │ ├── GetPublicPath.ts │ │ ├── SerializeWbotMsgId.ts │ │ ├── SendRefreshToken.ts │ │ ├── CheckContactSomeTicket.ts │ │ ├── MakeRandomId.ts │ │ ├── GetTicketWbot.ts │ │ ├── replaceExtension.ts │ │ ├── URLCharEncoder.ts │ │ ├── UpdateDeletedUserOpenTicketsStatus.ts │ │ ├── SerializeUser.ts │ │ ├── CheckContactOpenTickets.ts │ │ └── CreateTokens.ts │ ├── bootstrap.ts │ ├── gitinfo.ts │ ├── database │ │ └── migrations │ │ │ ├── 20250625163600-standardbackendtranslations.ts │ │ │ ├── 20250810094000-translations-for-greetings.ts │ │ │ ├── 20250810104500-translations-in-german-and-french.ts │ │ │ ├── 20241201102700-add-unaccent-extension.ts │ │ │ ├── 20200717133431-add-uuid-ossp.ts │ │ │ ├── 20200919124112-update-default-column-name-on-whatsappp.ts │ │ │ ├── 20220221014718-add-remoteJid-messages.ts │ │ │ ├── 20220221014719-add-jsonMessage-messages.ts │ │ │ ├── 20250522093400-fix-finidsedAt-on-TicketTraking.ts │ │ │ ├── 20220221014720-add-participant-messages.ts │ │ │ ├── 20230723301001-add-kanban-to-Tags.ts │ │ │ ├── 20250618151400-remove-online-from-users.ts │ │ │ ├── 20210108001431-add-unreadMessages-to-tickets.ts │ │ │ ├── 20210109192513-add-greetingMessage-to-whatsapp.ts │ │ │ ├── 20210109192535-add-column-ratingMessage-to-whatsapp.ts │ │ │ ├── 20210818102609-add-token-to-Whatsapps.ts │ │ │ ├── 20220220014719-add-farewellMessage-to-whatsapp.ts │ │ │ ├── 20222016014719-add-channel-session.ts │ │ │ ├── 20220122160900-add-status-to-schedules.ts │ │ │ ├── 20222016014719-add-channel-tokenUser.ts │ │ │ ├── 20230528202116-add-transferMessage-field-to-Whatsapp.ts │ │ │ ├── 20210109192527-add-column-super-to-Users-table.ts │ │ │ ├── 20210109192532-add-column-online-to-Users-table.ts │ │ │ ├── 20220221014717-add-provider-whatsapp.ts │ │ │ ├── 20222016014719-add-channel-to-message.ts │ │ │ ├── 20222016014719-add-channel-to-ticket.ts │ │ │ ├── 20240820082900-set-expired-to-TicketTraking.ts │ │ │ ├── 20250325080200-add-index-id-fromme-to-messages.ts │ │ │ ├── 20250831112100-add-currency-to-Plans.ts │ │ │ ├── 20210109192525-add-column-complationMessage-to-whatsapp.ts │ │ │ ├── 20210109192526-add-column-outOfHoursMessage-to-whatsapp .ts │ │ │ ├── 20220406000000-add-column-dueDate-to-Companies.ts │ │ │ ├── 20222016014719-add-channel-to-contacts.ts │ │ │ ├── 20240522165800-add-disablebot-to-contact.ts │ │ │ ├── 20240524011900-add-presence-to-contact.ts │ │ │ ├── 20222016014719-add-channel-token.ts │ │ │ ├── 20250901093900-add-currency-to-Invoices.ts │ │ │ ├── 20200904220257-add-name-to-whatsapp.ts │ │ │ ├── 20222016014719-add-facebookUserId-whatsapp.ts │ │ │ ├── 20230915212800-add-public-to-plans.ts │ │ │ ├── 20240820082900-add-expired-to-TicketTraking.ts │ │ │ ├── 20200723202116-add-email-field-to-contacts.ts │ │ │ ├── 20200730153545-add-fromMe-to-messages.ts │ │ │ ├── 20222016014719-add-facebookPageUserId-whatsapp.ts │ │ │ ├── 20200901235509-add-profile-column-to-users.ts │ │ │ ├── 20201026215410-add-retries-to-whatsapps.ts │ │ │ ├── 20200813114236-change-ticket-lastMessage-column-type.ts │ │ │ ├── 20200929145451-add-user-tokenVersion-column.ts │ │ │ ├── 20200930162323-add-isGroup-column-to-tickets.ts │ │ │ ├── 20200930194808-add-isGroup-column-to-contacts.ts │ │ │ ├── 20200906122228-add-name-default-field-to-whatsapp.ts │ │ │ ├── 20200927220708-add-isDeleted-column-to-messages.ts │ │ │ ├── 20210109192528-change-column-message-to-quick-messages-table.ts │ │ │ ├── 20231219153800-add-isEdited-column-to-messages.ts │ │ │ ├── 20250410205700-add-index-ticketId-to-ticketTraking.ts │ │ │ ├── 20220406000001-add-column-recurrence-to-Companies.ts │ │ │ ├── 20250725084700-remove-incoherent-queue-associations.ts │ │ │ ├── 20241128175800-add-save-messages-to-schedules.ts │ │ │ ├── 20220814000000-add-value-to-plans.ts │ │ │ ├── 20250327164700-add-index-quotedMsgId-to-messages.ts │ │ │ ├── 20210818102607-remove-unique-indexes-to-Contacts-table.ts │ │ │ ├── 20240417150500-add-unique-constrait-to-Contacts-table.ts │ │ │ ├── 20210108164504-add-queueId-to-tickets.ts │ │ │ ├── 20210109192530-add-unique-constraint-to-Contacts-table.ts │ │ │ ├── 20232016014719-add-column-mediaType-to-ChatMessages.ts │ │ │ ├── 20240827171200-add-thumbnailurl-to-messages.ts │ │ │ ├── 20210109192523-add-column-planId-to-Companies.ts │ │ │ ├── 20210109192536-add-unique-constraint-to-Tickets-table.ts │ │ │ ├── 20231230100900-remove-unique-constraint-to-ticket-table.ts │ │ │ ├── 20200730153237-remove-user-association-from-messages.ts │ │ │ ├── 20200906155658-add-whatsapp-field-to-tickets.ts │ │ │ ├── 20201004150008-add-contactId-column-to-messages.ts │ │ │ ├── 20201028124427-add-quoted-msg-to-messages.ts │ │ │ ├── 20210109192516-add-column-companyId-to-Users-table.ts │ │ │ ├── 20220404000000-add-column-queueId-to-Messages-table.ts │ │ │ ├── 20210109192519-add-column-companyId-to-Queues-table.ts │ │ │ ├── 20210109192521-add-column-companyId-to-Tickets-table.ts │ │ │ ├── 20210818102606-add-uuid-to-tickets.ts │ │ │ ├── 20210109192515-add-column-companyId-to-Settings-table.ts │ │ │ ├── 20210109192517-add-column-companyId-to-Contacts-table.ts │ │ │ ├── 20210109192518-add-column-companyId-to-Messages-table.ts │ │ │ ├── 20220115114088-add-column-userId-to-QuickMessages-table.ts │ │ │ ├── 20250516095500-add-chatbotendat-to-tickettraking.ts │ │ │ ├── 20201004155719-add-vcardContactId-column-to-messages.ts │ │ │ ├── 20210109192520-add-column-companyId-to-Whatsapps-table.ts │ │ │ ├── 20201004955719-remove-vcardContactId-column-to-messages.ts │ │ │ ├── 20250326110400-add-indexes-to-baileyskeys.ts │ │ │ ├── 20250702085100-add-index-on-name-tags.ts │ │ │ ├── 20230813114236-change-ticket-lastMessage-column-type.ts │ │ │ ├── 20250702083400-immutable-unaccent-function.ts │ │ │ ├── 20241128175500-create-insensitive-index-for-users-email.ts │ │ │ ├── 20250506155200-fix-groq-typo-on-settings.ts │ │ │ ├── 20241013110200-whatsapps-channel-default.ts │ │ │ ├── 20230904212800-alter-value-to-plans.ts │ │ │ ├── 20230918122800-add-media-to-Queues.ts │ │ │ ├── 20250415163900-add-index-timestamps-to-tickets.ts │ │ │ ├── 20230918142800-add-media-to-QueueOptions.ts │ │ │ ├── 20250415164200-add-index-timestamps-to-messages.ts │ │ │ ├── 20250226163500-fix-queueoptions-constraints.ts │ │ │ ├── 20241110080800-change-QueueOptions-forwardQueueId-column.ts │ │ │ ├── 20210109192523-add-column-status-and-schedules-to-Companies.ts │ │ │ ├── 20210108204708-associate-users-queue.ts │ │ │ ├── 20220411000002-add-column-schedules-and-outOfHoursMessage-to-Queues.ts │ │ │ ├── 20250321122300-add-indexes-to-messages.ts │ │ │ ├── 20200903215941-create-settings.ts │ │ │ ├── 20210109192534-add-rated-to-TicketTraking.ts │ │ │ ├── 20210108174594-associate-whatsapp-queue.ts │ │ │ ├── 20210818102608-add-unique-indexes-to-Queues-table.ts │ │ │ ├── 20250415164000-add-index-timestamps-to-tickettraking.ts │ │ │ ├── 20250520150600-change-duedate-to-dateonly.ts │ │ │ └── 20211212125704-add-chatbot-to-tickets.ts │ ├── config │ │ ├── redis.ts │ │ ├── upload.ts │ │ ├── privateFiles.ts │ │ └── database.ts │ ├── routes │ │ ├── versionRoutes.ts │ │ ├── pwaRoutes.ts │ │ ├── subScriptionRoutes.ts │ │ ├── campaignSettingRoutes.ts │ │ ├── ticketzOSSRoutes.ts │ │ ├── ticketTagRoutes.ts │ │ ├── authRoutes.ts │ │ ├── userRoutes.ts │ │ ├── i18nRoutes.ts │ │ ├── wavoipRoutes.ts │ │ ├── helpRoutes.ts │ │ ├── invoicesRoutes.ts │ │ ├── scheduleRoutes.ts │ │ ├── quickMessageRoutes.ts │ │ ├── tagRoutes.ts │ │ ├── dashboardRoutes.ts │ │ └── ticketNoteRoutes.ts │ ├── services │ │ ├── HelpServices │ │ │ ├── FindService.ts │ │ │ ├── FindAllService.ts │ │ │ ├── ShowService.ts │ │ │ ├── DeleteService.ts │ │ │ └── UpdateService.ts │ │ ├── ChatService │ │ │ ├── FindAllService.ts │ │ │ ├── ShowService.ts │ │ │ ├── ShowFromUuidService.ts │ │ │ ├── DeleteService.ts │ │ │ └── FindService.ts │ │ ├── TicketNoteService │ │ │ ├── FindAllTicketNotesService.ts │ │ │ ├── ShowTicketNoteService.ts │ │ │ ├── DeleteTicketNoteService.ts │ │ │ └── UpdateTicketNoteService.ts │ │ ├── QueueOptionService │ │ │ ├── DeleteService.ts │ │ │ ├── CreateService.ts │ │ │ ├── UpdateService.ts │ │ │ └── ShowService.ts │ │ ├── CampaignService │ │ │ ├── FindAllService.ts │ │ │ ├── RestartService.ts │ │ │ ├── FindService.ts │ │ │ └── DeleteService.ts │ │ ├── ContactListService │ │ │ ├── FindAllService.ts │ │ │ ├── ShowService.ts │ │ │ ├── DeleteService.ts │ │ │ ├── FindService.ts │ │ │ ├── UpdateService.ts │ │ │ └── CreateService.ts │ │ ├── AnnouncementService │ │ │ ├── FindAllService.ts │ │ │ ├── ShowService.ts │ │ │ ├── DeleteService.ts │ │ │ ├── FindService.ts │ │ │ └── UpdateService.ts │ │ ├── QuickMessageService │ │ │ ├── FindAllService.ts │ │ │ ├── ShowService.ts │ │ │ ├── DeleteService.ts │ │ │ └── UpdateService.ts │ │ ├── WhatsappService │ │ │ ├── AssociateWhatsappQueue.ts │ │ │ ├── DeleteWhatsAppService.ts │ │ │ └── SocketSendWhatsappUpdate.ts │ │ ├── ContactListItemService │ │ │ ├── FindAllService.ts │ │ │ ├── ShowService.ts │ │ │ ├── DeleteService.ts │ │ │ └── FindService.ts │ │ ├── QueueService │ │ │ ├── DeleteQueueService.ts │ │ │ ├── ListQueuesService.ts │ │ │ └── ShowQueueService.ts │ │ ├── TagServices │ │ │ ├── ShowService.ts │ │ │ ├── DeleteService.ts │ │ │ └── KanbanListService.ts │ │ ├── PlanService │ │ │ ├── ShowPlanService.ts │ │ │ ├── DeletePlanService.ts │ │ │ └── FindAllPlanService.ts │ │ ├── BaileysServices │ │ │ ├── DeleteBaileysService.ts │ │ │ └── ShowBaileysService.ts │ │ ├── CompanyService │ │ │ ├── ShowCompanyService.ts │ │ │ ├── FindAllCompaniesService.ts │ │ │ ├── UpdateSchedulesService.ts │ │ │ └── DeleteCompanyService.ts │ │ ├── ScheduleServices │ │ │ ├── DeleteService.ts │ │ │ └── ShowService.ts │ │ ├── SettingServices │ │ │ ├── GetSuperSettingService.ts │ │ │ ├── ListSettingsService.ts │ │ │ └── GetPublicSettingService.ts │ │ ├── InvoicesService │ │ │ ├── FindAllInvoiceService.ts │ │ │ ├── UpdateInvoiceService.ts │ │ │ └── ShowInvoiceService.ts │ │ ├── MessageServices │ │ │ └── GetMessagesService.ts │ │ ├── AuthServices │ │ │ └── FindUserFromToken.ts │ │ ├── WbotServices │ │ │ ├── GetProfilePicUrl.ts │ │ │ ├── getJidOf.ts │ │ │ ├── StartAllWhatsAppsSessions.ts │ │ │ ├── CheckIsValidContact.ts │ │ │ └── StartWhatsAppSession.ts │ │ ├── UserServices │ │ │ └── SimpleListService.ts │ │ └── ContactServices │ │ │ ├── DeleteContactService.ts │ │ │ └── ShowContactService.ts │ ├── errors │ │ └── AppError.ts │ ├── controllers │ │ ├── ImportPhoneContactsController.ts │ │ └── VersionController.ts │ ├── middleware │ │ ├── isAdmin.ts │ │ ├── isSuper.ts │ │ ├── isCompliant.ts │ │ ├── envTokenAuth.ts │ │ └── tokenAuth.ts │ └── models │ │ ├── UserQueue.ts │ │ ├── Translation.ts │ │ ├── Wavoip.ts │ │ ├── Help.ts │ │ ├── WhatsappQueue.ts │ │ ├── Baileys.ts │ │ ├── ContactTag.ts │ │ ├── TicketTag.ts │ │ ├── BaileysKeys.ts │ │ ├── Setting.ts │ │ ├── ContactCustomField.ts │ │ ├── OutOfTicketMessages.ts │ │ ├── CampaignSetting.ts │ │ ├── UserSocketSession.ts │ │ ├── Plan.ts │ │ └── ChatUser.ts ├── .dockerignore ├── .eslintignore ├── .DS_Store ├── prettier.config.js ├── private │ └── index.html ├── .editorconfig ├── .gitignore ├── .sequelizerc ├── .env.dev └── tsconfig.json ├── confs ├── nginx-ticketz.conf └── pgadmin4-servers.json ├── .env-cloudflared ├── .env-frontend-cloudflare ├── frontend ├── .dockerignore ├── nginx │ ├── include.d │ │ ├── allcache.conf │ │ ├── nocache.conf │ │ └── spa.conf │ └── conf.d │ │ └── default.conf ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── mercadopago.png │ ├── ticketzpix.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── config-dev-example.json │ ├── gitinfo.json │ └── manifest.json ├── src │ ├── assets │ │ ├── sound.mp3 │ │ ├── sound.ogg │ │ ├── sacmais.png │ │ ├── wavoip.webp │ │ ├── planilha.xlsx │ │ ├── chat_notify.mp3 │ │ ├── wa-background.png │ │ ├── wa-background-dark.png │ │ └── wa-background-light.png │ ├── helpers │ │ ├── numPad.js │ │ ├── isMobile.js │ │ ├── formatTimeInterval.js │ │ ├── generateSecureToken.js │ │ ├── getTimezoneOffset.js │ │ ├── copyToClipboard.js │ │ ├── safeValueFormat.js │ │ ├── getInitials.js │ │ ├── exportCsv.js │ │ ├── i18nToast.js │ │ ├── getISOStringWithTimezone.js │ │ └── loadJSON.js │ ├── components │ │ ├── CheckoutPage │ │ │ ├── index.js │ │ │ ├── ReviewOrder │ │ │ │ ├── index.js │ │ │ │ ├── styles.js │ │ │ │ └── ReviewOrder.js │ │ │ ├── CheckoutSuccess │ │ │ │ └── index.js │ │ │ ├── styles.js │ │ │ └── FormModel │ │ │ │ ├── formInitialValues.js │ │ │ │ └── validationSchema.js │ │ ├── SubscriptionStepper │ │ │ └── index.js │ │ ├── OnlyForSuperUser │ │ │ └── index.js │ │ ├── FormFields │ │ │ ├── index.js │ │ │ └── InputField.js │ │ ├── Title │ │ │ └── index.js │ │ ├── TicketAdvancedLayout │ │ │ └── index.js │ │ ├── TabPanel │ │ │ └── index.js │ │ ├── MessageOptionsMenu │ │ │ └── style.js │ │ ├── MainHeaderButtonsWrapper │ │ │ └── index.js │ │ ├── BackdropLoading │ │ │ └── index.js │ │ ├── GoogleAnalytics │ │ │ └── index.js │ │ ├── TicketHeader │ │ │ └── index.js │ │ └── MainHeader │ │ │ └── index.js │ ├── services │ │ ├── socket.js │ │ └── api.js │ ├── hooks │ │ ├── useQueues │ │ │ └── index.js │ │ └── useLocalStorage │ │ │ └── index.js │ ├── pages │ │ ├── Dashboard │ │ │ ├── Title.js │ │ │ └── CustomTooltip.js │ │ └── TicketResponsiveContainer │ │ │ └── index.js │ ├── layout │ │ └── themeContext.js │ ├── context │ │ ├── WhatsApp │ │ │ └── WhatsAppsContext.js │ │ ├── ReplyingMessage │ │ │ └── ReplyingMessageContext.js │ │ ├── EditingMessage │ │ │ └── EditingMessageContext.js │ │ ├── Auth │ │ │ └── AuthContext.js │ │ └── Tickets │ │ │ └── TicketsContext.js │ ├── rules.js │ └── translate │ │ ├── i18n.js │ │ └── languages │ │ └── index.js └── .gitignore ├── .env-frontend-local ├── .env-frontend-acme ├── .env-backend-cloudflare ├── .github └── ISSUE_TEMPLATE │ └── feature_request.md ├── .env-backend-local └── .env-backend-acme /.gitignore: -------------------------------------------------------------------------------- 1 | .docker 2 | *~ 3 | /.project 4 | -------------------------------------------------------------------------------- /backend/certs/coloque_seus_certificado_aqui: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /backend/src/waversion.json: -------------------------------------------------------------------------------- 1 | [2, 3000, 1030831524] 2 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | dist 4 | -------------------------------------------------------------------------------- /backend/.eslintignore: -------------------------------------------------------------------------------- 1 | /*.js 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /confs/nginx-ticketz.conf: -------------------------------------------------------------------------------- 1 | client_max_body_size 64m; 2 | -------------------------------------------------------------------------------- /.env-cloudflared: -------------------------------------------------------------------------------- 1 | TUNNEL_TOKEN=TunnelTokenAsProvidedByCloudflare 2 | -------------------------------------------------------------------------------- /.env-frontend-cloudflare: -------------------------------------------------------------------------------- 1 | BACKEND_HOST=apiticketz.example.com 2 | -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public/config.json 3 | dist 4 | -------------------------------------------------------------------------------- /backend/src/@types/qrcode-terminal.d.ts: -------------------------------------------------------------------------------- 1 | declare module "qrcode-terminal"; 2 | -------------------------------------------------------------------------------- /backend/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/backend/.DS_Store -------------------------------------------------------------------------------- /frontend/nginx/include.d/allcache.conf: -------------------------------------------------------------------------------- 1 | expires 1y; 2 | add_header Cache-Control "public"; 3 | access_log off; 4 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/src/assets/sound.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/sound.mp3 -------------------------------------------------------------------------------- /frontend/src/assets/sound.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/sound.ogg -------------------------------------------------------------------------------- /frontend/src/helpers/numPad.js: -------------------------------------------------------------------------------- 1 | export const numPad = (num,places = 2) => String(num).padStart(places, '0'); 2 | -------------------------------------------------------------------------------- /backend/certs/producaoActom.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/backend/certs/producaoActom.p12 -------------------------------------------------------------------------------- /frontend/public/mercadopago.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/mercadopago.png -------------------------------------------------------------------------------- /frontend/public/ticketzpix.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/ticketzpix.png -------------------------------------------------------------------------------- /frontend/src/assets/sacmais.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/sacmais.png -------------------------------------------------------------------------------- /frontend/src/assets/wavoip.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/wavoip.webp -------------------------------------------------------------------------------- /backend/certs/homologacaoActom.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/backend/certs/homologacaoActom.p12 -------------------------------------------------------------------------------- /frontend/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/favicon-16x16.png -------------------------------------------------------------------------------- /frontend/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/favicon-32x32.png -------------------------------------------------------------------------------- /frontend/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/mstile-150x150.png -------------------------------------------------------------------------------- /frontend/src/assets/planilha.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/planilha.xlsx -------------------------------------------------------------------------------- /frontend/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/apple-touch-icon.png -------------------------------------------------------------------------------- /frontend/src/assets/chat_notify.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/chat_notify.mp3 -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/index.js: -------------------------------------------------------------------------------- 1 | import CheckoutPage from './CheckoutPage'; 2 | export default CheckoutPage; 3 | -------------------------------------------------------------------------------- /backend/certs/producao-448607-Admin.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/backend/certs/producao-448607-Admin.p12 -------------------------------------------------------------------------------- /frontend/src/assets/wa-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/wa-background.png -------------------------------------------------------------------------------- /backend/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: false, 3 | trailingComma: "none", 4 | arrowParens: "avoid" 5 | }; 6 | -------------------------------------------------------------------------------- /backend/src/helpers/parseToMilliseconds.ts: -------------------------------------------------------------------------------- 1 | export function parseToMilliseconds(seconds: number) { 2 | return seconds * 1000; 3 | } 4 | -------------------------------------------------------------------------------- /backend/src/helpers/randomValue.ts: -------------------------------------------------------------------------------- 1 | export function randomValue(min, max) { 2 | return Math.floor(Math.random() * max) + min; 3 | } 4 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/ReviewOrder/index.js: -------------------------------------------------------------------------------- 1 | import ReviewOrder from './ReviewOrder'; 2 | export default ReviewOrder; 3 | -------------------------------------------------------------------------------- /frontend/src/helpers/isMobile.js: -------------------------------------------------------------------------------- 1 | export function isMobile() { 2 | return /Mobi|Android|iPhone/i.test(navigator.userAgent); 3 | } 4 | -------------------------------------------------------------------------------- /frontend/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /frontend/src/assets/wa-background-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/wa-background-dark.png -------------------------------------------------------------------------------- /frontend/src/assets/wa-background-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ticketz-oss/ticketz/HEAD/frontend/src/assets/wa-background-light.png -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/CheckoutSuccess/index.js: -------------------------------------------------------------------------------- 1 | import CheckoutSuccess from './CheckoutSuccess'; 2 | export default CheckoutSuccess; 3 | -------------------------------------------------------------------------------- /backend/src/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | 3 | dotenv.config({ 4 | path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" 5 | }); 6 | -------------------------------------------------------------------------------- /backend/private/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Private Folder 4 | 5 | 6 |

Private Folder

7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/src/services/socket.js: -------------------------------------------------------------------------------- 1 | export function socketConnection(_) { 2 | throw new Error("socketConnection not supported anymore. Change to SocketContext"); 3 | } 4 | -------------------------------------------------------------------------------- /frontend/public/config-dev-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "BACKEND_PROTOCOL": "http", 3 | "BACKEND_HOST": "localhost", 4 | "BACKEND_PORT": "8080", 5 | "LOG_LEVEL": "debug" 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/gitinfo.ts: -------------------------------------------------------------------------------- 1 | export const GitInfo = { 2 | commitHash: "", 3 | commitTimestamp: "", 4 | branchName: "", 5 | tagName: "v1.0.x", 6 | buildTimestamp: "custom build" 7 | }; 8 | -------------------------------------------------------------------------------- /frontend/public/gitinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "commitHash": "custom", 3 | "commitTimestamp": "", 4 | "branchName": "", 5 | "tagName": "v1.0.x", 6 | "buildTimestamp": "custom build" 7 | } 8 | -------------------------------------------------------------------------------- /frontend/src/components/SubscriptionStepper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function index() { 4 | return ( 5 |
index
6 | ) 7 | } 8 | 9 | export default index -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /.well-known 4 | /.env 5 | /build 6 | /.project 7 | /public/config.json 8 | /public/socket-admin 9 | /public/ogv 10 | /public/opus-recorder 11 | -------------------------------------------------------------------------------- /backend/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /public/* 4 | /.well-known 5 | /.env 6 | /.externalToolBuilders/ 7 | /.project 8 | /.settings/ 9 | /private/* 10 | !/private/index.html 11 | /src/generated 12 | -------------------------------------------------------------------------------- /frontend/nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | client_max_body_size 20M; 2 | 3 | server { 4 | index index.html; 5 | root /var/www/public/; 6 | 7 | server_name _; 8 | 9 | include sites.d/frontend.conf; 10 | } 11 | -------------------------------------------------------------------------------- /frontend/nginx/include.d/nocache.conf: -------------------------------------------------------------------------------- 1 | add_header Last-Modified $date_gmt; 2 | add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'; 3 | if_modified_since off; 4 | expires off; 5 | etag off; -------------------------------------------------------------------------------- /backend/src/@types/express.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Express { 2 | export interface Request { 3 | user: { id: string; profile: string; isSuper: boolean; companyId: number }; 4 | companyId: number | undefined; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250625163600-standardbackendtranslations.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | up: async () => { 3 | // no-op - replaced by posterior migration 4 | }, 5 | down: async () => { 6 | // no-op 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250810094000-translations-for-greetings.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | up: async () => { 3 | // no-op - replaced by posterior migration 4 | }, 5 | down: async () => { 6 | // no-op 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /.env-frontend-local: -------------------------------------------------------------------------------- 1 | FRONTEND_HOST=localhost 2 | FRONTEND_PORT=3000 3 | BACKEND_PATH=/backend 4 | 5 | BACKEND_HOST=${FRONTEND_HOST} 6 | BACKEND_PROTOCOL=http 7 | BACKEND_PORT=${FRONTEND_PORT} 8 | 9 | EMAIL_ADDRESS=admin@ticketz.host 10 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250810104500-translations-in-german-and-french.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | up: async () => { 3 | // no-op - replaced by posterior migration 4 | }, 5 | down: async () => { 6 | // no-op 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /frontend/src/helpers/formatTimeInterval.js: -------------------------------------------------------------------------------- 1 | export function formatTimeInterval(seconds) { 2 | const hours = Math.floor(seconds / 3600); 3 | const minutes = Math.floor((seconds % 3600) / 60); 4 | return `${hours}h ${minutes}m`; 5 | } 6 | -------------------------------------------------------------------------------- /backend/src/config/redis.ts: -------------------------------------------------------------------------------- 1 | export const REDIS_URI_CONNECTION = process.env.REDIS_URI || ""; 2 | export const REDIS_OPT_LIMITER_MAX = process.env.REDIS_OPT_LIMITER_MAX || 1; 3 | export const REDIS_OPT_LIMITER_DURATION = 4 | process.env.REDIS_OPT_LIMITER_DURATION || 3000; 5 | -------------------------------------------------------------------------------- /frontend/src/components/OnlyForSuperUser/index.js: -------------------------------------------------------------------------------- 1 | const OnlyForSuperUser = ({ user, yes, no }) => user?.super ? yes() : no(); 2 | 3 | OnlyForSuperUser.defaultProps = { 4 | user: {}, 5 | yes: () => null, 6 | no: () => null, 7 | }; 8 | 9 | export default OnlyForSuperUser; -------------------------------------------------------------------------------- /backend/src/routes/versionRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as VersionController from "../controllers/VersionController"; 3 | 4 | const versionRoutes = express.Router(); 5 | 6 | versionRoutes.get("/", VersionController.version); 7 | 8 | export default versionRoutes; 9 | -------------------------------------------------------------------------------- /frontend/src/helpers/generateSecureToken.js: -------------------------------------------------------------------------------- 1 | const generateSecureToken = function (length) { 2 | const array = new Uint8Array(length); 3 | window.crypto.getRandomValues(array); 4 | return btoa(String.fromCharCode.apply(null, array)); 5 | }; 6 | 7 | export { generateSecureToken }; 8 | -------------------------------------------------------------------------------- /frontend/src/components/FormFields/index.js: -------------------------------------------------------------------------------- 1 | import InputField from './InputField'; 2 | import CheckboxField from './CheckboxField'; 3 | import SelectField from './SelectField'; 4 | import DatePickerField from './DatePickerField'; 5 | export { InputField, CheckboxField, SelectField, DatePickerField }; 6 | -------------------------------------------------------------------------------- /backend/src/helpers/GetWhatsappWbot.ts: -------------------------------------------------------------------------------- 1 | import { getWbot } from "../libs/wbot"; 2 | import Whatsapp from "../models/Whatsapp"; 3 | 4 | const GetWhatsappWbot = async (whatsapp: Whatsapp) => { 5 | const wbot = await getWbot(whatsapp.id); 6 | return wbot; 7 | }; 8 | 9 | export default GetWhatsappWbot; 10 | -------------------------------------------------------------------------------- /frontend/src/components/Title/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | 4 | export default function Title(props) { 5 | return ( 6 | 7 | {props.children} 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/helpers/getTimezoneOffset.js: -------------------------------------------------------------------------------- 1 | export function getTimezoneOffset() { 2 | const offset = new Date().getTimezoneOffset(); 3 | const sign = offset <= 0 ? '+' : '-'; 4 | const pad = n => String(Math.floor(Math.abs(n))).padStart(2, '0'); 5 | return sign + pad(offset / 60) + ':' + pad(offset % 60); 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/hooks/useQueues/index.js: -------------------------------------------------------------------------------- 1 | import api from "../../services/api"; 2 | 3 | const useQueues = () => { 4 | const findAll = async () => { 5 | const { data } = await api.get("/queue"); 6 | return data; 7 | } 8 | 9 | return { findAll }; 10 | }; 11 | 12 | export default useQueues; 13 | -------------------------------------------------------------------------------- /backend/src/services/HelpServices/FindService.ts: -------------------------------------------------------------------------------- 1 | import Help from "../../models/Help"; 2 | 3 | const FindService = async (): Promise => { 4 | const notes: Help[] = await Help.findAll({ 5 | order: [["title", "ASC"]] 6 | }); 7 | 8 | return notes; 9 | }; 10 | 11 | export default FindService; 12 | -------------------------------------------------------------------------------- /backend/src/services/HelpServices/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import Help from "../../models/Help"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: Help[] = await Help.findAll({ 5 | order: [["title", "ASC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /backend/src/services/ChatService/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import Chat from "../../models/Chat"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: Chat[] = await Chat.findAll({ 5 | order: [["createdAt", "DESC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /backend/src/services/TicketNoteService/FindAllTicketNotesService.ts: -------------------------------------------------------------------------------- 1 | import TicketNote from "../../models/TicketNote"; 2 | 3 | const FindAllTicketNotesService = async (): Promise => { 4 | const ticketNote = await TicketNote.findAll(); 5 | return ticketNote; 6 | }; 7 | 8 | export default FindAllTicketNotesService; 9 | -------------------------------------------------------------------------------- /backend/src/routes/pwaRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import * as PwaController from "../controllers/PwaController"; 3 | 4 | const pwaRoutes = express.Router(); 5 | 6 | pwaRoutes.get("/manifest.json", PwaController.manifest); 7 | pwaRoutes.get("/favicon.ico", PwaController.favicon); 8 | 9 | export default pwaRoutes; 10 | -------------------------------------------------------------------------------- /backend/src/services/QueueOptionService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import ShowService from "./ShowService"; 2 | 3 | const DeleteService = async (queueOptionId: number | string): Promise => { 4 | const queueOption = await ShowService(queueOptionId); 5 | 6 | await queueOption.destroy(); 7 | }; 8 | 9 | export default DeleteService; 10 | -------------------------------------------------------------------------------- /frontend/src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getBackendURL } from "../services/config"; 3 | 4 | const api = axios.create({ 5 | baseURL: getBackendURL(), 6 | withCredentials: true, 7 | }); 8 | 9 | export const openApi = axios.create({ 10 | baseURL: getBackendURL() 11 | }); 12 | 13 | export default api; 14 | -------------------------------------------------------------------------------- /backend/src/services/CampaignService/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import Campaign from "../../models/Campaign"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: Campaign[] = await Campaign.findAll({ 5 | order: [["name", "ASC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /backend/src/helpers/GetPublicPath.ts: -------------------------------------------------------------------------------- 1 | // get public folder 2 | 3 | import path from "path"; 4 | 5 | export const getPublicPath = () => { 6 | const publicFolder = __dirname.endsWith("/dist") 7 | ? path.resolve(__dirname, "..", "public") 8 | : path.resolve(__dirname, "..", "..", "public"); 9 | 10 | return publicFolder; 11 | }; 12 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/ReviewOrder/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | export default makeStyles(theme => ({ 3 | listItem: { 4 | padding: theme.spacing(1, 0) 5 | }, 6 | total: { 7 | fontWeight: '700' 8 | }, 9 | title: { 10 | marginTop: theme.spacing(2) 11 | } 12 | })); 13 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/Title.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | 4 | const Title = props => { 5 | return ( 6 | 7 | {props.children} 8 | 9 | ); 10 | }; 11 | 12 | export default Title; 13 | -------------------------------------------------------------------------------- /frontend/src/components/TicketAdvancedLayout/index.js: -------------------------------------------------------------------------------- 1 | import { styled } from '@material-ui/core/styles'; 2 | import Paper from '@material-ui/core/Paper'; 3 | 4 | const TicketAdvancedLayout = styled(Paper)({ 5 | height: `calc(100% - 48px)`, 6 | display: "grid", 7 | gridTemplateRows: "56px 1fr" 8 | }) 9 | 10 | export default TicketAdvancedLayout; -------------------------------------------------------------------------------- /backend/src/services/ContactListService/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import ContactList from "../../models/ContactList"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: ContactList[] = await ContactList.findAll({ 5 | order: [["name", "ASC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /backend/.sequelizerc: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | 3 | module.exports = { 4 | "config": resolve(__dirname, "dist", "config", "database.js"), 5 | "modules-path": resolve(__dirname, "dist", "models"), 6 | "migrations-path": resolve(__dirname, "dist", "database", "migrations"), 7 | "seeders-path": resolve(__dirname, "dist", "database", "seeds"), 8 | }; 9 | -------------------------------------------------------------------------------- /backend/src/services/AnnouncementService/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import Announcement from "../../models/Announcement"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: Announcement[] = await Announcement.findAll({ 5 | order: [["createdAt", "DESC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /backend/src/services/QuickMessageService/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import QuickMessage from "../../models/QuickMessage"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: QuickMessage[] = await QuickMessage.findAll({ 5 | order: [["shortcode", "ASC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /confs/pgadmin4-servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Servers": { 3 | "1": { 4 | "Name": "Ticketz Postgres", 5 | "Group": "Servers", 6 | "Port": 5432, 7 | "Username": "ticketz", 8 | "Host": "postgres", 9 | "SSLMode": "prefer", 10 | "MaintenanceDB": "postgres" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/AssociateWhatsappQueue.ts: -------------------------------------------------------------------------------- 1 | import Whatsapp from "../../models/Whatsapp"; 2 | 3 | const AssociateWhatsappQueue = async ( 4 | whatsapp: Whatsapp, 5 | queueIds: number[] 6 | ): Promise => { 7 | await whatsapp.$set("queues", queueIds); 8 | 9 | await whatsapp.reload(); 10 | }; 11 | 12 | export default AssociateWhatsappQueue; 13 | -------------------------------------------------------------------------------- /frontend/src/helpers/copyToClipboard.js: -------------------------------------------------------------------------------- 1 | const copyToClipboard = (text) => { 2 | console.log('text', text) 3 | var textField = document.createElement('textarea') 4 | textField.innerText = text 5 | document.body.appendChild(textField) 6 | textField.select() 7 | document.execCommand('copy') 8 | textField.remove() 9 | } 10 | 11 | export { copyToClipboard }; 12 | -------------------------------------------------------------------------------- /backend/src/services/ContactListItemService/FindAllService.ts: -------------------------------------------------------------------------------- 1 | import ContactListItem from "../../models/ContactListItem"; 2 | 3 | const FindAllService = async (): Promise => { 4 | const records: ContactListItem[] = await ContactListItem.findAll({ 5 | order: [["name", "ASC"]] 6 | }); 7 | return records; 8 | }; 9 | 10 | export default FindAllService; 11 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/DeleteQueueService.ts: -------------------------------------------------------------------------------- 1 | import ShowQueueService from "./ShowQueueService"; 2 | 3 | const DeleteQueueService = async ( 4 | queueId: number | string, 5 | companyId: number 6 | ): Promise => { 7 | const queue = await ShowQueueService(queueId, companyId); 8 | 9 | await queue.destroy(); 10 | }; 11 | 12 | export default DeleteQueueService; 13 | -------------------------------------------------------------------------------- /frontend/src/layout/themeContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ColorModeContext = React.createContext({ 4 | toggleColorMode: () => { }, 5 | setPrimaryColorLight: (_) => { }, 6 | setPrimaryColorDark: (_) => { }, 7 | setAppLogoLight: (_) => { }, 8 | setAppLogoDark: (_) => { }, 9 | setAppLogoFavicon: (_) => { }, 10 | }); 11 | 12 | export default ColorModeContext; -------------------------------------------------------------------------------- /.env-frontend-acme: -------------------------------------------------------------------------------- 1 | FRONTEND_HOST=ticketz.exemplo.com.br 2 | EMAIL_ADDRESS=ticketz@exemplo.com.br 3 | 4 | ### Normalmente não é necessário alterar estes valores 5 | 6 | BACKEND_HOST=${FRONTEND_HOST} 7 | BACKEND_PATH=/backend 8 | 9 | RECAPTCHA_SITE_KEY= 10 | 11 | VIRTUAL_HOST=${FRONTEND_HOST} 12 | VIRTUAL_PORT=80 13 | LETSENCRYPT_HOST=${FRONTEND_HOST} 14 | LETSENCRYPT_EMAIL=${EMAIL_ADDRESS} 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20241201102700-add-unaccent-extension.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.sequelize.query("CREATE EXTENSION IF NOT EXISTS unaccent") 7 | ]); 8 | }, 9 | 10 | down: _ => { 11 | return Promise.resolve(); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/errors/AppError.ts: -------------------------------------------------------------------------------- 1 | class AppError { 2 | public readonly message: string; 3 | 4 | public readonly statusCode: number; 5 | 6 | public readonly level: string; 7 | 8 | constructor(message: string, statusCode = 400, level = "warn") { 9 | this.message = message; 10 | this.statusCode = statusCode; 11 | this.level = level; 12 | } 13 | } 14 | 15 | export default AppError; 16 | -------------------------------------------------------------------------------- /backend/src/services/TagServices/ShowService.ts: -------------------------------------------------------------------------------- 1 | import Tag from "../../models/Tag"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const TagService = async (id: string | number): Promise => { 5 | const tag = await Tag.findByPk(id); 6 | 7 | if (!tag) { 8 | throw new AppError("ERR_NO_TAG_FOUND", 404); 9 | } 10 | 11 | return tag; 12 | }; 13 | 14 | export default TagService; 15 | -------------------------------------------------------------------------------- /frontend/src/helpers/safeValueFormat.js: -------------------------------------------------------------------------------- 1 | export function safeValueFormat(value, currencyCode) { 2 | if (typeof value === 'number') { 3 | try { 4 | return new Intl.NumberFormat(navigator.language, { 5 | style: 'currency', 6 | currency: currencyCode, 7 | }).format(value); 8 | } catch (e) { 9 | return value.toString(); 10 | } 11 | } 12 | return "-"; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/services/ChatService/ShowService.ts: -------------------------------------------------------------------------------- 1 | import Chat from "../../models/Chat"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowService = async (id: string | number): Promise => { 5 | const record = await Chat.findByPk(id); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_CHAT_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowService; 15 | -------------------------------------------------------------------------------- /backend/src/services/HelpServices/ShowService.ts: -------------------------------------------------------------------------------- 1 | import Help from "../../models/Help"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowService = async (id: string | number): Promise => { 5 | const record = await Help.findByPk(id); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_HELP_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowService; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200717133431-add-uuid-ossp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes, Sequelize } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.sequelize.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp"'), 7 | ]); 8 | }, 9 | 10 | down: (_) => { 11 | return Promise.resolve(); 12 | } 13 | 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/services/PlanService/ShowPlanService.ts: -------------------------------------------------------------------------------- 1 | import Plan from "../../models/Plan"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowPlanService = async (id: string | number): Promise => { 5 | const plan = await Plan.findByPk(id); 6 | 7 | if (!plan) { 8 | throw new AppError("ERR_NO_PLAN_FOUND", 404); 9 | } 10 | 11 | return plan; 12 | }; 13 | 14 | export default ShowPlanService; 15 | -------------------------------------------------------------------------------- /backend/src/helpers/SerializeWbotMsgId.ts: -------------------------------------------------------------------------------- 1 | import Message from "../models/Message"; 2 | import Ticket from "../models/Ticket"; 3 | 4 | const SerializeWbotMsgId = (ticket: Ticket, message: Message): string => { 5 | const serializedMsgId = `${message.fromMe}_${ticket.contact.number}@${ 6 | ticket.isGroup ? "g" : "c" 7 | }.us_${message.id}`; 8 | 9 | return serializedMsgId; 10 | }; 11 | 12 | export default SerializeWbotMsgId; 13 | -------------------------------------------------------------------------------- /backend/src/services/BaileysServices/DeleteBaileysService.ts: -------------------------------------------------------------------------------- 1 | import Baileys from "../../models/Baileys"; 2 | 3 | const DeleteBaileysService = async (id: string | number): Promise => { 4 | const baileysData = await Baileys.findOne({ 5 | where: { 6 | whatsappId: id 7 | } 8 | }); 9 | 10 | if (baileysData) { 11 | await baileysData.destroy(); 12 | } 13 | }; 14 | 15 | export default DeleteBaileysService; 16 | -------------------------------------------------------------------------------- /backend/src/services/CampaignService/RestartService.ts: -------------------------------------------------------------------------------- 1 | import Campaign from "../../models/Campaign"; 2 | import { campaignQueue } from "../../queues/campaign"; 3 | 4 | export async function RestartService(id: number) { 5 | const campaign = await Campaign.findByPk(id); 6 | await campaign.update({ status: "EM_ANDAMENTO" }); 7 | 8 | await campaignQueue.add("ProcessCampaign", { 9 | id: campaign.id, 10 | delay: 3000 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/TabPanel/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TabPanel = ({ children, value, name, ...rest }) => { 4 | if (value === name) { 5 | return ( 6 |
12 | <>{children} 13 |
14 | ); 15 | } else return null; 16 | }; 17 | 18 | export default TabPanel; 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200919124112-update-default-column-name-on-whatsappp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.renameColumn("Whatsapps", "default", "isDefault"); 6 | }, 7 | 8 | down: (queryInterface: QueryInterface) => { 9 | return queryInterface.renameColumn("Whatsapps", "isDefault", "default"); 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/services/ChatService/ShowFromUuidService.ts: -------------------------------------------------------------------------------- 1 | import Chat from "../../models/Chat"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowFromUuidService = async (uuid: string): Promise => { 5 | const record = await Chat.findOne({ where: { uuid } }); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_CHAT_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowFromUuidService; 15 | -------------------------------------------------------------------------------- /backend/src/services/TagServices/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import Tag from "../../models/Tag"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string | number): Promise => { 5 | const tag = await Tag.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!tag) { 10 | throw new AppError("ERR_NO_TAG_FOUND", 404); 11 | } 12 | 13 | await tag.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/services/ChatService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import Chat from "../../models/Chat"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await Chat.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_CHAT_FOUND", 404); 11 | } 12 | 13 | await record.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/services/HelpServices/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import Help from "../../models/Help"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await Help.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_HELP_FOUND", 404); 11 | } 12 | 13 | await record.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/services/CompanyService/ShowCompanyService.ts: -------------------------------------------------------------------------------- 1 | import Company from "../../models/Company"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowCompanyService = async (id: string | number): Promise => { 5 | const company = await Company.findByPk(id); 6 | 7 | if (!company) { 8 | throw new AppError("ERR_NO_COMPANY_FOUND", 404); 9 | } 10 | 11 | return company; 12 | }; 13 | 14 | export default ShowCompanyService; 15 | -------------------------------------------------------------------------------- /backend/src/services/ContactListService/ShowService.ts: -------------------------------------------------------------------------------- 1 | import ContactList from "../../models/ContactList"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowService = async (id: string | number): Promise => { 5 | const record = await ContactList.findByPk(id); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_TICKETNOTE_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowService; 15 | -------------------------------------------------------------------------------- /backend/src/services/PlanService/DeletePlanService.ts: -------------------------------------------------------------------------------- 1 | import Plan from "../../models/Plan"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeletePlanService = async (id: string): Promise => { 5 | const plan = await Plan.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!plan) { 10 | throw new AppError("ERR_NO_PLAN_FOUND", 404); 11 | } 12 | 13 | await plan.destroy(); 14 | }; 15 | 16 | export default DeletePlanService; 17 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/ListQueuesService.ts: -------------------------------------------------------------------------------- 1 | import Queue from "../../models/Queue"; 2 | 3 | interface Request { 4 | companyId: number; 5 | } 6 | 7 | const ListQueuesService = async ({ companyId }: Request): Promise => { 8 | const queues = await Queue.findAll({ 9 | where: { 10 | companyId 11 | }, 12 | order: [["name", "ASC"]] 13 | }); 14 | 15 | return queues; 16 | }; 17 | 18 | export default ListQueuesService; 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220221014718-add-remoteJid-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "remoteJid", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Messages", "remoteJid"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220221014719-add-jsonMessage-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "dataJson", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Messages", "dataJson"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250522093400-fix-finidsedAt-on-TicketTraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.sequelize.query(` 6 | UPDATE "TicketTraking" 7 | SET "finishedAt" = "ratingAt" 8 | WHERE "ratingAt" IS NOT NULL; 9 | `); 10 | }, 11 | 12 | down: async () => { 13 | // no operation 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/services/AnnouncementService/ShowService.ts: -------------------------------------------------------------------------------- 1 | import Announcement from "../../models/Announcement"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowService = async (id: string | number): Promise => { 5 | const record = await Announcement.findByPk(id); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_ANNOUNCEMENT_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowService; 15 | -------------------------------------------------------------------------------- /backend/src/services/QuickMessageService/ShowService.ts: -------------------------------------------------------------------------------- 1 | import QuickMessage from "../../models/QuickMessage"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowService = async (id: string | number): Promise => { 5 | const record = await QuickMessage.findByPk(id); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_TICKETNOTE_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowService; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220221014720-add-participant-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "participant", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Messages", "participant"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230723301001-add-kanban-to-Tags.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tags", "kanban", { 6 | type: DataTypes.INTEGER, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Tags", "kanban"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250618151400-remove-online-from-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.removeColumn("Users", "online"); 6 | }, 7 | 8 | down: async (queryInterface: QueryInterface) => { 9 | await queryInterface.addColumn("Users", "online", { 10 | type: "BOOLEAN", 11 | allowNull: true 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108001431-add-unreadMessages-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "unreadMessages", { 6 | type: DataTypes.INTEGER 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Tickets", "unreadMessages"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192513-add-greetingMessage-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "greetingMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "greetingMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192535-add-column-ratingMessage-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "ratingMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "ratingMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210818102609-add-token-to-Whatsapps.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "token", { 6 | type: DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "token"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220220014719-add-farewellMessage-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "farewellMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "farewellMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-channel-session.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "channel", { 6 | type: DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "channel"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/services/ContactListItemService/ShowService.ts: -------------------------------------------------------------------------------- 1 | import ContactListItem from "../../models/ContactListItem"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowService = async (id: string | number): Promise => { 5 | const record = await ContactListItem.findByPk(id); 6 | 7 | if (!record) { 8 | throw new AppError("ERR_NO_CONTACTLISTITEM_FOUND", 404); 9 | } 10 | 11 | return record; 12 | }; 13 | 14 | export default ShowService; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220122160900-add-status-to-schedules.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Schedules", "status", { 6 | type: DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Schedules", "status"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-channel-tokenUser.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "tokenMeta", { 6 | type: DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "tokenMeta"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230528202116-add-transferMessage-field-to-Whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "transferMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "transferMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/services/ContactListService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import ContactList from "../../models/ContactList"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await ContactList.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_CONTACTLIST_FOUND", 404); 11 | } 12 | 13 | await record.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192527-add-column-super-to-Users-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "super", { 6 | type: DataTypes.BOOLEAN, 7 | defaultValue: false 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Users", "super"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192532-add-column-online-to-Users-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "online", { 6 | type: DataTypes.BOOLEAN, 7 | defaultValue: false 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Users", "online"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220221014717-add-provider-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "provider", { 6 | type: DataTypes.TEXT, 7 | defaultValue: "stable" 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "provider"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-channel-to-message.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "channel", { 6 | type: DataTypes.TEXT, 7 | defaultValue: "whatsapp", 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Messages", "channel"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-channel-to-ticket.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "channel", { 6 | type: DataTypes.TEXT, 7 | defaultValue: "whatsapp", 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Tickets", "channel"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240820082900-set-expired-to-TicketTraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, Op } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.bulkUpdate( 6 | "TicketTraking", 7 | { expired: true }, 8 | { 9 | rated: false, 10 | ratingAt: { [Op.not]: null } 11 | } 12 | ); 13 | }, 14 | 15 | down: () => { 16 | return Promise.resolve(true); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250325080200-add-index-id-fromme-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | return queryInterface.addIndex("Messages", ["id", "fromMe"], { 6 | name: "idx_messages_id_fromme" 7 | }); 8 | }, 9 | 10 | down: async (queryInterface: QueryInterface) => { 11 | return queryInterface.removeIndex("Messages", "idx_messages_id_fromme"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250831112100-add-currency-to-Plans.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Plans", "currency", { 6 | type: DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: async (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Plans", "currency"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/services/AnnouncementService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import Announcement from "../../models/Announcement"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await Announcement.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_ANNOUNCEMENT_FOUND", 404); 11 | } 12 | 13 | await record.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/services/QuickMessageService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import QuickMessage from "../../models/QuickMessage"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await QuickMessage.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_QUICKMESSAGE_FOUND", 404); 11 | } 12 | 13 | await record.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/controllers/ImportPhoneContactsController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import ImportContactsService from "../services/WbotServices/ImportContactsService"; 3 | 4 | export const store = async (req: Request, res: Response): Promise => { 5 | const { companyId } = req.user; 6 | const { whatsappId } = req.body; 7 | 8 | await ImportContactsService(companyId, whatsappId); 9 | 10 | return res.status(200).json({ message: "contacts imported" }); 11 | }; 12 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192525-add-column-complationMessage-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "complationMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "complationMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192526-add-column-outOfHoursMessage-to-whatsapp .ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "outOfHoursMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeColumn("Whatsapps", "outOfHoursMessage"); 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220406000000-add-column-dueDate-to-Companies.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Companies", "dueDate", { 6 | type: DataTypes.DATE, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Companies", "dueDate"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-channel-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "channel", { 6 | type: DataTypes.TEXT, 7 | defaultValue: "whatsapp", 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Contacts", "channel"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240522165800-add-disablebot-to-contact.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "disableBot", { 6 | type: DataTypes.BOOLEAN, 7 | defaultValue: false 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Contacts", "disableBot"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240524011900-add-presence-to-contact.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "presence", { 6 | type: DataTypes.STRING, 7 | defaultValue: "available" 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Contacts", "presence"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/DeleteWhatsAppService.ts: -------------------------------------------------------------------------------- 1 | import Whatsapp from "../../models/Whatsapp"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteWhatsAppService = async (id: string): Promise => { 5 | const whatsapp = await Whatsapp.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!whatsapp) { 10 | throw new AppError("ERR_NO_WAPP_FOUND", 404); 11 | } 12 | 13 | await whatsapp.destroy(); 14 | }; 15 | 16 | export default DeleteWhatsAppService; 17 | -------------------------------------------------------------------------------- /frontend/src/pages/TicketResponsiveContainer/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import withWidth, { isWidthUp } from '@material-ui/core/withWidth'; 3 | 4 | import Tickets from "../TicketsCustom" 5 | import TicketAdvanced from "../TicketsAdvanced"; 6 | 7 | function TicketResponsiveContainer (props) { 8 | if (isWidthUp('md', props.width)) { 9 | return ; 10 | } 11 | return 12 | } 13 | 14 | export default withWidth()(TicketResponsiveContainer); -------------------------------------------------------------------------------- /backend/.env.dev: -------------------------------------------------------------------------------- 1 | NODE_ENV= 2 | BACKEND_URL=http://localhost:8080 3 | FRONTEND_URL=http://localhost:3000 4 | #PROXY_PORT=8080 5 | PORT=8080 6 | 7 | DB_DIALECT=postgres 8 | DB_HOST=127.0.0.1 9 | DB_PORT=5432 10 | DB_USER=ticketz 11 | #DB_PASS= 12 | DB_NAME=ticketz 13 | #DB_DEBUG=true 14 | 15 | REDIS_URI=redis://127.0.0.1:6379 16 | REDIS_OPT_LIMITER_MAX=1 17 | REDIS_OPT_LIMITER_DURATION=3000 18 | 19 | USER_LIMIT=10000 20 | CONNECTIONS_LIMIT=100000 21 | CLOSED_SEND_BY_ME=true 22 | 23 | LOG_LEVEL=debug 24 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-channel-token.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "facebookUserToken", { 6 | type: DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "facebookUserToken"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250901093900-add-currency-to-Invoices.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Invoices", "currency", { 6 | type: DataTypes.STRING, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: async (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Invoices", "currency"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200904220257-add-name-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "name", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | unique: true 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Whatsapps", "name"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-facebookUserId-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "facebookUserId", { 6 | type: DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "facebookUserId"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230915212800-add-public-to-plans.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Plans", "isPublic", { 6 | type: DataTypes.BOOLEAN, 7 | defaultValue: true, 8 | allowNull: false 9 | }); 10 | }, 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Plans", "isPublic"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240820082900-add-expired-to-TicketTraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("TicketTraking", "expired", { 6 | type: DataTypes.BOOLEAN, 7 | defaultValue: false 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("TicketTraking", "expired"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/services/ContactListItemService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import ContactListItem from "../../models/ContactListItem"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await ContactListItem.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_CONTACTLISTITEM_FOUND", 404); 11 | } 12 | 13 | await record.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/src/services/QueueOptionService/CreateService.ts: -------------------------------------------------------------------------------- 1 | import QueueOption from "../../models/QueueOption"; 2 | 3 | interface QueueOptionData { 4 | queueId: number; 5 | title: string; 6 | option: string; 7 | message?: string; 8 | parentId?: number; 9 | } 10 | 11 | const CreateService = async (queueOptionData: QueueOptionData): Promise => { 12 | const queueOption = await QueueOption.create(queueOptionData); 13 | return queueOption; 14 | }; 15 | 16 | export default CreateService; 17 | -------------------------------------------------------------------------------- /backend/src/services/TicketNoteService/ShowTicketNoteService.ts: -------------------------------------------------------------------------------- 1 | import TicketNote from "../../models/TicketNote"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowTicketNoteService = async ( 5 | id: string | number 6 | ): Promise => { 7 | const ticketNote = await TicketNote.findByPk(id); 8 | 9 | if (!ticketNote) { 10 | throw new AppError("ERR_NO_TICKETNOTE_FOUND", 404); 11 | } 12 | 13 | return ticketNote; 14 | }; 15 | 16 | export default ShowTicketNoteService; 17 | -------------------------------------------------------------------------------- /frontend/src/components/MessageOptionsMenu/style.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core' 2 | 3 | export const useStyles = makeStyles(theme => ({ 4 | emojiButton: { 5 | cursor: "pointer", 6 | borderRadius: 5, 7 | '&:hover': { 8 | backgroundColor: '#888a' 9 | }, 10 | flexBasis: '25%', 11 | margin: 0, 12 | textAlign: 'center', 13 | }, 14 | flexContainer: { 15 | display: 'flex', 16 | flexWrap: 'wrap', 17 | justifyContent: 'space-around', 18 | }, 19 | })) -------------------------------------------------------------------------------- /backend/src/database/migrations/20200723202116-add-email-field-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "email", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | defaultValue: "" 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "email"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200730153545-add-fromMe-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "fromMe", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Messages", "fromMe"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20222016014719-add-facebookPageUserId-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "facebookPageUserId", { 6 | type: DataTypes.TEXT, 7 | allowNull: true 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeColumn("Whatsapps", "facebookPageUserId"); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/services/ScheduleServices/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import Schedule from "../../models/Schedule"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string | number, companyId: number): Promise => { 5 | const schedule = await Schedule.findOne({ 6 | where: { id, companyId } 7 | }); 8 | 9 | if (!schedule) { 10 | throw new AppError("ERR_NO_SCHEDULE_FOUND", 404); 11 | } 12 | 13 | await schedule.destroy(); 14 | }; 15 | 16 | export default DeleteService; 17 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "outDir": "./dist", 6 | "strict": false, 7 | "strictPropertyInitialization": false, 8 | "esModuleInterop": true, 9 | "experimentalDecorators": true, 10 | "emitDecoratorMetadata": true, 11 | "skipLibCheck": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "typeRoots": ["./node_modules/@types", "./src/@types"], 14 | "resolveJsonModule": true, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200901235509-add-profile-column-to-users.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "profile", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | defaultValue: "admin" 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Users", "profile"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201026215410-add-retries-to-whatsapps.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "retries", { 6 | type: DataTypes.INTEGER, 7 | defaultValue: 0, 8 | allowNull: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Whatsapps", "retries"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/helpers/SendRefreshToken.ts: -------------------------------------------------------------------------------- 1 | import { CookieOptions, Response } from "express"; 2 | 3 | export const SendRefreshToken = (res: Response, token: string): void => { 4 | const cookieOptions: CookieOptions = { 5 | httpOnly: true, 6 | expires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) 7 | }; 8 | 9 | if (process.env.BACKEND_URL.startsWith("https:")) { 10 | cookieOptions.sameSite = "none"; 11 | cookieOptions.secure = true; 12 | } 13 | 14 | res.cookie("jrt", token, cookieOptions); 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/services/TicketNoteService/DeleteTicketNoteService.ts: -------------------------------------------------------------------------------- 1 | import TicketNote from "../../models/TicketNote"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteTicketNoteService = async (id: string): Promise => { 5 | const ticketnote = await TicketNote.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!ticketnote) { 10 | throw new AppError("ERR_NO_TICKETNOTE_FOUND", 404); 11 | } 12 | 13 | await ticketnote.destroy(); 14 | }; 15 | 16 | export default DeleteTicketNoteService; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200813114236-change-ticket-lastMessage-column-type.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.changeColumn("Tickets", "lastMessage", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.changeColumn("Tickets", "lastMessage", { 12 | type: DataTypes.STRING 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200929145451-add-user-tokenVersion-column.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "tokenVersion", { 6 | type: DataTypes.INTEGER, 7 | allowNull: false, 8 | defaultValue: 0 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Users", "tokenVersion"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200930162323-add-isGroup-column-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "isGroup", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Tickets", "isGroup"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200930194808-add-isGroup-column-to-contacts.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "isGroup", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Contacts", "isGroup"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /frontend/src/context/WhatsApp/WhatsAppsContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | 3 | import useWhatsApps from "../../hooks/useWhatsApps"; 4 | 5 | const WhatsAppsContext = createContext(); 6 | 7 | const WhatsAppsProvider = ({ children }) => { 8 | const { loading, whatsApps } = useWhatsApps(); 9 | 10 | return ( 11 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { WhatsAppsContext, WhatsAppsProvider }; 18 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200906122228-add-name-default-field-to-whatsapp.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "default", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Whatsapps", "default"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200927220708-add-isDeleted-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "isDeleted", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Messages", "isDeleted"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192528-change-column-message-to-quick-messages-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.changeColumn("QuickMessages", "message", { 6 | type: DataTypes.TEXT 7 | }); 8 | }, 9 | down: (queryInterface: QueryInterface) => { 10 | return queryInterface.changeColumn("QuickMessages", "message", { 11 | type: DataTypes.STRING 12 | }); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20231219153800-add-isEdited-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "isEdited", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Messages", "isEdited"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250410205700-add-index-ticketId-to-ticketTraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addIndex("TicketTraking", ["ticketId"], { 6 | name: "idx_tickettraking_ticketid" 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeIndex( 12 | "TicketTraking", 13 | "idx_tickettraking_ticketid" 14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/helpers/CheckContactSomeTicket.ts: -------------------------------------------------------------------------------- 1 | import { Op } from "sequelize"; 2 | import AppError from "../errors/AppError"; 3 | import Ticket from "../models/Ticket"; 4 | 5 | const CheckContactSomeTickets = async ( 6 | contactId: number, 7 | companyId: number 8 | ): Promise => { 9 | const ticket = await Ticket.findOne({ 10 | where: { contactId, companyId } 11 | }); 12 | 13 | if (ticket) { 14 | throw new AppError("ERR_OTHER_OPEN_TICKET"); 15 | } 16 | }; 17 | 18 | export default CheckContactSomeTickets; 19 | -------------------------------------------------------------------------------- /backend/src/middleware/isAdmin.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import AppError from "../errors/AppError"; 3 | import User from "../models/User"; 4 | 5 | const isAdmin = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ): Promise => { 10 | const { profile } = await User.findByPk(req.user.id); 11 | if (profile !== "admin") { 12 | throw new AppError("Acesso não permitido", 403); 13 | } 14 | 15 | return next(); 16 | }; 17 | 18 | export default isAdmin; 19 | -------------------------------------------------------------------------------- /backend/src/middleware/isSuper.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import AppError from "../errors/AppError"; 3 | import User from "../models/User"; 4 | 5 | const isSuper = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ): Promise => { 10 | const { super: isSuperUser } = await User.findByPk(req.user.id); 11 | if (!isSuperUser) { 12 | throw new AppError("Acesso não permitido", 401); 13 | } 14 | 15 | return next(); 16 | }; 17 | 18 | export default isSuper; 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220406000001-add-column-recurrence-to-Companies.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Companies", "recurrence", { 6 | type: DataTypes.STRING, 7 | allowNull: true, 8 | defaultValue: "" 9 | }); 10 | }, 11 | 12 | down: (queryInterface: QueryInterface) => { 13 | return queryInterface.removeColumn("Companies", "recurrence"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250725084700-remove-incoherent-queue-associations.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.sequelize.query(` 6 | UPDATE "Tickets" 7 | SET "queueId" = NULL 8 | WHERE "queueId" IS NOT NULL 9 | AND "companyId" != (SELECT "companyId" FROM "Queues" WHERE "id" = "Tickets"."queueId"); 10 | `); 11 | }, 12 | 13 | down: async () => { 14 | // no-op 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/services/BaileysServices/ShowBaileysService.ts: -------------------------------------------------------------------------------- 1 | import Baileys from "../../models/Baileys"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowBaileysService = async (id: string | number): Promise => { 5 | const baileysData = await Baileys.findOne({ 6 | where: { 7 | whatsappId: id 8 | } 9 | }); 10 | 11 | if (!baileysData) { 12 | throw new AppError("ERR_NO_BAILEYS_DATA_FOUND", 404); 13 | } 14 | 15 | return baileysData; 16 | }; 17 | 18 | export default ShowBaileysService; 19 | -------------------------------------------------------------------------------- /frontend/src/rules.js: -------------------------------------------------------------------------------- 1 | const rules = { 2 | user: { 3 | static: [], 4 | }, 5 | 6 | admin: { 7 | static: [ 8 | //"dashboard:view", 9 | "drawer-admin-items:view", 10 | "tickets-manager:showall", 11 | "user-modal:editProfile", 12 | "user-modal:editQueues", 13 | "ticket-options:deleteTicket", 14 | "contacts-page:deleteContact", 15 | "connections-page:actionButtons", 16 | "connections-page:addConnection", 17 | "connections-page:editOrDeleteConnection" 18 | ], 19 | }, 20 | }; 21 | 22 | export default rules; 23 | -------------------------------------------------------------------------------- /frontend/src/translate/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from "i18next"; 2 | import LanguageDetector from "i18next-browser-languagedetector"; 3 | 4 | import { messages } from "./languages"; 5 | 6 | i18n.use(LanguageDetector).init({ 7 | debug: false, 8 | detection: { 9 | order: ['localStorage', 'navigator'], 10 | lookupLocalStorage: 'language', 11 | caches: ['localStorage'], 12 | }, 13 | defaultNS: ["translations"], 14 | fallbackLng: "en", 15 | ns: ["translations"], 16 | resources: messages, 17 | }); 18 | 19 | export { i18n }; 20 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20241128175800-add-save-messages-to-schedules.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.addColumn("Schedules", "saveMessage", { 6 | type: DataTypes.BOOLEAN, 7 | allowNull: false, 8 | defaultValue: false 9 | }); 10 | }, 11 | 12 | down: async (queryInterface: QueryInterface) => { 13 | await queryInterface.removeColumn("Schedules", "saveMessage"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/routes/subScriptionRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as SubscriptionController from "../controllers/SubscriptionController"; 5 | 6 | const subscriptionRoutes = express.Router(); 7 | subscriptionRoutes.post( 8 | "/subscription", 9 | isAuth, 10 | SubscriptionController.createSubscription 11 | ); 12 | subscriptionRoutes.post( 13 | "/subscription/ticketz/webhook/:type?", 14 | SubscriptionController.webhook 15 | ); 16 | 17 | export default subscriptionRoutes; 18 | -------------------------------------------------------------------------------- /frontend/src/helpers/getInitials.js: -------------------------------------------------------------------------------- 1 | 2 | var getInitials = function(string) { 3 | if (!string) { 4 | return ""; 5 | } 6 | 7 | let initials = ""; 8 | 9 | var names = string.trim().split(' '); 10 | 11 | if (names.length > 0) { 12 | initials = Array.from(names[0])[0]; 13 | } 14 | 15 | if (names.length > 1) { 16 | initials += Array.from(names[names.length - 1])[0]; 17 | } 18 | 19 | if (!initials) { 20 | return "👤"; 21 | } 22 | 23 | return initials.toUpperCase(); 24 | }; 25 | 26 | export { getInitials }; 27 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220814000000-add-value-to-plans.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("Plans", "value", { 7 | type: DataTypes.INTEGER, 8 | allowNull: true, 9 | defaultValue: 199.99 10 | }) 11 | ]); 12 | }, 13 | 14 | down: (queryInterface: QueryInterface) => { 15 | return queryInterface.removeColumn("Plans", "value"); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250327164700-add-index-quotedMsgId-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addIndex("Messages", ["ticketId", "quotedMsgId"], { 6 | name: "idx_messages_ticketid_quotedmsgid" 7 | }); 8 | }, 9 | 10 | down: (queryInterface: QueryInterface) => { 11 | return queryInterface.removeIndex( 12 | "Messages", 13 | "idx_messages_ticketid_quotedmsgid" 14 | ); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /frontend/src/context/ReplyingMessage/ReplyingMessageContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext } from "react"; 2 | 3 | const ReplyMessageContext = createContext(); 4 | 5 | const ReplyMessageProvider = ({ children }) => { 6 | const [replyingMessage, setReplyingMessage] = useState(null); 7 | 8 | return ( 9 | 12 | {children} 13 | 14 | ); 15 | }; 16 | 17 | export { ReplyMessageContext, ReplyMessageProvider }; 18 | -------------------------------------------------------------------------------- /backend/src/services/PlanService/FindAllPlanService.ts: -------------------------------------------------------------------------------- 1 | import Plan from "../../models/Plan"; 2 | 3 | const FindAllPlanService = async (listPublic: boolean): Promise => { 4 | let plan: Plan[]; 5 | if (listPublic) 6 | { 7 | plan = await Plan.findAll({ 8 | where: { 9 | isPublic: true 10 | }, 11 | order: [["name", "ASC"]] 12 | }); 13 | } else { 14 | plan = await Plan.findAll({ 15 | order: [["name", "ASC"]] 16 | }); 17 | } 18 | return plan; 19 | }; 20 | 21 | export default FindAllPlanService; 22 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210818102607-remove-unique-indexes-to-Contacts-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.removeConstraint("Contacts","number_companyid_unique" ) 6 | 7 | }, 8 | 9 | down: async (queryInterface: QueryInterface) => { 10 | await queryInterface.addConstraint("Contacts", { fields: ["number", "companyId"], 11 | type: "unique", 12 | name: "number_companyid_unique" 13 | }); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/helpers/MakeRandomId.ts: -------------------------------------------------------------------------------- 1 | // generate random id string for file names, function got from: https://stackoverflow.com/a/1349426/1851801 2 | export function makeRandomId(length: number) { 3 | let result = ""; 4 | const characters = 5 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 6 | const charactersLength = characters.length; 7 | let counter = 0; 8 | while (counter < length) { 9 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 10 | counter += 1; 11 | } 12 | return result; 13 | } 14 | -------------------------------------------------------------------------------- /backend/src/helpers/GetTicketWbot.ts: -------------------------------------------------------------------------------- 1 | import { getWbot, Session } from "../libs/wbot"; 2 | import GetDefaultWhatsApp from "./GetDefaultWhatsApp"; 3 | import Ticket from "../models/Ticket"; 4 | 5 | const GetTicketWbot = async (ticket: Ticket): Promise => { 6 | if (!ticket.whatsappId) { 7 | const defaultWhatsapp = await GetDefaultWhatsApp(ticket.companyId); 8 | 9 | await ticket.$set("whatsapp", defaultWhatsapp); 10 | } 11 | 12 | const wbot = getWbot(ticket.whatsappId); 13 | 14 | return wbot; 15 | }; 16 | 17 | export default GetTicketWbot; 18 | -------------------------------------------------------------------------------- /backend/src/services/SettingServices/GetSuperSettingService.ts: -------------------------------------------------------------------------------- 1 | import Setting from "../../models/Setting"; 2 | 3 | interface Request { 4 | key: string; 5 | } 6 | 7 | const GetSuperSettingService = async ({ 8 | key 9 | }: Request): Promise => { 10 | 11 | if (!key.startsWith("_")) { 12 | return null; 13 | } 14 | 15 | const setting = await Setting.findOne({ 16 | where: { 17 | companyId: 1, 18 | key 19 | } 20 | }); 21 | 22 | return setting?.value; 23 | }; 24 | 25 | export default GetSuperSettingService; 26 | -------------------------------------------------------------------------------- /.env-backend-cloudflare: -------------------------------------------------------------------------------- 1 | BACKEND_URL=https://apiticketz.example.com 2 | FRONTEND_URL=https://ticketz.example.com 3 | TZ=America/Sao_Paulo 4 | 5 | ## normalmente não precisa editar daqui em diante 6 | 7 | DB_DIALECT=postgres 8 | DB_HOST=postgres 9 | DB_PORT=5432 10 | DB_USER=ticketz 11 | DB_NAME=ticketz 12 | DB_TIMEZONE=-03:00 13 | 14 | REDIS_URI=redis://redis:6379 15 | REDIS_OPT_LIMITER_MAX=1 16 | REDIS_OPT_LIMITER_DURATION=3000 17 | 18 | USER_LIMIT=10000 19 | CONNECTIONS_LIMIT=100000 20 | CLOSED_SEND_BY_ME=true 21 | 22 | VERIFY_TOKEN=ticketz 23 | SOCKET_ADMIN=true 24 | -------------------------------------------------------------------------------- /backend/src/config/upload.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import multer from "multer"; 3 | 4 | const publicFolder = __dirname.endsWith("/dist") 5 | ? path.resolve(__dirname, "..", "public") 6 | : path.resolve(__dirname, "..", "..", "public"); 7 | 8 | export default { 9 | directory: publicFolder, 10 | 11 | storage: multer.diskStorage({ 12 | destination: publicFolder, 13 | filename(req, file, cb) { 14 | const fileName = new Date().getTime() + path.extname(file.originalname); 15 | 16 | return cb(null, fileName); 17 | } 18 | }) 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240417150500-add-unique-constrait-to-Contacts-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addConstraint("Contacts", { fields: ["number", "companyId"], 6 | type: "unique", 7 | name: "number_companyid_unique" 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeConstraint( 13 | "Contacts", 14 | "number_companyid_unique" 15 | ); 16 | } 17 | }; -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108164504-add-queueId-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "queueId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Queues", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Tickets", "queueId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192530-add-unique-constraint-to-Contacts-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addConstraint("Contacts", { fields: ["number", "companyId"], 6 | type: "unique", 7 | name: "number_companyid_unique" 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeConstraint( 13 | "Contacts", 14 | "number_companyid_unique" 15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/helpers/replaceExtension.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Replace the file extension of a given filename. 3 | * 4 | * Up to 4 characters are considered for the original extension. 5 | * 6 | * @param {string} filename - The original filename. 7 | * @param {string} newExtension - The new file extension (without the dot). 8 | * @returns {string} - The filename with the new extension. 9 | */ 10 | export function replaceFileExtension( 11 | filename: string, 12 | newExtension: string 13 | ): string { 14 | return filename.replace(/(\.[^/.]{0,4})?$/, `.${newExtension}`); 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/models/UserQueue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | ForeignKey 8 | } from "sequelize-typescript"; 9 | import Queue from "./Queue"; 10 | import User from "./User"; 11 | 12 | @Table 13 | class UserQueue extends Model { 14 | @ForeignKey(() => User) 15 | @Column 16 | userId: number; 17 | 18 | @ForeignKey(() => Queue) 19 | @Column 20 | queueId: number; 21 | 22 | @CreatedAt 23 | createdAt: Date; 24 | 25 | @UpdatedAt 26 | updatedAt: Date; 27 | } 28 | 29 | export default UserQueue; 30 | -------------------------------------------------------------------------------- /backend/src/config/privateFiles.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import multer from "multer"; 3 | 4 | const privateFolder = __dirname.endsWith("/dist") 5 | ? path.resolve(__dirname, "..", "private") 6 | : path.resolve(__dirname, "..", "..", "private"); 7 | 8 | export default { 9 | directory: privateFolder, 10 | 11 | storage: multer.diskStorage({ 12 | destination: privateFolder, 13 | filename(req, file, cb) { 14 | const fileName = new Date().getTime() + path.extname(file.originalname); 15 | 16 | return cb(null, fileName); 17 | } 18 | }) 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/controllers/VersionController.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from "express"; 2 | import GetPublicSettingService from "../services/SettingServices/GetPublicSettingService"; 3 | import { GitInfo } from "../gitinfo"; 4 | 5 | export const version = async ( 6 | req: Request, 7 | res: Response 8 | ): Promise => { 9 | const appName = await GetPublicSettingService({ key: "appName" }); 10 | 11 | const data = { 12 | name: appName || "Ticketz - Chat Based Ticket System", 13 | ...GitInfo 14 | }; 15 | 16 | return res.status(200).json(data); 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20232016014719-add-column-mediaType-to-ChatMessages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | module.exports = { 3 | up: (queryInterface: QueryInterface) => { 4 | return Promise.all([ 5 | queryInterface.addColumn("ChatMessages", "mediaType", { 6 | type: DataTypes.TEXT, 7 | allowNull: true, 8 | }), 9 | ]); 10 | }, 11 | down: (queryInterface: QueryInterface) => { 12 | return Promise.all([ 13 | queryInterface.removeColumn("ChatMessages", "mediaType"), 14 | ]); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20240827171200-add-thumbnailurl-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("Messages", "thumbnailUrl", { 7 | type: DataTypes.STRING, 8 | allowNull: true 9 | }) 10 | ]); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return Promise.all([ 15 | queryInterface.removeColumn("Messages", "thumbnailUrl") 16 | ]); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/context/EditingMessage/EditingMessageContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState, createContext } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const EditMessageContext = createContext(); 5 | 6 | const EditMessageProvider = ({ children }) => { 7 | const [editingMessage, setEditingMessage] = useState(null); 8 | 9 | return ( 10 | 13 | {children} 14 | 15 | ); 16 | }; 17 | 18 | export { EditMessageContext, EditMessageProvider}; 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192523-add-column-planId-to-Companies.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Companies", "planId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Plans", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Companies", "planId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192536-add-unique-constraint-to-Tickets-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addConstraint("Tickets", { fields: ["contactId", "companyId"], 6 | type: "unique", 7 | name: "contactid_companyid_unique" 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.removeConstraint( 13 | "Tickets", 14 | "contactid_companyid_unique" 15 | ); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20231230100900-remove-unique-constraint-to-ticket-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.removeConstraint( 6 | "Tickets", 7 | "contactid_companyid_unique" 8 | ); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.addConstraint("Tickets", { fields: ["contactId", "companyId"], 13 | type: "unique", 14 | name: "contactid_companyid_unique" 15 | }); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /backend/src/models/Translation.ts: -------------------------------------------------------------------------------- 1 | import { Table, Column, Model, Index } from "sequelize-typescript"; 2 | 3 | @Table({ 4 | tableName: "Translations", 5 | timestamps: false 6 | }) 7 | class Translation extends Model { 8 | @Index("index_language") 9 | @Column({ primaryKey: true }) 10 | language: string; 11 | 12 | @Index("index_namespace") 13 | @Column({ primaryKey: true }) 14 | namespace: string; 15 | 16 | @Index("index_key") 17 | @Column({ primaryKey: true }) 18 | key: string; 19 | 20 | @Column 21 | value: string; 22 | } 23 | 24 | export default Translation; 25 | -------------------------------------------------------------------------------- /frontend/src/helpers/exportCsv.js: -------------------------------------------------------------------------------- 1 | import { unparse } from 'papaparse'; 2 | import { toast } from "react-toastify"; 3 | 4 | export function exportCsv(data, filename) { 5 | const result = unparse(data); 6 | if (!result) { 7 | toast.error("Error generating CSV"); 8 | return; 9 | } 10 | 11 | const blob = new Blob([result], { type: "text/csv;charset=utf-8;" }); 12 | const url = URL.createObjectURL(blob); 13 | const a = document.createElement("a"); 14 | a.href = url; 15 | a.download = filename || "report.csv"; 16 | a.click(); 17 | URL.revokeObjectURL(url); 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200730153237-remove-user-association-from-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.removeColumn("Messages", "userId"); 6 | }, 7 | 8 | down: (queryInterface: QueryInterface) => { 9 | return queryInterface.addColumn("Messages", "userId", { 10 | type: DataTypes.INTEGER, 11 | references: { model: "Users", key: "id" }, 12 | onUpdate: "CASCADE", 13 | onDelete: "SET NULL" 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200906155658-add-whatsapp-field-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "whatsappId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Whatsapps", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Tickets", "whatsappId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201004150008-add-contactId-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "contactId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Contacts", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "CASCADE" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "contactId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201028124427-add-quoted-msg-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "quotedMsgId", { 6 | type: DataTypes.STRING, 7 | references: { model: "Messages", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "quotedMsgId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192516-add-column-companyId-to-Users-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Users", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Users", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220404000000-add-column-queueId-to-Messages-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "queueId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Queues", key: "id" }, 8 | onUpdate: "SET NULL", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "queueId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/routes/campaignSettingRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as CampaignSettingController from "../controllers/CampaignSettingController"; 5 | import multer from "multer"; 6 | import uploadConfig from "../config/upload"; 7 | 8 | const upload = multer(uploadConfig); 9 | 10 | const routes = express.Router(); 11 | 12 | routes.get("/campaign-settings", isAuth, CampaignSettingController.index); 13 | 14 | routes.post("/campaign-settings", isAuth, CampaignSettingController.store); 15 | 16 | export default routes; 17 | -------------------------------------------------------------------------------- /backend/src/services/CampaignService/FindService.ts: -------------------------------------------------------------------------------- 1 | import Campaign from "../../models/Campaign"; 2 | import Company from "../../models/Company"; 3 | 4 | type Params = { 5 | companyId: string; 6 | }; 7 | 8 | const FindService = async ({ companyId }: Params): Promise => { 9 | const notes: Campaign[] = await Campaign.findAll({ 10 | where: { 11 | companyId 12 | }, 13 | include: [{ model: Company, as: "company", attributes: ["id", "name"] }], 14 | order: [["name", "ASC"]] 15 | }); 16 | 17 | return notes; 18 | }; 19 | 20 | export default FindService; 21 | -------------------------------------------------------------------------------- /backend/src/services/CompanyService/FindAllCompaniesService.ts: -------------------------------------------------------------------------------- 1 | import Company from "../../models/Company"; 2 | import Plan from "../../models/Plan"; 3 | import Setting from "../../models/Setting"; 4 | 5 | const FindAllCompanyService = async (): Promise => { 6 | const companies = await Company.findAll({ 7 | order: [["name", "ASC"]], 8 | include: [ 9 | { model: Plan, as: "plan", attributes: ["id", "name", "value"] }, 10 | { model: Setting, as: "settings" } 11 | ] 12 | }); 13 | return companies; 14 | }; 15 | 16 | export default FindAllCompanyService; 17 | -------------------------------------------------------------------------------- /frontend/src/context/Auth/AuthContext.js: -------------------------------------------------------------------------------- 1 | import React, { createContext } from "react"; 2 | 3 | import useAuth from "../../hooks/useAuth.js"; 4 | 5 | const AuthContext = createContext(); 6 | 7 | const AuthProvider = ({ children }) => { 8 | const { loading, user, isAuth, handleLogin, handleImpersonate, handleLogout } = useAuth(); 9 | 10 | return ( 11 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export { AuthContext, AuthProvider }; 20 | -------------------------------------------------------------------------------- /frontend/src/helpers/i18nToast.js: -------------------------------------------------------------------------------- 1 | import { toast as realToast } from 'react-toastify'; 2 | import { i18n } from '../translate/i18n'; 3 | 4 | export const i18nToast = { 5 | error: (message, options) => { 6 | return realToast.error(i18n.t(message), options); 7 | }, 8 | success: (message, options) => { 9 | return realToast.success(i18n.t(message), options); 10 | }, 11 | info: (message, options) => { 12 | return realToast.info(i18n.t(message), options); 13 | }, 14 | warn: (message, options) => { 15 | return realToast.warn(i18n.t(message), options); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192519-add-column-companyId-to-Queues-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Queues", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Queues", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192521-add-column-companyId-to-Tickets-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Tickets", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Tickets", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210818102606-add-uuid-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes, Sequelize } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("Tickets", "uuid", { 7 | type: DataTypes.UUID, 8 | allowNull: true, 9 | defaultValue: Sequelize.literal('uuid_generate_v4()') 10 | }) 11 | ]); 12 | }, 13 | 14 | down: (queryInterface: QueryInterface) => { 15 | return queryInterface.removeColumn("Tickets", "uuid"); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/src/components/MainHeaderButtonsWrapper/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | 5 | const useStyles = makeStyles(theme => ({ 6 | MainHeaderButtonsWrapper: { 7 | flex: "none", 8 | marginLeft: "auto", 9 | "& > *": { 10 | margin: theme.spacing(1), 11 | }, 12 | }, 13 | })); 14 | 15 | const MainHeaderButtonsWrapper = ({ children }) => { 16 | const classes = useStyles(); 17 | 18 | return
{children}
; 19 | }; 20 | 21 | export default MainHeaderButtonsWrapper; 22 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192515-add-column-companyId-to-Settings-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Settings", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Settings", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192517-add-column-companyId-to-Contacts-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Contacts", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Contacts", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192518-add-column-companyId-to-Messages-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220115114088-add-column-userId-to-QuickMessages-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("QuickMessages", "userId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Users", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "CASCADE" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("QuickMessages", "userId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250516095500-add-chatbotendat-to-tickettraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.addColumn("TicketTraking", "chatbotendAt", { 6 | type: DataTypes.DATE, 7 | allowNull: true, 8 | comment: "Timestamp indicating when the chatbot interaction ended" 9 | }); 10 | }, 11 | 12 | down: async (queryInterface: QueryInterface) => { 13 | await queryInterface.removeColumn("TicketTraking", "chatbotendAt"); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /backend/src/helpers/URLCharEncoder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Encodes a string for use in a URL, while preserving certain characters. 3 | * This function encodes the string using encodeURIComponent, 4 | * but then replaces specific encoded characters 5 | * to ensure they remain intact. 6 | * 7 | * @param {string} str - The string to encode. 8 | * @return {string} - The encoded string with specific characters preserved. 9 | */ 10 | export function URLCharEncoder(str) { 11 | return encodeURIComponent(str) 12 | .replace(/%2F/g, "/") 13 | .replace(/%3A/g, ":") 14 | .replace(/%2E/g, "."); 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/models/Wavoip.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | DataType, 6 | PrimaryKey, 7 | AutoIncrement, 8 | ForeignKey, 9 | BelongsTo 10 | } from "sequelize-typescript"; 11 | import Whatsapp from "./Whatsapp"; 12 | 13 | @Table 14 | export default class Wavoip extends Model { 15 | @PrimaryKey 16 | @AutoIncrement 17 | @Column 18 | id: number; 19 | 20 | @Column(DataType.TEXT) 21 | token: string; 22 | 23 | @ForeignKey(() => Whatsapp) 24 | @Column 25 | whatsappId: number; 26 | 27 | @BelongsTo(() => Whatsapp) 28 | whatsapp: Whatsapp; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/routes/ticketzOSSRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import isAuth from "../middleware/isAuth"; 4 | import isSuper from "../middleware/isAdmin"; 5 | import * as TicketzOSSController from "../controllers/TicketzOSSController"; 6 | 7 | const ticketzOSSRoutes = Router(); 8 | 9 | ticketzOSSRoutes.get( 10 | "/ticketz/registry", 11 | isAuth, 12 | isSuper, 13 | TicketzOSSController.show 14 | ); 15 | 16 | ticketzOSSRoutes.post( 17 | "/ticketz/registry", 18 | isAuth, 19 | isSuper, 20 | TicketzOSSController.store 21 | ); 22 | 23 | export default ticketzOSSRoutes; 24 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/styles.js: -------------------------------------------------------------------------------- 1 | import { makeStyles } from '@material-ui/core/styles'; 2 | export default makeStyles(theme => ({ 3 | stepper: { 4 | padding: theme.spacing(3, 0, 5) 5 | }, 6 | buttons: { 7 | display: 'flex', 8 | justifyContent: 'flex-end' 9 | }, 10 | button: { 11 | marginTop: theme.spacing(3), 12 | marginLeft: theme.spacing(1) 13 | }, 14 | wrapper: { 15 | margin: theme.spacing(1), 16 | position: 'relative' 17 | }, 18 | buttonProgress: { 19 | position: 'absolute', 20 | top: '50%', 21 | left: '50%' 22 | } 23 | })); 24 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201004155719-add-vcardContactId-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Messages", "vcardContactId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Contacts", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "CASCADE" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Messages", "vcardContactId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192520-add-column-companyId-to-Whatsapps-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Whatsapps", "companyId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Companies", key: "id" }, 8 | onUpdate: "CASCADE", 9 | onDelete: "SET NULL" 10 | }); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return queryInterface.removeColumn("Whatsapps", "companyId"); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/services/InvoicesService/FindAllInvoiceService.ts: -------------------------------------------------------------------------------- 1 | import Invoices from "../../models/Invoices"; 2 | 3 | const FindAllPlanService = async (companyId: number): Promise => { 4 | const invoice = await Invoices.findAll({ 5 | attributes: [ 6 | "id", 7 | "detail", 8 | "value", 9 | "currency", 10 | "dueDate", 11 | "status", 12 | "createdAt", 13 | "updatedAt" 14 | ], 15 | where: { 16 | companyId 17 | }, 18 | order: [["id", "ASC"]] 19 | }); 20 | return invoice; 21 | }; 22 | 23 | export default FindAllPlanService; 24 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20201004955719-remove-vcardContactId-column-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.removeColumn("Messages", "vcardContactId"); 6 | }, 7 | 8 | down: (queryInterface: QueryInterface) => { 9 | return queryInterface.addColumn("Messages", "vcardContactId", { 10 | type: DataTypes.INTEGER, 11 | references: { model: "Contacts", key: "id" }, 12 | onUpdate: "CASCADE", 13 | onDelete: "CASCADE" 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /backend/src/services/ContactListService/FindService.ts: -------------------------------------------------------------------------------- 1 | import ContactList from "../../models/ContactList"; 2 | import Company from "../../models/Company"; 3 | 4 | type Params = { 5 | companyId: string; 6 | }; 7 | 8 | const FindService = async ({ companyId }: Params): Promise => { 9 | const notes: ContactList[] = await ContactList.findAll({ 10 | where: { 11 | companyId 12 | }, 13 | include: [{ model: Company, as: "company", attributes: ["id", "name"] }], 14 | order: [["name", "ASC"]] 15 | }); 16 | 17 | return notes; 18 | }; 19 | 20 | export default FindService; 21 | -------------------------------------------------------------------------------- /backend/src/services/MessageServices/GetMessagesService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Message from "../../models/Message"; 3 | 4 | interface Request { 5 | id: string; 6 | ticketId: number; 7 | } 8 | 9 | const GetMessageService = async ({ 10 | id, 11 | ticketId 12 | }: Request): Promise => { 13 | const messageExists = await Message.findOne({ 14 | where: { id, ticketId } 15 | }); 16 | 17 | if (!messageExists) { 18 | throw new AppError("MESSAGE_NOT_FIND"); 19 | } 20 | 21 | return messageExists; 22 | }; 23 | 24 | export default GetMessageService; 25 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250326110400-add-indexes-to-baileyskeys.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | return queryInterface.addIndex( 6 | "BaileysKeys", 7 | ["whatsappId", "type", "key"], 8 | { 9 | name: "idx_baileyskeys_whatsappid_type_key" 10 | } 11 | ); 12 | }, 13 | 14 | down: async (queryInterface: QueryInterface) => { 15 | return queryInterface.removeIndex( 16 | "BaileysKeys", 17 | "idx_baileyskeys_whatsappid_type_key" 18 | ); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src/services/CampaignService/DeleteService.ts: -------------------------------------------------------------------------------- 1 | import Campaign from "../../models/Campaign"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const DeleteService = async (id: string): Promise => { 5 | const record = await Campaign.findOne({ 6 | where: { id } 7 | }); 8 | 9 | if (!record) { 10 | throw new AppError("ERR_NO_CAMPAIGN_FOUND", 404); 11 | } 12 | 13 | if (record.status === "EM_ANDAMENTO") { 14 | throw new AppError("Não é permitido excluir campanha em andamento", 400); 15 | } 16 | 17 | await record.destroy(); 18 | }; 19 | 20 | export default DeleteService; 21 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250702085100-add-index-on-name-tags.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.sequelize.query(` 6 | CREATE EXTENSION IF NOT EXISTS unaccent; 7 | 8 | CREATE INDEX tags_name_unaccent_lower_index 9 | ON "Tags" (immutable_unaccent(LOWER("name"))); 10 | `); 11 | }, 12 | 13 | down: async (queryInterface: QueryInterface) => { 14 | await queryInterface.sequelize.query(` 15 | DROP INDEX tags_name_unaccent_lower_index; 16 | `); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /backend/src/services/AnnouncementService/FindService.ts: -------------------------------------------------------------------------------- 1 | import Announcement from "../../models/Announcement"; 2 | import Company from "../../models/Company"; 3 | 4 | type Params = { 5 | companyId: string; 6 | }; 7 | 8 | const FindService = async ({ companyId }: Params): Promise => { 9 | const notes: Announcement[] = await Announcement.findAll({ 10 | where: { 11 | companyId 12 | }, 13 | include: [{ model: Company, as: "company", attributes: ["id", "name"] }], 14 | order: [["createdAt", "DESC"]] 15 | }); 16 | 17 | return notes; 18 | }; 19 | 20 | export default FindService; 21 | -------------------------------------------------------------------------------- /backend/src/services/QueueService/ShowQueueService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Queue from "../../models/Queue"; 3 | 4 | const ShowQueueService = async ( 5 | queueId: number | string, 6 | companyId: number 7 | ): Promise => { 8 | const queue = await Queue.findByPk(queueId); 9 | 10 | if (queue?.companyId !== companyId) { 11 | throw new AppError("Não é possível consultar registros de outra empresa"); 12 | } 13 | 14 | if (!queue) { 15 | throw new AppError("ERR_QUEUE_NOT_FOUND"); 16 | } 17 | 18 | return queue; 19 | }; 20 | 21 | export default ShowQueueService; 22 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230813114236-change-ticket-lastMessage-column-type.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.changeColumn("Tickets", "lastMessage", { 6 | defaultValue: "", 7 | type: DataTypes.TEXT 8 | }); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.changeColumn("Tickets", "lastMessage", { 13 | defaultValue: "", 14 | type: DataTypes.TEXT 15 | }); 16 | } 17 | }; -------------------------------------------------------------------------------- /backend/src/database/migrations/20250702083400-immutable-unaccent-function.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.sequelize.query(` 6 | CREATE OR REPLACE FUNCTION immutable_unaccent(text) 7 | RETURNS text AS $$ 8 | SELECT unaccent($1); 9 | $$ LANGUAGE sql IMMUTABLE; 10 | `); 11 | }, 12 | 13 | down: async (queryInterface: QueryInterface) => { 14 | await queryInterface.sequelize.query(` 15 | DROP FUNCTION IF EXISTS immutable_unaccent(text); 16 | `); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20241128175500-create-insensitive-index-for-users-email.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.addIndex("Users", { 6 | fields: [ 7 | queryInterface.sequelize.fn( 8 | "LOWER", 9 | queryInterface.sequelize.col("email") 10 | ) 11 | ], 12 | name: "idx_lower_email" 13 | }); 14 | }, 15 | 16 | down: async (queryInterface: QueryInterface) => { 17 | await queryInterface.removeIndex("Users", "idx_lower_email"); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/services/ContactListService/UpdateService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import ContactList from "../../models/ContactList"; 3 | 4 | interface Data { 5 | id: number | string; 6 | name: string; 7 | } 8 | 9 | const UpdateService = async (data: Data): Promise => { 10 | const { id, name } = data; 11 | 12 | const record = await ContactList.findByPk(id); 13 | 14 | if (!record) { 15 | throw new AppError("ERR_NO_CONTACTLIST_FOUND", 404); 16 | } 17 | 18 | await record.update({ 19 | name 20 | }); 21 | 22 | return record; 23 | }; 24 | 25 | export default UpdateService; 26 | -------------------------------------------------------------------------------- /backend/src/services/HelpServices/UpdateService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Help from "../../models/Help"; 3 | 4 | interface Data { 5 | id: number; 6 | title: string; 7 | description?: string; 8 | video?: string; 9 | link?: string; 10 | } 11 | 12 | const UpdateService = async (data: Data): Promise => { 13 | const { id } = data; 14 | 15 | const record = await Help.findByPk(id); 16 | 17 | if (!record) { 18 | throw new AppError("ERR_NO_HELP_FOUND", 404); 19 | } 20 | 21 | await record.update(data); 22 | 23 | return record; 24 | }; 25 | 26 | export default UpdateService; 27 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250506155200-fix-groq-typo-on-settings.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.sequelize.query(` 6 | UPDATE "Settings" 7 | SET "value" = 'groq' 8 | WHERE "key" = 'aiProvider' AND "value" = 'grok'; 9 | `); 10 | }, 11 | 12 | down: async (queryInterface: QueryInterface) => { 13 | await queryInterface.sequelize.query(` 14 | UPDATE "Settings" 15 | SET "value" = 'grok' 16 | WHERE "key" = 'aiProvider' AND "value" = 'groq'; 17 | `); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/middleware/isCompliant.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | import AppError from "../errors/AppError"; 3 | import { checkCompanyCompliant } from "../helpers/CheckCompanyCompliant"; 4 | 5 | const isCompliant = async ( 6 | req: Request, 7 | res: Response, 8 | next: NextFunction 9 | ): Promise => { 10 | if (!req.companyId) { 11 | throw new AppError("ERR_UNAUTHORIZED", 401); 12 | } 13 | if (!(await checkCompanyCompliant(req.companyId))) { 14 | throw new AppError("ERR_SUBSCRIPTION_EXPIRED", 402); 15 | } 16 | 17 | return next(); 18 | }; 19 | 20 | export default isCompliant; 21 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20241013110200-whatsapps-channel-default.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | return queryInterface.changeColumn("Whatsapps", "channel", { 6 | type: DataTypes.STRING, 7 | allowNull: false, 8 | defaultValue: "whatsapp" 9 | }); 10 | }, 11 | 12 | down: async (queryInterface: QueryInterface) => { 13 | return queryInterface.changeColumn("Whatsapps", "channel", { 14 | type: DataTypes.STRING, 15 | allowNull: true, 16 | defaultValue: null 17 | }); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /backend/src/helpers/UpdateDeletedUserOpenTicketsStatus.ts: -------------------------------------------------------------------------------- 1 | import Ticket from "../models/Ticket"; 2 | import UpdateTicketService from "../services/TicketServices/UpdateTicketService"; 3 | 4 | const UpdateDeletedUserOpenTicketsStatus = async ( 5 | tickets: Ticket[], 6 | companyId: number 7 | ): Promise => { 8 | tickets.forEach(async t => { 9 | const ticketId = t.id.toString(); 10 | 11 | await UpdateTicketService({ 12 | ticketData: { status: "pending" }, 13 | ticketId: Number.parseInt(ticketId, 10), 14 | companyId 15 | }); 16 | }); 17 | }; 18 | 19 | export default UpdateDeletedUserOpenTicketsStatus; 20 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name" : "Ticketz", 3 | "name" : "Ticketz - Chat Based Ticket System", 4 | "icons" : [{ 5 | "src" : "vector/favicon.ico", 6 | "sizes" : "512x512 192x192 64x64 32x32 24x24 16x16", 7 | "type" : "image/svg+xml" 8 | },{ 9 | "src" : "favicon.ico", 10 | "sizes" : "64x64 32x32 24x24 16x16", 11 | "type" : "image/x-icon" 12 | }, { 13 | "src" : "/android-chrome-192x192.png", 14 | "sizes" : "192x192", 15 | "type" : "image/png" 16 | } 17 | ], 18 | "start_url" : ".", 19 | "display" : "standalone", 20 | "theme_color" : "#000000", 21 | "background_color" : "#ffffff" 22 | } -------------------------------------------------------------------------------- /backend/src/services/AuthServices/FindUserFromToken.ts: -------------------------------------------------------------------------------- 1 | import { verify } from "jsonwebtoken"; 2 | import ShowUserService from "../UserServices/ShowUserService"; 3 | import authConfig from "../../config/auth"; 4 | import User from "../../models/User"; 5 | 6 | interface RefreshTokenPayload { 7 | id: string; 8 | tokenVersion: number; 9 | companyId: number; 10 | } 11 | 12 | export default async function FindUserFromToken(token: string): Promise { 13 | const decoded = verify(token, authConfig.refreshSecret); 14 | const { id } = decoded as RefreshTokenPayload; 15 | 16 | const user = await ShowUserService(id); 17 | return user; 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/models/Help.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement 9 | } from "sequelize-typescript"; 10 | 11 | @Table({ 12 | tableName: "Helps" 13 | }) 14 | class Help extends Model { 15 | @PrimaryKey 16 | @AutoIncrement 17 | @Column 18 | id: number; 19 | 20 | @Column 21 | title: string; 22 | 23 | @Column 24 | description: string; 25 | 26 | @Column 27 | video: string; 28 | 29 | @Column 30 | link: string; 31 | 32 | @CreatedAt 33 | createdAt: Date; 34 | 35 | @UpdatedAt 36 | updatedAt: Date; 37 | } 38 | 39 | export default Help; 40 | -------------------------------------------------------------------------------- /backend/src/services/CompanyService/UpdateSchedulesService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Company from "../../models/Company"; 3 | 4 | type ScheduleData = { 5 | id: number | string; 6 | schedules: []; 7 | }; 8 | 9 | const UpdateSchedulesService = async ({ 10 | id, 11 | schedules 12 | }: ScheduleData): Promise => { 13 | const company = await Company.findByPk(id); 14 | 15 | if (!company) { 16 | throw new AppError("ERR_NO_COMPANY_FOUND", 404); 17 | } 18 | 19 | await company.update({ 20 | schedules 21 | }); 22 | 23 | return company; 24 | }; 25 | 26 | export default UpdateSchedulesService; 27 | -------------------------------------------------------------------------------- /backend/src/services/QueueOptionService/UpdateService.ts: -------------------------------------------------------------------------------- 1 | import QueueOption from "../../models/QueueOption"; 2 | import ShowService from "./ShowService"; 3 | 4 | interface QueueData { 5 | queueId?: number; 6 | title?: string; 7 | option?: string; 8 | message?: string; 9 | parentId?: number; 10 | } 11 | 12 | const UpdateService = async ( 13 | queueOptionId: number | string, 14 | queueOptionData: QueueData 15 | ): Promise => { 16 | 17 | const queueOption = await ShowService(queueOptionId); 18 | 19 | await queueOption.update(queueOptionData); 20 | 21 | return queueOption; 22 | }; 23 | 24 | export default UpdateService; 25 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/ReviewOrder/ReviewOrder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useFormikContext } from 'formik'; 3 | import { Typography, Grid } from '@material-ui/core'; 4 | import ShippingDetails from './ShippingDetails'; 5 | 6 | export default function ReviewOrder() { 7 | const { values: formValues } = useFormikContext(); 8 | return ( 9 | 10 | 11 | Resumo da assinatura 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /backend/src/services/TagServices/KanbanListService.ts: -------------------------------------------------------------------------------- 1 | import { Op } from "sequelize"; 2 | import Tag from "../../models/Tag"; 3 | import Ticket from "../../models/Ticket"; 4 | import TicketTag from "../../models/TicketTag"; 5 | 6 | interface Request { 7 | companyId: number; 8 | } 9 | 10 | const KanbanListService = async ({ 11 | companyId 12 | }: Request): Promise => { 13 | const tags = await Tag.findAll({ 14 | where: { 15 | kanban: 1, 16 | companyId: companyId, 17 | }, 18 | order: [["id", "ASC"]], 19 | raw: true, 20 | }); 21 | //console.log(tags); 22 | return tags; 23 | }; 24 | 25 | export default KanbanListService; -------------------------------------------------------------------------------- /frontend/nginx/include.d/spa.conf: -------------------------------------------------------------------------------- 1 | # X-Frame-Options is to prevent from clickJacking attack 2 | add_header X-Frame-Options SAMEORIGIN; 3 | 4 | # disable content-type sniffing on some browsers. 5 | add_header X-Content-Type-Options nosniff; 6 | 7 | # This header enables the Cross-site scripting (XSS) filter 8 | add_header X-XSS-Protection "1; mode=block"; 9 | 10 | # This will enforce HTTP browsing into HTTPS and avoid ssl stripping attack 11 | add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;"; 12 | 13 | add_header Referrer-Policy "no-referrer-when-downgrade"; 14 | 15 | # Enables response header of "Vary: Accept-Encoding" 16 | gzip_vary on; -------------------------------------------------------------------------------- /backend/src/routes/ticketTagRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as TicketTagController from "../controllers/TicketTagController"; 5 | 6 | const ticketTagRoutes = express.Router(); 7 | 8 | ticketTagRoutes.put( 9 | "/ticket-tags/:ticketId/:tagId", 10 | isAuth, 11 | TicketTagController.store 12 | ); 13 | 14 | ticketTagRoutes.delete( 15 | "/ticket-tags/:ticketId/:tagId", 16 | isAuth, 17 | TicketTagController.remove 18 | ); 19 | 20 | ticketTagRoutes.delete( 21 | "/ticket-tags/:ticketId", 22 | isAuth, 23 | TicketTagController.removeAll 24 | ); 25 | 26 | export default ticketTagRoutes; 27 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230904212800-alter-value-to-plans.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.changeColumn("Plans", "value", { 7 | type: DataTypes.FLOAT, 8 | allowNull: true, 9 | }) 10 | ]); 11 | }, 12 | 13 | down: (queryInterface: QueryInterface) => { 14 | return Promise.all([ 15 | queryInterface.changeColumn("Plans", "value", { 16 | type: DataTypes.INTEGER, 17 | allowNull: true, 18 | defaultValue: 199.99 19 | }) 20 | ]); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/GetProfilePicUrl.ts: -------------------------------------------------------------------------------- 1 | import { cacheLayer } from "../../libs/cache"; 2 | import { Session } from "../../libs/wbot"; 3 | 4 | const GetProfilePicUrl = async ( 5 | number: string, 6 | type: "preview" | "image", 7 | wbot: Session 8 | ): Promise => { 9 | const redisKey = `picurl_${type}:${number}`; 10 | 11 | const profilePicUrl = await cacheLayer.get(redisKey); 12 | if (profilePicUrl) { 13 | return profilePicUrl; 14 | } 15 | 16 | return wbot.profilePictureUrl(`${number}`, type).then(pic => { 17 | cacheLayer.set(redisKey, pic, "EX", 60 * 60 * 24 * 5); 18 | }); 19 | }; 20 | 21 | export default GetProfilePicUrl; 22 | -------------------------------------------------------------------------------- /frontend/src/components/BackdropLoading/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Backdrop from "@material-ui/core/Backdrop"; 4 | import CircularProgress from "@material-ui/core/CircularProgress"; 5 | import { makeStyles } from "@material-ui/core/styles"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | backdrop: { 9 | zIndex: theme.zIndex.drawer + 1, 10 | color: "#fff", 11 | }, 12 | })); 13 | 14 | const BackdropLoading = () => { 15 | const classes = useStyles(); 16 | return ( 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default BackdropLoading; 24 | -------------------------------------------------------------------------------- /backend/src/models/WhatsappQueue.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | ForeignKey, 8 | BelongsTo 9 | } from "sequelize-typescript"; 10 | import Queue from "./Queue"; 11 | import Whatsapp from "./Whatsapp"; 12 | 13 | @Table 14 | class WhatsappQueue extends Model { 15 | @ForeignKey(() => Whatsapp) 16 | @Column 17 | whatsappId: number; 18 | 19 | @ForeignKey(() => Queue) 20 | @Column 21 | queueId: number; 22 | 23 | @CreatedAt 24 | createdAt: Date; 25 | 26 | @UpdatedAt 27 | updatedAt: Date; 28 | 29 | @BelongsTo(() => Queue) 30 | queue: Queue; 31 | } 32 | 33 | export default WhatsappQueue; 34 | -------------------------------------------------------------------------------- /backend/src/services/InvoicesService/UpdateInvoiceService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Invoice from "../../models/Invoices"; 3 | 4 | interface InvoiceData { 5 | status: string; 6 | id?: number | string; 7 | } 8 | 9 | const UpdateInvoiceService = async ( 10 | invoiceData: InvoiceData 11 | ): Promise => { 12 | const { id, status } = invoiceData; 13 | 14 | const invoice = await Invoice.findByPk(id); 15 | 16 | if (!invoice) { 17 | throw new AppError("ERR_NO_PLAN_FOUND", 404); 18 | } 19 | 20 | await invoice.update({ 21 | status 22 | }); 23 | 24 | return invoice; 25 | }; 26 | 27 | export default UpdateInvoiceService; 28 | -------------------------------------------------------------------------------- /backend/src/routes/authRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import * as SessionController from "../controllers/SessionController"; 3 | import isAuth from "../middleware/isAuth"; 4 | import isSuper from "../middleware/isAdmin"; 5 | 6 | const authRoutes = Router(); 7 | 8 | authRoutes.post("/login", SessionController.store); 9 | authRoutes.get( 10 | "/impersonate/:companyId", 11 | isAuth, 12 | isSuper, 13 | SessionController.impersonate 14 | ); 15 | authRoutes.post("/refresh_token", SessionController.update); 16 | authRoutes.delete("/logout", isAuth, SessionController.remove); 17 | authRoutes.get("/me", isAuth, SessionController.me); 18 | 19 | export default authRoutes; 20 | -------------------------------------------------------------------------------- /backend/src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | 3 | import isAuth from "../middleware/isAuth"; 4 | import * as UserController from "../controllers/UserController"; 5 | 6 | const userRoutes = Router(); 7 | 8 | userRoutes.get("/users", isAuth, UserController.index); 9 | 10 | userRoutes.get("/users/list", isAuth, UserController.list); 11 | 12 | userRoutes.post("/users", isAuth, UserController.store); 13 | 14 | userRoutes.put("/users/:userId", isAuth, UserController.update); 15 | 16 | userRoutes.get("/users/:userId", isAuth, UserController.show); 17 | 18 | userRoutes.delete("/users/:userId", isAuth, UserController.remove); 19 | 20 | export default userRoutes; 21 | -------------------------------------------------------------------------------- /backend/src/services/WhatsappService/SocketSendWhatsappUpdate.ts: -------------------------------------------------------------------------------- 1 | import { getIO } from "../../libs/socket"; 2 | import Whatsapp from "../../models/Whatsapp"; 3 | 4 | export function sendWhatsappUpdate(whatsapp: Whatsapp) { 5 | const { id, name, channel, status, qrcode, isDefault, updatedAt } = whatsapp; 6 | 7 | const io = getIO(); 8 | io.to(`company-${whatsapp.companyId}-admin`).emit( 9 | `company-${whatsapp.companyId}-whatsapp`, 10 | { 11 | action: "update", 12 | whatsapp: { 13 | id, 14 | name, 15 | channel, 16 | status, 17 | qrcode, 18 | isDefault, 19 | updatedAt 20 | } 21 | } 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/services/InvoicesService/ShowInvoiceService.ts: -------------------------------------------------------------------------------- 1 | import Invoice from "../../models/Invoices"; 2 | import AppError from "../../errors/AppError"; 3 | 4 | const ShowInvoceService = async ( 5 | Invoiceid: string | number 6 | ): Promise => { 7 | const invoice = await Invoice.findByPk(Invoiceid, { 8 | attributes: [ 9 | "id", 10 | "detail", 11 | "value", 12 | "currency", 13 | "dueDate", 14 | "status", 15 | "createdAt", 16 | "updatedAt" 17 | ] 18 | }); 19 | 20 | if (!invoice) { 21 | throw new AppError("ERR_NO_PLAN_FOUND", 404); 22 | } 23 | 24 | return invoice; 25 | }; 26 | 27 | export default ShowInvoceService; 28 | -------------------------------------------------------------------------------- /backend/src/services/AnnouncementService/UpdateService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import Announcement from "../../models/Announcement"; 3 | 4 | interface Data { 5 | id: number; 6 | priority: number; 7 | title: string; 8 | text: string; 9 | status: boolean; 10 | companyId: number; 11 | } 12 | 13 | const UpdateService = async (data: Data): Promise => { 14 | const { id } = data; 15 | 16 | const record = await Announcement.findByPk(id); 17 | 18 | if (!record) { 19 | throw new AppError("ERR_NO_ANNOUNCEMENT_FOUND", 404); 20 | } 21 | 22 | await record.update(data); 23 | 24 | return record; 25 | }; 26 | 27 | export default UpdateService; 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /backend/src/services/QueueOptionService/ShowService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import QueueOption from "../../models/QueueOption"; 3 | 4 | const ShowService = async (queueOptionId: number | string): Promise => { 5 | const queue = await QueueOption.findOne({ 6 | where: { 7 | id: queueOptionId 8 | }, 9 | include: [ 10 | { 11 | model: QueueOption, 12 | as: 'parent', 13 | where: { parentId: queueOptionId }, 14 | required: false 15 | }, 16 | ] 17 | }); 18 | 19 | if (!queue) { 20 | throw new AppError("ERR_QUEUE_NOT_FOUND"); 21 | } 22 | 23 | return queue; 24 | }; 25 | 26 | export default ShowService; 27 | -------------------------------------------------------------------------------- /backend/src/services/SettingServices/ListSettingsService.ts: -------------------------------------------------------------------------------- 1 | import { Op, WhereOptions } from "sequelize"; 2 | import Setting from "../../models/Setting"; 3 | import User from "../../models/User"; 4 | 5 | interface Request { 6 | isSuper: boolean, 7 | companyId: number; 8 | } 9 | 10 | const ListSettingsService = async ({ 11 | isSuper, companyId 12 | }: Request): Promise => { 13 | const where: WhereOptions = { companyId }; 14 | if (!isSuper) { 15 | where.key = { 16 | [Op.notLike]: "\\_%" 17 | } 18 | } 19 | 20 | const settings = await Setting.findAll({ 21 | where 22 | }); 23 | 24 | return settings; 25 | }; 26 | 27 | export default ListSettingsService; 28 | -------------------------------------------------------------------------------- /backend/src/routes/i18nRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | import isSuper from "../middleware/isSuper"; 4 | import * as I18nController from "../controllers/I18nController"; 5 | 6 | const translationRoutes = express.Router(); 7 | 8 | translationRoutes.get( 9 | "/translations/languages", 10 | isAuth, 11 | isSuper, 12 | I18nController.listLanguages 13 | ); 14 | translationRoutes.get( 15 | "/translations", 16 | isAuth, 17 | isSuper, 18 | I18nController.getKeysAndValues 19 | ); 20 | translationRoutes.post( 21 | "/translations", 22 | isAuth, 23 | isSuper, 24 | I18nController.upsertTranslation 25 | ); 26 | 27 | export default translationRoutes; 28 | -------------------------------------------------------------------------------- /backend/src/routes/wavoipRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | import isAdmin from "../middleware/isAdmin"; 4 | 5 | import * as WavoipController from "../controllers/WavoipController"; 6 | 7 | const wavoipRoutes = express.Router(); 8 | 9 | wavoipRoutes.post( 10 | "/wavoip/:whatsappId", 11 | isAuth, 12 | isAdmin, 13 | WavoipController.saveToken 14 | ); 15 | 16 | wavoipRoutes.delete( 17 | "/wavoip/:whatsappId", 18 | isAuth, 19 | isAdmin, 20 | WavoipController.deleteToken 21 | ); 22 | 23 | wavoipRoutes.get( 24 | "/wavoip/:whatsappId", 25 | isAuth, 26 | isAdmin, 27 | WavoipController.getToken 28 | ); 29 | 30 | export default wavoipRoutes; 31 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230918122800-add-media-to-Queues.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("Queues", "mediaName", { 6 | type: DataTypes.TEXT, 7 | defaultValue: "", 8 | allowNull: true 9 | }), 10 | queryInterface.addColumn("Queues", "mediaPath", { 11 | type: DataTypes.TEXT, 12 | defaultValue: "", 13 | allowNull: true 14 | }); 15 | }, 16 | down: (queryInterface: QueryInterface) => { 17 | return queryInterface.removeColumn("Queues", "mediaName"), 18 | queryInterface.removeColumn("Queues", "mediaPath"); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.env-backend-local: -------------------------------------------------------------------------------- 1 | FRONTEND_HOST=localhost 2 | FRONTEND_PORT=3000 3 | BACKEND_PATH=/backend 4 | 5 | EMAIL_ADDRESS=admin@ticketz.host 6 | TZ=America/Sao_Paulo 7 | 8 | BACKEND_URL=http://${FRONTEND_HOST}:${FRONTEND_PORT}${BACKEND_PATH} 9 | FRONTEND_URL=http://${FRONTEND_HOST}:${FRONTEND_PORT} 10 | 11 | DB_DIALECT=postgres 12 | DB_HOST=postgres 13 | DB_PORT=5432 14 | DB_USER=ticketz 15 | DB_NAME=ticketz 16 | DB_TIMEZONE=-03:00 17 | 18 | 19 | REDIS_URI=redis://redis:6379 20 | REDIS_OPT_LIMITER_MAX=1 21 | REDIS_OPT_LIMITER_DURATION=3000 22 | 23 | USER_LIMIT=10000 24 | CONNECTIONS_LIMIT=100000 25 | CLOSED_SEND_BY_ME=true 26 | 27 | FACEBOOK_APP_ID= 28 | FACEBOOK_APP_SECRET= 29 | 30 | VERIFY_TOKEN=ticketz 31 | SOCKET_ADMIN=true 32 | -------------------------------------------------------------------------------- /backend/src/routes/helpRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | import isSuper from "../middleware/isSuper"; 4 | 5 | import * as HelpController from "../controllers/HelpController"; 6 | 7 | const routes = express.Router(); 8 | 9 | routes.get("/helps/list", isAuth, HelpController.findList); 10 | 11 | routes.get("/helps", isAuth, HelpController.index); 12 | 13 | routes.get("/helps/:id", isAuth, HelpController.show); 14 | 15 | routes.post("/helps", isAuth, isSuper, HelpController.store); 16 | 17 | routes.put("/helps/:id", isAuth, isSuper, HelpController.update); 18 | 19 | routes.delete("/helps/:id", isAuth, isSuper, HelpController.remove); 20 | 21 | export default routes; 22 | -------------------------------------------------------------------------------- /backend/src/services/TicketNoteService/UpdateTicketNoteService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import TicketNote from "../../models/TicketNote"; 3 | 4 | interface TicketNoteData { 5 | note: string; 6 | id?: number | string; 7 | } 8 | 9 | const UpdateTicketNoteService = async ( 10 | ticketNoteData: TicketNoteData 11 | ): Promise => { 12 | const { id, note } = ticketNoteData; 13 | 14 | const ticketNote = await TicketNote.findByPk(id); 15 | 16 | if (!ticketNote) { 17 | throw new AppError("ERR_NO_TICKETNOTE_FOUND", 404); 18 | } 19 | 20 | await ticketNote.update({ 21 | note 22 | }); 23 | 24 | return ticketNote; 25 | }; 26 | 27 | export default UpdateTicketNoteService; 28 | -------------------------------------------------------------------------------- /backend/src/routes/invoicesRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | import * as QueueOptionController from "../controllers/QueueOptionController"; 4 | import * as InvoicesController from "../controllers/InvoicesController" 5 | 6 | const invoiceRoutes = express.Router(); 7 | 8 | invoiceRoutes.get("/invoices", isAuth, InvoicesController.index); 9 | invoiceRoutes.get("/invoices/list", isAuth, InvoicesController.list); 10 | invoiceRoutes.get("/invoices/all", isAuth, InvoicesController.list); 11 | invoiceRoutes.get("/invoices/:Invoiceid", isAuth, InvoicesController.show); 12 | invoiceRoutes.put("/invoices/:id", isAuth, InvoicesController.update); 13 | 14 | export default invoiceRoutes; 15 | -------------------------------------------------------------------------------- /backend/src/routes/scheduleRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as ScheduleController from "../controllers/ScheduleController"; 5 | 6 | const scheduleRoutes = express.Router(); 7 | 8 | scheduleRoutes.get("/schedules", isAuth, ScheduleController.index); 9 | 10 | scheduleRoutes.post("/schedules", isAuth, ScheduleController.store); 11 | 12 | scheduleRoutes.put("/schedules/:scheduleId", isAuth, ScheduleController.update); 13 | 14 | scheduleRoutes.get("/schedules/:scheduleId", isAuth, ScheduleController.show); 15 | 16 | scheduleRoutes.delete( 17 | "/schedules/:scheduleId", 18 | isAuth, 19 | ScheduleController.remove 20 | ); 21 | 22 | export default scheduleRoutes; 23 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250415163900-add-index-timestamps-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addIndex("Tickets", ["createdAt"], { 7 | name: "idx_tickets_createdat" 8 | }), 9 | queryInterface.addIndex("Tickets", ["updatedAt"], { 10 | name: "idx_tickets_updatedat" 11 | }) 12 | ]); 13 | }, 14 | 15 | down: (queryInterface: QueryInterface) => { 16 | return Promise.all([ 17 | queryInterface.removeIndex("Tickets", "idx_tickets_createdat"), 18 | queryInterface.removeIndex("Tickets", "idx_tickets_updatedat") 19 | ]); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /backend/src/models/Baileys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | Default, 10 | ForeignKey 11 | } from "sequelize-typescript"; 12 | import Whatsapp from "./Whatsapp"; 13 | 14 | @Table 15 | class Baileys extends Model { 16 | @PrimaryKey 17 | @AutoIncrement 18 | @Column 19 | id: number; 20 | 21 | @Default(null) 22 | @Column 23 | contacts: string; 24 | 25 | @Default(null) 26 | @Column 27 | chats: string; 28 | 29 | @CreatedAt 30 | createdAt: Date; 31 | 32 | @UpdatedAt 33 | updatedAt: Date; 34 | 35 | @ForeignKey(() => Whatsapp) 36 | @Column 37 | whatsappId: number; 38 | } 39 | 40 | export default Baileys; 41 | -------------------------------------------------------------------------------- /backend/src/services/QuickMessageService/UpdateService.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import QuickMessage from "../../models/QuickMessage"; 3 | 4 | interface Data { 5 | shortcode: string; 6 | message: string; 7 | userId: number; 8 | id?: number; 9 | } 10 | 11 | const UpdateService = async (data: Data): Promise => { 12 | const { id, shortcode, message, userId } = data; 13 | 14 | const record = await QuickMessage.findByPk(id); 15 | 16 | if (!record) { 17 | throw new AppError("ERR_NO_TICKETNOTE_FOUND", 404); 18 | } 19 | 20 | await record.update({ 21 | shortcode, 22 | message, 23 | userId 24 | }); 25 | 26 | return record; 27 | }; 28 | 29 | export default UpdateService; 30 | -------------------------------------------------------------------------------- /backend/src/models/ContactTag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | ForeignKey, 8 | BelongsTo 9 | } from "sequelize-typescript"; 10 | import Tag from "./Tag"; 11 | import Contact from "./Contact"; 12 | 13 | @Table({ 14 | tableName: "ContactTags" 15 | }) 16 | class ContactTag extends Model { 17 | @ForeignKey(() => Contact) 18 | @Column 19 | contactId: number; 20 | 21 | @ForeignKey(() => Tag) 22 | @Column 23 | tagId: number; 24 | 25 | @CreatedAt 26 | createdAt: Date; 27 | 28 | @UpdatedAt 29 | updatedAt: Date; 30 | 31 | @BelongsTo(() => Contact) 32 | contact: Contact; 33 | 34 | @BelongsTo(() => Tag) 35 | tag: Tag; 36 | } 37 | 38 | export default ContactTag; 39 | -------------------------------------------------------------------------------- /backend/src/models/TicketTag.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | ForeignKey, 8 | BelongsTo 9 | } from "sequelize-typescript"; 10 | import Tag from "./Tag"; 11 | import Ticket from "./Ticket"; 12 | 13 | @Table({ 14 | tableName: 'TicketTags' 15 | }) 16 | class TicketTag extends Model { 17 | @ForeignKey(() => Ticket) 18 | @Column 19 | ticketId: number; 20 | 21 | @ForeignKey(() => Tag) 22 | @Column 23 | tagId: number; 24 | 25 | @CreatedAt 26 | createdAt: Date; 27 | 28 | @UpdatedAt 29 | updatedAt: Date; 30 | 31 | @BelongsTo(() => Ticket) 32 | ticket: Ticket; 33 | 34 | @BelongsTo(() => Tag) 35 | tag: Tag; 36 | } 37 | 38 | export default TicketTag; 39 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20230918142800-add-media-to-QueueOptions.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.addColumn("QueueOptions", "mediaName", { 6 | type: DataTypes.TEXT, 7 | defaultValue: "", 8 | allowNull: true 9 | }), 10 | queryInterface.addColumn("QueueOptions", "mediaPath", { 11 | type: DataTypes.TEXT, 12 | defaultValue: "", 13 | allowNull: true 14 | }); 15 | }, 16 | down: (queryInterface: QueryInterface) => { 17 | return queryInterface.removeColumn("QueueOptions", "mediaName"), 18 | queryInterface.removeColumn("QueueOptions", "mediaPath"); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250415164200-add-index-timestamps-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addIndex("Messages", ["createdAt"], { 7 | name: "idx_messages_createdat" 8 | }), 9 | queryInterface.addIndex("Messages", ["updatedAt"], { 10 | name: "idx_messages_updatedat" 11 | }) 12 | ]); 13 | }, 14 | 15 | down: (queryInterface: QueryInterface) => { 16 | return Promise.all([ 17 | queryInterface.removeIndex("Messages", "idx_messages_createdat"), 18 | queryInterface.removeIndex("Messages", "idx_messages_updatedat") 19 | ]); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /frontend/src/components/FormFields/InputField.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { at } from 'lodash'; 3 | import { useField } from 'formik'; 4 | import { TextField } from '@material-ui/core'; 5 | 6 | export default function InputField(props) { 7 | const { errorText, ...rest } = props; 8 | const [field, meta] = useField(props); 9 | 10 | function _renderHelperText() { 11 | const [touched, error] = at(meta, 'touched', 'error'); 12 | if (touched && error) { 13 | return error; 14 | } 15 | } 16 | 17 | return ( 18 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/services/ChatService/FindService.ts: -------------------------------------------------------------------------------- 1 | import Chat from "../../models/Chat"; 2 | import Company from "../../models/Company"; 3 | import User from "../../models/User"; 4 | 5 | type Params = { 6 | companyId: number; 7 | ownerId?: number; 8 | }; 9 | 10 | const FindService = async ({ ownerId, companyId }: Params): Promise => { 11 | const chats: Chat[] = await Chat.findAll({ 12 | where: { 13 | ownerId, 14 | companyId 15 | }, 16 | include: [ 17 | { model: Company, as: "company", attributes: ["id", "name"] }, 18 | { model: User, as: "owner", attributes: ["id", "name"] } 19 | ], 20 | order: [["createdAt", "DESC"]] 21 | }); 22 | 23 | return chats; 24 | }; 25 | 26 | export default FindService; 27 | -------------------------------------------------------------------------------- /backend/src/services/UserServices/SimpleListService.ts: -------------------------------------------------------------------------------- 1 | import User from "../../models/User"; 2 | import AppError from "../../errors/AppError"; 3 | import Queue from "../../models/Queue"; 4 | 5 | interface Params { 6 | companyId: string | number; 7 | } 8 | 9 | const SimpleListService = async ({ companyId }: Params): Promise => { 10 | const users = await User.findAll({ 11 | where: { 12 | companyId 13 | }, 14 | attributes: ["name", "id", "email"], 15 | include: [ 16 | { model: Queue, as: 'queues' } 17 | ], 18 | order: [["id", "ASC"]] 19 | }); 20 | 21 | if (!users) { 22 | throw new AppError("ERR_NO_USER_FOUND", 404); 23 | } 24 | 25 | return users; 26 | }; 27 | 28 | export default SimpleListService; 29 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250226163500-fix-queueoptions-constraints.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.removeConstraint( 6 | "QueueOptions", 7 | "QueueOptions_forwardQueueId_fkey" 8 | ); 9 | }, 10 | 11 | down: (queryInterface: QueryInterface) => { 12 | return queryInterface.addConstraint("QueueOptions", { 13 | fields: ["forwardQueueId"], 14 | type: "foreign key", 15 | name: "QueueOptions_forwardQueueId_fkey", 16 | references: { 17 | table: "Queues", 18 | field: "id" 19 | }, 20 | onUpdate: "NO ACTION", 21 | onDelete: "NO ACTION" 22 | }); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/models/BaileysKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | ForeignKey, 6 | BelongsTo, 7 | DataType, 8 | PrimaryKey, 9 | } from "sequelize-typescript"; 10 | import Whatsapp from "./Whatsapp"; 11 | 12 | @Table({ timestamps: false }) 13 | class BaileysKeys extends Model { 14 | @PrimaryKey 15 | @ForeignKey(() => Whatsapp) 16 | @Column(DataType.INTEGER) 17 | whatsappId: number; 18 | 19 | @BelongsTo(() => Whatsapp) 20 | whatsapp: Whatsapp; 21 | 22 | @PrimaryKey 23 | @Column(DataType.TEXT) 24 | type: string; 25 | 26 | @PrimaryKey 27 | @Column(DataType.TEXT) 28 | key: string; 29 | 30 | @Column(DataType.TEXT) 31 | value: string; 32 | } 33 | 34 | export default BaileysKeys; 35 | -------------------------------------------------------------------------------- /backend/src/models/Setting.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | ForeignKey, 9 | BelongsTo, 10 | AutoIncrement 11 | } from "sequelize-typescript"; 12 | 13 | import Company from "./Company"; 14 | 15 | @Table 16 | class Setting extends Model { 17 | @PrimaryKey 18 | @AutoIncrement 19 | @Column 20 | id: number; 21 | 22 | @Column 23 | key: string; 24 | 25 | @Column 26 | value: string; 27 | 28 | @CreatedAt 29 | createdAt: Date; 30 | 31 | @UpdatedAt 32 | updatedAt: Date; 33 | 34 | @ForeignKey(() => Company) 35 | @Column 36 | companyId: number; 37 | 38 | @BelongsTo(() => Company) 39 | company: Company; 40 | } 41 | 42 | export default Setting; 43 | -------------------------------------------------------------------------------- /backend/src/config/database.ts: -------------------------------------------------------------------------------- 1 | import "../bootstrap"; 2 | 3 | module.exports = { 4 | define: { 5 | charset: "utf8mb4", 6 | collate: "utf8mb4_bin" 7 | }, 8 | pool: { 9 | max: process.env.DB_MAX_CONNECTIONS || 60, 10 | min: process.env.DB_MIN_CONNECTIONS || 5, 11 | acquire: process.env.DB_ACQUIRE || 30000, 12 | idle: process.env.DB_IDLE || 10000 13 | }, 14 | dialect: process.env.DB_DIALECT || "postgres", 15 | timezone: process.env.DB_TIMEZONE || "-03:00", 16 | host: process.env.DB_HOST, 17 | port: process.env.DB_PORT || 5432, 18 | database: process.env.DB_NAME, 19 | username: process.env.DB_USER, 20 | password: process.env.DB_PASS, 21 | logging: process.env.DB_DEBUG && console.log, 22 | seederStorage: "sequelize" 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/src/components/GoogleAnalytics/index.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Helmet } from 'react-helmet'; 3 | 4 | const ga4Tag = "G-VXMW3XS5BD"; 5 | const scriptSrc = `https://www.googletagmanager.com/gtag/js?id={ga4Tag}`; 6 | 7 | const GoogleAnalytics = () => { 8 | useEffect(() => { 9 | // Inicialização do GA4 10 | window.dataLayer = window.dataLayer || []; 11 | function gtag(){window.dataLayer.push(arguments);} 12 | gtag('js', new Date()); 13 | 14 | gtag('config', ga4Tag, { 15 | 'page_title': window.location.host 16 | } ); 17 | }, []); 18 | 19 | return ( 20 | 21 | 22 | 23 | ); 24 | }; 25 | 26 | export default GoogleAnalytics; 27 | -------------------------------------------------------------------------------- /backend/src/helpers/SerializeUser.ts: -------------------------------------------------------------------------------- 1 | import Queue from "../models/Queue"; 2 | import Company from "../models/Company"; 3 | import User from "../models/User"; 4 | import Setting from "../models/Setting"; 5 | 6 | interface SerializedUser { 7 | id: number; 8 | name: string; 9 | email: string; 10 | profile: string; 11 | companyId: number; 12 | company: Company | null; 13 | super: boolean; 14 | queues: Queue[]; 15 | } 16 | 17 | export const SerializeUser = async (user: User): Promise => { 18 | return { 19 | id: user.id, 20 | name: user.name, 21 | email: user.email, 22 | profile: user.profile, 23 | companyId: user.companyId, 24 | company: user.company, 25 | super: user.super, 26 | queues: user.queues 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /backend/src/routes/quickMessageRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as QuickMessageController from "../controllers/QuickMessageController"; 5 | 6 | const routes = express.Router(); 7 | 8 | routes.get("/quick-messages/list", isAuth, QuickMessageController.findList); 9 | 10 | routes.get("/quick-messages", isAuth, QuickMessageController.index); 11 | 12 | routes.get("/quick-messages/:id", isAuth, QuickMessageController.show); 13 | 14 | routes.post("/quick-messages", isAuth, QuickMessageController.store); 15 | 16 | routes.put("/quick-messages/:id", isAuth, QuickMessageController.update); 17 | 18 | routes.delete("/quick-messages/:id", isAuth, QuickMessageController.remove); 19 | 20 | export default routes; 21 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/getJidOf.ts: -------------------------------------------------------------------------------- 1 | import Contact from "../../models/Contact"; 2 | import Ticket from "../../models/Ticket"; 3 | 4 | export function getJidOf(reference: string | Contact | Ticket) { 5 | let address = reference; 6 | let isGroup = false; 7 | if (reference instanceof Contact) { 8 | isGroup = reference.isGroup; 9 | address = reference.number; 10 | } else if (reference instanceof Ticket) { 11 | isGroup = reference.isGroup; 12 | address = reference.contact.number; 13 | } 14 | 15 | if (typeof address !== "string") { 16 | throw new Error("Invalid reference type"); 17 | } 18 | 19 | if (address.includes("@")) { 20 | return address; 21 | } 22 | 23 | return `${address}@${isGroup ? "g.us" : "s.whatsapp.net"}`; 24 | } 25 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20241110080800-change-QueueOptions-forwardQueueId-column.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.changeColumn("QueueOptions", "forwardQueueId", { 6 | type: DataTypes.INTEGER, 7 | references: { model: "Queues", key: "id" }, 8 | onDelete: "SET NULL", 9 | onUpdate: "CASCADE", 10 | allowNull: true 11 | }); 12 | }, 13 | 14 | down: async (queryInterface: QueryInterface) => { 15 | await queryInterface.changeColumn("QueueOptions", "forwardQueueId", { 16 | type: DataTypes.INTEGER, 17 | references: { model: "Queues", key: "id" }, 18 | allowNull: true 19 | }); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /backend/src/services/SettingServices/GetPublicSettingService.ts: -------------------------------------------------------------------------------- 1 | import Setting from "../../models/Setting"; 2 | 3 | interface Request { 4 | key: string; 5 | } 6 | 7 | const publicSettingsKeys = [ 8 | "allowSignup", 9 | "primaryColorLight", 10 | "primaryColorDark", 11 | "appLogoLight", 12 | "appLogoDark", 13 | "appLogoFavicon", 14 | "appName" 15 | ]; 16 | 17 | const GetPublicSettingService = async ({ 18 | key 19 | }: Request): Promise => { 20 | if (!publicSettingsKeys.includes(key)) { 21 | return null; 22 | } 23 | 24 | const setting = await Setting.findOne({ 25 | where: { 26 | companyId: 1, 27 | key 28 | } 29 | }); 30 | 31 | return setting?.value || null; 32 | }; 33 | 34 | export default GetPublicSettingService; 35 | -------------------------------------------------------------------------------- /frontend/src/translate/languages/index.js: -------------------------------------------------------------------------------- 1 | import { messages as portugueseMessages } from "./pt"; 2 | import { messages as portuguesePortugalMessages } from "./pt_PT"; 3 | import { messages as englishMessages } from "./en"; 4 | import { messages as spanishMessages } from "./es"; 5 | import { messages as frenchMessages } from "./fr"; 6 | import { messages as germanMessages } from "./de"; 7 | import { messages as italianMessages } from "./it"; 8 | import { messages as indonesianMessages } from "./id"; 9 | 10 | const messages = { 11 | ...portugueseMessages, 12 | ...portuguesePortugalMessages, 13 | ...englishMessages, 14 | ...spanishMessages, 15 | ...frenchMessages, 16 | ...germanMessages, 17 | ...italianMessages, 18 | ...indonesianMessages, 19 | }; 20 | 21 | export { messages }; 22 | -------------------------------------------------------------------------------- /.env-backend-acme: -------------------------------------------------------------------------------- 1 | FRONTEND_HOST=ticketz.exemplo.com.br 2 | EMAIL_ADDRESS=ticketz@exemplo.com.br 3 | TZ=America/Sao_Paulo 4 | 5 | # Normalmente não é necessário alterar estes valores 6 | 7 | BACKEND_PATH=/backend 8 | 9 | RECAPTCHA_SECRET_KEY= 10 | 11 | BACKEND_URL=https://${FRONTEND_HOST}${BACKEND_PATH} 12 | FRONTEND_URL=https://${FRONTEND_HOST} 13 | 14 | DB_DIALECT=postgres 15 | DB_HOST=postgres 16 | DB_PORT=5432 17 | DB_USER=ticketz 18 | DB_NAME=ticketz 19 | DB_TIMEZONE=-03:00 20 | 21 | 22 | REDIS_URI=redis://redis:6379 23 | REDIS_OPT_LIMITER_MAX=1 24 | REDIS_OPT_LIMITER_DURATION=3000 25 | 26 | USER_LIMIT=10000 27 | CONNECTIONS_LIMIT=100000 28 | CLOSED_SEND_BY_ME=true 29 | 30 | FACEBOOK_APP_ID= 31 | FACEBOOK_APP_SECRET= 32 | 33 | VERIFY_TOKEN=ticketz 34 | 35 | SOCKET_ADMIN=true 36 | -------------------------------------------------------------------------------- /frontend/src/components/TicketHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { Card } from "@material-ui/core"; 4 | import { makeStyles } from "@material-ui/core/styles"; 5 | import TicketHeaderSkeleton from "../TicketHeaderSkeleton"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | ticketHeader: { 9 | display: "flex", 10 | flex: "none", 11 | borderBottom: "1px solid rgba(0, 0, 0, 0.12)", 12 | }, 13 | })); 14 | 15 | const TicketHeader = ({ loading, children }) => { 16 | const classes = useStyles(); 17 | 18 | return ( 19 | <> 20 | {loading ? ( 21 | 22 | ) : ( 23 | 24 | {children} 25 | 26 | )} 27 | 28 | ); 29 | }; 30 | 31 | export default TicketHeader; 32 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192523-add-column-status-and-schedules-to-Companies.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("Companies", "status", { 7 | type: DataTypes.BOOLEAN, 8 | defaultValue: true 9 | }), 10 | queryInterface.addColumn("Companies", "schedules", { 11 | type: DataTypes.JSONB, 12 | defaultValue: [] 13 | }) 14 | ]); 15 | }, 16 | 17 | down: (queryInterface: QueryInterface) => { 18 | return Promise.all([ 19 | queryInterface.removeColumn("Companies", "schedules"), 20 | queryInterface.removeColumn("Companies", "status") 21 | ]); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/models/ContactCustomField.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | ForeignKey, 10 | BelongsTo 11 | } from "sequelize-typescript"; 12 | import Contact from "./Contact"; 13 | 14 | @Table 15 | class ContactCustomField extends Model { 16 | @PrimaryKey 17 | @AutoIncrement 18 | @Column 19 | id: number; 20 | 21 | @Column 22 | name: string; 23 | 24 | @Column 25 | value: string; 26 | 27 | @ForeignKey(() => Contact) 28 | @Column 29 | contactId: number; 30 | 31 | @BelongsTo(() => Contact) 32 | contact: Contact; 33 | 34 | @CreatedAt 35 | createdAt: Date; 36 | 37 | @UpdatedAt 38 | updatedAt: Date; 39 | } 40 | 41 | export default ContactCustomField; 42 | -------------------------------------------------------------------------------- /backend/src/services/CompanyService/DeleteCompanyService.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import { join } from "path"; 3 | import Company from "../../models/Company"; 4 | import AppError from "../../errors/AppError"; 5 | import { getPublicPath } from "../../helpers/GetPublicPath"; 6 | 7 | const DeleteCompanyService = async (id: string): Promise => { 8 | const company = await Company.findOne({ 9 | where: { id } 10 | }); 11 | 12 | if (!company) { 13 | throw new AppError("ERR_NO_COMPANY_FOUND", 404); 14 | } 15 | 16 | await company.destroy(); 17 | 18 | const companyMediaPath = join(getPublicPath(), "media", id); 19 | 20 | // recursively remove company media folder 21 | fs.rmSync(companyMediaPath, { recursive: true }); 22 | }; 23 | 24 | export default DeleteCompanyService; 25 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/FormModel/formInitialValues.js: -------------------------------------------------------------------------------- 1 | import checkoutFormModel from './checkoutFormModel'; 2 | const { 3 | formField: { 4 | firstName, 5 | lastName, 6 | address1, 7 | city, 8 | state, 9 | zipcode, 10 | country, 11 | useAddressForPaymentDetails, 12 | nameOnCard, 13 | cardNumber, 14 | invoiceId, 15 | cvv 16 | } 17 | } = checkoutFormModel; 18 | 19 | export default { 20 | [firstName.name]: '', 21 | [lastName.name]: '', 22 | [address1.name]: '', 23 | [city.name]: '', 24 | [state.name]: '', 25 | [zipcode.name]: '', 26 | [country.name]: '', 27 | [useAddressForPaymentDetails.name]: false, 28 | [nameOnCard.name]: '', 29 | [cardNumber.name]: '', 30 | [invoiceId.name]: '', 31 | [cvv.name]: '' 32 | }; 33 | -------------------------------------------------------------------------------- /backend/src/helpers/CheckContactOpenTickets.ts: -------------------------------------------------------------------------------- 1 | import { Op, WhereOptions } from "sequelize"; 2 | import AppError from "../errors/AppError"; 3 | import Ticket from "../models/Ticket"; 4 | 5 | const CheckContactOpenTickets = async ( 6 | contactId: number, 7 | whatsappId?: number, 8 | returnTicket = false 9 | ): Promise => { 10 | const where: WhereOptions = { 11 | contactId, 12 | status: { [Op.or]: ["open", "pending"] } 13 | }; 14 | 15 | if (whatsappId) { 16 | where.whatsappId = whatsappId; 17 | } 18 | 19 | const ticket = await Ticket.findOne({ 20 | where 21 | }); 22 | 23 | if (ticket && !returnTicket) { 24 | throw new AppError("ERR_OTHER_OPEN_TICKET"); 25 | } 26 | 27 | return ticket; 28 | }; 29 | 30 | export default CheckContactOpenTickets; 31 | -------------------------------------------------------------------------------- /backend/src/routes/tagRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as TagController from "../controllers/TagController"; 5 | 6 | const tagRoutes = express.Router(); 7 | 8 | tagRoutes.get("/tags/list", isAuth, TagController.list); 9 | 10 | tagRoutes.get("/tags/kanban", isAuth, TagController.kanban); 11 | 12 | tagRoutes.get("/tags", isAuth, TagController.index); 13 | 14 | tagRoutes.post("/tags", isAuth, TagController.store); 15 | 16 | tagRoutes.put("/tags/:tagId", isAuth, TagController.update); 17 | 18 | tagRoutes.get("/tags/:tagId", isAuth, TagController.show); 19 | 20 | tagRoutes.delete("/tags/:tagId", isAuth, TagController.remove); 21 | 22 | tagRoutes.post("/tags/sync", isAuth, TagController.syncTags); 23 | 24 | export default tagRoutes; 25 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108204708-associate-users-queue.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("UserQueues", { 6 | userId: { 7 | type: DataTypes.INTEGER, 8 | primaryKey: true 9 | }, 10 | queueId: { 11 | type: DataTypes.INTEGER, 12 | primaryKey: true 13 | }, 14 | createdAt: { 15 | type: DataTypes.DATE, 16 | allowNull: false 17 | }, 18 | updatedAt: { 19 | type: DataTypes.DATE, 20 | allowNull: false 21 | } 22 | }); 23 | }, 24 | 25 | down: (queryInterface: QueryInterface) => { 26 | return queryInterface.dropTable("UserQueues"); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20220411000002-add-column-schedules-and-outOfHoursMessage-to-Queues.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("Queues", "schedules", { 7 | type: DataTypes.JSONB, 8 | defaultValue: [] 9 | }), 10 | queryInterface.addColumn("Queues", "outOfHoursMessage", { 11 | type: DataTypes.TEXT, 12 | allowNull: true 13 | }) 14 | ]); 15 | }, 16 | 17 | down: (queryInterface: QueryInterface) => { 18 | return Promise.all([ 19 | queryInterface.removeColumn("Queues", "schedules"), 20 | queryInterface.removeColumn("Queues", "outOfHoursMessage") 21 | ]); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/models/OutOfTicketMessages.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | ForeignKey, 9 | BelongsTo, 10 | DataType 11 | } from "sequelize-typescript"; 12 | 13 | import Whatsapp from "./Whatsapp"; 14 | 15 | @Table 16 | class OutOfTicketMessage extends Model { 17 | @PrimaryKey 18 | @Column 19 | id: string; 20 | 21 | @Column(DataType.STRING) 22 | dataJson: string; 23 | 24 | @CreatedAt 25 | @Column(DataType.DATE(6)) 26 | createdAt: Date; 27 | 28 | @UpdatedAt 29 | @Column(DataType.DATE(6)) 30 | updatedAt: Date; 31 | 32 | @ForeignKey(() => Whatsapp) 33 | @Column 34 | whatsappId: string; 35 | 36 | @BelongsTo(() => Whatsapp) 37 | whatsapp: Whatsapp; 38 | } 39 | 40 | export default OutOfTicketMessage; 41 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/StartAllWhatsAppsSessions.ts: -------------------------------------------------------------------------------- 1 | import Whatsapp from "../../models/Whatsapp"; 2 | import { logger } from "../../utils/logger"; 3 | import { StartWhatsAppSession } from "./StartWhatsAppSession"; 4 | 5 | export const StartAllWhatsAppsSessions = async ( 6 | companyId: number 7 | ): Promise => { 8 | try { 9 | const whatsapps = await Whatsapp.findAll({ where: { companyId } }); 10 | if (whatsapps.length > 0) { 11 | whatsapps.forEach(whatsapp => { 12 | if (whatsapp.channel === "whatsapp") { 13 | StartWhatsAppSession(whatsapp, companyId); 14 | } 15 | }); 16 | } 17 | } catch (e) { 18 | logger.error( 19 | { message: e.message, stack: e.stack }, 20 | "Error starting WhatsApp sessions" 21 | ); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250321122300-add-indexes-to-messages.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | await queryInterface.addIndex("Messages", ["companyId", "contactId"], { 6 | name: "idx_messages_companyid_contactid" 7 | }); 8 | 9 | await queryInterface.addIndex("Messages", ["ticketId", "companyId"], { 10 | name: "idx_messages_ticketid_companyid" 11 | }); 12 | }, 13 | 14 | down: async (queryInterface: QueryInterface) => { 15 | await queryInterface.removeIndex( 16 | "Messages", 17 | "idx_messages_companyid_contactid" 18 | ); 19 | await queryInterface.removeIndex( 20 | "Messages", 21 | "idx_messages_ticketid_companyid" 22 | ); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20200903215941-create-settings.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("Settings", { 6 | key: { 7 | type: DataTypes.STRING, 8 | primaryKey: true, 9 | allowNull: false 10 | }, 11 | value: { 12 | type: DataTypes.TEXT, 13 | allowNull: false 14 | }, 15 | createdAt: { 16 | type: DataTypes.DATE, 17 | allowNull: false 18 | }, 19 | updatedAt: { 20 | type: DataTypes.DATE, 21 | allowNull: false 22 | } 23 | }); 24 | }, 25 | 26 | down: (queryInterface: QueryInterface) => { 27 | return queryInterface.dropTable("Settings"); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210109192534-add-rated-to-TicketTraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("TicketTraking", "ratingAt", { 7 | type: DataTypes.DATE, 8 | allowNull: true, 9 | defaultValue: null 10 | }), 11 | queryInterface.addColumn("TicketTraking", "rated", { 12 | type: DataTypes.BOOLEAN, 13 | defaultValue: false 14 | }) 15 | ]); 16 | }, 17 | 18 | down: (queryInterface: QueryInterface) => { 19 | return Promise.all([ 20 | queryInterface.removeColumn("TicketTraking", "ratingAt"), 21 | queryInterface.removeColumn("TicketTraking", "rated") 22 | ]); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/middleware/envTokenAuth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | import AppError from "../errors/AppError"; 4 | 5 | type TokenPayload = { 6 | token: string | undefined; 7 | }; 8 | 9 | const envTokenAuth = ( 10 | req: Request, 11 | res: Response, 12 | next: NextFunction 13 | ): void => { 14 | try { 15 | const { token: bodyToken } = req.body as TokenPayload; 16 | const { token: queryToken } = req.query as TokenPayload; 17 | 18 | if (queryToken === process.env.ENV_TOKEN) { 19 | return next(); 20 | } 21 | 22 | if (bodyToken === process.env.ENV_TOKEN) { 23 | return next(); 24 | } 25 | } catch (e) { 26 | console.log(e); 27 | } 28 | 29 | throw new AppError("Token inválido", 403); 30 | }; 31 | 32 | export default envTokenAuth; 33 | -------------------------------------------------------------------------------- /backend/src/models/CampaignSetting.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | ForeignKey, 10 | BelongsTo 11 | } from "sequelize-typescript"; 12 | import Company from "./Company"; 13 | 14 | @Table({ tableName: "CampaignSettings" }) 15 | class CampaignSetting extends Model { 16 | @PrimaryKey 17 | @AutoIncrement 18 | @Column 19 | id: number; 20 | 21 | @Column 22 | key: string; 23 | 24 | @Column 25 | value: string; 26 | 27 | @CreatedAt 28 | createdAt: Date; 29 | 30 | @UpdatedAt 31 | updatedAt: Date; 32 | 33 | @ForeignKey(() => Company) 34 | @Column 35 | companyId: number; 36 | 37 | @BelongsTo(() => Company) 38 | company: Company; 39 | } 40 | 41 | export default CampaignSetting; 42 | -------------------------------------------------------------------------------- /backend/src/routes/dashboardRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as DashboardController from "../controllers/DashboardController"; 5 | import isAdmin from "../middleware/isAdmin"; 6 | import isCompliant from "../middleware/isCompliant"; 7 | 8 | const routes = express.Router(); 9 | 10 | routes.get( 11 | "/dashboard/status", 12 | isAuth, 13 | isAdmin, 14 | isCompliant, 15 | DashboardController.statusSummary 16 | ); 17 | 18 | routes.get( 19 | "/dashboard/tickets", 20 | isAuth, 21 | isAdmin, 22 | isCompliant, 23 | DashboardController.ticketsStatistic 24 | ); 25 | 26 | routes.get( 27 | "/dashboard/users", 28 | isAuth, 29 | isAdmin, 30 | isCompliant, 31 | DashboardController.usersReport 32 | ); 33 | 34 | export default routes; 35 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210108174594-associate-whatsapp-queue.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return queryInterface.createTable("WhatsappQueues", { 6 | whatsappId: { 7 | type: DataTypes.INTEGER, 8 | primaryKey: true 9 | }, 10 | queueId: { 11 | type: DataTypes.INTEGER, 12 | primaryKey: true 13 | }, 14 | createdAt: { 15 | type: DataTypes.DATE, 16 | allowNull: false 17 | }, 18 | updatedAt: { 19 | type: DataTypes.DATE, 20 | allowNull: false 21 | } 22 | }); 23 | }, 24 | 25 | down: (queryInterface: QueryInterface) => { 26 | return queryInterface.dropTable("WhatsappQueues"); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /backend/src/models/UserSocketSession.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | Model, 5 | DataType, 6 | ForeignKey, 7 | PrimaryKey, 8 | Default, 9 | CreatedAt, 10 | UpdatedAt, 11 | BelongsTo 12 | } from "sequelize-typescript"; 13 | import User from "./User"; // Importação da model User 14 | 15 | @Table 16 | class UserSocketSession extends Model { 17 | @PrimaryKey 18 | @Column(DataType.STRING) 19 | id: string; 20 | 21 | @ForeignKey(() => User) 22 | @Column 23 | userId: number; 24 | 25 | @Default(true) 26 | @Column(DataType.BOOLEAN) 27 | active: boolean; 28 | 29 | @CreatedAt 30 | createdAt: Date; 31 | 32 | @UpdatedAt 33 | updatedAt: Date; 34 | 35 | @BelongsTo(() => User) 36 | user: User; // Associação com User 37 | } 38 | 39 | export default UserSocketSession; 40 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20210818102608-add-unique-indexes-to-Queues-table.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addConstraint("Queues", { fields: ["color", "companyId"], 7 | name: "Queues_color_key", 8 | type: 'unique' 9 | }), 10 | queryInterface.addConstraint("Queues", { fields: ["name", "companyId"], 11 | name: "Queues_name_key", 12 | type: 'unique' 13 | }), 14 | ]); 15 | }, 16 | 17 | down: (queryInterface: QueryInterface) => { 18 | return Promise.all([ 19 | queryInterface.removeConstraint("Queues", "Queues_color_key"), 20 | queryInterface.removeConstraint("Queues", "Queues_name_key"), 21 | ]); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250415164000-add-index-timestamps-to-tickettraking.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addIndex("TicketTraking", ["createdAt"], { 7 | name: "idx_tickettraking_createdat" 8 | }), 9 | queryInterface.addIndex("TicketTraking", ["updatedAt"], { 10 | name: "idx_tickettraking_updatedat" 11 | }) 12 | ]); 13 | }, 14 | 15 | down: (queryInterface: QueryInterface) => { 16 | return Promise.all([ 17 | queryInterface.removeIndex( 18 | "TicketTraking", 19 | "idx_tickettraking_createdat" 20 | ), 21 | queryInterface.removeIndex("TicketTraking", "idx_tickettraking_updatedat") 22 | ]); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /backend/src/services/ContactListItemService/FindService.ts: -------------------------------------------------------------------------------- 1 | import ContactListItem from "../../models/ContactListItem"; 2 | import Company from "../../models/Company"; 3 | 4 | type Params = { 5 | companyId: number; 6 | contactListId: number; 7 | }; 8 | 9 | const FindService = async ({ 10 | companyId, 11 | contactListId 12 | }: Params): Promise => { 13 | let where: any = { 14 | companyId 15 | }; 16 | 17 | if (contactListId) { 18 | where = { 19 | ...where, 20 | contactListId 21 | }; 22 | } 23 | 24 | const notes: ContactListItem[] = await ContactListItem.findAll({ 25 | where, 26 | include: [{ model: Company, as: "company", attributes: ["id", "name"] }], 27 | order: [["name", "ASC"]] 28 | }); 29 | 30 | return notes; 31 | }; 32 | 33 | export default FindService; 34 | -------------------------------------------------------------------------------- /frontend/src/hooks/useLocalStorage/index.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import toastError from "../../errors/toastError"; 3 | 4 | export function useLocalStorage(key, initialValue) { 5 | const [storedValue, setStoredValue] = useState(() => { 6 | try { 7 | const item = localStorage.getItem(key); 8 | return item ? JSON.parse(item) : initialValue; 9 | } catch (error) { 10 | toastError(error); 11 | return initialValue; 12 | } 13 | }); 14 | 15 | const setValue = value => { 16 | try { 17 | const valueToStore = 18 | value instanceof Function ? value(storedValue) : value; 19 | 20 | setStoredValue(valueToStore); 21 | 22 | localStorage.setItem(key, JSON.stringify(valueToStore)); 23 | } catch (error) { 24 | toastError(error); 25 | } 26 | }; 27 | 28 | return [storedValue, setValue]; 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/middleware/tokenAuth.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | import AppError from "../errors/AppError"; 4 | import Whatsapp from "../models/Whatsapp"; 5 | 6 | const tokenAuth = async ( 7 | req: Request, 8 | res: Response, 9 | next: NextFunction 10 | ): Promise => { 11 | try { 12 | const token = req.headers.authorization.replace("Bearer ", ""); 13 | const whatsapp = await Whatsapp.findOne({ where: { token } }); 14 | if (whatsapp) { 15 | req.params = { 16 | whatsappId: whatsapp.id.toString() 17 | }; 18 | req.companyId = whatsapp.companyId; 19 | } else { 20 | throw new Error(); 21 | } 22 | } catch (err) { 23 | throw new AppError("Acesso não permitido", 401); 24 | } 25 | 26 | return next(); 27 | }; 28 | 29 | export default tokenAuth; 30 | -------------------------------------------------------------------------------- /backend/src/services/ContactListService/CreateService.ts: -------------------------------------------------------------------------------- 1 | import * as Yup from "yup"; 2 | import AppError from "../../errors/AppError"; 3 | import ContactList from "../../models/ContactList"; 4 | 5 | interface Data { 6 | name: string; 7 | companyId: number; 8 | } 9 | 10 | const CreateService = async (data: Data): Promise => { 11 | const { name } = data; 12 | 13 | const ticketnoteSchema = Yup.object().shape({ 14 | name: Yup.string() 15 | .min(3, "ERR_CONTACTLIST_INVALID_NAME") 16 | .required("ERR_CONTACTLIST_REQUIRED") 17 | }); 18 | 19 | try { 20 | await ticketnoteSchema.validate({ name }); 21 | } catch (err: any) { 22 | throw new AppError(err.message); 23 | } 24 | 25 | const record = await ContactList.create(data); 26 | 27 | return record; 28 | }; 29 | 30 | export default CreateService; 31 | -------------------------------------------------------------------------------- /backend/src/models/Plan.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | AllowNull, 10 | Unique, 11 | Default 12 | } from "sequelize-typescript"; 13 | 14 | @Table 15 | class Plan extends Model { 16 | @PrimaryKey 17 | @AutoIncrement 18 | @Column 19 | id: number; 20 | 21 | @AllowNull(false) 22 | @Unique 23 | @Column 24 | name: string; 25 | 26 | @Column 27 | users: number; 28 | 29 | @Column 30 | connections: number; 31 | 32 | @Column 33 | queues: number; 34 | 35 | @Column 36 | value: number; 37 | 38 | @Column 39 | currency: string; 40 | 41 | @CreatedAt 42 | createdAt: Date; 43 | 44 | @UpdatedAt 45 | updatedAt: Date; 46 | 47 | @Default(true) 48 | @Column 49 | isPublic: boolean; 50 | } 51 | 52 | export default Plan; 53 | -------------------------------------------------------------------------------- /frontend/src/components/CheckoutPage/FormModel/validationSchema.js: -------------------------------------------------------------------------------- 1 | import * as Yup from 'yup'; 2 | import checkoutFormModel from './checkoutFormModel'; 3 | const { 4 | formField: { 5 | firstName, 6 | address1, 7 | city, 8 | zipcode, 9 | country, 10 | } 11 | } = checkoutFormModel; 12 | 13 | 14 | export default [ 15 | Yup.object().shape({ 16 | [firstName.name]: Yup.string().required(`${firstName.requiredErrorMsg}`), 17 | [address1.name]: Yup.string().required(`${address1.requiredErrorMsg}`), 18 | [city.name]: Yup.string() 19 | .nullable() 20 | .required(`${city.requiredErrorMsg}`), 21 | [zipcode.name]: Yup.string() 22 | .required(`${zipcode.requiredErrorMsg}`), 23 | 24 | [country.name]: Yup.string() 25 | .nullable() 26 | .required(`${country.requiredErrorMsg}`) 27 | }), 28 | 29 | ]; 30 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20250520150600-change-duedate-to-dateonly.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface } from "sequelize"; 2 | 3 | export default { 4 | up: async (queryInterface: QueryInterface) => { 5 | // Change the column to DATE using a raw SQL query to preserve UTC date 6 | await queryInterface.sequelize.query(` 7 | ALTER TABLE "Companies" 8 | ALTER COLUMN "dueDate" 9 | TYPE DATE 10 | USING ("dueDate" AT TIME ZONE 'UTC')::DATE; 11 | `); 12 | }, 13 | down: async (queryInterface: QueryInterface) => { 14 | // Revert the column to TIMESTAMP using a raw SQL query 15 | await queryInterface.sequelize.query(` 16 | ALTER TABLE "Companies" 17 | ALTER COLUMN "dueDate" 18 | TYPE TIMESTAMP WITH TIME ZONE 19 | USING "dueDate"::timestamp with time zone; 20 | `); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /frontend/src/pages/Dashboard/CustomTooltip.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { i18n } from '../../translate/i18n'; 3 | 4 | const CustomTooltip = ({ payload, label, active, i18nBase }) => { 5 | if (active && payload && payload.length) { 6 | return ( 7 |
8 |
{label}
9 | {payload.map((item, index) => ( 10 |
11 | {`${i18nBase ? i18n.t(i18nBase+"."+item.name) : item.name}: ${item.value}`} 12 |
13 | ))} 14 |
15 | ); 16 | } 17 | return null; 18 | }; 19 | 20 | export default CustomTooltip; 21 | -------------------------------------------------------------------------------- /frontend/src/components/MainHeader/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { makeStyles } from "@material-ui/core/styles"; 4 | import whatsBackground from "../../assets/wa-background.png" 5 | import whatsBackgroundDark from "../../assets/wa-background-dark.png"; 6 | 7 | const useStyles = makeStyles(theme => ({ 8 | contactsHeader: { 9 | display: "flex", 10 | alignItems: "center", 11 | padding: "0px 6px 6px 6px", 12 | backgroundImage: theme.mode === 'light' ? `url(${whatsBackground})` : `url(${whatsBackgroundDark})`, 13 | backgroundPosition: 'center', 14 | backgroundSize: 'cover', 15 | backgroundRepeat: 'no-repeat', 16 | }, 17 | })); 18 | 19 | const MainHeader = ({ children }) => { 20 | const classes = useStyles(); 21 | 22 | return
{children}
; 23 | }; 24 | 25 | export default MainHeader; 26 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/DeleteContactService.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import fs from "fs"; 3 | import Contact from "../../models/Contact"; 4 | import AppError from "../../errors/AppError"; 5 | import { getPublicPath } from "../../helpers/GetPublicPath"; 6 | 7 | const DeleteContactService = async (id: string): Promise => { 8 | const contact = await Contact.findOne({ 9 | where: { id } 10 | }); 11 | 12 | if (!contact) { 13 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 14 | } 15 | 16 | const contactMediaPath = join( 17 | getPublicPath(), 18 | "media", 19 | `${contact.companyId}/${contact.id}` 20 | ); 21 | 22 | // recursively remove contact media folder 23 | fs.rmSync(contactMediaPath, { recursive: true, force: true }); 24 | 25 | await contact.destroy(); 26 | }; 27 | 28 | export default DeleteContactService; 29 | -------------------------------------------------------------------------------- /backend/src/services/ContactServices/ShowContactService.ts: -------------------------------------------------------------------------------- 1 | import Contact from "../../models/Contact"; 2 | import AppError from "../../errors/AppError"; 3 | import Company from "../../models/Company"; 4 | 5 | const ShowContactService = async ( 6 | id: string | number, 7 | companyId?: number 8 | ): Promise => { 9 | const contact = await Contact.findByPk(id, { 10 | include: [ 11 | "tags", 12 | "extraInfo", 13 | { 14 | model: Company, 15 | as: "company", 16 | attributes: ["id", "language"] 17 | } 18 | ] 19 | }); 20 | 21 | if (companyId && contact?.companyId !== companyId) { 22 | throw new AppError("ERR_FORBIDDEN", 403); 23 | } 24 | 25 | if (!contact) { 26 | throw new AppError("ERR_NO_CONTACT_FOUND", 404); 27 | } 28 | 29 | return contact; 30 | }; 31 | 32 | export default ShowContactService; 33 | -------------------------------------------------------------------------------- /frontend/src/context/Tickets/TicketsContext.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, createContext } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | const TicketsContext = createContext(); 5 | 6 | const TicketsContextProvider = ({ children }) => { 7 | const [currentTicket, setCurrentTicket] = useState({ id: null, code: null }); 8 | const history = useHistory(); 9 | 10 | useEffect(() => { 11 | if (currentTicket?.uuid) { 12 | history.push(`/tickets/${currentTicket.uuid}`); 13 | } 14 | // eslint-disable-next-line react-hooks/exhaustive-deps 15 | }, [currentTicket]) 16 | 17 | return ( 18 | 21 | {children} 22 | 23 | ); 24 | }; 25 | 26 | export { TicketsContext, TicketsContextProvider }; 27 | -------------------------------------------------------------------------------- /backend/src/models/ChatUser.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Table, 3 | Column, 4 | CreatedAt, 5 | UpdatedAt, 6 | Model, 7 | PrimaryKey, 8 | AutoIncrement, 9 | BelongsTo, 10 | ForeignKey 11 | } from "sequelize-typescript"; 12 | import User from "./User"; 13 | import Chat from "./Chat"; 14 | 15 | @Table({ tableName: "ChatUsers" }) 16 | class ChatUser extends Model { 17 | @PrimaryKey 18 | @AutoIncrement 19 | @Column 20 | id: number; 21 | 22 | @ForeignKey(() => Chat) 23 | @Column 24 | chatId: number; 25 | 26 | @ForeignKey(() => User) 27 | @Column 28 | userId: number; 29 | 30 | @Column 31 | unreads: number; 32 | 33 | @CreatedAt 34 | createdAt: Date; 35 | 36 | @UpdatedAt 37 | updatedAt: Date; 38 | 39 | @BelongsTo(() => Chat) 40 | chat: Chat; 41 | 42 | @BelongsTo(() => User) 43 | user: User; 44 | } 45 | 46 | export default ChatUser; 47 | -------------------------------------------------------------------------------- /backend/src/helpers/CreateTokens.ts: -------------------------------------------------------------------------------- 1 | import { sign } from "jsonwebtoken"; 2 | import authConfig from "../config/auth"; 3 | import User from "../models/User"; 4 | 5 | export const createAccessToken = (user: User): string => { 6 | const { secret, expiresIn } = authConfig; 7 | 8 | return sign( 9 | { 10 | username: user.name, 11 | profile: user.profile, 12 | super: user.super, 13 | id: user.id, 14 | companyId: user.companyId 15 | }, 16 | secret, 17 | { 18 | expiresIn 19 | } 20 | ); 21 | }; 22 | 23 | export const createRefreshToken = (user: User): string => { 24 | const { refreshSecret, refreshExpiresIn } = authConfig; 25 | 26 | return sign( 27 | { id: user.id, tokenVersion: user.tokenVersion, companyId: user.companyId }, 28 | refreshSecret, 29 | { 30 | expiresIn: refreshExpiresIn 31 | } 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /backend/src/routes/ticketNoteRoutes.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import isAuth from "../middleware/isAuth"; 3 | 4 | import * as TicketNoteController from "../controllers/TicketNoteController"; 5 | 6 | const ticketNoteRoutes = express.Router(); 7 | 8 | ticketNoteRoutes.get( 9 | "/ticket-notes/list", 10 | isAuth, 11 | TicketNoteController.findFilteredList 12 | ); 13 | 14 | ticketNoteRoutes.get("/ticket-notes", isAuth, TicketNoteController.index); 15 | 16 | ticketNoteRoutes.get("/ticket-notes/:id", isAuth, TicketNoteController.show); 17 | 18 | ticketNoteRoutes.post("/ticket-notes", isAuth, TicketNoteController.store); 19 | 20 | ticketNoteRoutes.put("/ticket-notes/:id", isAuth, TicketNoteController.update); 21 | 22 | ticketNoteRoutes.delete( 23 | "/ticket-notes/:id", 24 | isAuth, 25 | TicketNoteController.remove 26 | ); 27 | 28 | export default ticketNoteRoutes; 29 | -------------------------------------------------------------------------------- /backend/src/services/ScheduleServices/ShowService.ts: -------------------------------------------------------------------------------- 1 | import Schedule from "../../models/Schedule"; 2 | import AppError from "../../errors/AppError"; 3 | import Contact from "../../models/Contact"; 4 | import User from "../../models/User"; 5 | 6 | const ScheduleService = async (id: string | number, companyId: number): Promise => { 7 | const schedule = await Schedule.findByPk(id, { 8 | include: [ 9 | { model: Contact, as: "contact", attributes: ["id", "name"] }, 10 | { model: User, as: "user", attributes: ["id", "name"] }, 11 | ] 12 | }); 13 | 14 | if (schedule?.companyId !== companyId) { 15 | throw new AppError("Não é possível excluir registro de outra empresa"); 16 | } 17 | 18 | if (!schedule) { 19 | throw new AppError("ERR_NO_SCHEDULE_FOUND", 404); 20 | } 21 | 22 | return schedule; 23 | }; 24 | 25 | export default ScheduleService; 26 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/CheckIsValidContact.ts: -------------------------------------------------------------------------------- 1 | import AppError from "../../errors/AppError"; 2 | import GetDefaultWhatsApp from "../../helpers/GetDefaultWhatsApp"; 3 | import { getWbot } from "../../libs/wbot"; 4 | 5 | const CheckIsValidContact = async ( 6 | number: string, 7 | companyId: number 8 | ): Promise => { 9 | const defaultWhatsapp = await GetDefaultWhatsApp(companyId); 10 | 11 | const wbot = getWbot(defaultWhatsapp.id); 12 | 13 | try { 14 | const isValidNumber = await wbot.onWhatsApp(`${number}`); 15 | if (!isValidNumber) { 16 | throw new AppError("invalidNumber"); 17 | } 18 | } catch (err: any) { 19 | if (err.message === "invalidNumber") { 20 | throw new AppError("ERR_WAPP_INVALID_CONTACT"); 21 | } 22 | throw new AppError("ERR_WAPP_CHECK_CONTACT"); 23 | } 24 | }; 25 | 26 | export default CheckIsValidContact; 27 | -------------------------------------------------------------------------------- /frontend/src/helpers/getISOStringWithTimezone.js: -------------------------------------------------------------------------------- 1 | import { numPad as pad } from './numPad'; 2 | 3 | export function getISOStringWithTimezone(date = new Date()) { 4 | const year = date.getFullYear(); 5 | const month = pad(date.getMonth() + 1); 6 | const day = pad(date.getDate()); 7 | const hour = pad(date.getHours()); 8 | const minute = pad(date.getMinutes()); 9 | const second = pad(date.getSeconds()); 10 | const milliseconds = pad(date.getMilliseconds(), 3); 11 | 12 | const offsetMinutes = -date.getTimezoneOffset(); 13 | const sign = offsetMinutes >= 0 ? '+' : '-'; 14 | const absOffsetMinutes = Math.abs(offsetMinutes); 15 | const offsetHour = pad(Math.floor(absOffsetMinutes / 60)); 16 | const offsetMinute = pad(absOffsetMinutes % 60); 17 | 18 | return `${year}-${month}-${day}T${hour}:${minute}:${second}.${milliseconds}${sign}${offsetHour}:${offsetMinute}`; 19 | } 20 | -------------------------------------------------------------------------------- /backend/src/services/WbotServices/StartWhatsAppSession.ts: -------------------------------------------------------------------------------- 1 | import { initWASocket } from "../../libs/wbot"; 2 | import Whatsapp from "../../models/Whatsapp"; 3 | import { wbotMessageListener } from "./wbotMessageListener"; 4 | import wbotMonitor from "./wbotMonitor"; 5 | import { logger } from "../../utils/logger"; 6 | import { sendWhatsappUpdate } from "../WhatsappService/SocketSendWhatsappUpdate"; 7 | 8 | export const StartWhatsAppSession = async ( 9 | whatsapp: Whatsapp, 10 | companyId: number, 11 | isRefresh = false 12 | ): Promise => { 13 | await whatsapp.update({ status: "OPENING" }); 14 | 15 | sendWhatsappUpdate(whatsapp); 16 | 17 | try { 18 | const wbot = await initWASocket(whatsapp, null, isRefresh); 19 | wbotMessageListener(wbot, companyId); 20 | wbotMonitor(wbot, whatsapp, companyId); 21 | } catch (err) { 22 | logger.error(err); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/src/helpers/loadJSON.js: -------------------------------------------------------------------------------- 1 | // Load text with Ajax synchronously: takes path to file and optional MIME type 2 | function loadTextFileAjaxSync(filePath, mimeType) 3 | { 4 | var xmlhttp=new XMLHttpRequest(); 5 | xmlhttp.open("GET",filePath,false); 6 | if (mimeType != null) { 7 | if (xmlhttp.overrideMimeType) { 8 | xmlhttp.overrideMimeType(mimeType); 9 | } 10 | } 11 | xmlhttp.send(); 12 | if (xmlhttp.status === 200 && xmlhttp.readyState === 4 ) 13 | { 14 | return xmlhttp.responseText; 15 | } 16 | else { 17 | // TODO Throw exception 18 | return null; 19 | } 20 | } 21 | 22 | var loadJSON = function(filePath) { 23 | try { 24 | // Load json file; 25 | var json = loadTextFileAjaxSync(filePath, "application/json"); 26 | // Parse json 27 | return JSON.parse(json); 28 | } catch (e) { 29 | return null; 30 | } 31 | } 32 | 33 | export { loadJSON }; 34 | -------------------------------------------------------------------------------- /backend/src/database/migrations/20211212125704-add-chatbot-to-tickets.ts: -------------------------------------------------------------------------------- 1 | import { QueryInterface, DataTypes } from "sequelize"; 2 | 3 | module.exports = { 4 | up: (queryInterface: QueryInterface) => { 5 | return Promise.all([ 6 | queryInterface.addColumn("Tickets", "chatbot", { 7 | type: DataTypes.BOOLEAN, 8 | allowNull: true, 9 | defaultValue: false 10 | }), 11 | queryInterface.addColumn("Tickets", "queueOptionId", { 12 | type: DataTypes.INTEGER, 13 | references: { model: "QueueOptions", key: "id" }, 14 | onUpdate: "SET null", 15 | onDelete: "SET null", 16 | allowNull: true 17 | }) 18 | ]); 19 | }, 20 | 21 | down: (queryInterface: QueryInterface) => { 22 | return queryInterface.removeColumn("Tickets", "chatbot"), 23 | queryInterface.removeColumn("Tickets", "queueOptionId"); 24 | } 25 | }; 26 | --------------------------------------------------------------------------------