UNPKG

mtt-simple

Version:

Biblioteca de componentes y helpers para desarrollo de formularios en SIMPLE digital

310 lines (275 loc) 12.1 kB
'use strict' const $ = require('jquery') const utils = require('../utils') const core = require('../core') const textbox = require('./textbox') const { S3InputFileUploader } = require('./s3-input-file-uploader') /** * Opciones de configuración para componente * @typedef {Object} GestorDocumentacionOpcionesTypedef * @property {string} propDocumento - como resultado de la carga exitosa del documento se agregará una propiedad * al objeto con este nombre para acceder al dato como elemento[propDocumento] * @property {boolean} soloCargados - indica si se entregarán en el listado de salida sólo los datos de los documentos que fueron cargados, * de lo contrario se entregará el mismo listado de entrada completado con los datos de los documentos cargados en el proceso * @property {number} tamagnoMaximo - tamano en bytes (binario) como máximo para carga * @property {boolean} mostrarChecklist - indica si debe mostrar el listado de checks en caso de contener información */ /** @type {GestorDocumentacionOpcionesTypedef} */ const GESTOR_DOCUMENTACION_DEFAULTS = { soloCargados: true, propDocumento: '_archivo', tamagnoMaximo: 5242880, mostrarChecklist: true } /** * Componente para efectuar carga de documentos solicitados a servidor de SIMPLE usando File uploader * de AWS para S3 proveído por API SIMPLE (actúa como proxy de traspaso de contenido a su repositorio * final en la nube) */ class GestorDocumentacion { /** * @param {string} targetInput - nombre del campo SIMPLE usado como contenedor del resultado * @param {GrupoDocumentacionTypedef[]} groups */ constructor(targetInput, groups, options) { /** @type {GestorDocumentacionOpcionesTypedef} */ this.cfg = Object.assign({}, GESTOR_DOCUMENTACION_DEFAULTS, options) this._$target = textbox.usarInputComoHidden(targetInput) this._$wrapper = this._$target.closest('.campo.control-group') this._$wrapper.hide() const previousState = utils.hidratar(this._$target.val()) this._grupos = this._mergePreviousState(groups, previousState) this._saveState() this._uploaders = [] this.draw() this._addValidations() this._linkEvents() this._saveState() this._$wrapper.show() } /** * Reestablece la información anterior del campo asociado dentro de los grupos * acerca de documentos ya cargados asociados a los nuevos datos * @param {GrupoDocumentacionTypedef[]} data - datos agrupados para alimentar el componente * @param {GrupoDocumentacionTypedef[]} previousState - datos ya guardados producto de un estado anterior */ _mergePreviousState(data, previousState) { if (!previousState) return data; data.forEach(g => { const _grupo = previousState.find(_g => _g.idGrupo === g.idGrupo) if (_grupo) { g.documentos.forEach(d => { const _doc = _grupo.documentos.find(_d => _d.codigo === d.codigo && _d[this.cfg.propDocumento]) if (_doc) { d[this.cfg.propDocumento] = _doc[this.cfg.propDocumento] } }) } }) return data; } /** identifica si el elemento especificado posee información válida de un documento cargado */ _hasDocumentAttached(data) { return !data[this.cfg.propDocumento] || !data[this.cfg.propDocumento].url || data[this.cfg.propDocumento].url.trim() === '' } /** identificar filas con documentos requeridos faltantes */ _markMissingFiles() { let hayError = false this._grupos.forEach(g => g.documentos.filter(d => d.esObligatorio).forEach(d => { const $tr = this._$wrapper.find(`[data-doc=${d.codigo}]`) if (this._hasDocumentAttached(d)) { $tr.addClass('error') hayError = true } else { $tr.removeClass('error') } })) return hayError } _isEnabled() { return this._$wrapper.attr('data-dependiente-campo') === 'dependiente' || (this._$wrapper.attr('data-dependiente-campo') !== 'dependiente' && this._$wrapper.is(':visible')) } /** enganchar validaciones del componente en el botón submit */ _addValidations() { const _self = this const btn = this._$wrapper.closest('form').find('button[type=submit]') $(btn).on('click', (evt) => { const mustValidate = _self._isEnabled() if (mustValidate) { $(this).blur() $(this).removeClass('active') if (_self._markMissingFiles()) { evt.stopPropagation() utils.agregarNotificacion('Hay documentos requeridos faltantes, debe proporcionar los documentos marcados para poder continuar.', 'danger') return false } else { return true } } else { return true } }) } draw() { const $tg = this._$target const $wrapper = $(`<div />`) this._grupos.forEach(g => $wrapper.append(this._buildGroupUI(g))); $wrapper.insertAfter($tg) } _setDocumentLoaded($input, archivoCargado) { const idGrupo = $input.attr('data-id-grupo') const codigoDoc = $input.attr('data-id-doc') const grupo = this._grupos.find(g => g.idGrupo === idGrupo) const doc = grupo.documentos.find(d => d.codigo.toString() === codigoDoc) doc[this.cfg.propDocumento] = archivoCargado } _linkEvents() { const self = this // replicar click en inputfile $('.upload-documentos').on('click', 'button.upload, button.replace', function (evt) { const $tr = $(this).closest('tr') $tr.find('label.click-to-upload').click() }) // remover el archivo cargado desde el objeto asociado $('.upload-documentos').on('click', 'button.remove', function (evt) { const $tr = $(this).closest('tr') $tr.removeClass('uploaded') const $inputfile = $tr.find('input[type=file]') $inputfile.val('') self._setDocumentLoaded($inputfile) self._saveState() }) // escuchar por el inicio de la carga $('.upload-documentos').on('load-start', 'input[type=file]', function (evt) { const $tr = $(this).closest('tr') $tr.addClass('uploading') }) // al momento de terminar la carga se entrega la url de descarga $('.upload-documentos').on('complete', 'input[type=file]', function (evt, uploadedFile) { const $tr = $(this).closest('tr') $tr.find('a.link-archivo').attr('href', uploadedFile.url) .find('span').text(`${uploadedFile.name} (${uploadedFile.totalSize} bytes)`) $tr.removeClass('uploading') $tr.removeClass('error') $tr.addClass('uploaded') self._setDocumentLoaded($(this), uploadedFile) self._saveState() }) // para cualquier error en el proceso $('.upload-documentos').on('error', 'input[type=file]', function (_evt, err) { const $tr = $(this).closest('tr') $tr.removeClass('uploading') $tr.find('input[type=file]').val('') alert(err.message) }) // en la actualización de avance en la carga del archivo al servidor $('.upload-documentos').on('progress', 'input[type=file]', function (_evt, data) { const $tr = $(this).closest('tr') const $progress = $tr.find('div.uploader-bar-progress') $progress.width(`${data.totalLoaded}%`) }) } /** * Serializar datos en campo asociado */ _saveState() { const { soloCargados } = this.cfg const state = this._grupos.map(g => { const ret = utils._clone(g) ret.documentos = ret.documentos.filter(d => !soloCargados || d[this.cfg.propDocumento]) return ret }).filter(g => !soloCargados || g.documentos.length) this._$target.val(utils.deshidratar(state)) } /** * @param {GrupoDocumentacionTypedef} g */ _buildGroupUI(g) { const $wrapper = $(`<div class="upload-documentos"><span class="titulo-grupo">${g.titulo}</span></div>`) if (this.cfg.mostrarChecklist) { $wrapper.append(this._buildChecklist(g.checklist)) } const $t = $(`<table data-grupo="${g.idGrupo}" />`) let docs = g.documentos.filter(d => d.esObligatorio) if (docs && docs.length > 0) { this._buildDocumentsGroupUI($t, g.idGrupo, 'Listado de documentos Requeridos', docs) } docs = g.documentos.filter(d => !d.esObligatorio) if (docs && docs.length > 0) { this._buildDocumentsGroupUI($t, g.idGrupo, 'Listado de documentos Opcionales', docs) } $wrapper.append($t) return $wrapper } /** * Listado de textos para mostrar * @param {string[]} textos */ _buildChecklist(textos) { if (textos && textos.length) { const $ul = $(`<ul></ul>`) textos.forEach(t => $ul.append(`<li class="check"><span class="material-icons">done</span><span>${t}</span></li>`)) return $(`<div class="resumen"></div`).append($ul) } else { return null } } /** * @param {string} titulo * @param {DocumentoSolicitadoTypedef[]} documentos */ _buildDocumentsGroupUI($tabla, idGrupo, titulo, documentos) { $(`<thead><th colspan="3"><span class="titulo-obligatoriedad">${titulo}</span></th></thead>`).appendTo($tabla) const $tbody = $('<tbody />') documentos.forEach((d, i) => $tbody.append(this._buildDocumentRowUI(idGrupo, d, i))) $tbody.appendTo($tabla) return $tabla } /** * @param {DocumentoSolicitadoTypedef} documento */ _buildDocumentRowUI(idGrupo, documento, i) { const d = documento const arch = d[this.cfg.propDocumento] || {} const tamano = arch.size ? `(${arch.size})` : '' const idInput = `g${idGrupo}_${d.codigo}_${i}` const uploaded = arch.url ? 'uploaded' : '' const tpl = ` <tr data-doc="${d.codigo}" class="hoverable ${uploaded}"> <td class="estado"> <i class="check material-icons">check_circle</i> <i class="error material-icons">report</i> </td> <td class="documento" title="${d.descripcion ? 'Razón solicitud: ' + d.descripcion : ''}"> <span class="nombre">${d.nombre}</span> <span class="desc">${d.descripcion ? d.descripcion : ''}</span> <a class="link-archivo" href="${arch.url || ''}" target="_blank" class="file"><span>${arch.name || ''} ${tamano}</span><i class="material-icons">link</i></a> <div class="uploader-bar"><div class="uploader-bar-progress"></div></div> <label style="display:none;" class="click-to-upload" for="${idInput}">up</label> <input style="display:none;" type="file" id="${idInput}" data-id-grupo="${idGrupo}" data-id-doc="${d.codigo}" /> </td> <td class="acciones"> <button type="button" class="btn btn-outline-primary remove" title="Remover"> <i class="material-icons">delete</i> </button> <button type="button" class="btn btn-outline-primary uploading" title="Cargando"> <i class="material-icons spin">loop</i> </button> <button type="button" class="btn btn-primary upload" title="Cargar"> <i class="material-icons">cloud_upload</i> </button> <button type="button" class="btn btn-primary replace" title="Reemplazar" > <i class="material-icons">cloud_done</i> </button> </td> </tr>` const $row = $(tpl) const $input = $row.find('input[type=file]') this._uploaders.push(new S3InputFileUploader(this._$target.attr('id'), $input, { maxSize: this.cfg.tamagnoMaximo })) return $row } } module.exports = { GESTOR_DOCUMENTACION_DEFAULTS, GestorDocumentacion }