Feature: Add ReviewReceipts page for receipt-to-transaction mapping
New page shows unmapped receipts with scored transaction candidates, allowing manual mapping or bulk auto-map. Displays confidence scores and match quality indicators. Also shows recently mapped receipts for verification with unmap option. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,9 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h2>Receipts</h2>
|
<h2>Receipts</h2>
|
||||||
<div>
|
<div>
|
||||||
|
<a asp-page="/ReviewReceipts" class="btn btn-warning me-2">
|
||||||
|
Review Mappings
|
||||||
|
</a>
|
||||||
<button type="button" class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#uploadReceiptModal">
|
<button type="button" class="btn btn-primary me-2" data-bs-toggle="modal" data-bs-target="#uploadReceiptModal">
|
||||||
Upload Receipt
|
Upload Receipt
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
283
MoneyMap/Pages/ReviewReceipts.cshtml
Normal file
283
MoneyMap/Pages/ReviewReceipts.cshtml
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
@page
|
||||||
|
@model MoneyMap.Pages.ReviewReceiptsModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Review Receipts";
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2>Review Receipts</h2>
|
||||||
|
<div>
|
||||||
|
@if (Model.UnmappedReceipts.Any())
|
||||||
|
{
|
||||||
|
<form method="post" asp-page-handler="AutoMapAll" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-success me-2">
|
||||||
|
Auto-Map All Unmapped
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
<a asp-page="/Receipts" class="btn btn-outline-secondary">Back to Receipts</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Message))
|
||||||
|
{
|
||||||
|
<div class="alert @(Model.IsSuccess ? "alert-success" : "alert-warning") alert-dismissible fade show" role="alert">
|
||||||
|
@Model.Message
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Unmapped Receipts Section -->
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-header bg-warning text-dark">
|
||||||
|
<strong>Unmapped Receipts Needing Review</strong>
|
||||||
|
<span class="badge bg-dark ms-2">@Model.UnmappedReceipts.Count</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@if (Model.UnmappedReceipts.Any())
|
||||||
|
{
|
||||||
|
@foreach (var item in Model.UnmappedReceipts)
|
||||||
|
{
|
||||||
|
var receipt = item.Receipt;
|
||||||
|
var scoreClass = item.BestScore >= 0.85 ? "bg-success" :
|
||||||
|
item.BestScore >= 0.50 ? "bg-warning text-dark" : "bg-secondary";
|
||||||
|
<div class="border-bottom p-3">
|
||||||
|
<div class="row">
|
||||||
|
<!-- Receipt Info -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="d-flex align-items-start gap-2">
|
||||||
|
@if (receipt.ContentType.StartsWith("image/"))
|
||||||
|
{
|
||||||
|
<span class="fs-4">🖼️</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="fs-4">📄</span>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<div class="fw-bold">@receipt.FileName</div>
|
||||||
|
<div class="small text-muted">
|
||||||
|
Uploaded: @receipt.UploadedAtUtc.ToLocalTime().ToString("MMM d, yyyy h:mm tt")
|
||||||
|
</div>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(receipt.Merchant))
|
||||||
|
{
|
||||||
|
<div class="mt-1"><strong>Merchant:</strong> @receipt.Merchant</div>
|
||||||
|
}
|
||||||
|
@if (receipt.ReceiptDate.HasValue)
|
||||||
|
{
|
||||||
|
<div><strong>Date:</strong> @receipt.ReceiptDate.Value.ToString("yyyy-MM-dd")</div>
|
||||||
|
}
|
||||||
|
@if (receipt.DueDate.HasValue)
|
||||||
|
{
|
||||||
|
<div class="text-warning"><strong>Due:</strong> @receipt.DueDate.Value.ToString("yyyy-MM-dd")</div>
|
||||||
|
}
|
||||||
|
@if (receipt.Total.HasValue)
|
||||||
|
{
|
||||||
|
<div><strong>Total:</strong> @receipt.Total.Value.ToString("C")</div>
|
||||||
|
}
|
||||||
|
<div class="mt-2">
|
||||||
|
<a asp-page="/ViewReceipt" asp-route-id="@receipt.Id" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||||
|
View Receipt
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Candidate Transactions -->
|
||||||
|
<div class="col-md-8">
|
||||||
|
@if (item.ScoredCandidates.Any())
|
||||||
|
{
|
||||||
|
<div class="mb-2">
|
||||||
|
<span class="badge @scoreClass">Best Match: @(item.BestScore.ToString("P0"))</span>
|
||||||
|
@if (item.BestScore >= 0.85)
|
||||||
|
{
|
||||||
|
<span class="text-success small ms-2">High confidence - will auto-map</span>
|
||||||
|
}
|
||||||
|
else if (item.BestScore >= 0.50)
|
||||||
|
{
|
||||||
|
<span class="text-warning small ms-2">Medium confidence - LLM will review</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="text-secondary small ms-2">Low confidence - manual review needed</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm table-hover mb-2" style="font-size: 0.85rem;">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th style="width: 60px;">Score</th>
|
||||||
|
<th style="width: 100px;">Date</th>
|
||||||
|
<th class="text-end" style="width: 100px;">Amount</th>
|
||||||
|
<th>Name</th>
|
||||||
|
<th style="width: 120px;">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var candidate in item.ScoredCandidates)
|
||||||
|
{
|
||||||
|
var rowClass = candidate.Score >= 0.85 ? "table-success" :
|
||||||
|
candidate.Score >= 0.50 ? "table-warning" : "";
|
||||||
|
<tr class="@rowClass">
|
||||||
|
<td>
|
||||||
|
<span class="badge @(candidate.Score >= 0.85 ? "bg-success" : candidate.Score >= 0.50 ? "bg-warning text-dark" : "bg-secondary")">
|
||||||
|
@(candidate.Score.ToString("P0"))
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>@candidate.Transaction.Date.ToString("yyyy-MM-dd")</td>
|
||||||
|
<td class="text-end">@candidate.Transaction.Amount.ToString("C")</td>
|
||||||
|
<td>
|
||||||
|
<div>@candidate.Transaction.Name</div>
|
||||||
|
@if (candidate.Transaction.Merchant != null)
|
||||||
|
{
|
||||||
|
<small class="text-muted">@candidate.Transaction.Merchant.Name</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" asp-page-handler="MapToTransaction" class="d-inline">
|
||||||
|
<input type="hidden" name="receiptId" value="@receipt.Id" />
|
||||||
|
<input type="hidden" name="transactionId" value="@candidate.Transaction.Id" />
|
||||||
|
<button type="submit" class="btn btn-sm btn-success">Map</button>
|
||||||
|
</form>
|
||||||
|
<a asp-page="/EditTransaction" asp-route-id="@candidate.Transaction.Id"
|
||||||
|
class="btn btn-sm btn-outline-secondary" target="_blank">View</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="alert alert-secondary mb-2">
|
||||||
|
No matching transactions found within the date range.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<form method="post" asp-page-handler="AutoMap" class="d-inline">
|
||||||
|
<input type="hidden" name="receiptId" value="@receipt.Id" />
|
||||||
|
<button type="submit" class="btn btn-sm btn-primary">
|
||||||
|
Run Auto-Map
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
No unmapped receipts need review. All parsed receipts are mapped!
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Recently Mapped Section -->
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-success text-white">
|
||||||
|
<strong>Recently Mapped (Last 7 Days)</strong>
|
||||||
|
<span class="badge bg-light text-dark ms-2">@Model.RecentlyMappedReceipts.Count</span>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0">
|
||||||
|
@if (Model.RecentlyMappedReceipts.Any())
|
||||||
|
{
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover table-sm mb-0">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Receipt</th>
|
||||||
|
<th>Mapped To</th>
|
||||||
|
<th style="width: 100px;">Match</th>
|
||||||
|
<th style="width: 150px;">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach (var item in Model.RecentlyMappedReceipts)
|
||||||
|
{
|
||||||
|
var receipt = item.Receipt;
|
||||||
|
var transaction = receipt.Transaction;
|
||||||
|
|
||||||
|
// Calculate if amounts match
|
||||||
|
var amountMatch = receipt.Total.HasValue && transaction != null
|
||||||
|
? Math.Abs((double)(receipt.Total.Value - Math.Abs(transaction.Amount)) / (double)receipt.Total.Value) <= 0.02
|
||||||
|
: false;
|
||||||
|
var dateMatch = receipt.ReceiptDate.HasValue && transaction != null
|
||||||
|
? Math.Abs((transaction.Date - receipt.ReceiptDate.Value).TotalDays) <= 3
|
||||||
|
: false;
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
@if (receipt.ContentType.StartsWith("image/"))
|
||||||
|
{
|
||||||
|
<span>🖼️</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span>📄</span>
|
||||||
|
}
|
||||||
|
<div>
|
||||||
|
<div>@receipt.FileName</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
@(receipt.Merchant ?? "Unknown") |
|
||||||
|
@(receipt.ReceiptDate?.ToString("yyyy-MM-dd") ?? "No date") |
|
||||||
|
@(receipt.Total?.ToString("C") ?? "No amount")
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (transaction != null)
|
||||||
|
{
|
||||||
|
<div>@transaction.Name</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
@transaction.Date.ToString("yyyy-MM-dd") |
|
||||||
|
@transaction.Amount.ToString("C")
|
||||||
|
@if (transaction.Merchant != null)
|
||||||
|
{
|
||||||
|
<span>| @transaction.Merchant.Name</span>
|
||||||
|
}
|
||||||
|
</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (amountMatch && dateMatch)
|
||||||
|
{
|
||||||
|
<span class="badge bg-success">Good</span>
|
||||||
|
}
|
||||||
|
else if (amountMatch || dateMatch)
|
||||||
|
{
|
||||||
|
<span class="badge bg-warning text-dark">Partial</span>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<span class="badge bg-danger">Check</span>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a asp-page="/ViewReceipt" asp-route-id="@receipt.Id" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||||
|
View
|
||||||
|
</a>
|
||||||
|
<form method="post" asp-page-handler="Unmap" class="d-inline"
|
||||||
|
onsubmit="return confirm('Unmap this receipt? You can re-run auto-mapping after.');">
|
||||||
|
<input type="hidden" name="receiptId" value="@receipt.Id" />
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-warning">Unmap</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="p-4 text-center text-muted">
|
||||||
|
No receipts mapped in the last 7 days.
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
195
MoneyMap/Pages/ReviewReceipts.cshtml.cs
Normal file
195
MoneyMap/Pages/ReviewReceipts.cshtml.cs
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using MoneyMap.Data;
|
||||||
|
using MoneyMap.Models;
|
||||||
|
using MoneyMap.Services;
|
||||||
|
|
||||||
|
namespace MoneyMap.Pages
|
||||||
|
{
|
||||||
|
public class ReviewReceiptsModel : PageModel
|
||||||
|
{
|
||||||
|
private readonly MoneyMapContext _db;
|
||||||
|
private readonly IReceiptManager _receiptManager;
|
||||||
|
private readonly IReceiptAutoMapper _autoMapper;
|
||||||
|
|
||||||
|
public ReviewReceiptsModel(
|
||||||
|
MoneyMapContext db,
|
||||||
|
IReceiptManager receiptManager,
|
||||||
|
IReceiptAutoMapper autoMapper)
|
||||||
|
{
|
||||||
|
_db = db;
|
||||||
|
_receiptManager = receiptManager;
|
||||||
|
_autoMapper = autoMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReceiptReviewItem> UnmappedReceipts { get; set; } = new();
|
||||||
|
public List<ReceiptReviewItem> RecentlyMappedReceipts { get; set; } = new();
|
||||||
|
|
||||||
|
[TempData]
|
||||||
|
public string? Message { get; set; }
|
||||||
|
|
||||||
|
[TempData]
|
||||||
|
public bool IsSuccess { get; set; }
|
||||||
|
|
||||||
|
public async Task OnGetAsync()
|
||||||
|
{
|
||||||
|
await LoadReceiptsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAutoMapAsync(long receiptId)
|
||||||
|
{
|
||||||
|
var result = await _autoMapper.AutoMapReceiptAsync(receiptId);
|
||||||
|
|
||||||
|
switch (result.Status)
|
||||||
|
{
|
||||||
|
case AutoMapStatus.Success:
|
||||||
|
Message = $"Receipt auto-mapped to transaction #{result.TransactionId}.";
|
||||||
|
IsSuccess = true;
|
||||||
|
break;
|
||||||
|
case AutoMapStatus.AlreadyMapped:
|
||||||
|
Message = "Receipt is already mapped.";
|
||||||
|
IsSuccess = false;
|
||||||
|
break;
|
||||||
|
case AutoMapStatus.MultipleMatches:
|
||||||
|
Message = $"Found {result.MultipleMatches.Count} potential matches. Please select manually.";
|
||||||
|
IsSuccess = false;
|
||||||
|
break;
|
||||||
|
case AutoMapStatus.NoMatch:
|
||||||
|
Message = "No matching transaction found.";
|
||||||
|
IsSuccess = false;
|
||||||
|
break;
|
||||||
|
case AutoMapStatus.NotParsed:
|
||||||
|
Message = "Receipt has not been parsed yet. Parse it first.";
|
||||||
|
IsSuccess = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Message = result.Message ?? "Auto-mapping failed.";
|
||||||
|
IsSuccess = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostMapToTransactionAsync(long receiptId, long transactionId)
|
||||||
|
{
|
||||||
|
if (transactionId <= 0)
|
||||||
|
{
|
||||||
|
Message = "Please select a transaction.";
|
||||||
|
IsSuccess = false;
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await _receiptManager.MapReceiptToTransactionAsync(receiptId, transactionId);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Message = $"Receipt mapped to transaction #{transactionId}.";
|
||||||
|
IsSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Message = "Failed to map receipt.";
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostUnmapAsync(long receiptId)
|
||||||
|
{
|
||||||
|
var success = await _receiptManager.UnmapReceiptAsync(receiptId);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
Message = "Receipt unmapped. You can now re-run auto-mapping.";
|
||||||
|
IsSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Message = "Failed to unmap receipt.";
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> OnPostAutoMapAllAsync()
|
||||||
|
{
|
||||||
|
var result = await _autoMapper.AutoMapUnmappedReceiptsAsync();
|
||||||
|
|
||||||
|
if (result.MappedCount > 0)
|
||||||
|
{
|
||||||
|
Message = $"Auto-mapped {result.MappedCount} receipt(s). " +
|
||||||
|
$"{result.NoMatchCount} had no match. " +
|
||||||
|
$"{result.MultipleMatchesCount} need manual review.";
|
||||||
|
IsSuccess = true;
|
||||||
|
}
|
||||||
|
else if (result.TotalProcessed == 0)
|
||||||
|
{
|
||||||
|
Message = "No unmapped receipts to process.";
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Message = $"Unable to auto-map any receipts. {result.NoMatchCount} had no match. " +
|
||||||
|
$"{result.MultipleMatchesCount} need manual review.";
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RedirectToPage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadReceiptsAsync()
|
||||||
|
{
|
||||||
|
// Get unmapped receipts with their scored candidates
|
||||||
|
var unmapped = await _db.Receipts
|
||||||
|
.Where(r => r.TransactionId == null)
|
||||||
|
.Where(r => r.Merchant != null || r.ReceiptDate != null || r.Total != null)
|
||||||
|
.OrderByDescending(r => r.UploadedAtUtc)
|
||||||
|
.Take(50)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var receipt in unmapped)
|
||||||
|
{
|
||||||
|
var candidates = await _autoMapper.GetScoredCandidatesAsync(receipt.Id);
|
||||||
|
|
||||||
|
UnmappedReceipts.Add(new ReceiptReviewItem
|
||||||
|
{
|
||||||
|
Receipt = receipt,
|
||||||
|
ScoredCandidates = candidates.Take(5).ToList(),
|
||||||
|
BestScore = candidates.FirstOrDefault()?.Score ?? 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recently mapped receipts (last 7 days) for verification
|
||||||
|
var sevenDaysAgo = DateTime.UtcNow.AddDays(-7);
|
||||||
|
var recentlyMapped = await _db.Receipts
|
||||||
|
.Include(r => r.Transaction)
|
||||||
|
.ThenInclude(t => t!.Merchant)
|
||||||
|
.Where(r => r.TransactionId != null)
|
||||||
|
.Where(r => r.UploadedAtUtc >= sevenDaysAgo)
|
||||||
|
.OrderByDescending(r => r.UploadedAtUtc)
|
||||||
|
.Take(20)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var receipt in recentlyMapped)
|
||||||
|
{
|
||||||
|
RecentlyMappedReceipts.Add(new ReceiptReviewItem
|
||||||
|
{
|
||||||
|
Receipt = receipt,
|
||||||
|
ScoredCandidates = new List<ScoredCandidate>(),
|
||||||
|
BestScore = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ReceiptReviewItem
|
||||||
|
{
|
||||||
|
public required Receipt Receipt { get; set; }
|
||||||
|
public List<ScoredCandidate> ScoredCandidates { get; set; } = new();
|
||||||
|
public double BestScore { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user