Merge pull request '280 - OsTicket weelky ticket report' (#612) from 2890-osticket_report into dev
gitea/salix/pipeline/head This commit looks good Details

Reviewed-on: #612
Reviewed-by: Carlos Jimenez Ruiz <carlosjr@verdnatura.es>
This commit is contained in:
Carlos Jimenez Ruiz 2021-04-29 08:25:19 +00:00
commit b4f1e293e2
29 changed files with 385 additions and 87 deletions

View File

@ -22,13 +22,18 @@
"displayHeaderFooter": true,
"printBackground": true
},
"mysql": {
"host": "localhost",
"port": 3306,
"database": "vn",
"user": "root",
"password": "root"
},
"datasources": [
{
"name": "default",
"options": {
"host": "localhost",
"port": 3306,
"database": "vn",
"user": "root",
"password": "root"
}
}
],
"smtp": {
"host": "localhost",
"port": 465,

View File

@ -8,7 +8,7 @@ header .logo {
}
header .logo img {
width: 50%
width: 300px
}
header .topbar {

View File

@ -33,13 +33,13 @@ module.exports = {
SELECT s.name, s.street, s.postCode, s.city, s.phone
FROM company c
JOIN supplier s ON s.id = c.id
WHERE c.code = :code`, {code});
WHERE c.code = ?`, [code]);
},
getFiscalAddress(code) {
return db.findOne(`
SELECT nif, register FROM company c
JOIN supplier s ON s.id = c.id
WHERE c.code = :code`, {code});
WHERE c.code = ?`, [code]);
}
},
props: ['companyCode']

View File

@ -1,16 +1,38 @@
const mysql = require('mysql2/promise');
const mysql = require('mysql2');
const config = require('./config.js');
const fs = require('fs-extra');
const PromisePoolConnection = mysql.PromisePoolConnection;
const PoolConnection = mysql.PoolConnection;
module.exports = {
init() {
if (!this.pool) {
this.pool = mysql.createPool(config.mysql);
this.pool.on('connection', connection => {
connection.config.namedPlaceholders = true;
});
const datasources = config.datasources;
const pool = mysql.createPoolCluster();
for (let datasource of datasources)
pool.add(datasource.name, datasource.options);
this.pool = pool;
}
return this.pool;
},
/**
* Retuns a pool connection from specific cluster node
* @param {String} name - The cluster name
*
* @return {Object} - Pool connection
*/
getConnection(name) {
let pool = this.pool;
return new Promise((resolve, reject) => {
pool.getConnection(name, function(error, connection) {
if (error) return reject(error);
resolve(connection);
});
});
},
/**
@ -23,12 +45,27 @@ module.exports = {
*/
rawSql(query, params, connection) {
let pool = this.pool;
if (params instanceof PromisePoolConnection)
if (params instanceof PoolConnection)
connection = params;
if (connection) pool = connection;
return pool.query(query, params).then(([rows]) => {
return rows;
return new Promise((resolve, reject) => {
if (!connection) {
pool.getConnection('default', function(error, conn) {
if (error) return reject(error);
conn.query(query, params, (error, rows) => {
if (error) return reject(error);
conn.release();
resolve(rows);
});
});
} else {
connection.query(query, params, (error, rows) => {
if (error) return reject(error);
resolve(rows);
});
}
});
},

View File

@ -4,6 +4,14 @@ const db = require('../database');
const dbHelper = {
methods: {
/**
* Retuns a pool connection from specific cluster node
* @param {String} name - The cluster name
*
* @return {Object} - Pool connection
*/
getConnection: name => db.getConnection(name),
/**
* Makes a query from a raw sql
* @param {String} query - The raw SQL query

View File

@ -26,13 +26,13 @@ module.exports = {
}).finally(async() => {
await db.rawSql(`
INSERT INTO vn.mail (sender, replyTo, sent, subject, body, status)
VALUES (:recipient, :sender, 1, :subject, :body, :status)`, {
sender: options.replyTo,
recipient: options.to,
subject: options.subject,
body: options.text || options.html,
status: error && error.message || 'Sent'
});
VALUES (?, ?, 1, ?, ?, ?)`, [
options.replyTo,
options.to,
options.subject,
options.text || options.html,
error && error.message || 'Sent'
]);
});
}
};

View File

@ -23,12 +23,10 @@ module.exports = app => {
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
WHERE al.code = 'PACKED'
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
AND util.dayEnd(:to)
AND DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, {
to: reqArgs.to
});
GROUP BY e.ticketFk`, [reqArgs.to, reqArgs.to]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, req.args);
@ -40,13 +38,11 @@ module.exports = app => {
JOIN deliveryMethod dm ON dm.id = am.deliveryMethodFk
JOIN zone z ON z.id = t.zoneFk
SET t.routeFk = NULL
WHERE DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
AND util.dayEnd(:to)
WHERE DATE(t.shipped) BETWEEN DATE_ADD(?, INTERVAL -2 DAY)
AND util.dayEnd(?)
AND al.code NOT IN('DELIVERED','PACKED')
AND t.routeFk
AND z.name LIKE '%MADRID%'`, {
to: reqArgs.to
});
AND z.name LIKE '%MADRID%'`, [reqArgs.to, reqArgs.to]);
} catch (error) {
next(error);
}
@ -70,11 +66,9 @@ module.exports = app => {
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.id = :ticketId
AND t.id = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, {
ticketId: reqArgs.ticketId
});
GROUP BY e.ticketFk`, [reqArgs.ticketId]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
@ -108,16 +102,17 @@ module.exports = app => {
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.agencyModeFk IN(:agencyModeId)
AND t.warehouseFk = :warehouseId
AND t.agencyModeFk IN(?)
AND t.warehouseFk = ?
AND DATE(t.shipped) BETWEEN DATE_ADD(:to, INTERVAL -2 DAY)
AND util.dayEnd(:to)
AND util.dayEnd(?)
AND t.refFk IS NULL
GROUP BY e.ticketFk`, {
agencyModeId: agenciesId,
warehouseId: reqArgs.warehouseId,
to: reqArgs.to
});
GROUP BY e.ticketFk`, [
agenciesId,
reqArgs.warehouseId,
reqArgs.to,
reqArgs.to
]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
@ -144,11 +139,9 @@ module.exports = app => {
JOIN ticketState ts ON ts.ticketFk = t.id
JOIN alertLevel al ON al.alertLevel = ts.alertLevel
WHERE al.code = 'PACKED'
AND t.routeFk = :routeId
AND t.routeFk = ?
AND t.refFk IS NULL
GROUP BY e.ticketFk`, {
routeId: reqArgs.routeId
});
GROUP BY e.ticketFk`, [reqArgs.routeId]);
const ticketIds = tickets.map(ticket => ticket.id);
await closeAll(ticketIds, reqArgs);
@ -179,9 +172,7 @@ module.exports = app => {
for (const ticket of tickets) {
try {
await db.rawSql(`CALL vn.ticket_close(:ticketId)`, {
ticketId: ticket.id
});
await db.rawSql(`CALL vn.ticket_close(?)`, [ticket.id]);
const hasToInvoice = ticket.hasToInvoice && ticket.hasDailyInvoice;
if (!ticket.salesPersonFk || !ticket.isToBeMailed || hasToInvoice) continue;
@ -239,20 +230,19 @@ module.exports = app => {
}
async function invalidEmail(ticket) {
await db.rawSql(`UPDATE client SET email = NULL WHERE id = :clientId`, {
clientId: ticket.clientFk
});
await db.rawSql(`UPDATE client SET email = NULL WHERE id = ?`, [
ticket.clientFk
]);
const oldInstance = `{"email": "${ticket.recipient}"}`;
const newInstance = `{"email": ""}`;
await db.rawSql(`
INSERT INTO clientLog (originFk, userFk, action, changedModel, oldInstance, newInstance)
VALUES (:clientId, :userId, 'UPDATE', 'Client', :oldInstance, :newInstance)`, {
clientId: ticket.clientFk,
userId: null,
oldInstance: oldInstance,
newInstance: newInstance
});
VALUES (?, NULL, 'UPDATE', 'Client', ?, ?)`, [
ticket.clientFk,
oldInstance,
newInstance
]);
const body = `No se ha podido enviar el albarán <strong>${ticket.id}</strong>
al cliente <strong>${ticket.clientFk} - ${ticket.clientName}</strong>

View File

@ -0,0 +1,9 @@
const Stylesheet = require(`${appPath}/core/stylesheet`);
module.exports = new Stylesheet([
`${appPath}/common/css/spacing.css`,
`${appPath}/common/css/misc.css`,
`${appPath}/common/css/layout.css`,
`${appPath}/common/css/email.css`,
`${__dirname}/style.css`])
.mergeStyles();

View File

@ -0,0 +1,33 @@
.grid-block table.column-oriented {
max-width: 100%;
}
.grid-block table.column-oriented td.message {
overflow: hidden;
max-width: 300px
}
.grid-block table.column-oriented th a {
color: #333
}
.grid-block {
max-width: 98%
}
.table-title {
background-color: #95d831;
padding: 0 10px;
margin-bottom: 20px;
}
.table-title h2 {
margin: 10px 0
}
.external-link {
border: 2px dashed #8dba25;
border-radius: 3px;
text-align: center
}

View File

@ -0,0 +1,13 @@
subject: Informe de tickets semanal
title: Informe de tickets semanal
dear: Hola
description: A continuación se el resumen de incidencias resueltas desde <strong>{0 | date('%d-%m-%Y')}</strong> hasta <strong>{1}</strong>.
totalResolved: Un total de <strong>{0}</strong> tickets han sido resueltos durante la última semana.
author: Autor
dated: Fecha
opened: Abierto
closed: Cerrado
ticketSubject: Asunto
ticketDescription: Descripción
resolution: Resolución
grafanaLink: "Puedes ver la gráfica desde el siguiente enlace:"

View File

@ -0,0 +1,93 @@
<!DOCTYPE html>
<html v-bind:lang="$i18n.locale">
<head>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<title>{{ $t('subject') }}</title>
</head>
<body>
<table class="grid">
<tbody>
<tr>
<td>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
<!-- Header block -->
<div class="grid-row">
<div class="grid-block">
<email-header v-bind="$props"></email-header>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml">
<h1>{{ $t('title') }}</h1>
<p>{{$t('dear')}},</p>
<p v-html="$t('description', [started, ended])"></p>
<p v-html="$t('totalResolved', [resolvedTickets])"></p>
<p v-html="$t('grafanaLink')"></p>
<div class="external-link vn-pa-sm vn-m-md">
<a v-bind:href="'https://grafana.verdnatura.es/d/2kaHDi9Mk/osticket?orgId=1&from=' + startedTime + '&to=' + endedTime" target="_blank">
https://grafana.verdnatura.es/d/2kaHDi9Mk/osticket?orgId=1&from={{startedTime}}&to={{endedTime}}
</a>
</div>
</div>
</div>
<!-- Block -->
<div class="grid-row">
<div class="grid-block vn-pa-ml" v-for="technician in technicians">
<div class="table-title clearfix">
<h2>{{technician.name}} (<strong>{{technician.tickets.length}}</strong>)</h2>
</div>
<table class="column-oriented">
<thead>
<tr>
<th width="5%">{{$t('author')}}</th>
<th width="5%">{{$t('dated')}}</th>
<th width="30%">{{$t('ticketSubject')}}</th>
<th width="30%">{{$t('ticketDescription')}}</th>
<th width="30%">{{$t('resolution')}}</th>
</tr>
</thead>
<tbody v-for="ticket in technician.tickets">
<tr>
<td>{{ticket.author}}</td>
<td class="font light-gray">
<div v-bind:title="$t('opened')">
&#128275; {{ticket.created | date('%d-%m-%Y %H:%M')}}
</div>
<div v-bind:title="$t('closed')">
&#128274; {{ticket.closed | date('%d-%m-%Y %H:%M')}}
</div>
</td>
<td>
<a v-bind:href="'https://cau.verdnatura.es/scp/tickets.php?id=' + ticket.ticket_id">
{{ticket.number}} - {{ticket.subject}}
</a>
</td>
<td class="message" v-html="ticket.description"></td>
<td class="message" v-html="ticket.resolution"></td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Footer block -->
<div class="grid-row">
<div class="grid-block">
<email-footer v-bind="$props"></email-footer>
</div>
</div>
<!-- Empty block -->
<div class="grid-row">
<div class="grid-block empty"></div>
</div>
</td>
</tr>
</tbody>
</table>
</body>
</html>

View File

@ -0,0 +1,68 @@
const Component = require(`${appPath}/core/component`);
const emailHeader = new Component('email-header');
const emailFooter = new Component('email-footer');
module.exports = {
name: 'osticket-report',
async serverPrefetch() {
const tickets = await this.fetchTickets();
this.resolvedTickets = tickets.length;
const technicians = [];
for (let ticket of tickets) {
const technicianName = ticket.assigned;
let technician = technicians.find(technician => {
return technician.name == technicianName;
});
if (!technician) {
technician = {
name: technicianName,
tickets: []
};
technicians.push(technician);
}
technician.tickets.push(ticket);
}
this.technicians = technicians.sort((acumulator, value) => {
return value.tickets.length - acumulator.tickets.length;
});
if (!this.technicians)
throw new Error('Something went wrong');
},
computed: {
dated: function() {
const filters = this.$options.filters;
return filters.date(new Date(), '%d-%m-%Y');
},
startedTime: function() {
return new Date(this.started).getTime();
},
endedTime: function() {
return new Date(this.ended).getTime();
}
},
methods: {
fetchDateRange() {
return this.findOneFromDef('dateRange');
},
async fetchTickets() {
const {started, ended} = await this.fetchDateRange();
this.started = started;
this.ended = ended;
const connection = await this.getConnection('osticket');
return this.rawSqlFromDef('tickets', [started, ended], connection);
}
},
components: {
'email-header': emailHeader.build(),
'email-footer': emailFooter.build()
},
props: {}
};

View File

@ -0,0 +1,8 @@
SELECT @lastWeekMondayTime AS started, @lastWeekFridayTime AS ended
FROM (
SELECT @lastWeek := DATE_ADD(CURDATE(), INTERVAL -1 WEEK),
@lastWeekMonday := DATE_ADD(@lastWeek, INTERVAL (-WEEKDAY(@lastWeek)) DAY),
@lastWeekFriday := DATE_ADD(@lastWeekMonday, INTERVAL (+6) DAY),
@lastWeekMondayTime := ADDTIME(DATE(@lastWeekMonday), '00:00:00'),
@lastWeekFridayTime := ADDTIME(DATE(@lastWeekFriday), '23:59:59')
) t

View File

@ -0,0 +1,26 @@
SELECT * FROM (
SELECT DISTINCT ot.ticket_id,
ot.number,
ot.created,
ot.closed,
otu.name AS author,
otsf.username AS assigned,
otc.subject,
ote.body AS description,
oter.body AS resolution
FROM ost_ticket ot
JOIN ost_ticket__cdata otc ON ot.ticket_id = otc.ticket_id
JOIN ost_ticket_status ots ON ot.status_id = ots.id
JOIN ost_user otu ON ot.user_id = otu.id
LEFT JOIN ost_staff otsf ON ot.staff_id = otsf.staff_id
JOIN ost_thread oth ON ot.ticket_id = oth.object_id
AND oth.object_type = 'T'
LEFT JOIN ost_thread_entry ote ON oth.id = ote.thread_id
AND ote.type = 'M'
LEFT JOIN ost_thread_entry oter ON oth.id = oter.thread_id
AND oter.type = 'R'
WHERE ots.state = 'closed'
AND closed BETWEEN ? AND ?
ORDER BY oter.created DESC
) ot GROUP BY ot.ticket_id
ORDER BY ot.assigned

View File

@ -17,7 +17,7 @@ module.exports = {
},
methods: {
fetchPayMethod(clientId) {
return this.findOneFromDef('payMethod', {clientId: clientId});
return this.findOneFromDef('payMethod', [clientId]);
}
},
components: {

View File

@ -5,4 +5,4 @@ SELECT
pm.code
FROM client c
JOIN payMethod pm ON pm.id = c.payMethodFk
WHERE c.id = :clientId
WHERE c.id = ?

View File

@ -27,10 +27,10 @@ module.exports = {
},
methods: {
fetchRoutes(routesId) {
return this.rawSqlFromDef('routes', {routesId});
return this.rawSqlFromDef('routes', [routesId]);
},
fetchTickets(routesId) {
return this.rawSqlFromDef('tickets', {routesId});
return this.rawSqlFromDef('tickets', [routesId]);
}
},
components: {

View File

@ -13,4 +13,4 @@ FROM route r
LEFT JOIN worker w ON w.id = r.workerFk
LEFT JOIN account.user u ON u.id = w.userFk
LEFT JOIN agencyMode am ON am.id = r.agencyModeFk
WHERE r.id IN(:routesId)
WHERE r.id IN(?)

View File

@ -24,11 +24,11 @@ FROM route r
LEFT JOIN address a ON a.id = t.addressFk
LEFT JOIN client c ON c.id = t.clientFk
LEFT JOIN worker w ON w.id = client_getSalesPerson(t.clientFk, CURDATE())
LEFT JOIN account.user u ON u.id = w.userFk
LEFT JOIN account.user u ON u.id = w.id
LEFT JOIN ticketObservation tob ON tob.ticketFk = t.id AND tob.observationTypeFk = 3
LEFT JOIN province p ON a.provinceFk = p.id
LEFT JOIN warehouse wh ON wh.id = t.warehouseFk
LEFT JOIN agencyMode am ON am.id = t.agencyModeFk
LEFT JOIN stowaway s ON s.id = t.id
WHERE r.id IN(:routesId)
WHERE r.id IN(?)
ORDER BY t.priority, t.id

View File

@ -73,7 +73,7 @@ module.exports = {
return this.rawSqlFromDef('tickets', [invoiceId]);
},
async fetchSales(invoiceId) {
const connection = await db.pool.getConnection();
const connection = await db.getConnection('default');
await this.rawSql(`DROP TEMPORARY TABLE IF EXISTS tmp.invoiceTickets`, connection);
await this.rawSqlFromDef('invoiceTickets', [invoiceId], connection);

View File

@ -26,10 +26,10 @@ module.exports = {
return this.findOneFromDef('client', [clientId]);
},
fetchSales(clientId, companyId) {
return this.findOneFromDef('sales', {
clientId: clientId,
companyId: companyId,
});
return this.findOneFromDef('sales', [
clientId,
companyId
]);
},
getBalance(sale) {
if (sale.debtOut)

View File

@ -1 +1 @@
CALL vn.clientGetDebtDiary(:clientId, :companyId)
CALL vn.clientGetDebtDiary(?, ?)

View File

@ -20,10 +20,18 @@ const rptSepaCore = {
},
methods: {
fetchClient(clientId, companyId) {
return this.findOneFromDef('client', {companyId, clientId});
return this.findOneFromDef('client', [
companyId,
companyId,
clientId
]);
},
fetchSupplier(clientId, companyId) {
return this.findOneFromDef('supplier', {companyId, clientId});
return this.findOneFromDef('supplier', [
companyId,
companyId,
clientId
]);
}
},
components: {

View File

@ -13,7 +13,7 @@ SELECT
FROM client c
JOIN country ct ON ct.id = c.countryFk
LEFT JOIN mandate m ON m.clientFk = c.id
AND m.companyFk = :companyId AND m.finished IS NULL
AND m.companyFk = ? AND m.finished IS NULL
LEFT JOIN province p ON p.id = c.provinceFk
WHERE (m.companyFk = :companyId OR m.companyFk IS NULL) AND c.id = :clientId
WHERE (m.companyFk = ? OR m.companyFk IS NULL) AND c.id = ?
ORDER BY m.created DESC LIMIT 1

View File

@ -8,10 +8,10 @@ SELECT
sp.name province
FROM client c
LEFT JOIN mandate m ON m.clientFk = c.id
AND m.companyFk = :companyId AND m.finished IS NULL
AND m.companyFk = ? AND m.finished IS NULL
LEFT JOIN supplier s ON s.id = m.companyFk
LEFT JOIN country sc ON sc.id = s.countryFk
LEFT JOIN province sp ON sp.id = s.provinceFk
LEFT JOIN province p ON p.id = c.provinceFk
WHERE (m.companyFk = :companyId OR m.companyFk IS NULL) AND c.id = :clientId
WHERE (m.companyFk = ? OR m.companyFk IS NULL) AND c.id = ?
ORDER BY m.created DESC LIMIT 1

View File

@ -29,5 +29,5 @@ SELECT
FROM buy b
JOIN item i ON i.id = b.itemFk
JOIN itemType it ON it.id = i.typeFk
WHERE b.entryFk IN(:entriesId) AND b.quantity > 0
WHERE b.entryFk IN(?) AND b.quantity > 0
ORDER BY i.typeFk , i.name

View File

@ -40,7 +40,7 @@ module.exports = {
return this.rawSqlFromDef('entries', [supplierId, from, to]);
},
fetchBuys(entriesId) {
return this.rawSqlFromDef('buys', {entriesId});
return this.rawSqlFromDef('buys', [entriesId]);
}
},
components: {

View File

@ -6,4 +6,4 @@ SELECT
FROM route r
JOIN agencyMode am ON am.id = r.agencyModeFk
JOIN vehicle v ON v.id = r.vehicleFk
WHERE r.id = :routeId
WHERE r.id = ?

View File

@ -8,7 +8,7 @@ module.exports = {
},
methods: {
fetchZone(routeId) {
return this.findOneFromDef('zone', {routeId});
return this.findOneFromDef('zone', [routeId]);
}
},
props: {