cinu
This commit is contained in:
177
RS_system/Controllers/DiezmoCatalogoController.cs
Normal file
177
RS_system/Controllers/DiezmoCatalogoController.cs
Normal file
@@ -0,0 +1,177 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Rs_system.Data;
|
||||
using Rs_system.Filters;
|
||||
using Rs_system.Models;
|
||||
using Rs_system.Models.ViewModels.Catalogos;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
[Permission("Diezmo.Index")] // Requiere permisos base del módulo
|
||||
public class DiezmoCatalogoController : Controller
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
|
||||
public DiezmoCatalogoController(ApplicationDbContext context)
|
||||
{
|
||||
_context = context;
|
||||
}
|
||||
|
||||
private string UsuarioActual() => User.FindFirst(ClaimTypes.Name)?.Value ?? User.Identity?.Name ?? "Sistema";
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Tipos de Salida
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
public async Task<IActionResult> TiposSalida()
|
||||
{
|
||||
var lista = await _context.DiezmoTiposSalida
|
||||
.Where(x => !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.ToListAsync();
|
||||
return View(lista);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> GuardarTipoSalida(TipoSalidaViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Datos inválidos: " + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage));
|
||||
return RedirectToAction(nameof(TiposSalida));
|
||||
}
|
||||
|
||||
if (vm.Id == 0) // Crear
|
||||
{
|
||||
var nuevo = new DiezmoTipoSalida
|
||||
{
|
||||
Nombre = vm.Nombre,
|
||||
Descripcion = vm.Descripcion,
|
||||
EsEntregaPastor = vm.EsEntregaPastor,
|
||||
CreadoPor = UsuarioActual(),
|
||||
CreadoEn = DateTime.UtcNow
|
||||
};
|
||||
_context.DiezmoTiposSalida.Add(nuevo);
|
||||
TempData["SuccessMessage"] = "Tipo de salida creado.";
|
||||
}
|
||||
else // Editar
|
||||
{
|
||||
var dbItem = await _context.DiezmoTiposSalida.FindAsync(vm.Id);
|
||||
if (dbItem == null || dbItem.Eliminado) return NotFound();
|
||||
|
||||
dbItem.Nombre = vm.Nombre;
|
||||
dbItem.Descripcion = vm.Descripcion;
|
||||
dbItem.EsEntregaPastor = vm.EsEntregaPastor;
|
||||
dbItem.ActualizadoEn = DateTime.UtcNow;
|
||||
_context.Update(dbItem);
|
||||
|
||||
TempData["SuccessMessage"] = "Tipo de salida actualizado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(TiposSalida));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EliminarTipoSalida(long id)
|
||||
{
|
||||
var dbItem = await _context.DiezmoTiposSalida.FindAsync(id);
|
||||
if (dbItem == null) return NotFound();
|
||||
|
||||
// Validación simple (si ya hay salidas con este tipo no borrar duro)
|
||||
var enUso = await _context.DiezmoSalidas.AnyAsync(s => s.TipoSalidaId == id && !s.Eliminado);
|
||||
if (enUso)
|
||||
{
|
||||
dbItem.Activo = false; // Desactivar en lugar de borrar
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Tipo de salida desactivado (Estaba en uso).";
|
||||
}
|
||||
else
|
||||
{
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Tipo de salida eliminado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(TiposSalida));
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Beneficiarios
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
public async Task<IActionResult> Beneficiarios()
|
||||
{
|
||||
var lista = await _context.DiezmoBeneficiarios
|
||||
.Where(x => !x.Eliminado)
|
||||
.OrderBy(x => x.Nombre)
|
||||
.ToListAsync();
|
||||
return View(lista);
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> GuardarBeneficiario(BeneficiarioViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
TempData["ErrorMessage"] = "Datos inválidos: " + string.Join(", ", ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage));
|
||||
return RedirectToAction(nameof(Beneficiarios));
|
||||
}
|
||||
|
||||
if (vm.Id == 0)
|
||||
{
|
||||
var nuevo = new DiezmoBeneficiario
|
||||
{
|
||||
Nombre = vm.Nombre,
|
||||
Descripcion = vm.Descripcion,
|
||||
CreadoPor = UsuarioActual()
|
||||
};
|
||||
_context.DiezmoBeneficiarios.Add(nuevo);
|
||||
TempData["SuccessMessage"] = "Beneficiario creado.";
|
||||
}
|
||||
else
|
||||
{
|
||||
var dbItem = await _context.DiezmoBeneficiarios.FindAsync(vm.Id);
|
||||
if (dbItem == null || dbItem.Eliminado) return NotFound();
|
||||
|
||||
dbItem.Nombre = vm.Nombre;
|
||||
dbItem.Descripcion = vm.Descripcion;
|
||||
dbItem.ActualizadoPor = UsuarioActual();
|
||||
dbItem.ActualizadoEn = DateTime.UtcNow;
|
||||
_context.Update(dbItem);
|
||||
|
||||
TempData["SuccessMessage"] = "Beneficiario actualizado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(Beneficiarios));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> EliminarBeneficiario(long id)
|
||||
{
|
||||
var dbItem = await _context.DiezmoBeneficiarios.FindAsync(id);
|
||||
if (dbItem == null) return NotFound();
|
||||
|
||||
var enUso = await _context.DiezmoSalidas.AnyAsync(s => s.BeneficiarioId == id && !s.Eliminado);
|
||||
if (enUso)
|
||||
{
|
||||
dbItem.Activo = false;
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Beneficiario desactivado (estaba en uso).";
|
||||
}
|
||||
else
|
||||
{
|
||||
dbItem.Eliminado = true;
|
||||
TempData["SuccessMessage"] = "Beneficiario eliminado.";
|
||||
}
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
return RedirectToAction(nameof(Beneficiarios));
|
||||
}
|
||||
}
|
||||
356
RS_system/Controllers/DiezmoController.cs
Normal file
356
RS_system/Controllers/DiezmoController.cs
Normal file
@@ -0,0 +1,356 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Rs_system.Filters;
|
||||
using Rs_system.Models.ViewModels;
|
||||
using Rs_system.Services;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Rs_system.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public class DiezmoController : Controller
|
||||
{
|
||||
private readonly IDiezmoCierreService _cierreService;
|
||||
private readonly IDiezmoReciboService _reciboService;
|
||||
private readonly IMiembroService _miembroService;
|
||||
|
||||
public DiezmoController(
|
||||
IDiezmoCierreService cierreService,
|
||||
IDiezmoReciboService reciboService,
|
||||
IMiembroService miembroService)
|
||||
{
|
||||
_cierreService = cierreService;
|
||||
_reciboService = reciboService;
|
||||
_miembroService = miembroService;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo — Listado de cierres
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Index")]
|
||||
public async Task<IActionResult> Index(int? anio)
|
||||
{
|
||||
anio ??= DateTime.Today.Year;
|
||||
var cierres = await _cierreService.GetCierresAsync(anio);
|
||||
|
||||
var vm = cierres.Select(c => new DiezmoCierreListViewModel
|
||||
{
|
||||
Id = c.Id,
|
||||
Fecha = c.Fecha,
|
||||
Cerrado = c.Cerrado,
|
||||
TotalRecibido = c.TotalRecibido,
|
||||
TotalNeto = c.TotalNeto,
|
||||
TotalSalidas = c.TotalSalidas,
|
||||
SaldoFinal = c.SaldoFinal,
|
||||
NumeroDetalles = c.Detalles?.Count ?? 0,
|
||||
NumeroSalidas = c.Salidas?.Count ?? 0
|
||||
}).ToList();
|
||||
|
||||
ViewBag.AnioActual = anio;
|
||||
ViewBag.Anios = GetAniosSelectList();
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo/Create
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Create")]
|
||||
public IActionResult Create()
|
||||
=> View(new DiezmoCierreCreateViewModel());
|
||||
|
||||
// POST: /Diezmo/Create
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.Create")]
|
||||
public async Task<IActionResult> Create(DiezmoCierreCreateViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
return View(vm);
|
||||
|
||||
try
|
||||
{
|
||||
var cierre = await _cierreService.CrearCierreAsync(
|
||||
vm.Fecha, vm.Observaciones, UsuarioActual());
|
||||
|
||||
TempData["SuccessMessage"] = $"Cierre del {cierre.Fecha:dd/MM/yyyy} creado exitosamente.";
|
||||
return RedirectToAction(nameof(Detail), new { id = cierre.Id });
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
ModelState.AddModelError("Fecha", ex.Message);
|
||||
return View(vm);
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo/Detail/{id} — Pantalla operativa
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Index")]
|
||||
public async Task<IActionResult> Detail(long id)
|
||||
{
|
||||
var cierre = await _cierreService.GetCierreByIdAsync(id);
|
||||
if (cierre == null || cierre.Eliminado) return NotFound();
|
||||
|
||||
var tiposSalida = await _cierreService.GetTiposSalidaActivosAsync();
|
||||
var beneficiarios = await _cierreService.GetBeneficiariosActivosAsync();
|
||||
var todosMiembros = await _miembroService.GetAllAsync();
|
||||
var miembrosSelect = todosMiembros
|
||||
.Where(m => m.Activo)
|
||||
.OrderBy(m => m.NombreCompleto)
|
||||
.Select(m => new SelectListItem(m.NombreCompleto, m.Id.ToString()))
|
||||
.ToList();
|
||||
|
||||
var vm = new DiezmoCierreDetalleViewModel
|
||||
{
|
||||
Id = cierre.Id,
|
||||
Fecha = cierre.Fecha,
|
||||
Cerrado = cierre.Cerrado,
|
||||
Observaciones = cierre.Observaciones,
|
||||
CerradoPor = cierre.CerradoPor,
|
||||
FechaCierre = cierre.FechaCierre,
|
||||
TotalRecibido = cierre.TotalRecibido,
|
||||
TotalCambio = cierre.TotalCambio,
|
||||
TotalNeto = cierre.TotalNeto,
|
||||
TotalSalidas = cierre.TotalSalidas,
|
||||
SaldoFinal = cierre.SaldoFinal,
|
||||
|
||||
Detalles = cierre.Detalles.Select(d => new DiezmoDetalleRowViewModel
|
||||
{
|
||||
Id = d.Id,
|
||||
MiembroId = d.MiembroId,
|
||||
NombreMiembro = d.Miembro?.Persona?.NombreCompleto ?? "—",
|
||||
MontoEntregado = d.MontoEntregado,
|
||||
CambioEntregado = d.CambioEntregado,
|
||||
MontoNeto = d.MontoNeto,
|
||||
Observaciones = d.Observaciones,
|
||||
Fecha = d.Fecha
|
||||
}).ToList(),
|
||||
|
||||
Salidas = cierre.Salidas.Select(s => new DiezmoSalidaRowViewModel
|
||||
{
|
||||
Id = s.Id,
|
||||
TipoSalidaNombre = s.TipoSalida?.Nombre ?? "—",
|
||||
BeneficiarioNombre = s.Beneficiario?.Nombre,
|
||||
Monto = s.Monto,
|
||||
Concepto = s.Concepto,
|
||||
NumeroRecibo = s.NumeroRecibo,
|
||||
Fecha = s.Fecha
|
||||
}).ToList(),
|
||||
|
||||
MiembrosSelect = miembrosSelect,
|
||||
TiposSalidaSelect = tiposSalida.Select(t =>
|
||||
new SelectListItem(t.Nombre, t.Id.ToString())).ToList(),
|
||||
BeneficiariosSelect = beneficiarios.Select(b =>
|
||||
new SelectListItem(b.Nombre, b.Id.ToString())).ToList()
|
||||
};
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/AddDetalle
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddDetalle")]
|
||||
public async Task<IActionResult> AddDetalle(long cierreId, DiezmoDetalleFormViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest("Datos inválidos.");
|
||||
|
||||
TempData["ErrorMessage"] = "Datos inválidos. Verifique el formulario.";
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _cierreService.AgregarDetalleAsync(cierreId, vm, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Diezmo registrado correctamente.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// POST: /Diezmo/DeleteDetalle
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddDetalle")]
|
||||
public async Task<IActionResult> DeleteDetalle(long detalleId, long cierreId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.EliminarDetalleAsync(detalleId, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Detalle eliminado.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/AddSalida
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddSalida")]
|
||||
public async Task<IActionResult> AddSalida(long cierreId, DiezmoSalidaFormViewModel vm)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest("Datos inválidos.");
|
||||
|
||||
TempData["ErrorMessage"] = "Datos inválidos. Verifique el formulario.";
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await _cierreService.AgregarSalidaAsync(cierreId, vm, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Salida registrada correctamente.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// POST: /Diezmo/DeleteSalida
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.AddSalida")]
|
||||
public async Task<IActionResult> DeleteSalida(long salidaId, long cierreId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.EliminarSalidaAsync(salidaId, UsuarioActual());
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return await GetTotalesJsonAsync(cierreId);
|
||||
|
||||
TempData["SuccessMessage"] = "Salida eliminada.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
if (Request.Headers["X-Requested-With"] == "XMLHttpRequest")
|
||||
return BadRequest(ex.Message);
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id = cierreId });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/Close/{id}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.Close")]
|
||||
public async Task<IActionResult> Close(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.CerrarCierreAsync(id, UsuarioActual());
|
||||
TempData["SuccessMessage"] = "Cierre sellado exitosamente.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// POST: /Diezmo/Reopen/{id} — Solo Administrador
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[HttpPost]
|
||||
[ValidateAntiForgeryToken]
|
||||
[Permission("Diezmo.Reopen")]
|
||||
public async Task<IActionResult> Reopen(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _cierreService.ReabrirCierreAsync(id, UsuarioActual());
|
||||
TempData["SuccessMessage"] = "Cierre reabierto.";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
TempData["ErrorMessage"] = ex.Message;
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Detail), new { id });
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// GET: /Diezmo/Recibo/{salidaId}
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
[Permission("Diezmo.Index")]
|
||||
public async Task<IActionResult> Recibo(long salidaId)
|
||||
{
|
||||
var numero = await _reciboService.GenerarNumeroReciboAsync(salidaId);
|
||||
var salida = await _reciboService.GetSalidaParaReciboAsync(salidaId);
|
||||
|
||||
if (salida == null) return NotFound();
|
||||
|
||||
ViewBag.NumeroRecibo = numero;
|
||||
ViewBag.Emisor = UsuarioActual();
|
||||
return View(salida);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// Helpers privados
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
private string UsuarioActual()
|
||||
=> User.FindFirst(ClaimTypes.Name)?.Value
|
||||
?? User.Identity?.Name
|
||||
?? "Sistema";
|
||||
|
||||
private static List<SelectListItem> GetAniosSelectList()
|
||||
{
|
||||
var anioActual = DateTime.Today.Year;
|
||||
return Enumerable.Range(anioActual - 3, 5)
|
||||
.Select(a => new SelectListItem(a.ToString(), a.ToString()))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private async Task<IActionResult> GetTotalesJsonAsync(long id)
|
||||
{
|
||||
var cierre = await _cierreService.GetCierreByIdAsync(id);
|
||||
if (cierre == null) return NotFound();
|
||||
|
||||
return Json(new {
|
||||
totalRecibido = cierre.TotalRecibido,
|
||||
totalCambio = cierre.TotalCambio,
|
||||
totalNeto = cierre.TotalNeto,
|
||||
totalSalidas = cierre.TotalSalidas,
|
||||
saldoFinal = cierre.SaldoFinal
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user