feat: Added new feature

feat/hitphone-socket-integration
adriano 2023-11-29 17:05:48 -03:00
commit 6b68602472
62 changed files with 41881 additions and 0 deletions

View File

@ -0,0 +1,4 @@
{
"extends": ["@commitlint/config-conventional"]
}

11
.gitignore vendored 100644
View File

@ -0,0 +1,11 @@
**/backend/node_modules/
**/frontend/node_modules/
**/backend/.env
**/frontend/.env
**/backend/public/jsonfiles
**/backend/public/uploads
/node_modules

View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit $1

View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx commitlint --edit

1
backend/Procfile 100644
View File

@ -0,0 +1 @@
web: node app.js

73
backend/app.js 100644
View File

@ -0,0 +1,73 @@
require('dotenv').config()
require('express-async-errors')
const { socketIO } = require('./utils')
const { initIO, getIO } = socketIO
// express
const express = require('express')
const app = express()
const session = require('express-session')
// rest of the packages
const morgan = require('morgan')
// const fileUpload = require('express-fileupload')
const rateLimiter = require('express-rate-limit')
// Swagger
const swaggerUI = require('swagger-ui-express')
const YAML = require('yamljs')
const swaggerDocument = YAML.load('./swagger.yaml')
const helmet = require('helmet')
const xss = require('xss-clean')
const cors = require('cors')
// routers
const crmRouter = require('./routes/crmRoute')
const notFoundMiddlware = require('./middleware/not-found')
const errorHandlerMiddleware = require('./middleware/error-handler')
//middleware
app.set('trust proxy', 1)
app.use(rateLimiter({
windowMs: 15 * 60 * 1000,
max: 60,
}))
// Use a session to keep track of client ID
app.use(session({
secret: Math.random().toString(36).substring(2),
resave: false,
saveUninitialized: true
}))
// Security packages
app.use(helmet())
app.use(cors())
app.use(xss())
app.use(morgan('tiny'))
app.use(express.json())
// app.use(fileUpload())
app.get('/', (req, res) => {
res.send('<h1>Sentiment API</h1><a href="/api-docs">Documentation</a>')
})
app.use('/api-docs', swaggerUI.serve, swaggerUI.setup(swaggerDocument))
app.use('/api/v1/crm', crmRouter)
app.use(notFoundMiddlware)
app.use(errorHandlerMiddleware)
const port = process.env.PORT || 3000
const server = app.listen(port, console.log(`Listening on port: ${port}...`))
initIO(server)

View File

@ -0,0 +1,231 @@
const path = require('path')
const { StatusCodes } = require("http-status-codes")
const { createCRMContact,
sendMessageSocket,
mustContainProperties,
journaling,
findProperty,
journalingRequest,
createContact,
lookupContactByPhone,
templateValidator } = require('../utils')
const fs = require("fs")
const { URL } = require('url')
const { oauth2 } = require('../utils')
const { exchangeForTokens } = oauth2
const Company = require('../models/Company')
const CRM = require('../models/CRM')
const CustomError = require('../errors')
const contactCreate = async (req, res) => {
const { companyId, crmFirstName, crmLastName, crmPhone, crmEmail } = req.body
mustContainProperties(req, ['companyId', 'crmPhone',])
await createCRMContact(companyId, crmFirstName, crmPhone, crmEmail, crmLastName)
res.status(StatusCodes.OK).send()
}
const deleteCrm = async (req, res) => {
const { companyId, crmBaseURL } = req.body
mustContainProperties(req, ['companyId', 'crmBaseURL',])
const crm = await CRM.findOne({
companyId, crmBaseURL: new URL(crmBaseURL.trim()).hostname
})
if (crm) await crm.deleteOne()
res.status(StatusCodes.OK).send()
}
const deleteCompany = async (req, res) => {
const { companyId } = req.body
mustContainProperties(req, ['companyId',])
const company = await Company.findOne({ companyId, })
if (company) await company.deleteOne()
res.status(StatusCodes.OK).send()
}
const callJournaling = async (req, res) => {
const { companyId, operation, crmPhone, crmAgent, crmCallDuration } = req.body
mustContainProperties(req, ['companyId', 'operation', 'crmPhone', 'crmAgent',])
if (operation == 'inboundAnsweredCall' && !crmCallDuration)
throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is inboundAnsweredCall`)
if (operation == 'outboundAsweredCall' && !crmCallDuration)
throw new CustomError.BadRequestError(`The crmCallDuration property must be provided when operation is outboundAsweredCall`)
await journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration,)
res.status(StatusCodes.OK).send()
}
const install = async (req, res) => {
const { authUrl, companyId } = req.query
// Store the authUrl in the session
req.session.authUrl = authUrl
res.redirect(authUrl)
}
const oauthCallBack = async (req, res) => {
const { code } = req.query
// Retrieve the stored authUrl from the session
const storedAuthUrl = req.session.authUrl
const parsedUrl = new URL(storedAuthUrl)
const clientId = parsedUrl.searchParams.get('client_id')
if (code) {
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId })
crmOauth = crmOauth.toObject()
const authCodeProof = {
grant_type: 'authorization_code',
client_id: crmOauth.crm.authentication.crmClientId,
client_secret: crmOauth.crm.authentication.crmClientSecret,
redirect_uri: process.env.URL_OAUTH_CALLBACK,
code
}
// Exchange the authorization code for an access token and refresh token
await exchangeForTokens(crmOauth, authCodeProof)
const url = `${process.env.URL_OAUTH_FRONTEND_SUCCESS_REDIRECT}?clientId=${encodeURIComponent(clientId)}`
return res.redirect(url)
// return res.redirect(process.env.URL_OAUTH_FRONTEND_SUCCESS_REDIRECT)
}
res.status(StatusCodes.OK).send()
}
const testTemplate = async (req, res) => {
const { clientId, companyId } = req.body
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId, testing: true })
if (!crmOauth)
return res.status(StatusCodes.OK).send()
crmOauth = crmOauth.toObject()
const { crmPhoneTest } = crmOauth.crm.authentication
await templateValidator(crmPhoneTest, crmOauth.crm, companyId)
crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId })
crmOauth.testing = false
await crmOauth.save()
res.status(StatusCodes.OK).send()
}
const uploadCrmConfig = async (req, res) => {
const { companyId, } = req.body
mustContainProperties(req, ['companyId',])
if (!req?.file)
throw new CustomError.BadRequestError(`The crm property file must be provided`)
const file = req.file
let newCrm = fs.readFileSync(file.path, "utf8")
newCrm = JSON.parse(newCrm)
const index = newCrm.crmRest.findIndex(rest => rest?.createContactRecord)
const crmBaseURL = new URL(newCrm.crmRest[index].createContactRecord.request.url.trim()).hostname
let company = await Company.findOne({ companyId })
if (!company) {
company = await Company.create({ companyId })
}
let crm = await CRM.findOne({ companyId, crmBaseURL })
if (crm) {
crm.testing = true
crm.crm = newCrm
await crm.save()
}
else {
crm = await CRM.create({
crmBaseURL,
companyId: company.companyId,
company,
testing: true,
crm: newCrm
})
}
fs.unlinkSync(file.path)
const { type, crmClientId, crmClientSecret, crmScopes, crmPhoneTest } = crm.crm.authentication
if (type == 'oauth2') {
const { request, body, response } = findProperty(crm.crm.crmRest, 'authorizationEndpoint')
let { url } = request
url = url.replace('crmClientId', crmClientId)
.replace('crmClientSecret', crmClientSecret)
.replace('crmScopes', crmScopes)
.replace('crmRedirectURI', process.env.URL_OAUTH_CALLBACK)
const authUrl = `http://localhost:6001/api/v1/crm/install?authUrl=${encodeURIComponent(url)}&companyId=${encodeURIComponent(companyId)}`
console.log('--------> authUrl: ', authUrl)
return res.status(StatusCodes.OK).json({ url: authUrl })
}
else {
await templateValidator(crmPhoneTest, newCrm, companyId)
crm.testing = false
await crm.save()
}
res.status(StatusCodes.OK).send()
}
module.exports = {
contactCreate,
uploadCrmConfig,
callJournaling,
oauthCallBack,
install,
deleteCrm,
deleteCompany,
testTemplate
}

View File

@ -0,0 +1,8 @@
const mongoose = require('mongoose')
const connectDB = async () => {
await mongoose.connect(process.env.DB_MONGO_URL, { dbName: process.env.DB_MONGO_NAME })
}
connectDB().catch((err) => console.log(err))
module.exports = mongoose

View File

@ -0,0 +1,11 @@
const { StatusCodes } = require('http-status-codes');
const CustomAPIError = require('./custom-api');
class BadRequestError extends CustomAPIError {
constructor(message) {
super(message);
this.statusCode = StatusCodes.BAD_REQUEST;
}
}
module.exports = BadRequestError;

View File

@ -0,0 +1,7 @@
class CustomAPIError extends Error {
constructor(message) {
super(message)
}
}
module.exports = CustomAPIError

View File

@ -0,0 +1,16 @@
const CustomAPIError = require('./custom-api');
const UnauthenticatedError = require('./unauthenticated');
const NotFoundError = require('./not-found');
const BadRequestError = require('./bad-request');
const UnauthorizedError = require('./unauthorized');
const InternalServerError = require('./internal-server-error');
module.exports = {
CustomAPIError,
UnauthenticatedError,
NotFoundError,
BadRequestError,
UnauthorizedError,
InternalServerError
};

View File

@ -0,0 +1,11 @@
const { StatusCodes } = require('http-status-codes')
const CustomAPIError = require('./custom-api')
class NotFoundError extends CustomAPIError {
constructor(message) {
super(message)
this.statusCode = StatusCodes.INTERNAL_SERVER_ERROR
}
}
module.exports = NotFoundError

View File

@ -0,0 +1,11 @@
const { StatusCodes } = require('http-status-codes');
const CustomAPIError = require('./custom-api');
class NotFoundError extends CustomAPIError {
constructor(message) {
super(message);
this.statusCode = StatusCodes.NOT_FOUND;
}
}
module.exports = NotFoundError;

View File

@ -0,0 +1,11 @@
const { StatusCodes } = require('http-status-codes');
const CustomAPIError = require('./custom-api');
class UnauthenticatedError extends CustomAPIError {
constructor(message) {
super(message);
this.statusCode = StatusCodes.UNAUTHORIZED;
}
}
module.exports = UnauthenticatedError;

View File

@ -0,0 +1,11 @@
const { StatusCodes } = require('http-status-codes');
const CustomAPIError = require('./custom-api');
class UnauthorizedError extends CustomAPIError {
constructor(message) {
super(message);
this.statusCode = StatusCodes.FORBIDDEN;
}
}
module.exports = UnauthorizedError;

View File

@ -0,0 +1,27 @@
const CustomError = require('../errors')
const authorization = async (req, res, next) => {
const authHeader = req.headers.authorization
if (!authHeader) {
throw new CustomError.BadRequestError('Authorization not found into header!')
}
const [, token] = authHeader.split(" ");
if (!token) {
throw new CustomError.BadRequestError('Authorization token not found into header!')
}
if (token != process.env.TOKEN){
throw new CustomError.UnauthorizedError('Authorization token Invalid')
}
next()
}
module.exports = {
authorization,
}

View File

@ -0,0 +1,28 @@
const { StatusCodes } = require('http-status-codes');
const errorHandlerMiddleware = (err, req, res, next) => {
let customError = {
// set default
statusCode: err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR,
msg: err.message || 'Something went wrong try again later',
};
if (err.name === 'ValidationError') {
customError.msg = Object.values(err.errors)
.map((item) => item.message)
.join(',');
customError.statusCode = 400;
}
if (err.code && err.code === 11000) {
customError.msg = `Duplicate value entered for ${Object.keys(
err.keyValue
)} field, please choose another value`;
customError.statusCode = 400;
}
if (err.name === 'CastError') {
customError.msg = `No item found with id : ${err.value}`;
customError.statusCode = 404;
}
return res.status(customError.statusCode).json({ msg: customError.msg });
};
module.exports = errorHandlerMiddleware;

View File

@ -0,0 +1,3 @@
const notFound = (req, res) => res.status(404).send('Route does not exist')
module.exports = notFound

View File

@ -0,0 +1,134 @@
const mongoose = require('../db/connect')
const { Schema } = mongoose
// Define sub-schemas
const RequestSchema = new Schema({
requestContentType: {
type: String,
enum: ['application/json', 'application/x-www-form-urlencoded', 'multipart/form-data', 'empty', 'none'],
default: 'application/json'
},
requestEncoding: {
type: String,
enum: ['Json', 'UrlEncoded', 'empty'],
default: 'Json'
},
requestType: {
type: String,
enum: ['Post', 'Get', 'empty'],
default: 'Post'
},
responseType: {
type: String,
enum: ['Json', 'XML', 'empty'],
default: 'Json'
},
url: {
type: String,
required: true
}
})
const CallsSchema = new Schema({
inboundAnsweredCall: Object,
inboundMissedCall: Object,
outboundAnsweredCall: Object,
outboundUnansweredCall: Object
})
const CallJournalingSchema = new Schema({
request: RequestSchema,
calls: [CallsSchema],
response: Object
})
// Define main schema
const CRMRestSchema = new Schema({
authorizationEndpoint: {
request: RequestSchema
},
tokenEndpoint: {
request: RequestSchema,
body: Object,
response: Object
},
createContactRecord: {
request: RequestSchema,
body: Object,
response: Object
},
lookupContactByPhone: {
request: RequestSchema,
response: Object
},
callJournaling: CallJournalingSchema
})
const AuthenticationSchema = new Schema({
type: {
type: String,
enum: ['basic', 'bearer', 'oauth2'],
default: 'bearer'
},
userName: String,
passWord: String,
token: String,
crmClientId: String,
crmClientSecret: String,
crmScopes: String,
crmOAuthRefreshToken: String,
crmOAuthToken: String,
crmPhoneTest: String
})
const crmContactSchema = new Schema({
crmBaseURL: {
type: String
},
companyId: {
type: String,
required: true
},
company: {
type: mongoose.Schema.ObjectId,
ref: 'company',
required: true
},
testing: {
type: Boolean,
default: true
},
enabled: {
type: Boolean,
default: true
},
crm: {
authentication: AuthenticationSchema,
crmRest: [CRMRestSchema]
}
}, { timestamps: true },
)
crmContactSchema.virtual('crm_contacts', {
ref: 'CRM_Contact',
localField: '_id',
foreignField: 'crm',
justOne: false,
// match: { rating: 4 }
})
crmContactSchema.pre('deleteOne', { document: true, query: false }, async function () {
await this.model('CRM_Contact').deleteMany({ crm: this._id })
}
)
const CRM = mongoose.model('CRM', crmContactSchema)
module.exports = CRM

View File

@ -0,0 +1,31 @@
const mongoose = require('../db/connect')
const { Schema } = mongoose
const crmContactSchema = new Schema({
companyId: {
type: String,
required: true,
},
crm: {
type: mongoose.Schema.ObjectId,
ref: 'crm',
required: true
},
crmBaseURL: {
type: String,
required: true,
},
contactId: {
type: String,
required: true,
},
phone: {
type: String,
required: true,
}
}, { timestamps: true })
const CRM_Contact = mongoose.model('CRM_Contact', crmContactSchema)
module.exports = CRM_Contact

View File

@ -0,0 +1,46 @@
const mongoose = require('../db/connect')
const { Schema } = mongoose
const crmCompany = new Schema({
companyId: {
type: String,
required: true,
},
name: {
type: String
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
})
crmCompany.virtual('crms', {
ref: 'CRM',
localField: '_id',
foreignField: 'company',
justOne: false,
// match: { rating: 4 }
})
crmCompany.pre('deleteOne', { document: true, query: false }, async function () {
const crms = await this.model('CRM').find({ company: this._id })
for (const crm of crms) {
await crm.deleteOne() // Delete each CRM
// Delete associated CRM_Contacts
const crmContacts = await this.model('CRM_Contact').find({ crm: crm._id })
for (const crmContact of crmContacts) {
await crmContact.deleteOne()
}
}
}
)
const Company = mongoose.model('Company', crmCompany)
module.exports = Company

4840
backend/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
{
"name": "crm_integrator",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"start": "node app.js",
"dev": "nodemon app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^1.6.1",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"express-rate-limit": "^5.4.1",
"express-session": "^1.17.3",
"flat": "^3.0.1",
"helmet": "^4.6.0",
"http-status-codes": "^2.1.4",
"ioredis": "^5.3.2",
"luxon": "^3.4.4",
"mongoose": "^7.3.1",
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"socket.io": "^4.7.2",
"swagger-ui-express": "^4.1.6",
"xss-clean": "^0.1.1",
"yamljs": "^0.3.0"
},
"devDependencies": {
"nodemon": "^2.0.9"
}
}

View File

@ -0,0 +1,16 @@
const express = require('express')
const router = express.Router()
const { authorization, } = require('../middleware/authentication')
const { contactCreate, testTemplate, uploadCrmConfig, callJournaling, oauthCallBack, install, deleteCrm, deleteCompany } = require('../controllers/crmController')
const { fileUpload } = require("../utils")
router.route('/create-contact').post(authorization, contactCreate)
router.route('/call-journaling').post(authorization, callJournaling)
router.route('/upload').post(fileUpload.single('crm'), authorization, uploadCrmConfig)
router.route('/delete').post(authorization, deleteCrm)
router.route('/delete-company').post(authorization, deleteCompany)
router.route('/oauth-callback').get(oauthCallBack)
router.route('/install').get(install)
router.route('/test').post(testTemplate)
module.exports = router

View File

@ -0,0 +1,204 @@
openapi: 3.0.0
info:
title: Natural Language API Google
description: This API describes the endpoints and parameters to use resources from Google Cloud API.
contact: {}
version: '1.0'
servers:
- url: http://localhost:6001/api/v1/nl
variables: {}
paths:
/upload-audio-to-transcript:
post:
tags:
- Speech to text async
summary: Speech to text job
operationId: Speechtotextjob
parameters: []
requestBody:
content:
multipart/form-data:
encoding: {}
schema:
required:
- audio
type: object
properties:
audio:
type: string
format: binary
languageCode:
type: string
description: 'If not provided, the default will be: pt-BR'
example: pt-BR
required: false
responses:
'200':
description: ''
headers: {}
deprecated: false
security:
- bearer: []
/query-job-status:
get:
tags:
- Speech to text async
summary: Speech to text job process
operationId: Speechtotextjobprocess
parameters:
- name: operationName
in: query
description: 'The job id returned after uploading the audio file that will be transcribed.'
required: true
style: form
explode: true
schema:
type: integer
format: int64
example: 2993135803178989324
responses:
'200':
description: ''
headers: {}
deprecated: false
security:
- bearer: []
/speech-to-text:
post:
tags:
- Speech to text sync
summary: Speech to text
operationId: Speechtotext
parameters: []
requestBody:
content:
multipart/form-data:
encoding: {}
schema:
required:
- audio
type: object
properties:
audio:
type: string
format: binary
languageCode:
description: 'If not provided, the default will be: pt-BR'
type: string
example: pt-BR
required: false
responses:
'200':
description: ''
headers: {}
deprecated: false
security:
- bearer: []
/sentiment:
post:
tags:
- Sentiment
summary: Get sentiment
operationId: Getsentiment
parameters: []
requestBody:
description: ''
content:
application/json:
schema:
allOf:
- example:
text: Toda vez a mesma coisa ja to cansado de ficar ligando pra resolver esses problemas de conexão!
example:
text: Toda vez a mesma coisa ja to cansado de ficar ligando pra resolver esses problemas de conexão!
required: true
responses:
'200':
description: ''
headers: {}
deprecated: false
security:
- bearer: []
/text-to-speech:
get:
tags:
- Text to speech
summary: Text to speech
operationId: Texttospeech
parameters:
- name: text
in: query
description: ''
required: true
style: form
explode: true
schema:
type: string
example: Vela branca na enxurrada la vou eu de léo em léo, se o navio é pequeno do tamanho de um chapeu, não importa a volta ao mundo, é viagem de brinquedo em um barquinho de papel.
- name: voice_name
in: query
description: 'Ex: pt-BR-Wavenet-C'
required: false
style: form
explode: true
schema:
type: string
example:
- name: voice_gender
in: query
description: 'Ex: FEMALE'
required: false
style: form
explode: true
schema:
type: string
example:
- name: languageCode
in: query
description: 'Ex: pt-BR'
required: false
style: form
explode: true
schema:
type: string
example:
responses:
'200':
description: ''
headers: {}
deprecated: false
security:
- bearer: []
/voice-config:
get:
tags:
- Text to speech
summary: Get voice config
operationId: Getvoiceconfig
parameters:
- name: languageCode
in: query
description: ''
required: false
style: form
explode: true
schema:
type: string
example: pt-Br
responses:
'200':
description: ''
headers: {}
deprecated: false
security: []
components:
securitySchemes:
bearer:
type: http
scheme: bearer
security: []
tags:
- name: Speech to text async
- name: Speech to text sync
- name: Text to speech
- name: Sentiment

View File

@ -0,0 +1,10 @@
function convertToIntegerIfNumber(str) {
if (/^\d+$/.test(str)) {
return parseInt(str, 10)
} else {
return str
}
}
module.exports = convertToIntegerIfNumber

View File

@ -0,0 +1,25 @@
const lookupContactByPhone = require('./lookupCRMContactByPhone')
const createContact = require('./createContact')
const loadCRM = require('./loadCRM')
async function createCRMContact(companyId, crmFirstName, crmPhone, crmEmail = '', crmLastName = '',) {
const crmFiles = await loadCRM(companyId)
for (const crmConfig of crmFiles) {
const { crmRest: rest, authentication } = crmConfig.crm
const contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId)
if (contact.exist) continue
await createContact(companyId, rest, authentication, crmPhone, crmFirstName, crmLastName, crmEmail,)
}
}
module.exports = createCRMContact

View File

@ -0,0 +1,77 @@
const flatten = require('flat')
const unflatten = require('flat').unflatten
const axios = require('axios')
const CRM_Contact = require('../models/CRM_Contact')
const { URL } = require('url')
const findProperty = require('./findProperty')
const CRM = require('../models/CRM')
const requestConfigHeader = require('./requestConfigHeader')
const sendMessageSocket = require('./sendMessageSocket')
async function createContact(companyId, rest, authentication, crmPhone, crmFirstName = '', crmLastName = '', crmEmail = '', test = {}) {
let { request, body, response } = findProperty(rest, 'createContactRecord')
const { requestContentType, requestEncoding, requestType, responseType, url } = request
body = flatten(body)
const mapping = {
crmFirstName,
crmLastName,
crmPhone,
crmEmail
}
for (const prop in body) {
if (mapping.hasOwnProperty(body[prop])) {
const variable = mapping[body[prop]]
if (variable) {
body[prop] = variable
} else {
if (body[prop] == 'crmLastName' && !crmLastName) {
body[prop] = 'unnamed'
}
else
delete body[prop]
}
}
}
body = unflatten(body)
const { type, userName, passWord, token, crmClientId } = authentication
//url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = ''
const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, body)
if (test?.testing) {
msg = `Tentanto criar contato de numero ${crmPhone} no crm`
sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } })
}
let { data } = await axios(config)
data = flatten(data)
let auxContactId
for (const prop in data) {
const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '')
if (_prop == response?.id?.trim()) {
auxContactId = data[prop]
break
}
}
if (auxContactId && !test?.testing) {
const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname })
await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: crmPhone })
}
return { exist: true, contactId: auxContactId, phone: crmPhone }
}
module.exports = createContact

View File

@ -0,0 +1,13 @@
const fs = require('fs')
function createDir(path) {
fs.mkdir(path, { recursive: true }, (err) => {
if (err) {
console.error('Error creating directory:', err)
} else {
console.log('Directory created successfully')
}
})
}
module.exports = createDir

View File

@ -0,0 +1,8 @@
const fs = require("fs")
const crmCompany = (pathFile) => {
const crmRest = fs.readFileSync(pathFile, "utf8")
return JSON.parse(crmRest)
}
module.exports = crmCompany

View File

@ -0,0 +1,54 @@
const { DateTime } = require('luxon')
function dateTime() {
const brazilTimeZone = 'America/Sao_Paulo'
const currentDateBrazil = DateTime.now().setZone(brazilTimeZone) // Get current date and time in Brazil timezone
const formattedDateTime = currentDateBrazil.toFormat("yyyy-MM-dd'T'HH:mm:ssZZ") // Format to ISO 8601
console.log('FORMATTED DATE TIME: ', formattedDateTime)
return formattedDateTime
}
function secondsFormat(seconds, format = '') {
seconds = parseInt(seconds, 10)
switch (format) {
case 'hh:mm':
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const formattedHours = String(hours).padStart(2, '0')
const formattedMinutes = String(minutes).padStart(2, '0')
console.log(`formate hours: ${formattedHours}:${formattedMinutes}`)
return `${formattedHours}:${formattedMinutes}`
case 'milliseconds':
return seconds * 1000
}
return seconds
}
function getPastDateTimeFromSeconds(currentTime, secondsToSubtract) {
const currentDateTime = DateTime.fromISO(currentTime)
// Subtract seconds from the current date and time
const pastDateTime = currentDateTime.minus({ seconds: secondsToSubtract })
// Extract timezone offset from the current time
const timezoneOffset = currentDateTime.toFormat('ZZ')
// Format the past date and time in the desired format manually
const formattedPastDateTime = pastDateTime.toFormat(`yyyy-MM-dd'T'HH:mm:ss${timezoneOffset}`)
return formattedPastDateTime
}
module.exports = { dateTime, secondsFormat, getPastDateTimeFromSeconds }

View File

@ -0,0 +1,14 @@
const fs = require('fs')
function dirExist(path) {
try {
fs.accessSync(path, fs.constants.F_OK)
return true
} catch (err) {
console.error('Directory does not exist')
}
return false
}
module.exports = dirExist

View File

@ -0,0 +1,40 @@
const multer = require('multer')
const path = require('path')
const CustomError = require('../errors')
const fs = require('fs')
const dirExist = require('./dirExist')
const createDir = require('./createDir')
//Destination to store the file
const fileStorage = multer.diskStorage({
destination: function (req, file, cb) {
const dir = path.join(process.cwd(), 'public', 'jsonfiles')
if (!dirExist(dir)) {
createDir(dir)
}
cb(null, dir)
},
filename: function (req, file, cb) {
cb(null, Date.now() + String(Math.floor(Math.random() * 1000)) + path.extname(file.originalname))
}
})
const fileUpload = multer({
storage: fileStorage,
fileFilter(req, file, cb) {
if (!file.originalname.match(/\.(json)$/i)) {
return cb(new CustomError.BadRequestError(`'Invalid file type. Send only .json file!`))
}
cb(undefined, true)
}
})
module.exports = fileUpload

View File

@ -0,0 +1,11 @@
function findProperty(array, property) {
const index = array.findIndex((item) => {
return item[property] !== undefined
})
return array[index][property]
}
module.exports = findProperty

View File

@ -0,0 +1,50 @@
// const { createJWT, isTokenValid, attachCookiesToResponse } = require('./jwt')
// const createTokenUser = require('./createTokenUser')
// const checkPermissions = require('./checkPermissions')
const fileUpload = require('./fileUpload')
const loadCRM = require('./loadCRM')
const crmCompany = require('./crmCompany')
const createCRMContact = require('./createCRMContact')
const lookupCRMContactByPhone = require('./lookupCRMContactByPhone')
const dirExist = require('./dirExist')
const createDir = require('./createDir')
const loadJsonFiles = require('./loadJsonFiles')
const journaling = require('./journaling')
const convertToIntegerIfNumber = require('./convertToIntegerIfNumber')
const createContact = require('./createContact')
const oauth2 = require('./oauth2')
const redisClient = require('./redisClient')
const findProperty = require('./findProperty')
const dateTime = require('./dateTime')
const mustContainProperties = require('./mustContainProperties')
const journalingRequest = require('./journalingRequest')
const lookupContactByPhone = require('./lookupCRMContactByPhone')
const socketIO = require('./socketIO')
const sendMessageSocket = require('./sendMessageSocket')
const templateValidator = require('./templateValidator')
module.exports = {
fileUpload,
loadCRM,
crmCompany,
createCRMContact,
lookupCRMContactByPhone,
dirExist,
createDir,
loadJsonFiles,
journaling,
convertToIntegerIfNumber,
createContact,
oauth2,
redisClient,
findProperty,
dateTime,
mustContainProperties,
journalingRequest,
lookupContactByPhone,
socketIO,
sendMessageSocket,
templateValidator
}

View File

@ -0,0 +1,34 @@
const loadCRM = require('./loadCRM')
const lookupContactByPhone = require('./lookupCRMContactByPhone')
const createContact = require('./createContact')
const findProperty = require('./findProperty')
const journalingRequest = require('./journalingRequest')
async function journaling(companyId, operation, crmPhone, crmAgent, crmCallDuration = 0) {
const crmFiles = await loadCRM(companyId)
for (const crmConfig of crmFiles) {
const { crmRest: rest, authentication } = crmConfig.crm
let contact = await lookupContactByPhone(rest, authentication, crmPhone, companyId)
if (!contact.exist) {
contact = await createContact(companyId, rest, authentication, crmPhone)
}
let { request, calls, response } = findProperty(rest, 'callJournaling')
let body = findProperty(calls, operation)
await journalingRequest(request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, rest)
}
}
module.exports = journaling

View File

@ -0,0 +1,111 @@
const { dateTime, secondsFormat, getPastDateTimeFromSeconds } = require('./dateTime')
const requestConfigHeader = require('./requestConfigHeader')
const flatten = require('flat')
const unflatten = require('flat').unflatten
const axios = require('axios')
const path = require('path')
const convertToIntegerIfNumber = require('./convertToIntegerIfNumber')
const sendMessageSocket = require('./sendMessageSocket')
async function journalingRequest(request, body, crmCallDuration, contact, crmAgent, crmPhone, authentication, test = {}) {
const { requestContentType, requestEncoding, requestType, responseType, url } = request
body = flatten(body)
let ignore = []
for (let key in body) {
const k = Object.keys(body).find(k => {
if (!ignore.includes(k) && k.includes('._prop') && k.replace('._prop', '._type') ==
key.replace('._prop', '._type')) {
ignore.push(key)
return true
}
}
)
if (k) {
const type = body[k.replace('._prop', '._type')]
const format = body[k.replace('._prop', '._format')]
const newKey = k.replace('._prop', '').replace('._type', '').replace('._format', '')
if (body[key] == 'crmCallDuration') {
switch (format) {
case 'hh:mm':
body[newKey] = secondsFormat(crmCallDuration, 'hh:mm')
break
case 'milliseconds':
body[newKey] = secondsFormat(crmCallDuration, 'milliseconds')
break
case 'seconds':
body[newKey] = secondsFormat(crmCallDuration, 'seconds')
break
}
}
else if (body[key] == 'crmCallDateTime') {
switch (format) {
case 'ISO8601':
body[newKey] = crmCallDuration ? getPastDateTimeFromSeconds(dateTime(), crmCallDuration) : dateTime()
break
}
}
else if (body[key] == 'crmContactId') {
body[newKey] = contact.contactId
}
else if (body[key] == 'crmAgent') {
body[newKey] = crmAgent
}
else if (body[key] == 'crmPhone') {
body[newKey] = crmPhone
}
const property = body[newKey] ? body[newKey] : body[key]
switch (type) {
case 'number':
body[newKey] = convertToIntegerIfNumber(property)
break
case 'string':
body[newKey] = `${property}`
break
case 'boolean':
body[newKey] = Boolean(property)
break
}
continue
}
switch (body[key]) {
case 'crmPhone':
body[key] = crmPhone
break
case 'crmContactId':
body[key] = contact.contactId
break
case 'crmAgent':
body[key] = crmAgent
break
case 'crmCallDuration':
body[key] = crmCallDuration
break
}
}
const data = unflatten(body)
const { type, userName, passWord, token, crmClientId } = authentication
const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data)
if (test?.testing && test?.companyId && test?.msg) {
sendMessageSocket({ companyId: test.companyId, status: 'processing', data: { request: config, msg: test.msg } })
}
// console.log('JOURNALING CONFIG: ', config)
const res = await axios(config)
}
module.exports = journalingRequest

View File

@ -0,0 +1,10 @@
const CRM = require('../models/CRM')
async function loadCRM(companyId) {
const crm = await CRM.find({ companyId, enabled: true, testing: false }).select('crm').lean()
return crm
}
module.exports = loadCRM

View File

@ -0,0 +1,20 @@
const fs = require('fs').promises
const path = require('path')
async function loadJSONFiles(dirPath) {
const files = await fs.readdir(dirPath)
const jsonFiles = files.filter(file => file.endsWith('.json'))
const filePromises = jsonFiles.map(async file => {
const filePath = path.join(dirPath, file)
const data = await fs.readFile(filePath, 'utf8')
return { crm: JSON.parse(data), path: filePath }
})
const jsonDataArray = await Promise.all(filePromises)
return jsonDataArray
}
module.exports = loadJSONFiles

View File

@ -0,0 +1,71 @@
const axios = require('axios')
const flatten = require('flat')
const CustomError = require('../errors')
const CRM_Contact = require('../models/CRM_Contact')
const CRM = require('../models/CRM')
const { URL } = require('url')
const { getAccessToken } = require('./oauth2')
const findProperty = require('./findProperty')
const requestConfigHeader = require('./requestConfigHeader')
const sendMessageSocket = require('./sendMessageSocket')
async function lookupContactByPhone(rest, authentication, crmPhone, companyId, test = {}) {
let { request, body, response } = findProperty(rest, 'lookupContactByPhone')
let { requestContentType, requestEncoding, requestType, responseType, url } = request
const { type, userName, passWord, token, crmClientId } = authentication
const crmInfo = await CRM_Contact.findOne({ companyId, crmBaseURL: new URL(url).hostname, phone: crmPhone })
if (crmInfo) return { exist: true, contactId: crmInfo.contactId, phone: crmPhone }
const config = await requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId)
if (test?.testing){
let msg = `Tentanto checar se o contato de numero ${crmPhone} existe no crm`
sendMessageSocket({ companyId, status: 'processing', data: { request: config, msg } })
}
let { data } = await axios(config)
data = flatten(data)
let auxPhone
let auxContactId
for (const prop in data) {
const _prop = prop.replace(/^\d+\./, '').replace(/(?:^|\.)\d+\b/g, '')
if (_prop == response?.phone?.trim()) {
auxPhone = data[prop].replace('+', '')
}
if (_prop == response?.id?.trim()) {
auxContactId = data[prop]
}
if (auxPhone && auxContactId) break
}
if (auxPhone) {
if (auxPhone && auxContactId) {
const crm = await CRM.findOne({ companyId, crmBaseURL: new URL(url).hostname })
await CRM_Contact.create({ companyId, crm, crmBaseURL: new URL(url).hostname, contactId: auxContactId, phone: auxPhone })
}
return { exist: true, contactId: auxContactId, phone: crmPhone }
}
return { exit: false }
}
module.exports = lookupContactByPhone

View File

@ -0,0 +1,12 @@
const CustomError = require('../errors')
function mustContainProperties(req, requiredProperties) {
const missingProperties = requiredProperties.filter(prop => !req.body[prop])
if (missingProperties.length > 0) {
throw new CustomError.BadRequestError(`Missing properties: ${missingProperties.join(', ')}`)
}
}
module.exports = mustContainProperties

View File

@ -0,0 +1,75 @@
const qs = require('qs')
const axios = require('axios')
const findProperty = require('./findProperty')
const CRM = require('../models/CRM')
const { set, get, del } = require('./redisClient')
const exchangeForTokens = async (crmOauth, exchangeProof) => {
const { request, body, response } = findProperty(crmOauth.crm.crmRest, 'tokenEndpoint')
let { requestContentType, requestEncoding, requestType, responseType, url } = request
let config = {
method: requestType,
url,
data: qs.stringify(exchangeProof)
}
if (requestContentType != 'none') {
config = {
...config, headers: {
'Content-Type': requestContentType
}
}
}
const { data } = await axios(config)
const { refresh_token, access_token, token_type, expires_in } = data
await set(crmOauth.crm.authentication.crmClientId, access_token, Math.round(expires_in * 0.75))
crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': crmOauth.crm.authentication.crmClientId })
if (refresh_token) {
crmOauth.crm.authentication.crmOAuthRefreshToken = refresh_token
}
crmOauth.crm.authentication.crmOAuthToken = access_token
await crmOauth.save()
return access_token
}
const refreshAccessToken = async (clientId) => {
let crmOauth = await CRM.findOne({ 'crm.authentication.crmClientId': clientId })
crmOauth = crmOauth.toObject()
const refreshTokenProof = {
grant_type: 'refresh_token',
client_id: crmOauth.crm.authentication.crmClientId,
client_secret: crmOauth.crm.authentication.crmClientSecret,
redirect_uri: process.env.URL_OAUTH_CALLBACK,
refresh_token: crmOauth.crm.authentication.crmOAuthRefreshToken
}
return await exchangeForTokens(crmOauth, refreshTokenProof)
}
const getAccessToken = async (clientId) => {
if (!await get(clientId)) {
console.log('Refreshing expired access token')
await refreshAccessToken(clientId)
}
return await get(clientId)
}
module.exports = {
exchangeForTokens,
refreshAccessToken,
getAccessToken
}

View File

@ -0,0 +1,48 @@
const Redis = require("ioredis")
const redis = new Redis(process.env.REDIS_URI)
// Function to set a token with expiration
async function set(key, value, expirationInSeconds) {
await redis.set(key, value, 'EX', expirationInSeconds)
console.log(`Token ${key} set successfully with expiration of ${expirationInSeconds} seconds!`)
}
// Function to get a token
async function get(key) {
const token = await redis.get(key)
if (token === null) {
console.log('Token not found')
} else {
console.log(`Token for ${key}: ${token}`)
return token
}
}
// Function to delete a token
async function del(key) {
const deletedCount = await redis.del(key)
if (deletedCount === 1) {
console.log(`Token ${key} deleted successfully!`)
} else {
console.log('Token not found')
}
}
module.exports = {
set, get, del
}
// // Example usage
// const tokenKey = 'userToken'
// const tokenValue = 'exampleTokenValue'
// const expirationSeconds = 300; // 300 seconds expiration (5 minutes)
// (async () => {
// await setTokenWithExpiration(tokenKey, tokenValue, expirationSeconds)
// await getToken(tokenKey)
// await updateToken(tokenKey, 'newTokenValue')
// await getToken(tokenKey)
// await deleteToken(tokenKey)
// await getToken(tokenKey) // Checking if token is deleted
// redis.quit() // Close Redis connection
// })()

View File

@ -0,0 +1,49 @@
const { getAccessToken } = require('./oauth2')
async function requestConfigHeader(url, crmPhone, requestType, requestContentType, type, userName, passWord, token, crmClientId, data = '') {
let config = {}
url = url.replace('crmPhone', crmPhone)
let commonConfig = {
method: requestType,
url,
headers: {
'Content-Type': requestContentType,
}
}
if (data) {
commonConfig = { ...commonConfig, data }
}
if (type === 'basic') {
const auth = Buffer.from(`${userName}:${passWord}`).toString('base64')
config = {
...commonConfig,
headers: {
...commonConfig.headers,
'Authorization': `Basic ${auth}`,
}
}
} else if (type === 'bearer') {
config = {
...commonConfig,
headers: {
...commonConfig.headers,
'Authorization': `Bearer ${token}`,
}
}
} else if (type === 'oauth2') {
const accessToken = await getAccessToken(crmClientId)
config = {
...commonConfig,
headers: {
...commonConfig.headers,
'Authorization': `Bearer ${accessToken}`,
}
}
}
return config
}
module.exports = requestConfigHeader

View File

@ -0,0 +1,16 @@
const { getIO } = require('./socketIO')
function sendMessageSocket({ companyId, action, status, data }) {
// console.log(`*** ${msg} ***`)
const io = getIO()
io.to(`company_${companyId.toString()}`).emit("crm_upload", {
action,
status,
data
})
}
module.exports = sendMessageSocket

View File

@ -0,0 +1,121 @@
const SocketIO = require('socket.io')
let io
const initIO = (httpServer) => {
io = SocketIO(httpServer, {
cors: {
origin: 'http://localhost:3000'
},
maxHttpBufferSize: 1e8
})
io.on("connection", socket => {
console.log('CLIENT CONNECTED')
socket.on("companySession", (companyId) => {
console.log(`A client joined a companySession channel: ${companyId}`)
socket.join(`company_${companyId}`)
});
// socket.on("joinWhatsSession", (whatsappId: string) => {
// logger.info(`A client joined a joinWhatsSession channel: ${whatsappId}`)
// socket.join(`session_${whatsappId}`)
// })
// socket.on("message_from_client", () => {
// socket.emit("message_from_server", "Sent an event from the server!")
// })
// socket.on("message_create", async (data: any) => {
// handleMessage(data.msg, data)
// })
// socket.on("media_uploaded", async (data: any) => {
// handleMessage(data.msg, data)
// })
// socket.on("message_ack", async (data: any) => {
// handleMsgAck(data.id, data.ack)
// })
// socket.on("joinChatBox", (ticketId: string) => {
// logger.info("A client joined a ticket channel")
// socket.join(ticketId)
// })
// socket.on("joinNotification", () => {
// logger.info("A client joined notification channel")
// socket.join("notification")
// })
// socket.on("joinTickets", (status: string) => {
// logger.info(`A client joined to ${status} tickets channel.`)
// socket.join(status)
// })
socket.on("disconnect", (data) => {
console.log(`Client disconnected socket: ${data}`)
})
// socket.on("disconnecting", async () => {
// console.log("socket.rooms: ", socket.rooms) // the Set contains at least the socket ID
// let rooms = socket.rooms
// console.log("rooms: ", rooms, " | rooms.size: ", rooms.size)
// if (rooms && rooms.size == 1) return
// if (rooms && rooms.size == 2 && ![...rooms][1].startsWith("session_"))
// return
// let whatsappIds: any = await Whatsapp.findAll({
// attributes: ["id"],
// raw: true
// })
// if (whatsappIds && whatsappIds.length > 0) {
// whatsappIds = whatsappIds.map((e: any) => `${e.id}`)
// console.log(
// "whatsappIds whatsappIds whatsappIds whatsappIds whatsappIds: ",
// whatsappIds
// )
// if (
// rooms &&
// rooms.size == 2 &&
// [...rooms][1].startsWith("session_") &&
// whatsappIds.includes([...rooms][1].replace("session_", ""))
// ) {
// console.log([...rooms][1])
// let whatsappId = [...rooms][1].replace("session_", "")
// const whatsapp = await Whatsapp.findByPk(whatsappId, {})
// if (whatsapp) {
// await whatsapp.update({ status: "OPENING" })
// io.emit("whatsappSession", {
// action: "update",
// session: whatsapp
// })
// }
// }
// }
// })
})
return io
}
const getIO = () => {
if (!io) {
throw new AppError("Socket IO not initialized")
}
return io
}
module.exports = { initIO, getIO }

View File

@ -0,0 +1,63 @@
const lookupContactByPhone = require('./lookupCRMContactByPhone')
const createContact = require('./createContact')
const sendMessageSocket = require('./sendMessageSocket')
const findProperty = require('./findProperty')
const journalingRequest = require('./journalingRequest')
async function templateValidator(crmPhoneTest, crm, companyId) {
let phoneTest = crmPhoneTest ? crmPhoneTest : '5511900112233'
let contact = await lookupContactByPhone(
rest = crm.crmRest,
authentication = crm.authentication,
crmPhone = phoneTest,
companyId,
test = { testing: true })
if (!contact.exist) {
console.log('O CONTATO SER CRIADO')
contact = await createContact(companyId,
rest = crm.crmRest,
authentication = crm.authentication,
crmPhone = phoneTest,
crmFirstName = '',
crmLastName = '',
crmEmail = '',
test = { testing: true })
sendMessageSocket({ companyId, status: 'processing', data: { msg } })
}
else {
msg = `Contato de numero ${phoneTest} já existe no crm`
sendMessageSocket({ companyId, status: 'processing', data: { msg } })
}
let { request, calls, response } = findProperty(crm.crmRest, 'callJournaling')
for (const operation of calls) {
msg = `Tentando inserir registro de chamada(call journal) ${Object.keys(operation)[0]} para o contato ${phoneTest}`
await journalingRequest(request,
body = operation[Object.keys(operation)[0]],
crmCallDuration = "900",
contact,
crmAgent = "2245",
crmPhone = phoneTest,
authentication = crm.authentication,
test = { testing: true, msg, companyId })
msg = `Registro de chamada inserido com sucesso ${Object.keys(operation)[0]} para o contato ${phoneTest}`
sendMessageSocket({ companyId, status: 'processing', data: { msg } })
}
sendMessageSocket({ companyId, status: 'finished', data: { crmPhoneTest: phoneTest } })
}
module.exports = templateValidator

30749
frontend/package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,47 @@
{
"name": "crm-upload-template-frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.5.0",
"@fortawesome/free-solid-svg-icons": "^6.5.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.3",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.3.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-modal": "^3.16.1",
"react-scripts": "5.0.0",
"socket.io-client": "^4.7.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,13 @@
import Title from './components/Title'
import UploadFile from './components/UploadFile'
function App() {
return (
<main>
<Title />
<UploadFile />
</main>
)
}
export default App

View File

@ -0,0 +1,10 @@
const Title = () => {
return (
<div className='title'>
<h1>
<span>CRM</span> upload template
</h1>
</div>
);
};
export default Title;

View File

@ -0,0 +1,204 @@
import React, { useState, useEffect } from 'react'
import axios from 'axios'
import socketIOClient from 'socket.io-client'
import Modal from 'react-modal'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faCheckCircle } from '@fortawesome/free-solid-svg-icons'
Modal.setAppElement('#root')
const UploadFile = () => {
const [isModalOpen, setIsModalOpen] = useState(false)
const [payload, setPayload] = useState('')
const [message, setMessage] = useState('')
const [status, setStatus] = useState('')
const [errorResponse, setErrorResponse] = useState('')
const [crmPhoneTest, setCrmPhoneTest] = useState('')
const [file, setFile] = useState(null)
const [uploadStatus, setUploadStatus] = useState('')
const handleFileChange = (e) => {
const selectedFile = e.target.files[0]
setFile(selectedFile)
}
const handleUpload = async () => {
if (!file) {
alert('Please select a file.')
return
}
const formData = new FormData()
formData.append('crm', file)
formData.append('companyId', process.env.REACT_APP_COMPANY_ID)
try {
const response = await axios.post(`${process.env.REACT_APP_URL_API}/api/v1/crm/upload`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': 'Bearer 2ivck10D3o9qAZi0pkKudVDl9bdEVXY2s8gdxZ0jYgL1DZWTgDz6wDiIjlWgYmJtVOoqf0b42ZTLBRrfo8WoAaScRsujz3jQUNXdchSg0o43YilZGmVhheGJNAeIQRknHEll4nRJ7avcFgmDGoYbEey7TSC8EHS4Z3gzeufYYSfnKNDBwwzBURIQrTOxYFe3tBHsGOzwnuD2lU5tnEx7tr2XRO4zRNYeNY4lMBOFM0mRuyAe4kuqTrKXmJ8As200'
},
})
if (response.status === 200) {
const { url } = response.data
console.log('-----> url: ', url)
if (url)
window.location.href = url
setUploadStatus('File uploaded successfully!')
} else {
setUploadStatus('Failed to upload file.')
console.log('error')
}
} catch (error) {
console.error('Error uploading file:', error)
setUploadStatus('Error uploading file.')
setStatus('error')
setErrorResponse(error.response.data.msg)
}
}
useEffect(() => {
console.log('process.env.REACT_APP_COMPANY_ID: ', process.env.REACT_APP_COMPANY_ID)
const socket = socketIOClient(process.env.REACT_APP_URL_API)
const currentURL = window.location.href
const urlObject = new URL(currentURL)
const clientId = urlObject.searchParams.get('clientId')
console.log('clientId: ', clientId)
if (clientId) {
const postData = async () => {
try {
await axios.post(`${process.env.REACT_APP_URL_API}/api/v1/crm/test`, {
clientId,
companyId: process.env.REACT_APP_COMPANY_ID,
})
} catch (error) {
setStatus('error')
console.error('-----> Error:', error)
console.log('TEST: ', error.response.data.msg)
setErrorResponse(error.response.data.msg)
}
}
postData()
}
socket.emit('companySession', process.env.REACT_APP_COMPANY_ID)
socket.on('crm_upload', (data) => {
console.log('Received data from server:', data)
setStatus(data?.status)
setMessage(data?.data?.msg)
if (data?.data?.request) {
setPayload(data.data.request, null, 2)
}
if (data?.data?.crmPhoneTest) {
setCrmPhoneTest(data.data.crmPhoneTest)
}
setIsModalOpen(true)
})
return () => {
socket.disconnect()
}
}, [])
const closeModal = () => {
setIsModalOpen(false)
}
const handleAfterOpenModal = () => {
}
return (
<div className="centered">
<input type="file" onChange={handleFileChange} />
<button onClick={handleUpload}>Upload</button>
{uploadStatus && <p>{uploadStatus}</p>}
<Modal
isOpen={isModalOpen}
onRequestClose={handleAfterOpenModal}
contentLabel="Example Modal"
>
<div className="modal-title">
{status === 'finished' && <h2>Template validado com sucesso</h2>}
{status === 'error' && <h2>Template com erro no processo abaixo</h2>}
{status === 'processing' && <h2>Testando template. Aguarde...</h2>}
</div>
<div className="error-message">
{status === 'error' && (
<p>
{errorResponse}
</p>
)}
</div>
<div className="success-message">
{status === 'finished' && (
<p>
Verifique no seu CRM os dados inseridos para o contato de teste: {crmPhoneTest}
</p>
)}
</div>
<div className="icon-container">
{status === 'finished' &&
<div className="checkmark-container">
<FontAwesomeIcon icon={faCheckCircle} size="5x" color="green" />
</div>
}
</div>
{status === 'finished' ? <></> : <> <MessageComponent title={message} />{payload ? <PayloadComponent formattedJSON={payload} /> : <></>}</>}
<button className="close-button" onClick={closeModal}>Close Modal</button>
</Modal>
</div>
)
}
const MessageComponent = ({ title }) => {
return (
<div>
<h4>{title}</h4>
</div>
)
}
const PayloadComponent = ({ formattedJSON }) => {
return (
<div>
<pre>{JSON.stringify(formattedJSON, null, 2)}</pre>
</div>
)
}
export default UploadFile

View File

@ -0,0 +1,344 @@
*,
::after,
::before {
box-sizing: border-box;
}
/* fonts */
/* @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;600&family=Montserrat&display=swap'); */
html {
font-size: 100%;
} /*16px*/
:root {
/* colors */
--primary-100: #e2e0ff;
--primary-200: #c1beff;
--primary-300: #a29dff;
--primary-400: #837dff;
--primary-500: #645cff;
--primary-600: #504acc;
--primary-700: #3c3799;
--primary-800: #282566;
--primary-900: #141233;
/* grey */
--grey-50: #f8fafc;
--grey-100: #f1f5f9;
--grey-200: #e2e8f0;
--grey-300: #cbd5e1;
--grey-400: #94a3b8;
--grey-500: #64748b;
--grey-600: #475569;
--grey-700: #334155;
--grey-800: #1e293b;
--grey-900: #0f172a;
/* rest of the colors */
--black: #222;
--white: #fff;
--red-light: #f8d7da;
--red-dark: #842029;
--green-light: #d1e7dd;
--green-dark: #0f5132;
/* fonts */
/* --headingFont: 'Roboto', sans-serif;
--bodyFont: 'Nunito', sans-serif; */
--small-text: 0.875rem;
--extra-small-text: 0.7em;
/* rest of the vars */
--backgroundColor: var(--grey-50);
--textColor: var(--grey-900);
--borderRadius: 0.25rem;
--letterSpacing: 1px;
--transition: 0.3s ease-in-out all;
--max-width: 1120px;
--fixed-width: 600px;
/* box shadow*/
--shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
--shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
--shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
--shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
body {
background: var(--backgroundColor);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-weight: 400;
line-height: 1.75;
color: var(--textColor);
}
p {
margin: 0;
max-width: 40em;
}
h1,
h2,
h3,
h4,
h5 {
margin: 0;
font-family: var(--headingFont);
font-weight: 400;
line-height: 1.3;
text-transform: capitalize;
letter-spacing: var(--letterSpacing);
}
h1 {
margin: 0;
font-size: 3.052rem;
}
h2 {
font-size: 2.441rem;
}
h3 {
font-size: 1.953rem;
}
h4 {
font-size: 1.563rem;
}
h5 {
font-size: 1.25rem;
}
small,
.text-small {
font-size: var(--small-text);
}
a {
text-decoration: none;
}
ul {
list-style-type: none;
padding: 0;
}
.img {
width: 100%;
display: block;
object-fit: cover;
}
/* buttons */
.btn {
cursor: pointer;
color: var(--white);
background: var(--primary-500);
border: transparent;
border-radius: var(--borderRadius);
letter-spacing: var(--letterSpacing);
padding: 0.375rem 0.75rem;
box-shadow: var(--shadow-1);
transition: var(--transition);
text-transform: capitalize;
display: inline-block;
}
.btn:hover {
background: var(--primary-700);
box-shadow: var(--shadow-3);
}
.btn-hipster {
color: var(--primary-500);
background: var(--primary-200);
}
.btn-hipster:hover {
color: var(--primary-200);
background: var(--primary-700);
}
.btn-block {
width: 100%;
}
/* alerts */
.alert {
padding: 0.375rem 0.75rem;
margin-bottom: 1rem;
border-color: transparent;
border-radius: var(--borderRadius);
}
.alert-danger {
color: var(--red-dark);
background: var(--red-light);
}
.alert-success {
color: var(--green-dark);
background: var(--green-light);
}
/* form */
.form {
width: 90vw;
max-width: var(--fixed-width);
background: var(--white);
border-radius: var(--borderRadius);
box-shadow: var(--shadow-2);
padding: 2rem 2.5rem;
margin: 3rem auto;
}
.form-label {
display: block;
font-size: var(--small-text);
margin-bottom: 0.5rem;
text-transform: capitalize;
letter-spacing: var(--letterSpacing);
}
.form-input,
.form-textarea {
width: 100%;
padding: 0.375rem 0.75rem;
border-radius: var(--borderRadius);
background: var(--backgroundColor);
border: 1px solid var(--grey-200);
}
.form-row {
margin-bottom: 1rem;
}
.form-textarea {
height: 7rem;
}
::placeholder {
font-family: inherit;
color: var(--grey-400);
}
.form-alert {
color: var(--red-dark);
letter-spacing: var(--letterSpacing);
text-transform: capitalize;
}
/* alert */
@keyframes spinner {
to {
transform: rotate(360deg);
}
}
.loading {
width: 6rem;
height: 6rem;
border: 5px solid var(--grey-400);
border-radius: 50%;
border-top-color: var(--primary-500);
animation: spinner 0.6s linear infinite;
}
.loading {
margin: 0 auto;
}
/* title */
.title {
text-align: center;
margin: 2rem 0;
}
.title-underline {
background: var(--primary-500);
width: 10rem;
height: 0.25rem;
margin: 0 auto;
}
.title span {
color: var(--primary-500);
}
/*
===============
Axios Tutorial
===============
*/
.text-center {
text-align: center;
}
.btn-block {
margin-top: 0.75rem;
}
.section {
width: 90vw;
max-width: var(--fixed-width);
margin: 0 auto;
padding-bottom: 2rem;
}
.dad-joke {
margin-top: 1rem;
}
.centered {
display: flex;
justify-content: center;
align-items: center;
height: 50vh; /* Set the height of the container to the full viewport height */
}
.modal-title {
text-align: center;
margin-bottom: 0px; /* Adjust spacing as needed */
font-weight: 800;
}
.error-message {
color: red;
font-weight: bold;
text-align: center;
display: flex;
justify-content: center;
}
.success-message {
color: blue;
font-weight: bold;
text-align: center;
display: flex;
justify-content: center;
}
.checkmark-container {
display: flex;
justify-content: center;
align-items: center;
/* margin-bottom: 20px; */
}
.close-button {
position: absolute;
bottom: 10px;
right: 10px;
padding: 10px 20px;
background-color: #007bff; /* Example color - Change as needed */
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: background-color 0.3s ease;
}
.close-button:hover {
background-color: #0056b3; /* Example color - Change as needed */
}
/* Style for the modal */
.custom-modal {
border: 10px solid #000; /* Black border with 10px thickness */
pointer-events: auto;
/* Other modal styles here */
}

View File

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

3704
package-lock.json generated 100644

File diff suppressed because it is too large Load Diff

17
package.json 100644
View File

@ -0,0 +1,17 @@
{
"name": "api-crm",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "Adriano <adriano08andrade@hotmail.com>",
"license": "MIT",
"devDependencies": {
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"husky": "^8.0.3"
}
}