mtt-simple
Version:
Biblioteca de componentes y helpers para desarrollo de formularios en SIMPLE digital
310 lines (275 loc) • 12.1 kB
JavaScript
'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
}