From 0234e14c6b70e0f3896a541e4dc6bf6a6eaa3cd6 Mon Sep 17 00:00:00 2001 From: Juan Ferrer Toribio Date: Tue, 13 Dec 2022 18:29:04 +0100 Subject: [PATCH] #4922 invoices & orders --- quasar.config.js | 4 +- src/boot/app.js | 7 +- src/boot/error-handler.js | 10 +- src/boot/i18n.js | 2 + src/css/app.scss | 8 ++ src/css/quasar.variables.scss | 8 ++ src/css/responsive.scss | 5 + src/css/width.scss | 25 +++++ src/i18n/en-US/index.js | 53 +++++++++- src/i18n/es-ES/index.js | 53 +++++++++- src/js/db/connection.js | 4 +- src/js/vn/json-connection.js | 21 ++-- src/layouts/MainLayout.vue | 25 +++++ src/lib/filters.js | 74 ++++++++++++++ src/pages/Cms/Home.vue | 6 +- src/pages/Ecomerce/Invoices.vue | 165 ++++++++++++++++++++++++++++++++ src/pages/Ecomerce/Orders.vue | 116 +++++++++++++++++++--- src/pages/Ecomerce/Ticket.vue | 127 ++++++++++++++++++++++++ src/router/routes.js | 8 ++ src/stores/tpv.js | 87 +++++++++++++++++ 20 files changed, 774 insertions(+), 34 deletions(-) create mode 100644 src/css/responsive.scss create mode 100644 src/css/width.scss create mode 100644 src/lib/filters.js create mode 100644 src/pages/Ecomerce/Invoices.vue create mode 100644 src/pages/Ecomerce/Ticket.vue create mode 100644 src/stores/tpv.js diff --git a/quasar.config.js b/quasar.config.js index 08fc8ee3..34413d4e 100644 --- a/quasar.config.js +++ b/quasar.config.js @@ -33,7 +33,9 @@ module.exports = configure(function (ctx) { // https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js#Property%3A-css css: [ - 'app.scss' + 'app.scss', + 'width.scss', + 'responsive.scss' ], // https://github.com/quasarframework/quasar/tree/dev/extras diff --git a/src/boot/app.js b/src/boot/app.js index 08b64067..211e2092 100644 --- a/src/boot/app.js +++ b/src/boot/app.js @@ -1,7 +1,10 @@ import { boot } from 'quasar/wrappers' import { appStore } from 'stores/app' +import { userStore } from 'stores/user' export default boot(({ app }) => { - const myApp = appStore() - app.config.globalProperties.$app = myApp + const props = app.config.globalProperties + props.$app = appStore() + props.$user = userStore() + props.$actions = document.createElement('div') }) diff --git a/src/boot/error-handler.js b/src/boot/error-handler.js index cbf541d2..dda7ac3e 100644 --- a/src/boot/error-handler.js +++ b/src/boot/error-handler.js @@ -20,7 +20,15 @@ export default async ({ app }) => { function errorHandler (err, vm) { let message let tMessage - const res = err.response + let res = err.response + + // XXX: Compatibility with old JSON service + if (err.name === 'JsonException') { + res = { + status: err.statusCode, + data: { error: { message: err.message } } + } + } if (res) { const status = res.status diff --git a/src/boot/i18n.js b/src/boot/i18n.js index 3b264658..be1fd53e 100644 --- a/src/boot/i18n.js +++ b/src/boot/i18n.js @@ -13,4 +13,6 @@ export default boot(({ app }) => { // Set i18n instance on app app.use(i18n) + + window.i18n = i18n.global }) diff --git a/src/css/app.scss b/src/css/app.scss index 5e320667..6ff5ab78 100644 --- a/src/css/app.scss +++ b/src/css/app.scss @@ -8,6 +8,11 @@ font-family: 'Open Sans'; src: url(./opensans.ttf) format('truetype'); } +@mixin mobile { + @media screen and (max-width: 960px) { + @content; + } +} body { font-family: 'Poppins', 'Verdana', 'Sans'; @@ -25,3 +30,6 @@ a.link { border-radius: 7px; box-shadow: 0 0 3px rgba(0, 0, 0, .1); } +.q-page-sticky.fixed-bottom-right { + margin: 18px; +} diff --git a/src/css/quasar.variables.scss b/src/css/quasar.variables.scss index aa576173..67ad10cf 100644 --- a/src/css/quasar.variables.scss +++ b/src/css/quasar.variables.scss @@ -23,3 +23,11 @@ $positive : #21BA45; $negative : #C10015; $info : #31CCEC; $warning : #F2C037; + +// Width + +$width-xs: 400px; +$width-sm: 544px; +$width-md: 800px; +$width-lg: 1280px; +$width-xl: 1600px; diff --git a/src/css/responsive.scss b/src/css/responsive.scss new file mode 100644 index 00000000..5e170ab8 --- /dev/null +++ b/src/css/responsive.scss @@ -0,0 +1,5 @@ +@mixin mobile { + @media screen and (max-width: 1023px) { + @content; + } +} diff --git a/src/css/width.scss b/src/css/width.scss new file mode 100644 index 00000000..6e01fe61 --- /dev/null +++ b/src/css/width.scss @@ -0,0 +1,25 @@ + +%margin-auto { + margin-left: auto; + margin-right: auto; +} +.vn-w-xs { + @extend %margin-auto; + max-width: $width-xs; +} +.vn-w-sm { + @extend %margin-auto; + max-width: $width-sm; +} +.vn-w-md { + @extend %margin-auto; + max-width: $width-md; +} +.vn-w-lg { + @extend %margin-auto; + max-width: $width-lg; +} +.vn-w-xl { + @extend %margin-auto; + max-width: $width-xl; +} diff --git a/src/i18n/en-US/index.js b/src/i18n/en-US/index.js index 0eaf5814..77206823 100644 --- a/src/i18n/en-US/index.js +++ b/src/i18n/en-US/index.js @@ -8,5 +8,56 @@ export default { somethingWentWrong: 'Something went wrong', loginFailed: 'Login failed', authenticationRequired: 'Authentication required', - notFound: 'Not found' + notFound: 'Not found', + today: 'Today', + yesterday: 'Yesterday', + tomorrow: 'Tomorrow', + date: { + days: [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday' + ], + daysShort: [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat' + ], + months: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December' + ], + shortMonths: [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec' + ] + } } diff --git a/src/i18n/es-ES/index.js b/src/i18n/es-ES/index.js index 174bb108..807b0074 100644 --- a/src/i18n/es-ES/index.js +++ b/src/i18n/es-ES/index.js @@ -8,5 +8,56 @@ export default { somethingWentWrong: 'Algo salió mal', loginFailed: 'Usuario o contraseña incorrectos', authenticationRequired: 'Autenticación requerida', - notFound: 'No encontrado' + notFound: 'No encontrado', + today: 'Hoy', + yesterday: 'Ayer', + tomorrow: 'Mañana', + date: { + days: [ + 'Domingo', + 'Lunes', + 'Martes', + 'Miércoles', + 'Jueves', + 'Viernes', + 'Sábado' + ], + daysShort: [ + 'Do', + 'Lu', + 'Mi', + 'Mi', + 'Ju', + 'Vi', + 'Sa' + ], + months: [ + 'Enero', + 'Febrero', + 'Marzo', + 'Abril', + 'Mayo', + 'Junio', + 'Julio', + 'Agosto', + 'Septiembre', + 'Octubre', + 'Noviembre', + 'Diciembre' + ], + shortMonths: [ + 'Ene', + 'Feb', + 'Mar', + 'Abr', + 'May', + 'Jun', + 'Jul', + 'Ago', + 'Sep', + 'Oct', + 'Nov', + 'Dic' + ] + } } diff --git a/src/js/db/connection.js b/src/js/db/connection.js index f8c87bf3..d70364a1 100644 --- a/src/js/db/connection.js +++ b/src/js/db/connection.js @@ -100,9 +100,9 @@ export class Connection extends JsonConnection { * @return {ResultSet} The result */ async execQuery (query, params) { - const sql = query.replace(/#\w+/g, function (key) { + const sql = query.replace(/#\w+/g, key => { const value = params[key.substring(1)] - return value ? this.renderValue(params) : key + return value ? this.renderValue(value) : key }) return await this.execSql(sql) diff --git a/src/js/vn/json-connection.js b/src/js/vn/json-connection.js index 9a3e4cb9..51cafa41 100644 --- a/src/js/vn/json-connection.js +++ b/src/js/vn/json-connection.js @@ -60,7 +60,13 @@ export class JsonConnection extends VnObject { * Called when REST response is received. */ async sendWithUrl (method, url, params) { - const urlParams = new URLSearchParams(params) + const urlParams = new URLSearchParams() + for (const key in params) { + if (params[key] != null) { + urlParams.set(key, params[key]) + } + } + return this.request({ method, url, @@ -81,7 +87,9 @@ export class JsonConnection extends VnObject { const headers = config.headers if (headers) { - for (const header in headers) { request.setRequestHeader(header, headers[header]) } + for (const header in headers) { + request.setRequestHeader(header, headers[header]) + } } const promise = new Promise((resolve, reject) => { @@ -143,8 +151,9 @@ export class JsonConnection extends VnObject { data = jsData } else { let exception = jsData.exception - const error = jsData.error + const err = new JsonException() + err.statusCode = request.status if (exception) { exception = exception @@ -158,14 +167,8 @@ export class JsonConnection extends VnObject { err.file = jsData.file err.line = jsData.line err.trace = jsData.trace - err.statusCode = request.status - } else if (error) { - err.message = error.message - err.code = error.code - err.statusCode = request.status } else { err.message = request.statusText - err.statusCode = request.status } throw err diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 061e9a9d..30ca0dc2 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -12,6 +12,8 @@ Home +
+
+ + + en-US: + noInvoicesFound: No invoices found + serial: Serial + issued: Date + amount: Import + downloadInvoicePdf: Download invoice PDF + notDownloadable: Not available for download, request the invoice to your salesperson + es-ES: + noInvoicesFound: No se han encontrado facturas + serial: Serie + issued: Fecha + amount: Importe + downloadInvoicePdf: Descargar factura en PDF + notDownloadable: No disponible para descarga, solicita la factura a tu comercial + ca-ES: + noInvoicesFound: No s'han trobat factures + serial: Sèrie + issued: Data + amount: Import + downloadInvoicePdf: Descarregar PDF + notDownloadable: No disponible per cescarrega, sol·licita la factura al teu comercial + fr-FR: + noInvoicesFound: Aucune facture trouvée + serial: Série + issued: Date + amount: Montant + downloadInvoicePdf: Télécharger le PDF + notDownloadable: Non disponible en téléchargement, demander la facture à votre commercial + pt-PT: + noInvoicesFound: Nenhuma fatura encontrada + serial: Serie + issued: Data + amount: Importe + downloadInvoicePdf: Baixar PDF + notDownloadable: Não disponível para download, solicite a fatura ao seu comercial + diff --git a/src/pages/Ecomerce/Orders.vue b/src/pages/Ecomerce/Orders.vue index 1a103f77..e7fb2455 100644 --- a/src/pages/Ecomerce/Orders.vue +++ b/src/pages/Ecomerce/Orders.vue @@ -1,55 +1,139 @@ + + @@ -58,7 +142,11 @@ export default { en-US: startOrder: Start order noOrdersFound: No orders found + makePayment: Make payment + shoppingCart: Shopping cart es-ES: startOrder: Empezar pedido noOrdersFound: No se encontrado pedidos + makePayment: Realizar pago + shoppingCart: Cesta de la compra diff --git a/src/pages/Ecomerce/Ticket.vue b/src/pages/Ecomerce/Ticket.vue new file mode 100644 index 00000000..8d732b72 --- /dev/null +++ b/src/pages/Ecomerce/Ticket.vue @@ -0,0 +1,127 @@ + + + + + diff --git a/src/router/routes.js b/src/router/routes.js index cbd90663..b9874ec2 100644 --- a/src/router/routes.js +++ b/src/router/routes.js @@ -34,6 +34,14 @@ const routes = [ name: 'orders', path: '/ecomerce/orders', component: () => import('pages/Ecomerce/Orders.vue') + }, { + name: 'ticket', + path: '/ecomerce/ticket/:id', + component: () => import('pages/Ecomerce/Ticket.vue') + }, { + name: 'invoices', + path: '/ecomerce/invoices', + component: () => import('pages/Ecomerce/Invoices.vue') } ] }, diff --git a/src/stores/tpv.js b/src/stores/tpv.js new file mode 100644 index 00000000..ea740c30 --- /dev/null +++ b/src/stores/tpv.js @@ -0,0 +1,87 @@ +import { defineStore } from 'pinia' +import { jApi } from 'boot/axios' + +export const tpvStore = defineStore('tpv', { + actions: { + async check (route) { + const order = route.query.tpvOrder + const status = route.query.tpvStatus + if (!(order && status)) return null + + await jApi.execQuery( + 'CALL myTpvTransaction_end(#order, #status)', + { order, status } + ) + + if (status === 'ko') { + const retry = confirm('retryPayQuestion') + if (retry) { this.retryPay(order) } + } + + return status + }, + + async pay (amount, company) { + await this.realPay(amount * 100, company) + }, + + async retryPay (order) { + const payment = await jApi.getObject( + `SELECT t.amount, m.companyFk + FROM myTpvTransaction t + JOIN tpvMerchant m ON m.id = t.merchantFk + WHERE t.id = #order`, + { order } + ) + await this.realPay(payment.amount, payment.companyFk) + }, + + async realPay (amount, company) { + if (!isNumeric(amount) || amount <= 0) { + throw new Error('payAmountError') + } + + const json = await jApi.send('tpv/transaction', { + amount: parseInt(amount), + urlOk: this.makeUrl('ok'), + urlKo: this.makeUrl('ko'), + company + }) + + const postValues = json.postValues + + const form = document.createElement('form') + form.method = 'POST' + form.action = json.url + document.body.appendChild(form) + + for (const field in postValues) { + const input = document.createElement('input') + input.type = 'hidden' + input.name = field + form.appendChild(input) + + if (postValues[field]) { input.value = postValues[field] } + } + + form.submit() + }, + + makeUrl (status) { + let path = location.protocol + '//' + location.hostname + path += location.port ? ':' + location.port : '' + path += location.pathname + path += location.search ? location.search : '' + path += '#/ecomerce/orders' + path += '?' + new URLSearchParams({ + tpvStatus: status, + tpvOrder: '_transactionId_' + }).toString() + return path + } + } +}) + +function isNumeric (n) { + return !isNaN(parseFloat(n)) && isFinite(n) +}