feature: Barcode/QR Cover Sheet System

This commit is contained in:
voltsrage
2026-06-27 21:50:32 +08:00
parent 5db60f46eb
commit 756cff332c
43 changed files with 8203 additions and 21 deletions
@@ -0,0 +1,138 @@
using Microsoft.EntityFrameworkCore;
public class CoverSheetService : ICoverSheetService
{
private readonly AppDbContext _db;
private readonly ILogger<CoverSheetService> _logger;
public CoverSheetService(AppDbContext db, ILogger<CoverSheetService> logger)
{
_db = db;
_logger = logger;
}
public async Task<List<CoverSheet>> GenerateAsync(
GenerateCoverSheetsRequest request, Guid actorUserId)
{
var count = Math.Clamp(request.Count, 1, 100);
var batchType = BatchTypeExtensions.FromDbString(request.BatchType.ToUpperInvariant());
var track = string.IsNullOrEmpty(request.Track)
? BatchTrack.Backfill
: BatchTrackExtensions.FromDbString(request.Track.ToUpperInvariant());
if (request.PatientId.HasValue)
{
var patientExists = await _db.Patients.AnyAsync(p => p.Id == request.PatientId.Value);
if (!patientExists)
throw new NotFoundException("Patient not found.", "PATIENT_NOT_FOUND");
}
if (request.AssignToUserId.HasValue)
{
var user = await _db.Users.FindAsync(request.AssignToUserId.Value);
if (user is null || !user.IsActive)
throw new NotFoundException("User not found or inactive.", "USER_NOT_FOUND");
}
var sheets = new List<CoverSheet>(count);
for (var i = 0; i < count; i++)
{
var code = $"VCR-CS-{Guid.NewGuid().ToString("N")[..8].ToUpperInvariant()}";
sheets.Add(new CoverSheet
{
Id = Guid.NewGuid(),
Code = code,
BatchType = batchType,
Track = track,
PatientId = request.PatientId,
AssignToUserId = request.AssignToUserId,
GeneratedByUserId = actorUserId,
IsUsed = false,
CreatedAt = DateTimeOffset.UtcNow,
});
}
_db.CoverSheets.AddRange(sheets);
await _db.SaveChangesAsync();
_logger.LogInformation(
"Generated {Count} cover sheets for batch type {BatchType}, track {Track}",
count, batchType.ToDbString(), track.ToDbString());
return sheets;
}
public async Task<CoverSheet?> LookupByCodeAsync(string code)
{
return await _db.CoverSheets
.Include(c => c.Patient)
.Include(c => c.AssignToUser)
.FirstOrDefaultAsync(c => c.Code == code.ToUpperInvariant().Trim());
}
public async Task<CoverSheet?> LookupByIdAsync(Guid id)
{
return await _db.CoverSheets
.Include(c => c.Patient)
.Include(c => c.AssignToUser)
.FirstOrDefaultAsync(c => c.Id == id);
}
public async Task<List<CoverSheet>> GetByIdsAsync(IReadOnlyList<Guid> ids)
{
if (ids.Count == 0)
return new List<CoverSheet>();
var idSet = ids.Distinct().ToList();
var sheets = await _db.CoverSheets
.AsNoTracking()
.Include(c => c.Patient)
.Include(c => c.AssignToUser)
.Where(c => idSet.Contains(c.Id))
.ToListAsync();
var byId = sheets.ToDictionary(c => c.Id);
return idSet
.Where(byId.ContainsKey)
.Select(id => byId[id])
.ToList();
}
public async Task RedeemAsync(Guid coverSheetId, Guid batchId)
{
var sheet = await _db.CoverSheets.FindAsync(coverSheetId);
if (sheet is null)
throw new NotFoundException("Cover sheet not found.", "COVER_SHEET_NOT_FOUND");
if (sheet.IsUsed)
throw new ConflictException(
$"Cover sheet {sheet.Code} has already been used for batch {sheet.BatchId}.",
"COVER_SHEET_ALREADY_USED");
sheet.IsUsed = true;
sheet.BatchId = batchId;
sheet.UsedAt = DateTimeOffset.UtcNow;
await _db.SaveChangesAsync();
}
public async Task<List<CoverSheet>> ListAsync(
bool? isUsed, Guid? patientId, int page, int pageSize)
{
var query = _db.CoverSheets
.AsNoTracking()
.Include(c => c.Patient)
.Include(c => c.AssignToUser)
.AsQueryable();
if (isUsed.HasValue)
query = query.Where(c => c.IsUsed == isUsed.Value);
if (patientId.HasValue)
query = query.Where(c => c.PatientId == patientId.Value);
return await query
.OrderByDescending(c => c.CreatedAt)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
}
}