lab_formularios
This commit is contained in:
@@ -17,11 +17,15 @@ try {
|
||||
if (!empty($_GET['paciente_id'])) $filtros['paciente_id'] = (int)$_GET['paciente_id'];
|
||||
if (!empty($_GET['estado'])) $filtros['estado'] = $_GET['estado'];
|
||||
if (!empty($_GET['fecha_desde'])) $filtros['fecha_desde'] = $_GET['fecha_desde'];
|
||||
if (!empty($_GET['fecha_hasta'])) $filtros['fecha_hasta'] = $_GET['fecha_hasta'];
|
||||
if (!empty($_GET['paciente'])) $filtros['paciente'] = trim($_GET['paciente']);
|
||||
// Enfermero solo ve los suyos
|
||||
if (userRole() === 'enfermero') {
|
||||
$filtros['enviado_por'] = adminId();
|
||||
}
|
||||
jsonOk(['data' => $form->listarEnvios($filtros)]);
|
||||
$pagina = max(1, (int)($_GET['page'] ?? 1));
|
||||
$porPagina = !empty($_GET['export']) ? 9999 : max(1, min(100, (int)($_GET['limit'] ?? 20)));
|
||||
jsonOk($form->listarEnvios($filtros, $pagina, $porPagina));
|
||||
}
|
||||
|
||||
// Plantilla específica
|
||||
|
||||
@@ -173,7 +173,7 @@ class Formulario {
|
||||
/**
|
||||
* Listar envíos (admin/enfermero).
|
||||
*/
|
||||
public function listarEnvios(array $filtros = []): array {
|
||||
public function listarEnvios(array $filtros = [], int $pagina = 1, int $porPagina = 20): array {
|
||||
$where = ['1=1'];
|
||||
$params = [];
|
||||
if (!empty($filtros['formulario_id'])) { $where[] = 'e.formulario_id = ?'; $params[] = $filtros['formulario_id']; }
|
||||
@@ -181,8 +181,22 @@ class Formulario {
|
||||
if (!empty($filtros['enviado_por'])) { $where[] = 'e.enviado_por = ?'; $params[] = $filtros['enviado_por']; }
|
||||
if (!empty($filtros['estado'])) { $where[] = 'e.estado = ?'; $params[] = $filtros['estado']; }
|
||||
if (!empty($filtros['fecha_desde'])) { $where[] = 'DATE(e.created_at) >= ?'; $params[] = $filtros['fecha_desde']; }
|
||||
$w = implode(' AND ', $where);
|
||||
return $this->db->fetchAll("
|
||||
if (!empty($filtros['fecha_hasta'])) { $where[] = 'DATE(e.created_at) <= ?'; $params[] = $filtros['fecha_hasta']; }
|
||||
if (!empty($filtros['paciente'])) { $where[] = 'p.nombre_completo LIKE ?'; $params[] = '%' . $filtros['paciente'] . '%'; }
|
||||
$w = implode(' AND ', $where);
|
||||
$offset = ($pagina - 1) * $porPagina;
|
||||
|
||||
$total = $this->db->fetch(
|
||||
"SELECT COUNT(*) AS n
|
||||
FROM lab_form_envios e
|
||||
JOIN lab_formularios f ON f.id = e.formulario_id
|
||||
LEFT JOIN lab_pacientes p ON p.id = e.paciente_id
|
||||
LEFT JOIN admin_users u ON u.id = e.enviado_por
|
||||
WHERE $w",
|
||||
$params
|
||||
)['n'] ?? 0;
|
||||
|
||||
$rows = $this->db->fetchAll("
|
||||
SELECT e.*, f.nombre AS form_nombre, f.categoria, f.esquema,
|
||||
p.nombre_completo AS paciente_nombre,
|
||||
u.full_name AS enviado_por_nombre
|
||||
@@ -192,8 +206,16 @@ class Formulario {
|
||||
LEFT JOIN admin_users u ON u.id = e.enviado_por
|
||||
WHERE $w
|
||||
ORDER BY e.created_at DESC
|
||||
LIMIT 200
|
||||
", $params);
|
||||
LIMIT ? OFFSET ?
|
||||
", array_merge($params, [$porPagina, $offset]));
|
||||
|
||||
return [
|
||||
'data' => $rows,
|
||||
'total' => (int)$total,
|
||||
'pagina' => $pagina,
|
||||
'por_pagina' => $porPagina,
|
||||
'paginas' => (int)ceil($total / max(1, $porPagina)),
|
||||
];
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
+115
-178
@@ -29,6 +29,10 @@ $puedeEscribir = $esAdmin && hasModuleWrite('lab_formularios');
|
||||
transition:box-shadow .2s; }
|
||||
.form-card:hover { box-shadow:0 3px 12px rgba(0,0,0,.1); }
|
||||
|
||||
/* ── Altura uniforme filtros ─────────────────────── */
|
||||
#panel-envios .form-control-sm,
|
||||
#panel-envios .form-select-sm { height:31px; padding-top:.25rem; padding-bottom:.25rem; }
|
||||
|
||||
/* ── Colores estado envío ───────────────────────── */
|
||||
.badge-pendiente { background:#6c757d; }
|
||||
.badge-completado { background:#0d6efd; }
|
||||
@@ -401,7 +405,7 @@ const tabs = {
|
||||
document.querySelectorAll('#formTabs .nav-link').forEach((a,i) =>
|
||||
a.classList.toggle('active', (tab==='lista') === (i===0))
|
||||
);
|
||||
if (tab === 'envios') envios.cargar();
|
||||
if (tab === 'envios') { envios.poblarSelectFormulario(); envios.cargar(1); }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -508,182 +512,130 @@ const ESTADO_ENV = {
|
||||
firmado: { cls:'badge-firmado', icon:'✍️' },
|
||||
expirado: { cls:'badge-expirado', icon:'⏰' },
|
||||
};
|
||||
const POR_PAGINA = 15;
|
||||
const POR_PAGINA = 20;
|
||||
const envios = {
|
||||
_data: [],
|
||||
_filtrado: [],
|
||||
_pagina: 1,
|
||||
_pagina: 1,
|
||||
_total: 0,
|
||||
_paginas: 1,
|
||||
_paginaData: [],
|
||||
_debTimer: null,
|
||||
|
||||
async cargar() {
|
||||
// Mostrar spinner, ocultar tabla
|
||||
_params() {
|
||||
return new URLSearchParams({
|
||||
envios: 1,
|
||||
page: this._pagina,
|
||||
limit: POR_PAGINA,
|
||||
fecha_desde: $('env-f-desde').value || '',
|
||||
fecha_hasta: $('env-f-hasta').value || '',
|
||||
paciente: $('env-f-paciente').value.trim() || '',
|
||||
formulario_id: $('env-f-formulario').value || '',
|
||||
estado: $('env-f-estado').value || '',
|
||||
});
|
||||
},
|
||||
|
||||
async cargar(pag = 1) {
|
||||
this._pagina = pag;
|
||||
const spinner = $('env-spinner');
|
||||
const tabla = $('env-tabla-wrap');
|
||||
const wrap = $('env-tabla-wrap');
|
||||
if (spinner) spinner.style.display = '';
|
||||
if (tabla) tabla.style.display = 'none';
|
||||
|
||||
const r = await fetch('api/lab/get_formularios.php?envios=1');
|
||||
const d = await r.json();
|
||||
this._data = d.data || [];
|
||||
this._pagina = 1;
|
||||
this._poblarSelectFormulario();
|
||||
this._aplicarFiltros();
|
||||
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
if (tabla) tabla.style.display = '';
|
||||
if (wrap) wrap.style.display = 'none';
|
||||
try {
|
||||
const r = await fetch('api/lab/get_formularios.php?' + this._params());
|
||||
const d = await r.json();
|
||||
this._paginaData = d.data || [];
|
||||
this._total = d.total || 0;
|
||||
this._paginas = d.paginas || 1;
|
||||
this._render();
|
||||
} catch(e) {
|
||||
const tb = $('tbody-envios');
|
||||
if (tb) tb.innerHTML = '<tr><td colspan="6" class="text-center text-danger py-4">Error al cargar envíos</td></tr>';
|
||||
} finally {
|
||||
if (spinner) spinner.style.display = 'none';
|
||||
if (wrap) wrap.style.display = '';
|
||||
}
|
||||
},
|
||||
|
||||
filtrar() {
|
||||
this._pagina = 1;
|
||||
this._aplicarFiltros();
|
||||
clearTimeout(this._debTimer);
|
||||
this._debTimer = setTimeout(() => this.cargar(1), 350);
|
||||
},
|
||||
|
||||
limpiarFiltros() {
|
||||
$('env-f-desde').value = '';
|
||||
$('env-f-hasta').value = '';
|
||||
$('env-f-paciente').value = '';
|
||||
$('env-f-formulario').value = '';
|
||||
$('env-f-estado').value = '';
|
||||
this.filtrar();
|
||||
$('env-f-desde').value = '';
|
||||
$('env-f-hasta').value = '';
|
||||
$('env-f-paciente').value = '';
|
||||
$('env-f-formulario').value = '';
|
||||
$('env-f-estado').value = '';
|
||||
this.cargar(1);
|
||||
},
|
||||
|
||||
_poblarSelectFormulario() {
|
||||
poblarSelectFormulario() {
|
||||
const sel = $('env-f-formulario');
|
||||
if (!sel) return;
|
||||
const valorActual = sel.value;
|
||||
sel.innerHTML = '<option value="">Todos los formularios</option>';
|
||||
const vistos = new Set();
|
||||
this._data.forEach(e => {
|
||||
if (e.formulario_id && !vistos.has(e.formulario_id)) {
|
||||
vistos.add(e.formulario_id);
|
||||
const opt = document.createElement('option');
|
||||
opt.value = e.formulario_id;
|
||||
opt.textContent = e.form_nombre || ('Formulario #' + e.formulario_id);
|
||||
sel.appendChild(opt);
|
||||
}
|
||||
if (!sel || sel.options.length > 1) return;
|
||||
(_formularios || []).forEach(f => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = f.id;
|
||||
opt.textContent = f.nombre || ('Formulario #' + f.id);
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
if (valorActual) sel.value = valorActual;
|
||||
},
|
||||
|
||||
_aplicarFiltros() {
|
||||
const desde = $('env-f-desde').value;
|
||||
const hasta = $('env-f-hasta').value;
|
||||
const paciente = $('env-f-paciente').value.trim().toLowerCase();
|
||||
const formId = $('env-f-formulario').value;
|
||||
const estadoFilt = $('env-f-estado').value;
|
||||
async exportarExcel() {
|
||||
const btnExcel = document.querySelector('[onclick="envios.exportarExcel()"]');
|
||||
if (btnExcel) { btnExcel.disabled = true; btnExcel.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Exportando…'; }
|
||||
let rows = [];
|
||||
try {
|
||||
const p = this._params();
|
||||
p.set('export', '1'); p.delete('page'); p.delete('limit');
|
||||
const r = await fetch('api/lab/get_formularios.php?' + p);
|
||||
const d = await r.json();
|
||||
rows = d.data || [];
|
||||
} finally {
|
||||
if (btnExcel) { btnExcel.disabled = false; btnExcel.innerHTML = '<i class="fas fa-file-excel me-1"></i><span class="d-none d-md-inline">Excel</span>'; }
|
||||
}
|
||||
if (!rows.length) { showToast('Sin datos para exportar', 'warning'); return; }
|
||||
|
||||
this._filtrado = this._data.filter(e => {
|
||||
const fecha = (e.created_at || '').slice(0, 10);
|
||||
if (desde && fecha < desde) return false;
|
||||
if (hasta && fecha > hasta) return false;
|
||||
if (paciente && !(e.paciente_nombre || '').toLowerCase().includes(paciente)) return false;
|
||||
if (formId && String(e.formulario_id) !== String(formId)) return false;
|
||||
if (estadoFilt && e.estado !== estadoFilt) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// badge de pendientes sobre el total sin filtrar
|
||||
const badgePend = $('badge-envios');
|
||||
const pend = this._data.filter(e => e.estado === 'pendiente').length;
|
||||
if (pend) { badgePend.textContent = pend; badgePend.style.display = ''; }
|
||||
else badgePend.style.display = 'none';
|
||||
|
||||
this._render();
|
||||
},
|
||||
|
||||
exportarExcel() {
|
||||
const rows = this._filtrado;
|
||||
if (!rows.length) { showToast('Sin datos para exportar'); return; }
|
||||
|
||||
// Columnas fijas
|
||||
const fixedCols = ['Formulario','Categoría','Paciente','Estado','Fecha envío','Fecha completado'];
|
||||
|
||||
// Recolectar todas las columnas dinámicas (campos del esquema) en orden de aparición
|
||||
const fieldMap = new Map(); // label → id (primer visto)
|
||||
const fieldMap = new Map();
|
||||
rows.forEach(e => {
|
||||
try {
|
||||
const esquema = JSON.parse(e.esquema || '[]');
|
||||
esquema.forEach(c => {
|
||||
if (!['separador','firma','firma_profesional','parrafo',
|
||||
'parrafo_inline','lista_marcable'].includes(c.tipo)
|
||||
&& c.id && c.label && !fieldMap.has(c.label)) {
|
||||
fieldMap.set(c.label, c.id);
|
||||
}
|
||||
});
|
||||
} catch(_) {}
|
||||
try { JSON.parse(e.esquema||'[]').forEach(c => {
|
||||
if (!['separador','firma','firma_profesional','parrafo','parrafo_inline','lista_marcable'].includes(c.tipo)
|
||||
&& c.id && c.label && !fieldMap.has(c.label)) fieldMap.set(c.label, c.id);
|
||||
}); } catch(_) {}
|
||||
});
|
||||
|
||||
const fieldCols = [...fieldMap.entries()]; // [[label, id], ...]
|
||||
const allCols = [...fixedCols, ...fieldCols.map(([l]) => l)];
|
||||
|
||||
const csvRows = [allCols];
|
||||
|
||||
const fieldCols = [...fieldMap.entries()];
|
||||
const csvRows = [[...fixedCols, ...fieldCols.map(([l])=>l)]];
|
||||
rows.forEach(e => {
|
||||
let cliente = {}, prefill = {};
|
||||
try { cliente = JSON.parse(e.datos_cliente || '{}'); } catch(_) {}
|
||||
try { prefill = JSON.parse(e.datos_prefilled || '{}'); } catch(_) {}
|
||||
const todos = { ...prefill, ...cliente };
|
||||
|
||||
// Para este envío construir mapa label → id
|
||||
const labelToId = new Map();
|
||||
try {
|
||||
JSON.parse(e.esquema || '[]').forEach(c => {
|
||||
if (c.id && c.label) labelToId.set(c.label, c.id);
|
||||
});
|
||||
} catch(_) {}
|
||||
|
||||
const fixed = [
|
||||
e.form_nombre || '',
|
||||
e.categoria || '',
|
||||
e.paciente_nombre || '',
|
||||
e.estado || '',
|
||||
(e.created_at || '').slice(0, 16).replace('T', ' '),
|
||||
(e.completado_en || '').slice(0, 16).replace('T', ' '),
|
||||
];
|
||||
|
||||
const campos = fieldCols.map(([label, defaultId]) => {
|
||||
const id = labelToId.get(label) || defaultId;
|
||||
const val = todos[id];
|
||||
if (val === undefined || val === null) return '';
|
||||
return Array.isArray(val) ? val.join('; ') : String(val);
|
||||
});
|
||||
|
||||
csvRows.push([...fixed, ...campos]);
|
||||
let cliente={}, prefill={};
|
||||
try { cliente=JSON.parse(e.datos_cliente||'{}'); } catch(_) {}
|
||||
try { prefill=JSON.parse(e.datos_prefilled||'{}'); } catch(_) {}
|
||||
const todos = {...prefill,...cliente};
|
||||
const lmap = new Map();
|
||||
try { JSON.parse(e.esquema||'[]').forEach(c=>{if(c.id&&c.label)lmap.set(c.label,c.id);}); } catch(_) {}
|
||||
csvRows.push([
|
||||
e.form_nombre||'', e.categoria||'', e.paciente_nombre||'', e.estado||'',
|
||||
(e.created_at||'').slice(0,16).replace('T',' '), (e.completado_en||'').slice(0,16).replace('T',' '),
|
||||
...fieldCols.map(([l,d])=>{ const v=todos[lmap.get(l)||d]; return v==null?'':(Array.isArray(v)?v.join('; '):String(v)); }),
|
||||
]);
|
||||
});
|
||||
|
||||
// Generar CSV con BOM UTF-8 para Excel
|
||||
const csv = '\uFEFF' + csvRows.map(row =>
|
||||
row.map(v => '"' + String(v).replace(/"/g, '""') + '"').join(',')
|
||||
).join('\r\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const csv = '\uFEFF' + csvRows.map(r=>r.map(v=>'"'+String(v).replace(/"/g,'""')+'"').join(',')).join('\r\n');
|
||||
const blob = new Blob([csv],{type:'text/csv;charset=utf-8;'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'envios_formularios_' + new Date().toISOString().slice(0, 10) + '.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
const a = Object.assign(document.createElement('a'),{href:url,download:'envios_formularios_'+new Date().toISOString().slice(0,10)+'.csv'});
|
||||
document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url);
|
||||
showToast('✅ Excel descargado — ' + rows.length + ' registro(s)', 'success', 4000);
|
||||
},
|
||||
|
||||
_render() {
|
||||
const tb = $('tbody-envios');
|
||||
const total = this._filtrado.length;
|
||||
|
||||
if (!total) {
|
||||
if (!this._paginaData.length) {
|
||||
tb.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-4">Sin envíos para los filtros seleccionados</td></tr>';
|
||||
$('env-pag-info').textContent = '';
|
||||
$('env-paginacion').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalPag = Math.ceil(total / POR_PAGINA);
|
||||
if (this._pagina > totalPag) this._pagina = totalPag;
|
||||
const inicio = (this._pagina - 1) * POR_PAGINA;
|
||||
const pagina = this._filtrado.slice(inicio, inicio + POR_PAGINA);
|
||||
|
||||
tb.innerHTML = pagina.map(e => {
|
||||
tb.innerHTML = this._paginaData.map(e => {
|
||||
const info = ESTADO_ENV[e.estado] || {};
|
||||
return `<tr>
|
||||
<td><span class="fw-semibold">${esc(e.form_nombre)}</span>
|
||||
@@ -694,54 +646,39 @@ const envios = {
|
||||
<td><small class="text-muted">${esc((e.expira_en||'').slice(0,10))}</small></td>
|
||||
<td>
|
||||
<button class="btn btn-xs btn-outline-secondary" title="Copiar link"
|
||||
onclick="envios.copiarTokenLink('${esc(e.token)}')">
|
||||
onclick="envios.copiarTokenLink('${esc(e.token)}')">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
${['completado','firmado'].includes(e.estado)
|
||||
? `<button class="btn btn-xs btn-outline-primary ms-1" title="Ver respuesta"
|
||||
onclick="verRespuesta.abrir(${e.id})">
|
||||
<i class="fas fa-eye"></i></button>
|
||||
<a class="btn btn-xs btn-outline-danger ms-1" title="Descargar PDF"
|
||||
href="ver_formulario_enviado.php?id=${e.id}" target="_blank">
|
||||
<i class="fas fa-file-pdf"></i></a>`
|
||||
? `<button class="btn btn-xs btn-outline-primary ms-1" onclick="verRespuesta.abrir(${e.id})"><i class="fas fa-eye"></i></button>
|
||||
<a class="btn btn-xs btn-outline-danger ms-1" href="ver_formulario_enviado.php?id=${e.id}" target="_blank"><i class="fas fa-file-pdf"></i></a>`
|
||||
: ''}
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Info
|
||||
$('env-pag-info').textContent =
|
||||
`Mostrando ${inicio + 1}–${Math.min(inicio + POR_PAGINA, total)} de ${total} envíos`;
|
||||
`Mostrando ${inicio+1}–${inicio+this._paginaData.length} de ${this._total} envío(s)`;
|
||||
|
||||
// Paginación
|
||||
const ul = $('env-paginacion');
|
||||
const ul = $('env-paginacion');
|
||||
const pag = this._pagina, tot = this._paginas;
|
||||
ul.innerHTML = '';
|
||||
const agregar = (label, pag, disabled, active) => {
|
||||
const li = document.createElement('li');
|
||||
li.className = `page-item${disabled?' disabled':''}${active?' active':''}`;
|
||||
li.innerHTML = `<a class="page-link" href="#" onclick="event.preventDefault();${!disabled?`envios.irAPagina(${pag})`:''}">${label}</a>`;
|
||||
ul.appendChild(li);
|
||||
if (tot <= 1) return;
|
||||
const li = (lbl, p, dis, act) => {
|
||||
const el = document.createElement('li');
|
||||
el.className = `page-item${dis?' disabled':''}${act?' active':''}`;
|
||||
el.innerHTML = `<a class="page-link" href="#" onclick="event.preventDefault();${!dis?`envios.cargar(${p})`:''}">${lbl}</a>`;
|
||||
ul.appendChild(el);
|
||||
};
|
||||
agregar('«', 1, this._pagina === 1, false);
|
||||
agregar('‹', this._pagina-1, this._pagina === 1, false);
|
||||
|
||||
// páginas con ventana deslizante ±2
|
||||
const rango = 2;
|
||||
const pMin = Math.max(1, this._pagina - rango);
|
||||
const pMax = Math.min(totalPag, this._pagina + rango);
|
||||
if (pMin > 1) { agregar('1', 1, false, false); if (pMin > 2) agregar('…', null, true, false); }
|
||||
for (let p = pMin; p <= pMax; p++) agregar(p, p, false, p === this._pagina);
|
||||
if (pMax < totalPag) { if (pMax < totalPag-1) agregar('…', null, true, false); agregar(totalPag, totalPag, false, false); }
|
||||
|
||||
agregar('›', this._pagina+1, this._pagina === totalPag, false);
|
||||
agregar('»', totalPag, this._pagina === totalPag, false);
|
||||
li('«',1,pag===1,false); li('‹',pag-1,pag===1,false);
|
||||
const pMin=Math.max(1,pag-2), pMax=Math.min(tot,pag+2);
|
||||
if(pMin>1){li('1',1,false,false);if(pMin>2)li('…',null,true,false);}
|
||||
for(let p=pMin;p<=pMax;p++) li(p,p,false,p===pag);
|
||||
if(pMax<tot){if(pMax<tot-1)li('…',null,true,false);li(tot,tot,false,false);}
|
||||
li('›',pag+1,pag===tot,false); li('»',tot,pag===tot,false);
|
||||
},
|
||||
|
||||
irAPagina(p) {
|
||||
this._pagina = p;
|
||||
this._render();
|
||||
$('panel-envios').scrollIntoView({ behavior:'smooth', block:'start' });
|
||||
},
|
||||
irAPagina(p) { this.cargar(p); $('panel-envios').scrollIntoView({behavior:'smooth',block:'start'}); },
|
||||
|
||||
copiarTokenLink(token) {
|
||||
const url = window.location.origin + window.location.pathname.replace(/\/[^/]+$/, '')
|
||||
@@ -919,7 +856,7 @@ const verRespuesta = {
|
||||
|
||||
async abrir(envioId) {
|
||||
this._envioId = envioId;
|
||||
const e = envios._data.find(x => x.id == envioId);
|
||||
const e = envios._paginaData.find(x => x.id == envioId);
|
||||
if (!e) return;
|
||||
|
||||
const cliente = JSON.parse(e.datos_cliente || '{}');
|
||||
|
||||
Reference in New Issue
Block a user