Finalización del módulo Luz Cámara: Corrección de errores JS, exportación profesional a PDF y reportes de deudores
This commit is contained in:
600
views/electricity/index.php
Executable file
600
views/electricity/index.php
Executable file
@@ -0,0 +1,600 @@
|
||||
<?php
|
||||
// views/electricity/index.php
|
||||
$periods = ElectricityBill::getPeriods();
|
||||
?>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h2><i class="bi bi-lightbulb"></i> Pagos de Luz - Cámara</h2>
|
||||
<p class="text-muted">Concentrado de pagos bimestrales por casa</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4 d-flex justify-content-between align-items-center flex-wrap gap-2">
|
||||
<div class="d-flex gap-3 align-items-center flex-wrap">
|
||||
<div>
|
||||
<label for="yearSelect" class="form-label me-2">Año:</label>
|
||||
<select id="yearSelect" class="form-select d-inline-block" style="width: auto;"
|
||||
onchange="window.location.href='dashboard.php?page=luz_camara&year='+this.value">
|
||||
<?php for ($y = 2024; $y <= date('Y') + 1; $y++): ?>
|
||||
<option value="<?= $y?>" <?= $y == $year ? 'selected' : ''?>>
|
||||
<?= $y?>
|
||||
</option>
|
||||
<?php
|
||||
endfor; ?>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-primary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="bi bi-download"></i> Exportar
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="exportPDF()">PDF</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="exportCSV()">CSV</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<?php if (Auth::isCapturist()): ?>
|
||||
<button onclick="saveChanges()" id="btnSaveTop" class="btn btn-warning position-relative" disabled>
|
||||
<i class="bi bi-save"></i> Guardar Cambios
|
||||
<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger"
|
||||
id="changesBadge" style="display: none;">
|
||||
0
|
||||
<span class="visually-hidden">cambios pendientes</span>
|
||||
</span>
|
||||
</button>
|
||||
<?php
|
||||
endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuración de Recibos Bimestrales -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3"><i class="bi bi-gear"></i> Configuración de Recibos (CFE)</h5>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Periodo</label>
|
||||
<select id="configPeriod" class="form-select form-select-sm" onchange="loadConfig()">
|
||||
<?php foreach ($periods as $period): ?>
|
||||
<option value="<?= $period?>">
|
||||
<?= $period?>
|
||||
</option>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small text-muted">Total CFE</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" id="configTotal" class="form-control" step="0.01" <?=!Auth::isCapturist()
|
||||
? 'disabled' : ''?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label small text-muted">Por Casa</label>
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text">$</span>
|
||||
<input type="number" id="configPerHouse" class="form-control" step="0.01" <?=!Auth::isCapturist()
|
||||
? 'disabled' : ''?>>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label small text-muted">Notas</label>
|
||||
<input type="text" id="configNotes" class="form-control form-control-sm" placeholder="Opcional..."
|
||||
<?=!Auth::isCapturist() ? 'disabled' : ''?>>
|
||||
</div>
|
||||
<?php if (Auth::isCapturist()): ?>
|
||||
<div class="col-md-2 d-flex align-items-end">
|
||||
<button class="btn btn-success btn-sm w-100" onclick="saveConfig()">
|
||||
<i class="bi bi-check2"></i> Guardar
|
||||
</button>
|
||||
</div>
|
||||
<?php
|
||||
endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Pagos -->
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body p-0">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered table-hover mb-0" id="electricityTable">
|
||||
<thead class="table-dark text-center sticky-top">
|
||||
<tr>
|
||||
<th style="width: 80px;">Casa</th>
|
||||
<th style="width: 100px;">Estado</th>
|
||||
<?php foreach ($periods as $period):
|
||||
$config = $electricityBills[$period] ?? [];
|
||||
$amountPerHouse = $config['amount_per_house'] ?? 0;
|
||||
?>
|
||||
<th>
|
||||
<?= $period?><br>
|
||||
<small class="text-white" style="font-weight: normal;">$
|
||||
<?= number_format($amountPerHouse, 2)?>
|
||||
</small>
|
||||
</th>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php
|
||||
$periodTotals = array_fill_keys($periods, 0);
|
||||
$grandTotal = 0;
|
||||
|
||||
foreach ($matrix['houses'] as $house):
|
||||
// Filtrar visibilidad para usuarios normales
|
||||
if (!Auth::canViewHouse($house['id']))
|
||||
continue;
|
||||
|
||||
$houseTotal = 0;
|
||||
?>
|
||||
<tr data-house-id="<?= $house['id']?>" data-house-number="<?= $house['number']?>">
|
||||
<td class="text-center fw-bold">
|
||||
<?= $house['number']?>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge <?= $house['status'] == 'activa' ? 'paid' : 'inactive'?>">
|
||||
<?= ucfirst($house['status'])?>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<?php foreach ($periods as $period):
|
||||
$payment = $matrix['payments'][$period][$house['id']] ?? null;
|
||||
$amount = $payment['amount'] ?? 0;
|
||||
$periodTotals[$period] += $amount;
|
||||
$houseTotal += $amount;
|
||||
|
||||
$config = $electricityBills[$period] ?? [];
|
||||
$expected = $config['amount_per_house'] ?? 0;
|
||||
|
||||
$cellClass = '';
|
||||
$cellText = '-';
|
||||
|
||||
if ($amount > 0) {
|
||||
if ($expected > 0 && $amount >= $expected) {
|
||||
$cellClass = 'paid'; // Paid
|
||||
}
|
||||
else {
|
||||
$cellClass = 'partial'; // Partial
|
||||
}
|
||||
$cellText = '$' . number_format($amount, 2);
|
||||
}
|
||||
else {
|
||||
if ($expected > 0) {
|
||||
$cellClass = 'pending'; // Unpaid
|
||||
$cellText = '-';
|
||||
}
|
||||
else {
|
||||
$cellClass = ''; // No config
|
||||
$cellText = '-';
|
||||
}
|
||||
|
||||
// Si está deshabitada y no hay pago, gris
|
||||
if ($house['status'] == 'deshabitada' && $amount == 0) {
|
||||
$cellClass = 'inactive';
|
||||
}
|
||||
}
|
||||
|
||||
$isEditable = Auth::isCapturist() && $house['status'] == 'activa';
|
||||
?>
|
||||
<td class="payment-cell text-center <?= $cellClass?>" data-house-id="<?= $house['id']?>"
|
||||
data-period="<?= $period?>" data-amount="<?= $amount?>" data-expected="<?= $expected?>"
|
||||
<?= $isEditable ? 'contenteditable="true"' : ''?>>
|
||||
<?= $cellText?>
|
||||
</td>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
|
||||
<td class="text-end fw-bold table-active">
|
||||
$
|
||||
<?= number_format($houseTotal, 2)?>
|
||||
</td>
|
||||
</tr>
|
||||
<?php $grandTotal += $houseTotal;
|
||||
endforeach; ?>
|
||||
</tbody>
|
||||
<tfoot class="fw-bold">
|
||||
<tr>
|
||||
<td colspan="2" class="text-end">Totales:</td>
|
||||
<?php foreach ($periods as $period): ?>
|
||||
<td class="text-center">$
|
||||
<?= number_format($periodTotals[$period], 2)?>
|
||||
</td>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
<td class="text-end">$
|
||||
<?= number_format($grandTotal, 2)?>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (Auth::isCapturist()): ?>
|
||||
<div class="d-flex justify-content-end mb-4 no-print mt-3">
|
||||
<button onclick="saveChanges()" id="btnSaveBottom" class="btn btn-warning" disabled>
|
||||
<i class="bi bi-save"></i> Guardar Cambios
|
||||
</button>
|
||||
</div>
|
||||
<?php
|
||||
endif; ?>
|
||||
|
||||
<div class="row mt-3 no-print">
|
||||
<div class="col-md-6">
|
||||
<div class="alert alert-info mb-0 py-2">
|
||||
<strong><i class="bi bi-info-circle"></i> Instrucciones:</strong>
|
||||
<ul class="mb-0 mt-1 list-inline">
|
||||
<li class="list-inline-item"><span class="badge paid">Verde</span> = Pagado</li>
|
||||
<li class="list-inline-item"><span class="badge partial">Amarillo</span> = Parcial</li>
|
||||
<li class="list-inline-item"><span class="badge pending">Rojo</span> = Pendiente</li>
|
||||
<li class="list-inline-item"><span class="badge inactive">Gris</span> = Inactivo/N.A.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Export PDF -->
|
||||
<div class="modal fade" id="exportPdfModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Exportar a PDF</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<h6>Selecciona los periodos a exportar:</h6>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="selectAllPeriods" checked>
|
||||
<label class="form-check-label" for="selectAllPeriods">Todos</label>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<?php foreach ($periods as $i => $period): ?>
|
||||
<div class="col-6">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input period-checkbox" type="checkbox" value="<?= $period?>"
|
||||
id="period_<?= $i?>" checked>
|
||||
<label class="form-check-label" for="period_<?= $i?>">
|
||||
<?= $period?>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancelar</button>
|
||||
<button type="button" class="btn btn-success" onclick="generatePDF()">
|
||||
<i class="bi bi-file-earmark-pdf"></i> Generar PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Datos de configuración iniciales
|
||||
const electricityConfig = <?= json_encode($electricityBills)?>;
|
||||
let pendingChanges = {};
|
||||
|
||||
// Cargar configuración al cambiar select
|
||||
function loadConfig() {
|
||||
const period = document.getElementById('configPeriod').value;
|
||||
const data = electricityConfig[period] || {};
|
||||
|
||||
document.getElementById('configTotal').value = data.total_amount || '';
|
||||
document.getElementById('configPerHouse').value = data.amount_per_house || '';
|
||||
document.getElementById('configNotes').value = data.notes || '';
|
||||
}
|
||||
|
||||
// Inicializar configuración
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadConfig();
|
||||
setupEditableCells();
|
||||
|
||||
// Listener para modal
|
||||
document.getElementById('selectAllPeriods').addEventListener('change', function () {
|
||||
document.querySelectorAll('.period-checkbox').forEach(cb => {
|
||||
cb.checked = this.checked;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupEditableCells() {
|
||||
document.querySelectorAll('.payment-cell[contenteditable="true"]').forEach(cell => {
|
||||
cell.addEventListener('focus', function () {
|
||||
this.classList.add('editing');
|
||||
const text = this.textContent.trim();
|
||||
// Limpiar si es guión, moneda o 0 al enfocar para facilitar edición
|
||||
if (text === '-' || text.startsWith('$') || parseAmount(text) === 0) {
|
||||
this.textContent = '';
|
||||
}
|
||||
});
|
||||
|
||||
cell.addEventListener('blur', function () {
|
||||
this.classList.remove('editing');
|
||||
let newText = this.textContent.trim();
|
||||
let newVal = parseAmount(newText);
|
||||
|
||||
// Formatear display
|
||||
if (newVal === 0) {
|
||||
const expected = parseFloat(this.dataset.expected);
|
||||
if (expected > 0) {
|
||||
this.textContent = '-'; // Pendiente
|
||||
} else {
|
||||
this.textContent = '-'; // Nada
|
||||
}
|
||||
} else {
|
||||
this.textContent = '$' + newVal.toFixed(2);
|
||||
}
|
||||
|
||||
trackChange(this, newVal);
|
||||
});
|
||||
|
||||
cell.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.blur();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function parseAmount(text) {
|
||||
if (text === '-' || !text) return 0;
|
||||
return parseFloat(text.replace(/[^0-9.-]+/g, '')) || 0;
|
||||
}
|
||||
|
||||
<?php if (Auth::isCapturist()): ?>
|
||||
// Guardar configuración
|
||||
async function saveConfig() {
|
||||
const period = document.getElementById('configPeriod').value;
|
||||
const total = document.getElementById('configTotal').value;
|
||||
const perHouse = document.getElementById('configPerHouse').value;
|
||||
const notes = document.getElementById('configNotes').value;
|
||||
|
||||
try {
|
||||
const response = await fetch('dashboard.php?page=luz_camara_config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
year: <?= $year?>,
|
||||
period: period,
|
||||
total_amount: total,
|
||||
amount_per_house: perHouse,
|
||||
notes: notes
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// Actualizar objeto local
|
||||
electricityConfig[period] = {
|
||||
year: <?= $year?>,
|
||||
period: period,
|
||||
total_amount: total,
|
||||
amount_per_house: perHouse,
|
||||
notes: notes
|
||||
};
|
||||
alert('Configuración guardada exitosamente');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error al guardar: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error de conexión al guardar configuración');
|
||||
}
|
||||
}
|
||||
|
||||
// Tracking de cambios
|
||||
function trackChange(cell, newVal) {
|
||||
const houseId = cell.dataset.houseId;
|
||||
const period = cell.dataset.period;
|
||||
const key = `${houseId}_${period}`;
|
||||
const original = parseFloat(cell.dataset.amount);
|
||||
|
||||
// Si el valor cambió respecto al original cargado
|
||||
if (newVal !== original) {
|
||||
pendingChanges[key] = {
|
||||
house_id: houseId,
|
||||
year: <?= $year?>,
|
||||
period: period,
|
||||
amount: newVal
|
||||
};
|
||||
cell.classList.add('table-info');
|
||||
cell.style.fontWeight = 'bold';
|
||||
} else {
|
||||
delete pendingChanges[key];
|
||||
cell.classList.remove('table-info');
|
||||
cell.style.fontWeight = 'normal';
|
||||
}
|
||||
|
||||
updateSaveButton();
|
||||
}
|
||||
|
||||
function updateSaveButton() {
|
||||
const count = Object.keys(pendingChanges).length;
|
||||
const btnTop = document.getElementById('btnSaveTop');
|
||||
const btnBottom = document.getElementById('btnSaveBottom');
|
||||
const badge = document.getElementById('changesBadge');
|
||||
|
||||
if (btnTop) {
|
||||
btnTop.disabled = count === 0;
|
||||
// Duplicar comportamiento: mostrar texto con conteo
|
||||
if (count > 0) {
|
||||
btnTop.innerHTML = `<i class="bi bi-save"></i> Guardar ${count} Cambios`;
|
||||
btnTop.classList.remove('btn-outline-secondary');
|
||||
btnTop.classList.add('btn-warning');
|
||||
} else {
|
||||
btnTop.innerHTML = `<i class="bi bi-save"></i> Guardar Cambios`;
|
||||
}
|
||||
}
|
||||
|
||||
if (btnBottom) {
|
||||
btnBottom.disabled = count === 0;
|
||||
btnBottom.innerHTML = `<i class="bi bi-save"></i> Guardar ${count} Cambios`;
|
||||
}
|
||||
|
||||
// Alerta de navegación si hay cambios
|
||||
if (count > 0) {
|
||||
window.onbeforeunload = () => "Tienes cambios sin guardar. ¿Seguro que quieres salir?";
|
||||
} else {
|
||||
window.onbeforeunload = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveChanges() {
|
||||
if (Object.keys(pendingChanges).length === 0) return;
|
||||
|
||||
const changes = Object.values(pendingChanges);
|
||||
const btnTop = document.getElementById('btnSaveTop');
|
||||
const btnBottom = document.getElementById('btnSaveBottom');
|
||||
|
||||
const originalTextTop = btnTop ? btnTop.innerHTML : '';
|
||||
const originalTextBottom = btnBottom ? btnBottom.innerHTML : '';
|
||||
|
||||
if (btnTop) {
|
||||
btnTop.disabled = true;
|
||||
btnTop.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Guardando...';
|
||||
}
|
||||
if (btnBottom) {
|
||||
btnBottom.disabled = true;
|
||||
btnBottom.innerHTML = '<span class="spinner-border spinner-border-sm"></span> Guardando...';
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('dashboard.php?page=luz_camara_actions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ changes: changes })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
window.onbeforeunload = null;
|
||||
alert('Cambios guardados exitosamente');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error al guardar: ' + (result.message || result.error));
|
||||
if (btnTop) {
|
||||
btnTop.disabled = false;
|
||||
btnTop.innerHTML = originalTextTop;
|
||||
}
|
||||
if (btnBottom) {
|
||||
btnBottom.disabled = false;
|
||||
btnBottom.innerHTML = originalTextBottom;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error de conexión');
|
||||
if (btnTop) {
|
||||
btnTop.disabled = false;
|
||||
btnTop.innerHTML = originalTextTop;
|
||||
}
|
||||
if (btnBottom) {
|
||||
btnBottom.disabled = false;
|
||||
btnBottom.innerHTML = originalTextBottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
<?php
|
||||
endif; ?>
|
||||
|
||||
function exportPDF() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('exportPdfModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
function generatePDF() {
|
||||
const checkboxes = document.querySelectorAll('.period-checkbox:checked');
|
||||
const selectedPeriods = Array.from(checkboxes).map(cb => cb.value);
|
||||
|
||||
let url = 'dashboard.php?page=luz_camara&action=export_electricity_pdf&year=<?= $year?>';
|
||||
selectedPeriods.forEach(period => {
|
||||
url += `&periods[]=${encodeURIComponent(period)}`;
|
||||
});
|
||||
|
||||
// Cerrar el modal antes de redirigir
|
||||
const exportPdfModal = bootstrap.Modal.getInstance(document.getElementById('exportPdfModal'));
|
||||
if (exportPdfModal) {
|
||||
exportPdfModal.hide();
|
||||
}
|
||||
|
||||
// Redirigir al usuario para iniciar la descarga del PDF
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function exportCSV() {
|
||||
let csv = [];
|
||||
const rows = document.querySelectorAll("#electricityTable tr");
|
||||
|
||||
for (const row of rows) {
|
||||
const cols = row.querySelectorAll("td,th");
|
||||
const rowData = [];
|
||||
for (const col of cols) {
|
||||
rowData.push('"' + col.innerText.replace(/(\r\n|\n|\r)/gm, " ").trim() + '"');
|
||||
}
|
||||
csv.push(rowData.join(","));
|
||||
}
|
||||
|
||||
const csvFile = new Blob([csv.join("\n")], { type: "text/csv" });
|
||||
const downloadLink = document.createElement("a");
|
||||
downloadLink.download = "Pagos_Luz_Camara_<?= $year?>.csv";
|
||||
downloadLink.href = window.URL.createObjectURL(csvFile);
|
||||
downloadLink.style.display = "none";
|
||||
document.body.appendChild(downloadLink);
|
||||
downloadLink.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.payment-cell {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.payment-cell:focus {
|
||||
outline: 2px solid #0d6efd;
|
||||
background-color: #fff !important;
|
||||
color: #000;
|
||||
z-index: 5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@media print {
|
||||
|
||||
.btn-group,
|
||||
#btnSaveTop,
|
||||
#btnSaveBottom,
|
||||
.card-header,
|
||||
.form-label,
|
||||
.input-group,
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none !important;
|
||||
shadow: none !important;
|
||||
}
|
||||
|
||||
.badge {
|
||||
border: 1px solid #000;
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user