initial commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.env
|
||||
node_modules
|
||||
uploads
|
||||
.DS_Store
|
||||
@@ -0,0 +1,28 @@
|
||||
# Salesforce PDF Attachment Automation
|
||||
|
||||
A Node.js utility that extracts text and metadata (like *Codice Fiscale* and Name variations) from PDF documents and automatically attaches them to the corresponding Account records in a Salesforce Sandbox.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
* **PDF Parsing:** Reads local PDF files and extracts metadata using regex matching.
|
||||
* **Smart Account Matching:** Queries Salesforce Accounts by `Codice Fiscale` or Name permutations (first name / last name variations).
|
||||
* **Automated Uploads:** Converts PDFs to Base64 strings and attaches them directly to Accounts in Salesforce via `ContentVersion`.
|
||||
* **Duplicate Protection:** Filters and sorts matching accounts by creation date.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running this project, ensure you have:
|
||||
|
||||
* [Node.js](https://nodejs.org/)
|
||||
* A **Salesforce Sandbox** account with API access
|
||||
* Salesforce Security Token
|
||||
|
||||
---
|
||||
|
||||
## sfdc2.js
|
||||
|
||||
This projects uses the ```sfdc2.js``` library which facilitates interaction with the Salesforce API.
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"access_token":"00D0Q0000008ipI!AQEAQMnYks5eNzLDp_OnKVFWoLjIRbH9.l5m1dWrXi9uincFgJLXfp2Xjsq3fUVLME_hWxZLUz6bi3MGn.kRRoS_dfH12yz1",
|
||||
"refresh_token":"5Aep861kbRszIADsvg2SfeYjNbk56Irp0JDnM8BJPqvw6JZavvYBjsOg_Ha2KvaCiqyM6RKdfGpRnsZtRPSVdRB",
|
||||
"signature":"oPM1SwaBGuPyYocnfh3h197bxQ5AzR0hK0+gdHhkJks=",
|
||||
"scope":"refresh_token web api",
|
||||
"instance_url":"https://almascuola--dev2021.sandbox.my.salesforce.com",
|
||||
"id":{
|
||||
"access_token":"00D0Q0000008ipI!AQEAQMnYks5eNzLDp_OnKVFWoLjIRbH9.l5m1dWrXi9uincFgJLXfp2Xjsq3fUVLME_hWxZLUz6bi3MGn.kRRoS_dfH12yz1",
|
||||
"refresh_token":"5Aep861kbRszIADsvg2SfeYjNbk56Irp0JDnM8BJPqvw6JZavvYBjsOg_Ha2KvaCiqyM6RKdfGpRnsZtRPSVdRB",
|
||||
"signature":"oPM1SwaBGuPyYocnfh3h197bxQ5AzR0hK0+gdHhkJks=",
|
||||
"scope":"refresh_token web api",
|
||||
"instance_url":"https://almascuola--dev2021.sandbox.my.salesforce.com",
|
||||
"id":"https://test.salesforce.com/id/00D0Q0000008ipIUAQ/005MA00000AiipLYAR",
|
||||
"token_type":"Bearer","issued_at":"1783859104265"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"access_token":"00D1t000000ss9x!AQEAQE1CnJO6.Pj0Dp.sNM8GJiLqg78JObdGkoMe_8bSvKiAghEx96_U4UerGqmWNjC0HhRqT3WxS9SAJ2mV.Sx_zZEB9cKb",
|
||||
"refresh_token":"5Aep8610F.RUa2F48BnJfSHjt4eCIOSyaqJl8Ge64SjsPyNpED5mcb5jPDcu_LKFFtZxiGH9VxtfA==",
|
||||
"signature":"BKDhT/jYJpboLOxPNz5Wc4jOH6WA1zoPesOYzQbpLyg=",
|
||||
"scope":"refresh_token chatter_api api full",
|
||||
"id_token":"eyJraWQiOiIyNjAiLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiNEs2VUplRmxJeXFaVVVURzE0dXRYUSIsInN1YiI6Imh0dHBzOi8vbG9naW4uc2FsZXNmb3JjZS5jb20vaWQvMDBEMXQwMDAwMDBzczl4RUFBLzAwNTF0MDAwMDAxcGRWckFBSSIsImF1ZCI6IjNNVkc5UmQzcUM2b01hbFVJQWhPMXlyYUpCU3NkT181WDB0VGgwTTNaVzJ3aWFfZEhaYXc0d2lrYlU5STVYVVpDakQ4RmI0dnZTMmd0djBranlLUjEiLCJhbXIiOlsiVG90cCIsIm1mYSIsIm90cCIsInB3ZCJdLCJpc3MiOiJodHRwczovL2xvZ2luLnNhbGVzZm9yY2UuY29tIiwiZXhwIjoxNzczNzMyMTY0LCJpYXQiOjE3NzM3MzIwNDR9.hNM0BuXYa40Bu8WAHfPJtA6AzgSRRaKgejUTu6XwD7-tdAUe8f_mSdU4OnSRrreW13ixGzqk8r0CEWGMvPPTNHYueePMoNYSRWZeERJlclbygijwLu-w6U2HVp6KTl7kl2-VSwT2xUZ2lOu-Cu0hAY1EoqVrq-oRHUWfSjKgjCanPeD4MIJ2yxo6NrOJ6EV6wQanInkMQeycVtMIJUGTlNqeQMuBJkk9HY_kjaMI_M4acIGnupUcl4i-h9-Kzxh4BCVCQBTdh45PQ69VbdfyJbhX1W3czaXxx0W0Tyr6J1EtX5ttlOB9Hyp16dkbE_ajpSilUSPil2-2_a_g1sLgWX3OkVO0OR8CLi1F4ZDLR8Li9NYVnZNABluM912siTHUJfhAJj1p5EsKN0TMpU4f-k7EL3h90C7oiwlFW1OmHs6r3XK9N3sVAqWz9ByRggyNw_l_hhyIxKNZNJisBSURRTr85thkhwqH9Gq8b8w0jnCxLN0pnz8rmDK6oxwbjcLLaOCGp-oqV8IamMGkJu45aCAqZVxSvr5oEz3zqb2PoeEyZDVEzzrRlw-CcTxH5tF14zxq4WJXqKV1kbRgIOGdgGAQzgP0fnm5HGO4cIDIHFDH2TYX9Km-AD3JkkHIKRvqcYOCVmW8ir5RJ10yNzNmn6C1IwjB8JnP4KNFmFQLQBc",
|
||||
"instance_url":"https://almascuola.my.salesforce.com",
|
||||
"id":"https://login.salesforce.com/id/00D1t000000ss9xEAA/0051t000001pdVrAAI",
|
||||
"token_type":"Bearer",
|
||||
"issued_at":"1773732044735"
|
||||
}
|
||||
Generated
+6795
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "1.0.0",
|
||||
"description": "basic node project",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "denise",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"axios": "^1.19.0",
|
||||
"cors": "^2.8.6",
|
||||
"dotenv": "^17.4.2",
|
||||
"exceljs": "^4.4.0",
|
||||
"express": "^5.2.1",
|
||||
"jest": "^30.4.2",
|
||||
"multer": "^2.2.0",
|
||||
"nodemon": "^3.1.14",
|
||||
"papaparse": "^5.5.4",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdf-parse": "^2.4.5",
|
||||
"supertest": "^7.2.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
const express = require('express');
|
||||
const multer = require('multer');
|
||||
const cors = require('cors');
|
||||
|
||||
const {
|
||||
initializeSalesforceConnection,
|
||||
attachPdfToAccount,
|
||||
splitExcelFile,
|
||||
splitPDF,
|
||||
cleanUp
|
||||
} = require('./services');
|
||||
|
||||
const app = express();
|
||||
app.use(cors({
|
||||
origin: 'http://localhost:5173'
|
||||
}));
|
||||
|
||||
const upload = multer({ dest: 'uploads/' });
|
||||
|
||||
app.post('/api/documents', upload.array('documents'), async (req, res) => {
|
||||
const documentTitle = req.body.title;
|
||||
|
||||
let excelFile = null;
|
||||
let pdfFile = null;
|
||||
|
||||
if (req.files && req.files.length > 0) {
|
||||
req.files.forEach((file) => {
|
||||
const fileName = file.originalname.toLowerCase();
|
||||
if (fileName.endsWith('.xlsx') || fileName.endsWith('.xls')) {
|
||||
excelFile = file.path;
|
||||
} else if (fileName.endsWith('.pdf')) {
|
||||
pdfFile = file.path;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!documentTitle || !excelFile || !pdfFile) {
|
||||
if (req.files) {
|
||||
cleanUp(req.files.map((f) => f.path));
|
||||
}
|
||||
return res.status(400).json({
|
||||
error: !documentTitle ? 'Document title is required' : 'Both excel and pdf files are required'
|
||||
});
|
||||
}
|
||||
|
||||
const tempFilePath = [];
|
||||
try {
|
||||
let resultExcel = await splitExcelFile(excelFile);
|
||||
let resultPdf = await splitPDF(resultExcel, pdfFile, tempFilePath);
|
||||
|
||||
await initializeSalesforceConnection();
|
||||
await attachPdfToAccount(resultPdf, documentTitle); // No need to pass sfdc
|
||||
|
||||
return res.status(200).json({
|
||||
ok: true,
|
||||
message: 'Files uploaded successfully!'
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error in processing the files: ', err);
|
||||
return res.status(500).json({
|
||||
ok: false,
|
||||
message: 'Error in processing and uploading the files'
|
||||
});
|
||||
} finally {
|
||||
cleanUp(tempFilePath);
|
||||
cleanUp([excelFile, pdfFile]);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { app };
|
||||
@@ -0,0 +1,12 @@
|
||||
// excel1 => /Users/ddatinguinoo/Downloads/Estratto IPP 5 Sic.xlsx
|
||||
// excel2 => /Users/ddatinguinoo/Downloads/Estratto Haccp IPP 5 (1).xlsx
|
||||
// excel3 di prova => /Users/ddatinguinoo/Downloads/File di prova.xlsx
|
||||
// pdf1 => /Users/ddatinguinoo/Downloads/G-S IPP 5.pdf
|
||||
// pdf2 => /Users/ddatinguinoo/Downloads/H IPP 5.pdf
|
||||
const { app } = require('./app');
|
||||
|
||||
// this opens the network port
|
||||
// this is separated from app.js so that it doesn't interfere with tests or serverless deployments
|
||||
app.listen(3000, () => {
|
||||
console.log('Server running on http://localhost:3000');
|
||||
});
|
||||
@@ -0,0 +1,226 @@
|
||||
const ExcelJS = require('exceljs');
|
||||
const { PDFParse } = require('pdf-parse');
|
||||
const { PDFDocument } = require('pdf-lib');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const sfdc = require('./sfdc2');
|
||||
|
||||
require('dotenv').config({ path: path.resolve(__dirname, '../.env') });
|
||||
|
||||
const authConfig = {
|
||||
sfdcTokenFile: path.resolve(__dirname, '..', process.env.SFDC_TOKEN_FILE || 'almadev21.json'),
|
||||
sfdcClientId: process.env.SFDC_CLIENT_ID,
|
||||
sfdcClientSecret: process.env.SFDC_CLIENT_SECRET
|
||||
};
|
||||
|
||||
async function initializeSalesforceConnection() {
|
||||
|
||||
// sfdc.setProductionBaseUrl(); // or sfdc.setSandboxBaseUrl();
|
||||
sfdc.setSandboxBaseUrl();
|
||||
sfdc.setTokenFile(authConfig.sfdcTokenFile);
|
||||
sfdc.setClientId(authConfig.sfdcClientId);
|
||||
sfdc.setClientSecret(authConfig.sfdcClientSecret);
|
||||
|
||||
// Try to load existing token or initiate OAuth flow
|
||||
if (!await sfdc.getSalesforceToken()) {
|
||||
console.log('No token found. Initiating OAuth2 flow...');
|
||||
await sfdc.initToken((loginUrl) => {
|
||||
console.log(`Please open this URL to authenticate: ${loginUrl}`);
|
||||
});
|
||||
} else {
|
||||
console.log('Token loaded successfully.');
|
||||
}
|
||||
// Ensure the access token is valid and refresh if necessary
|
||||
await sfdc.checkToken();
|
||||
console.log('Salesforce connection is ready.');
|
||||
}
|
||||
|
||||
// Escape single quotes for SOQL queries (e.g., D'Angelo -> D\'Angelo)
|
||||
function escapeSoql(str) {
|
||||
return str ? str.replace(/'/g, "\\'") : '';
|
||||
}
|
||||
|
||||
// the pdf files contain the identifying ids of the users such as 'Codice Fiscale' (CF) and we use them to attach
|
||||
// the page/pages to the specific account that has that id. If CFs are not available we search based on the
|
||||
// name an lastname of the user
|
||||
async function attachPdfToAccount(tempFiles, docTitle) {
|
||||
let parser;
|
||||
try {
|
||||
for (let i = 0; i < tempFiles.length; i++) {
|
||||
const pdfBuffer = fs.readFileSync(tempFiles[i]);
|
||||
const base64Pdf = pdfBuffer.toString('base64');
|
||||
parser = new PDFParse({ data: pdfBuffer });
|
||||
|
||||
try {
|
||||
const result = await parser.getText();
|
||||
let conditions = [];
|
||||
let matchAccount = null;
|
||||
|
||||
const userID = result.text.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i);
|
||||
const codiceFiscale = userID ? userID[1].trim() : null;
|
||||
|
||||
if (codiceFiscale) {
|
||||
const cfSoql = `SELECT Id, Name, CF__c FROM Account WHERE CF__c='${codiceFiscale}' ORDER BY CreatedDate ASC LIMIT 1`;
|
||||
const cfResult = await sfdc.query(cfSoql);
|
||||
|
||||
if (cfResult && cfResult.records && cfResult.records.length > 0) {
|
||||
matchAccount = cfResult.records[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchAccount) {
|
||||
// since the name and lastname of the user are reversed in some files, we need to put it in order
|
||||
// to match the name from the database
|
||||
const nameMatch = result.text.match(/Il\s+Sig\.\/La\s+Sig\.ra[ \t]+([A-Za-zÀ-ÿ']+(?:[ \t]+[A-Za-zÀ-ÿ']+)+)/i);
|
||||
if (nameMatch) {
|
||||
const rawName = nameMatch[1].trim();
|
||||
const parts = rawName.split(/\s+/);
|
||||
|
||||
if (parts.length >= 2) {
|
||||
const fullName = parts.join(' ');
|
||||
const reversedName = [...parts.slice(1), parts[0]].join(' ');
|
||||
|
||||
if (fullName !== reversedName) {
|
||||
conditions.push(`Name='${escapeSoql(reversedName)}'`);
|
||||
}
|
||||
} else {
|
||||
conditions.push(`Name='${escapeSoql(rawName)}'`);
|
||||
}
|
||||
|
||||
const nameSoql = `SELECT Id, Name, CF__c FROM Account WHERE (${ conditions.join(' OR ') }) ORDER BY CF__c DESC NULLS LAST, CreatedDate ASC LIMIT 1`;
|
||||
const nameResult = await sfdc.query(nameSoql);
|
||||
|
||||
if (nameResult && nameResult.records && nameResult.records.length > 0) {
|
||||
matchAccount = nameResult.records[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchAccount) {
|
||||
console.log('--- Match found! ---');
|
||||
const nowIsoString = new Date().toISOString();
|
||||
const newDocument = await sfdc.create('Documento__c', {
|
||||
Account__c: matchAccount.Id,
|
||||
Name: docTitle,
|
||||
Data_Caricamento__c: nowIsoString,
|
||||
Tipo_Documento__c: docTitle,
|
||||
Stato__c: 'Valido'
|
||||
});
|
||||
|
||||
await sfdc.create('ContentVersion', {
|
||||
Title: `${ docTitle } - ${ matchAccount.Name } -`,
|
||||
PathOnClient: `${ docTitle }_${ matchAccount.Name }.pdf`,
|
||||
VersionData: base64Pdf,
|
||||
FirstPublishLocationId: newDocument.id
|
||||
});
|
||||
} else {
|
||||
console.log('Account non trovato su Salesforce!');
|
||||
}
|
||||
} finally {
|
||||
if (parser) {
|
||||
await parser.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(`PDFs successfully attached!`);
|
||||
} catch (error) {
|
||||
console.error('Error uploading PDF: ', error);
|
||||
throw error; // Rethrow to let app.js catch the error
|
||||
}
|
||||
}
|
||||
|
||||
async function processPage(pdfDoc, pageIndex, tempFiles) {
|
||||
try {
|
||||
const newDoc = await PDFDocument.create();
|
||||
const [copiedPage] = await newDoc.copyPages(pdfDoc, [pageIndex]);
|
||||
newDoc.addPage(copiedPage);
|
||||
const pdfBytes = await newDoc.save();
|
||||
|
||||
const filepath = path.join(os.tmpdir(), `fileTemp_page_${pageIndex}.pdf`);
|
||||
fs.writeFileSync(filepath, pdfBytes);
|
||||
tempFiles.push(filepath);
|
||||
} catch (error) {
|
||||
console.error('Error in processPage:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function splitExcelFile(inputFile) {
|
||||
try {
|
||||
console.log(`Loading excel file: ${inputFile}...\n`);
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
await workbook.xlsx.readFile(inputFile);
|
||||
const worksheet = workbook.getWorksheet(1);
|
||||
|
||||
return worksheet.actualRowCount - 1;
|
||||
} catch (error) {
|
||||
console.error('Error processing the excel file: ', error);
|
||||
throw error; // Rethrow error
|
||||
}
|
||||
}
|
||||
|
||||
const getCodiceFiscale = (pageText) => {
|
||||
if (!pageText) return null;
|
||||
const match = pageText.match(/Codice fiscale:\s*([A-Za-z0-9]+)/i);
|
||||
return match ? match[1].toUpperCase() : null;
|
||||
};
|
||||
|
||||
async function splitPDF(numRows, inputFile, tempFiles) {
|
||||
console.log(`Loading pdf file: ${inputFile}...\n`);
|
||||
let parser;
|
||||
|
||||
try {
|
||||
const existingPdfBytes = fs.readFileSync(inputFile);
|
||||
parser = new PDFParse({ data: existingPdfBytes });
|
||||
const result = await parser.getText();
|
||||
const pagesNumber = result.pages.length;
|
||||
|
||||
const srcPdf = await PDFDocument.load(existingPdfBytes);
|
||||
|
||||
if (pagesNumber > numRows) {
|
||||
for (let i = 0; i < pagesNumber; i++) {
|
||||
// we use optional chaining ?. so that when we encounter null or undefined values, the code doesn't crash
|
||||
const prevCF = getCodiceFiscale(result.pages[i + 1]?.text);
|
||||
const currentCF = getCodiceFiscale(result.pages[i]?.text);
|
||||
if (prevCF && prevCF === currentCF) {
|
||||
continue;
|
||||
}
|
||||
await processPage(srcPdf, i, tempFiles);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < pagesNumber; i++) {
|
||||
await processPage(srcPdf, i, tempFiles);
|
||||
}
|
||||
}
|
||||
return tempFiles;
|
||||
} catch (error) {
|
||||
console.error(`Error processing the pdf file: `, error);
|
||||
throw error;
|
||||
} finally {
|
||||
if (parser) {
|
||||
await parser.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanUp(tempFiles) {
|
||||
if (!Array.isArray(tempFiles)) return;
|
||||
tempFiles.forEach((file) => {
|
||||
try {
|
||||
if (file && fs.existsSync(file)) {
|
||||
fs.unlinkSync(file);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed in deleting the temporary file ${file}: `, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeSalesforceConnection,
|
||||
attachPdfToAccount,
|
||||
splitExcelFile,
|
||||
splitPDF,
|
||||
cleanUp
|
||||
};
|
||||
@@ -0,0 +1,923 @@
|
||||
var fs = require('fs');
|
||||
var axios = require('axios');
|
||||
var Papa = require('papaparse');
|
||||
|
||||
|
||||
const sfdc = {
|
||||
baseurl:'',
|
||||
apiurl:'/services/data/v58.0',
|
||||
callbackUrl:'http://localhost:3000/auth',
|
||||
credentials: {},
|
||||
afterLoginUrl:undefined,
|
||||
|
||||
app: {},
|
||||
server: undefined,
|
||||
|
||||
logresult: function(resp) { console.log('internal--->'); console.log(JSON.stringify(resp,null,2)); },
|
||||
}
|
||||
|
||||
/**
|
||||
* Set token filename
|
||||
* @param {string} filename
|
||||
*/
|
||||
sfdc.setTokenFile=function(filename) {
|
||||
this.tokenfile=filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Custom URL
|
||||
* @param {string} customUrl
|
||||
*/
|
||||
sfdc.setCustomBaseUrl=function(customUrl) {
|
||||
this.baseurl=customUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Sandbox URL
|
||||
*/
|
||||
sfdc.setSandboxBaseUrl=function() {
|
||||
this.baseurl='https://test.salesforce.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Production URL
|
||||
*/
|
||||
sfdc.setProductionBaseUrl=function() {
|
||||
this.baseurl='https://login.salesforce.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ClientId
|
||||
* @param {string} clientId
|
||||
*/
|
||||
sfdc.setClientId=function(clientId) {
|
||||
this.clientId=clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ClientSecret
|
||||
* @param {string} clientSecret
|
||||
*/
|
||||
sfdc.setClientSecret=function(clientSecret) {
|
||||
this.clientSecret=clientSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* retrieve Token file
|
||||
* @returns null if not found, token struct if found
|
||||
*/
|
||||
sfdc.getSalesforceToken=async function() {
|
||||
try {
|
||||
let data=fs.readFileSync(this.tokenfile);
|
||||
this.credentials = JSON.parse(data);
|
||||
return this.credentials;
|
||||
} catch(err) {
|
||||
console.log('cannot find files');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* store Token file
|
||||
*/
|
||||
sfdc.storeSalesforceToken=async function() {
|
||||
console.log('save token');
|
||||
try {
|
||||
fs.writeFileSync(this.tokenfile, JSON.stringify(this.credentials));
|
||||
console.log('saved');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.log(this.credentials);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* init oauth2 sequence
|
||||
* listen for callback calls and store token when received
|
||||
*/
|
||||
sfdc.initToken=function(callback) {
|
||||
return new Promise( (resolve, reject) => {
|
||||
console.log('initToken');
|
||||
var express = require('express');
|
||||
this.app = express();
|
||||
this.server=this.app.listen(3000);
|
||||
this.app.get('/auth', async (req, res) => {
|
||||
await this.manageSalesforceCallback(req, res);
|
||||
if (this.oauth2phase==2) this.storeSalesforceToken();
|
||||
resolve();
|
||||
});
|
||||
let loginurl=this.requestAuthCode();
|
||||
if (callback) callback(loginurl);
|
||||
})
|
||||
}
|
||||
/**
|
||||
* initTokenExpress
|
||||
* use this when already bind to 3000
|
||||
* @param {express app} app
|
||||
* @returns
|
||||
*/
|
||||
sfdc.initTokenExpress=function(app) {
|
||||
return new Promise( (resolve, reject) => {
|
||||
app.get('/auth', async (req, res) => {
|
||||
await sfdc.manageSalesforceCallback(req, res);
|
||||
if (this.oauth2phase==2) sfdc.storeSalesforceToken();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* manage callback
|
||||
* internal function, receive express get method
|
||||
*/
|
||||
sfdc.manageSalesforceCallback=async function(req, res) {
|
||||
var refresh_token = req.query.refresh_token;
|
||||
var code = req.query.code;
|
||||
console.log('refresh token: ' + refresh_token);
|
||||
console.log('code: ' + code)
|
||||
|
||||
if (!sfdc.afterLoginUrl)
|
||||
res.send('ok');
|
||||
else
|
||||
res.redirect(sfdc.afterLoginUrl);
|
||||
|
||||
if (code != undefined) {
|
||||
this.oauth2phase=1;
|
||||
console.log('code received, get token');
|
||||
this.credentials=await this.getRefreshToken(code);
|
||||
this.storeSalesforceToken();
|
||||
if (this.server) this.server.close();
|
||||
}
|
||||
|
||||
if (refresh_token) {
|
||||
this.oauth2phase=2;
|
||||
console.log('refresh_token received');
|
||||
this.credentials=refresh_token;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create initial url for receiving auth code
|
||||
* @returns false if errors, login url if ok
|
||||
*/
|
||||
sfdc.requestAuthCode=async function() {
|
||||
console.log('requestAuthCode');
|
||||
var url = this.baseurl+'/services/oauth2/authorize';
|
||||
url += '?response_type=code';
|
||||
url += '&client_id=' + encodeURIComponent(this.clientId);
|
||||
url += '&redirect_uri=' + this.callbackUrl;
|
||||
|
||||
try {
|
||||
console.log(url);
|
||||
let response=await axios.get(url);
|
||||
|
||||
if (response.status == 200) {
|
||||
console.log('Please authenticate via this url in the browser:');
|
||||
console.log(response.request.res.responseUrl);
|
||||
return response.request.res.responseUrl;
|
||||
} else {
|
||||
console.log('Error on authenticate:');
|
||||
console.log(response.data.url);
|
||||
console.log(response.data.explanation);
|
||||
return false;
|
||||
}
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ask for new access token
|
||||
* @returns non lo so
|
||||
*/
|
||||
sfdc.getAccessToken=async function() {
|
||||
console.log('getAccessToken');
|
||||
var url = this.baseurl+'/services/oauth2/token';
|
||||
var body= 'grant_type=refresh_token';
|
||||
body += '&client_id=' + this.clientId;
|
||||
body += '&client_secret=' + this.clientSecret;
|
||||
body += '&refresh_token=' + this.credentials.refresh_token;
|
||||
|
||||
try {
|
||||
let response=await axios.post(url, body);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log('getAccessToken error:' + error.response.statusText);
|
||||
console.log('error:', error.response.data);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ask for refresh token
|
||||
* @param {string} code authcode for this scope
|
||||
* @returns false if error, refresh token structure if ok
|
||||
*/
|
||||
sfdc.getRefreshToken=async function(code) {
|
||||
console.log('getRefreshToken');
|
||||
var url = this.baseurl+'/services/oauth2/token';
|
||||
let body = 'grant_type=authorization_code';
|
||||
body += '&code=' + code;
|
||||
body += '&client_id=' + this.clientId;
|
||||
body += '&client_secret=' + this.clientSecret;
|
||||
body += '&redirect_uri=' + this.callbackUrl;
|
||||
|
||||
let opts = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||||
|
||||
try {
|
||||
console.log(url);
|
||||
let response=await axios.post(url, body, opts);
|
||||
console.log(response.data);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.log('getRefreshToken error:' + error.response.statusText);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* check autocode ttl
|
||||
* @returns true if authcode is still valid, false if not
|
||||
*/
|
||||
sfdc.isTokenValid=function() {
|
||||
|
||||
if (!this.credentials.issued_at) {
|
||||
console.log('no credentials available');
|
||||
return false;
|
||||
}
|
||||
|
||||
var h = 2; // token live in hours
|
||||
var im = h * 60 * 60 * 1000; // token live in milliseconds
|
||||
|
||||
var i1 = parseInt(this.credentials.issued_at)
|
||||
var i2 = new Date().valueOf();
|
||||
|
||||
// console.log('i2 vs i1 vs i1+im',i2,i1,i1+im);
|
||||
if (i2 > (i1 + im)) {
|
||||
console.log('authcode expired');
|
||||
return false;
|
||||
}
|
||||
console.log('authcode is still valid');
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* fresh authcode if old is expired
|
||||
* @returns authcode old or new
|
||||
*/
|
||||
sfdc.checkToken=async function() {
|
||||
console.log('checkToken');
|
||||
if (!this.isTokenValid()) {
|
||||
try {
|
||||
let res=await this.getAccessToken();
|
||||
|
||||
this.credentials.issued_at = res.issued_at;
|
||||
this.credentials.access_token = res.access_token;
|
||||
this.credentials.instance_url = res.instance_url;
|
||||
console.log('new access token generated...');
|
||||
return res;
|
||||
} catch(err) {
|
||||
console.log('checkToken: error on getting a new token!');
|
||||
console.log('err',err);
|
||||
return err;
|
||||
}
|
||||
} else {
|
||||
return this.credentials;
|
||||
}
|
||||
}
|
||||
|
||||
// ======================
|
||||
// Actions
|
||||
// ======================
|
||||
|
||||
/*
|
||||
|
||||
var soql="selct id from account limit 10",
|
||||
var nextBatch=false;
|
||||
var irow=0;
|
||||
do {
|
||||
let data=await sfdc.query(soql, nextBatch);
|
||||
console.log(`${irow} of ${data.totalSize}`);
|
||||
|
||||
// do what you need
|
||||
for(var a of data.records) {
|
||||
setData(a)
|
||||
}
|
||||
// do what you need
|
||||
|
||||
soql=data.nextRecordsUrl;
|
||||
nextBatch=!data.done;
|
||||
irow+=data.records.length;
|
||||
|
||||
} while (nextBatch);
|
||||
|
||||
*/
|
||||
|
||||
sfdc.search=async function(soql, nextRecord=false) {
|
||||
var url = soql; // next batch
|
||||
if(!nextRecord) url = this.credentials.instance_url + this.apiurl + '/search/?q=' + encodeURI(soql);
|
||||
else url = this.credentials.instance_url + soql;
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
var t1=new Date();
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
return response.data;
|
||||
} catch(err) {
|
||||
console.log('search error');
|
||||
console.log(err);
|
||||
console.log(err.response.data);
|
||||
return err.response.data;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.query=async function(soql, nextRecord=false) {
|
||||
var url = soql; // next batch
|
||||
if(!nextRecord) url = this.credentials.instance_url + this.apiurl + '/query/?q=' + encodeURIComponent(soql);
|
||||
else url = this.credentials.instance_url + soql;
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
var t1=new Date();
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
// console.log(soql);
|
||||
// console.log('query ok');
|
||||
|
||||
var t2=new Date();
|
||||
// console.log('duration (ms): '+(t2.valueOf()-t1.valueOf() ));
|
||||
// console.log('records:'+response.data.totalSize);
|
||||
return response.data;
|
||||
} catch(err) {
|
||||
console.log('query error');
|
||||
console.log(err);
|
||||
console.log(err.response.data);
|
||||
return err.response.data;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.retrieve=async function(obj, id, fields) {
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects/'+obj+'/'+id+'?fields='+fields.join(',');
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
console.log('retrieve ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('retrieve bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sfdc.upsert=async function(obj, key, records) {
|
||||
|
||||
if (!records || !records.length) return {};
|
||||
|
||||
var body={ "allOrNone" : false, records: [] };
|
||||
|
||||
records.forEach(function(r) {
|
||||
var r2=JSON.parse(JSON.stringify(r));
|
||||
r2['attributes']={ type: obj };
|
||||
body.records.push( r2 );
|
||||
});
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/composite/sobjects/'+obj+'/'+key;
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.patch(url, body, config);
|
||||
console.log('upsert ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('upsert bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sfdc.create=async function(obj, record) {
|
||||
|
||||
if (!record) return {};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects/'+obj;
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
var body=record;
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.post(url, body, config);
|
||||
console.log('create ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('create bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
sfdc.update=async function(obj, key, record) {
|
||||
|
||||
if (!record) return {};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects/'+obj+'/'+key;
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
var body=record;
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.patch(url, body, config);
|
||||
console.log('create ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('create bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// bulk api 2.0
|
||||
//
|
||||
|
||||
sfdc.bulkQuery=async function (query) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
var body = {
|
||||
"operation" : "query",
|
||||
"query" : query
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/query/';
|
||||
|
||||
try {
|
||||
let response=await axios.post(url, JSON.stringify(body), config)
|
||||
console.log('create ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('createJob bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
sfdc.bulkQueryStatus=async function (jobId) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/query/' + jobId + '/';
|
||||
|
||||
try {
|
||||
let response=await axios.get(url, config)
|
||||
console.log('jobStatus ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('jobStatus bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.bulkQueryStatusPolling=async function (jobId, waitTime, waitSteps) {
|
||||
|
||||
let bulkWait={state:''};
|
||||
let count=0;
|
||||
do {
|
||||
await new Promise(resolve => setTimeout(resolve, waitTime?waitTime:1000));
|
||||
bulkWait=await sfdc.bulkQueryStatus(jobId);
|
||||
count++;
|
||||
console.log('status:',bulkWait.state,'count:', count);
|
||||
} while ( (bulkWait.state!='JobComplete' && bulkWait.state!='Failed') || (waitSteps && count<waitSteps) )
|
||||
|
||||
return bulkWait.state;
|
||||
}
|
||||
|
||||
|
||||
sfdc.bulkQueryResultsLoop=async function (jobId, maxRecords) {
|
||||
|
||||
let locator=undefined;
|
||||
let data=[];
|
||||
do {
|
||||
let bulkId3=await sfdc.bulkQueryResults(jobId, locator, maxRecords?maxRecords:50000);
|
||||
console.log('locator:', bulkId3.locator, bulkId3.maxRecords);
|
||||
locator=bulkId3.locator;
|
||||
|
||||
let parsed=Papa.parse(bulkId3.data, {header:true, delimiter: ',', newline: "\n", skipEmptyLines: true});
|
||||
console.log(parsed.data.length);
|
||||
data=data.concat(parsed.data);
|
||||
} while (locator!='null');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
sfdc.bulkQueryResults=async function (jobId, locator, maxRecords) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'text/csv',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/query/' + jobId + '/results';
|
||||
|
||||
let params=[];
|
||||
if (locator) params.push('locator='+locator);
|
||||
if (maxRecords) params.push('maxRecords='+maxRecords);
|
||||
if (params.length) {
|
||||
url += '?';
|
||||
url += params.join('&');
|
||||
}
|
||||
|
||||
try {
|
||||
let response=await axios.get(url, config)
|
||||
console.log('jobStatus ok');
|
||||
let res={
|
||||
"locator": response.headers['sforce-locator'],
|
||||
"maxRecords": response.headers['sforce-numberofrecords'],
|
||||
data: response.data
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
} catch(error) {
|
||||
console.log('jobStatus bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.bulkCreate=async function (operation, obj, key) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
var body = {
|
||||
operation: operation,
|
||||
object: obj,
|
||||
contentType: "CSV",
|
||||
columnDelimiter: "COMMA",
|
||||
lineEnding: "LF",
|
||||
};
|
||||
if (key) body.externalIdFieldName=key;
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/ingest/';
|
||||
|
||||
try {
|
||||
let response=await axios.post(url, JSON.stringify(body), config)
|
||||
console.log('create ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('createJob bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
sfdc.bulkSendData=async function (jobId, data) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
},
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/ingest/' + jobId + '/batches/';
|
||||
|
||||
try {
|
||||
let response=await axios.put(url, data, config)
|
||||
console.log('sendBatch ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('sendBatch bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.bulkClose=async function (jobId) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
|
||||
var body = {
|
||||
state: "UploadComplete"
|
||||
};
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/ingest/' + jobId + '/';
|
||||
|
||||
try {
|
||||
let response=await axios.patch(url, JSON.stringify(body), config)
|
||||
console.log('closeJob ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('closeJob bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.bulkStatus=async function (jobId) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/ingest/' + jobId + '/';
|
||||
|
||||
try {
|
||||
let response=await axios.get(url, config)
|
||||
console.log('jobStatus ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('jobStatus bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.bulkGetResults=async function (jobId) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'text/csv',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/ingest/' + jobId + '/successfulResults';
|
||||
|
||||
try {
|
||||
let response=await axios.get(url, config)
|
||||
console.log('jobStatus ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('jobStatus bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.bulkGetFailed=async function (jobId) {
|
||||
await this.checkToken();
|
||||
|
||||
var config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json; charset=UTF-8',
|
||||
'Accept': 'text/csv',
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token
|
||||
}
|
||||
};
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/jobs/ingest/' + jobId + '/failedResults';
|
||||
|
||||
try {
|
||||
let response=await axios.get(url, config)
|
||||
console.log('jobStatus ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('jobStatus bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.createContentVersion=async function(Title, PathOnClient, VersionData, FirstPublishLocationId) {
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects/ContentVersion';
|
||||
|
||||
console.log(url);
|
||||
|
||||
var config = { headers: {
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token,
|
||||
// 'Content-Type': 'multipart/form-data; boundary="boundary_string" '
|
||||
'Content-Type': 'application/json'
|
||||
} };
|
||||
|
||||
let body = {
|
||||
Title: Title,
|
||||
PathOnClient: PathOnClient,
|
||||
VersionData: VersionData,
|
||||
FirstPublishLocationId: FirstPublishLocationId
|
||||
}
|
||||
|
||||
// let body='--boundary_string\nContent-Disposition: form-data; name="entity_content";\nContent-Type: application/json\n';
|
||||
// body+=JSON.stringify(record);
|
||||
// body+='\n--boundary_string\nContent-Disposition: form-data; name="VersionData"; filename="'+record.PathOnClient+'";\nContent-Type: application/octet-stream\n';
|
||||
// body+=data;
|
||||
// body+='\n--boundary_string\n';
|
||||
|
||||
|
||||
console.log(body);
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.post(url, body, config);
|
||||
console.log('createContentVersion ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('createContentVersion bad');
|
||||
if (error.response) console.log(error.response.data);
|
||||
else console.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
sfdc.getContentBody=async function(bodyUrl) {
|
||||
|
||||
var url = this.credentials.instance_url;
|
||||
url += bodyUrl;
|
||||
|
||||
console.log(url);
|
||||
|
||||
var config = {
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'Authorization': 'Bearer ' + this.credentials.access_token,
|
||||
} };
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
console.log('getContentBody ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('getContentBody bad');
|
||||
if (error.response) console.log(error.response.data);
|
||||
else console.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
sfdc.objectFields = async function(obj) {
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects/'+obj+'/describe';
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
console.log('objectFields ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('objectFields bad');
|
||||
console.log(error);
|
||||
// console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
sfdc.object = async function(obj) {
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects/'+obj;
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
console.log('object ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('object bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* List all available objects in Salesforce
|
||||
* @param {string} token - Salesforce access token
|
||||
* @returns {Promise<Array>} List of objects with their metadata
|
||||
*/
|
||||
sfdc.listobjects = async function(token) {
|
||||
var url = this.credentials.instance_url;
|
||||
url += this.apiurl + '/sobjects';
|
||||
|
||||
var config = { headers: { 'Authorization': 'Bearer ' + this.credentials.access_token } };
|
||||
|
||||
try {
|
||||
this.checkToken();
|
||||
let response=await axios.get(url, config);
|
||||
console.log('listobjects ok');
|
||||
return response.data;
|
||||
} catch(error) {
|
||||
console.log('listobjects bad');
|
||||
console.log(error.response.data);
|
||||
return error;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// ======================
|
||||
// Cache
|
||||
// ======================
|
||||
|
||||
/**
|
||||
* Example
|
||||
*
|
||||
var cache=sfdc.newCache(5);
|
||||
for (var i=0; i<upd.length; i++) {
|
||||
if (cache.add( upd[i] )) {
|
||||
await sfdc.upsert("Account", "Id", cache.content() );
|
||||
cache.reset();
|
||||
}
|
||||
}
|
||||
await sfdc.upsert("Account", "Id", cache.content() );
|
||||
*/
|
||||
|
||||
sfdc.newCache=function(maxrecords=100) {
|
||||
return {
|
||||
records:[],
|
||||
maxrecords:maxrecords,
|
||||
|
||||
add: function(record) {
|
||||
this.records.push(record);
|
||||
if (this.maxrecords>= this.records.length) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
isEmpty: function() { return (this.records.length==0); },
|
||||
|
||||
content: function() { return this.records; },
|
||||
|
||||
reset: function() { this.records=[]; }
|
||||
|
||||
}
|
||||
}
|
||||
//export default sfdc;
|
||||
|
||||
module.exports = sfdc;
|
||||
@@ -0,0 +1,52 @@
|
||||
const request = require('supertest');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const { app } = require('../src/app');
|
||||
const services = require('../src/services');
|
||||
jest.mock('../src/services'); // jest uses fake/dummy functions instead of the real ones
|
||||
|
||||
describe('POST /api/documents', () => {
|
||||
let dummyFileExcel;
|
||||
let dummyFilePdf;
|
||||
beforeAll(() => {
|
||||
dummyFileExcel = path.join(os.tmpdir(), 'test_file.xlsx');
|
||||
dummyFilePdf = path.join(os.tmpdir(), 'test_file.pdf');
|
||||
|
||||
fs.writeFileSync(dummyFileExcel, 'dummy file excel content');
|
||||
fs.writeFileSync(dummyFilePdf, 'dummy pdf file content');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(dummyFileExcel)) fs.unlinkSync(dummyFileExcel);
|
||||
if (fs.existsSync(dummyFilePdf)) fs.unlinkSync(dummyFilePdf);
|
||||
});
|
||||
|
||||
it('should process files and return status 200 when helpers succeed', async() => {
|
||||
// this tells the fake functions what to return
|
||||
services.splitExcelFile.mockResolvedValue(5); // Returns 5 rows
|
||||
services.splitPDF.mockResolvedValue(['/tmp/page1.pdf']); // Returns 1 temp file
|
||||
services.initializeSalesforceConnection.mockResolvedValue(); // Simulates success
|
||||
services.attachPdfToAccount.mockResolvedValue(); // Simulates success
|
||||
|
||||
// this makes test request to Express
|
||||
const response = await request(app)
|
||||
.post('/api/documents')
|
||||
.field('title', 'Quarterly Report')
|
||||
.attach('documents', dummyFileExcel)
|
||||
.attach('documents', dummyFilePdf);
|
||||
|
||||
// assertions => verify express responded properly and called our service
|
||||
expect(response.statusCode).toBe(200);
|
||||
expect(response.body).toEqual({
|
||||
ok: true,
|
||||
message: 'Files uploaded successfully!'
|
||||
});
|
||||
|
||||
// Verify helpers were actually called
|
||||
expect(services.splitExcelFile).toHaveBeenCalledTimes(1);
|
||||
expect(services.splitPDF).toHaveBeenCalledTimes(1);
|
||||
expect(services.initializeSalesforceConnection).toHaveBeenCalledTimes(1);
|
||||
expect(services.cleanUp).toHaveBeenCalledTimes(2);
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
|
||||
# Vite
|
||||
*.timestamp-*-*.mjs
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
# frontend
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="eng">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" href="/alma_logo.png" />
|
||||
<title>App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Generated
+2559
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.19.0",
|
||||
"sweetalert2": "^11.26.25",
|
||||
"vue": "^3.5.40"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.8",
|
||||
"vite": "^8.1.5",
|
||||
"vite-plugin-vue-devtools": "^8.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^22.18.0 || >=24.12.0"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,366 @@
|
||||
<template>
|
||||
<div class="container-parent">
|
||||
<div class="files-wrapper">
|
||||
<div class="container-file-excel">
|
||||
<FileUploadCard
|
||||
title="Excel File"
|
||||
iconClass="fa-file-excel"
|
||||
iconColor="rgb(56, 160, 80)"
|
||||
note=".xlsx, .xls"
|
||||
:disabled="maxFilesReached"
|
||||
@file-selected="handleFiles"
|
||||
/>
|
||||
<p id="result-container" v-if="getExcelFile">
|
||||
<span class="file-link" @click="previewFile(getExcelFile)">
|
||||
{{ getExcelFile }}
|
||||
</span>
|
||||
<i class="fa-solid fa-circle-xmark" id="circle-mark" @click="openDeleteModal(getExcelFile)"></i>
|
||||
</p>
|
||||
<div class="del-file" v-if="deleteFile">
|
||||
<ModalPopUp
|
||||
:nameFile="deleteFileName"
|
||||
@confirm="confirmDelete"
|
||||
@cancel="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-file-pdf">
|
||||
<FileUploadCard
|
||||
title="PDF File"
|
||||
iconClass="fa-file-pdf"
|
||||
iconColor="rgb(209, 43, 10)"
|
||||
note=".pdf"
|
||||
:disabled="maxFilesReached"
|
||||
@file-selected="handleFiles"
|
||||
/>
|
||||
<p id="result-container" v-if="getPdfFile">
|
||||
<span class="file-link" @click="previewFile(getPdfFile)">
|
||||
{{ getPdfFile }}
|
||||
</span>
|
||||
<i class="fa-solid fa-circle-xmark" id="circle-mark" @click="openDeleteModal(getPdfFile)"></i>
|
||||
</p>
|
||||
<div class="del-file" v-if="deleteFile">
|
||||
<ModalPopUp
|
||||
:nameFile="deleteFileName"
|
||||
@confirm="confirmDelete"
|
||||
@cancel="closeModal"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p id="alert-message"
|
||||
v-show="duplicateFileTypes"
|
||||
>The two files must be of two different types of extension (.xlsx/.xls - .pdf)!</p>
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" id="upload-btn"
|
||||
:disabled="!maxFilesReached"
|
||||
@click="chooseTitleDoc">Next</button>
|
||||
</div>
|
||||
<div class="result-http" v-if="resultHTTP">{{ resultHTTP }}</div>
|
||||
<div class="nameFile" v-if="chooseTitle">
|
||||
<SelectNameDoc
|
||||
@selected-value="val => nameDoc = val"
|
||||
@close-modal="closePopUp"
|
||||
@proceed="proceedUpload"
|
||||
:disabled="!nameDoc || !nameDoc.trim()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import FileUploadCard from './components/FileUploadCard.vue';
|
||||
import ModalPopUp from './components/ModalPopUp.vue';
|
||||
import SelectNameDoc from './components/SelectNameDoc.vue';
|
||||
|
||||
const allFiles = ref([]);
|
||||
const deleteFile = ref(false);
|
||||
const deleteFileName = ref(null);
|
||||
const duplicateFileTypes = ref(false);
|
||||
const getExcelFile = ref('');
|
||||
const getPdfFile = ref('');
|
||||
const nameDoc = ref('');
|
||||
const resultHTTP = ref('');
|
||||
const chooseTitle = ref(false);
|
||||
const data = ref(null);
|
||||
|
||||
const maxFilesReached = computed(() => allFiles.value.length >= 2);
|
||||
|
||||
function handleFiles(file) {
|
||||
if (!file || maxFilesReached.value) return;
|
||||
|
||||
const extension = '.' + file.name.split('.').pop().toLowerCase();
|
||||
const isExcel = extension === '.xlsx' || extension === '.xls';
|
||||
const isPdf = extension === '.pdf';
|
||||
|
||||
// Check if we already have a file of this type loaded
|
||||
const hasExcel = allFiles.value.some(f => {
|
||||
const ext = '.' + f.name.split('.').pop().toLowerCase();
|
||||
return ext === '.xlsx' || ext === '.xls';
|
||||
});
|
||||
|
||||
const hasPdf = allFiles.value.some(f => {
|
||||
const ext = '.' + f.name.split('.').pop().toLowerCase();
|
||||
return ext === '.pdf';
|
||||
});
|
||||
|
||||
if ((isExcel && hasExcel) || (isPdf && hasPdf)) {
|
||||
duplicateFileTypes.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
duplicateFileTypes.value = false;
|
||||
|
||||
if (isExcel) {
|
||||
getExcelFile.value = file.name;
|
||||
} else if (isPdf) {
|
||||
getPdfFile.value = file.name;
|
||||
}
|
||||
|
||||
allFiles.value.push(file);
|
||||
}
|
||||
|
||||
function previewFile(fileName) {
|
||||
// Find the actual File object from our array matching the file name
|
||||
const file = allFiles.value.find(f => f.name === fileName);
|
||||
if (!file) return;
|
||||
|
||||
// Create a temporary URL pointing to the file in browser memory
|
||||
const fileUrl = URL.createObjectURL(file);
|
||||
|
||||
// Open the file in a new browser tab
|
||||
window.open(fileUrl, '_blank');
|
||||
}
|
||||
|
||||
function openDeleteModal(fileName) {
|
||||
deleteFile.value = true;
|
||||
deleteFileName.value = fileName;
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
if (deleteFileName.value) {
|
||||
// Remove file from array by name matching
|
||||
allFiles.value = allFiles.value.filter(f => f.name !== deleteFileName.value);
|
||||
|
||||
// Reset reactive file names
|
||||
if (getExcelFile.value === deleteFileName.value) {
|
||||
getExcelFile.value = '';
|
||||
} else if (getPdfFile.value === deleteFileName.value) {
|
||||
getPdfFile.value = '';
|
||||
}
|
||||
}
|
||||
closeModal();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
deleteFile.value = false;
|
||||
deleteFileName.value = null;
|
||||
}
|
||||
|
||||
function chooseTitleDoc() {
|
||||
nameDoc.value = '';
|
||||
chooseTitle.value = true;
|
||||
}
|
||||
|
||||
function closePopUp() {
|
||||
nameDoc.value = '';
|
||||
chooseTitle.value = false;
|
||||
}
|
||||
|
||||
async function proceedUpload() {
|
||||
if (!nameDoc.value || !nameDoc.value.trim()) {
|
||||
return;
|
||||
}
|
||||
chooseTitle.value = false;
|
||||
data.value = [...allFiles.value];
|
||||
|
||||
const formData = new FormData();
|
||||
data.value.forEach((file) => {
|
||||
formData.append('documents', file);
|
||||
})
|
||||
|
||||
formData.append('title', nameDoc.value);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/documents', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`HTTP Error ${response.status}: ${response.statusText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Server response:', result);
|
||||
resultHTTP.value = 'File/s uploaded successfully!'
|
||||
resetForm(); // this resets UI state after successful upload
|
||||
}
|
||||
catch (err) {
|
||||
console.log('Error in procssing the files ',err);
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
allFiles.value = [];
|
||||
getExcelFile.value = '';
|
||||
getPdfFile.value = '';
|
||||
nameDoc.value = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.container-parent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: none;
|
||||
box-shadow: 0 0 5px #44986e,
|
||||
0 0 15px #44986e,
|
||||
0 0 30px #44986e,
|
||||
0 0 50px rgba(0, 243, 255, 0.5);
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
gap: 40px;
|
||||
height: 92vh;
|
||||
width: fit-content;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.files-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 60px;
|
||||
}
|
||||
|
||||
.container-file-excel,
|
||||
.container-file-pdf {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
gap: 30px;
|
||||
width: 450px;
|
||||
flex-shrink: 0; /*Tells browser layout engine that under no circumstances should these columns change size when child elements render.*/
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
#result-container {
|
||||
position: relative;
|
||||
border: solid rgb(51, 117, 117) 1px;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
padding-right: 35px;
|
||||
background-color: white;
|
||||
box-shadow: 4px 4px rgb(51, 117, 117);
|
||||
color: white;
|
||||
width: 450px;
|
||||
color: rgb(51, 117, 117);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
#result-container:has(#circle-mark:active) {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
.file-link {
|
||||
display: inline-block; /* Required for transform / translateY to work! */
|
||||
cursor: pointer;
|
||||
transition: transform 0.1s ease, color 0.1s ease; /* Move transition to base class for smooth click and release */
|
||||
}
|
||||
|
||||
.file-link:hover {
|
||||
color: rgb(49, 49, 236);
|
||||
}
|
||||
|
||||
.file-link:active {
|
||||
color: #193929;
|
||||
transform: translateY(3px); /* Smooth press animation */
|
||||
}
|
||||
|
||||
#circle-mark {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.del-file, .nameFile {
|
||||
/* this covers the entire screen */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
/* Semi-transparent overlay color */
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
|
||||
/* Blur effect for everything underneath */
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px); /* Safari support */
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
/* Sit above all other page elements */
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
#upload-btn {
|
||||
margin-top: 30px;
|
||||
color: white;
|
||||
background-color: rgb(49, 49, 236);
|
||||
border: none;
|
||||
border-radius: 14px;
|
||||
width: 300px;
|
||||
padding: 12px;
|
||||
transition: all 0.1s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#upload-btn:hover {
|
||||
background-color: rgb(60, 116, 238);
|
||||
}
|
||||
|
||||
#upload-btn:disabled {
|
||||
background-color: rgb(49, 109, 228);
|
||||
color: white;
|
||||
cursor: not-allowed;
|
||||
transform: none; /* Disables active transform effect */
|
||||
}
|
||||
|
||||
#upload-btn:not(:disabled):active {
|
||||
transform: translateY(2px);
|
||||
background-color: rgb(12, 12, 158);
|
||||
}
|
||||
|
||||
#alert-message {
|
||||
position: absolute;
|
||||
bottom: -50px; /* Positions it nicely below the cards without expanding the box */
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: red;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.result-http {
|
||||
color: rgb(78, 183, 78);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<div class="container">
|
||||
<div class="title">
|
||||
<span><i :class="['fa-solid', iconClass]" :style="{ color: iconColor }"></i></span>
|
||||
<h3>{{ title }}</h3>
|
||||
</div>
|
||||
<div
|
||||
class="container-file"
|
||||
:class="{ 'is-dragging': isDragging }"
|
||||
@dragover.prevent="!disabled && (isDragging = true)"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="handleDrop($event)"
|
||||
>
|
||||
<span><i class="fa-solid fa-upload"></i></span>
|
||||
<p>
|
||||
Drag and drop excel file here or choose a file
|
||||
</p>
|
||||
<p id="note">{{ note }}</p>
|
||||
<input
|
||||
type="file"
|
||||
:id="uniqueID"
|
||||
:accept="note"
|
||||
:disabled="disabled"
|
||||
style="display: none;"
|
||||
@change="onFileSelected"
|
||||
/>
|
||||
<label
|
||||
:for="disabled ? '' : uniqueID"
|
||||
id="btn"
|
||||
:class="{ 'btn-disabled': disabled }"
|
||||
>Browse file</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Swal from 'sweetalert2';
|
||||
import 'sweetalert2/dist/sweetalert2.min.css';
|
||||
export default {
|
||||
props: {
|
||||
title: { type: String, default: 'Upload File' },
|
||||
iconClass: { type: String, default: 'fa-file' },
|
||||
iconColor: { type: String, default: '#000' },
|
||||
note: { type: String, default: '' },
|
||||
disabled: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// Generates a unique ID so labels don't collide when multiple cards exist
|
||||
uniqueID: 'file-upload-' + Math.random().toString(36).substring(2, 9),
|
||||
isDragging: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isValidTypeFile(file) {
|
||||
const fileType = '.' + file.name.split('.').pop().toLowerCase();
|
||||
const allowedTypes = this.note
|
||||
.split(',')
|
||||
.map(type => type.trim().toLowerCase());
|
||||
|
||||
return allowedTypes.includes(fileType);
|
||||
},
|
||||
onFileSelected(event) {
|
||||
const file = event.target.files[0]
|
||||
if (this.isValidTypeFile(file)) {
|
||||
this.$emit('file-selected', file)
|
||||
} else {
|
||||
this.showAlert();
|
||||
}
|
||||
event.target.value = '';
|
||||
},
|
||||
handleDrop(event) {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
}
|
||||
this.isDragging = false; // Turn off visual drag highlight
|
||||
|
||||
// Get the dropped file from event.dataTransfer
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file && !this.disabled) {
|
||||
if (this.isValidTypeFile(file)) {
|
||||
this.$emit('file-selected', file);
|
||||
}
|
||||
else {
|
||||
this.showAlert();
|
||||
}
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
},
|
||||
showAlert() {
|
||||
Swal.fire({
|
||||
title: 'Wrong document type!',
|
||||
html: `Invalid file type! Only <b>'${ this.note }'</b> files are allowed here.`,
|
||||
icon: 'info',
|
||||
confirmButtonText: 'OK',
|
||||
confirmButtonColor: '#369E7B',
|
||||
background: '#f8f9fa',
|
||||
customClass: {
|
||||
popup: 'custom-swal-popup'
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
|
||||
|
||||
* {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.container-file {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
border: dashed black 2px;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Style highlight when dragging a file over the box */
|
||||
.container-file.is-dragging {
|
||||
border-color: rgb(54, 158, 123);
|
||||
background-color: rgba(54, 158, 123, 0.1);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
#note {
|
||||
margin-top: 5px;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
#btn {
|
||||
color: white;
|
||||
background-color: rgb(54, 158, 123);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
transition: all 0.1s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#btn:active {
|
||||
transform: translateY(2px);
|
||||
background-color: rgb(41, 122, 95);
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
transform: none !important;
|
||||
background-color: rgb(54, 158, 123) !important;
|
||||
}
|
||||
|
||||
.custom-swal-popup {
|
||||
border-radius: 12px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<div class="modal-container">
|
||||
<div class="title-container">
|
||||
<p id="title">Remove File</p>
|
||||
</div>
|
||||
<hr>
|
||||
<p id="modal-text" >Are you sure you want to remove "{{ nameFile }}"?</p>
|
||||
<div class="reply">
|
||||
<button @click="$emit('confirm')">Yes</button>
|
||||
<button @click="$emit('cancel')">No</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
nameFile: {
|
||||
type: String,
|
||||
default: 'file_name'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
* {
|
||||
color: white;
|
||||
font-family: monospace;
|
||||
box-sizing: border-box; /* Ensures padding doesn't expand height beyond 200px */
|
||||
}
|
||||
|
||||
#title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
width: 400px;
|
||||
height: 200px;
|
||||
background-color: rgb(32, 59, 50);
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
#modal-text {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.reply {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: rgb(37, 110, 85);
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: rgb(58, 171, 133);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: rgb(54, 158, 123);
|
||||
transform: translateY(3px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="container">
|
||||
<span>
|
||||
<i class="fa-solid
|
||||
fa-circle-xmark"
|
||||
id="circle-mark"
|
||||
@click="$emit('close-modal')"
|
||||
>
|
||||
</i>
|
||||
</span>
|
||||
<label for="names">Document type:</label><br>
|
||||
<select name="names"
|
||||
id="names"
|
||||
class="styled-select"
|
||||
required
|
||||
v-model="selectedDoc"
|
||||
:disabled="!!customDoc.trim()"
|
||||
@change="selectedValue"
|
||||
>
|
||||
<option value="" disabled selected hidden>Select the pdf doc type...</option>
|
||||
<option>Carta d'identità</option>
|
||||
<option>Certificato Lingua</option>
|
||||
<option>Certificato Medico</option>
|
||||
<option>CV</option>
|
||||
<option>Diploma</option>
|
||||
<option>Documento Medico</option>
|
||||
<option>Passaporto</option>
|
||||
<option>Patente</option>
|
||||
<option>Patto di Corresponsabilità</option>
|
||||
<option>Permesso di soggiorno</option>
|
||||
<option>Tampone attestante la negatività a COVID-19</option>
|
||||
<option>Tesi</option>
|
||||
<option>Motivational Letter</option>
|
||||
<option>Stage doc</option>
|
||||
<option>Dossier di Tirocinio</option>
|
||||
<option>Docuseal</option>
|
||||
</select><br><br>
|
||||
|
||||
<div class="chooseTitle">If not present, write the doc type here:
|
||||
<br><br>
|
||||
<input type="text"
|
||||
placeholder="Write here..."
|
||||
id="pdfTitle"
|
||||
:disabled="!!selectedDoc"
|
||||
v-model="customDoc"
|
||||
:class="{ 'customDoc-disabled': !!selectedDoc}"
|
||||
@input="onInputChange"
|
||||
>
|
||||
</div>
|
||||
<button id="next-btn"
|
||||
@click="$emit('proceed')"
|
||||
:disabled="disabled"
|
||||
:class="{ 'btn-disabled': disabled }"
|
||||
>Proceed
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
disabled: { type: Boolean, default: false }
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
selectedDoc: '',
|
||||
customDoc: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectedValue() {
|
||||
this.$emit('selected-value', this.selectedDoc);
|
||||
},
|
||||
onInputChange() {
|
||||
this.$emit('selected-value', this.customDoc);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css');
|
||||
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 14px;
|
||||
color: white;
|
||||
background-color: rgb(32, 59, 50);
|
||||
padding: 20px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
#circle-mark {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
select.styled-select:invalid {
|
||||
color: gray;
|
||||
}
|
||||
|
||||
#names, #pdfTitle {
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
border: none;
|
||||
box-shadow: 4px 4px 4px black;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.chooseTitle > input {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
#next-btn {
|
||||
margin-top: 40px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
padding: 14px;
|
||||
width: 80px;
|
||||
background-color: rgb(17, 166, 94);
|
||||
color: white;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
#next-btn:active {
|
||||
transform: translateY(3px);
|
||||
background-color: rgb(14, 138, 78);
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
.customDoc-disabled {
|
||||
background-color: white;
|
||||
color: gray;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,30 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
// Base public path
|
||||
base: '/',
|
||||
// Dev server proxy setup
|
||||
server: {
|
||||
port: 5173,
|
||||
proxy: {
|
||||
'/api/documents': {
|
||||
target: 'http://localhost:3000',
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user