ui fixers in 10/16

This commit is contained in:
2026-10-16 14:34:28 +05:30
parent 379ad204bd
commit e289b8430f
47 changed files with 1904 additions and 1046 deletions
@@ -8,6 +8,8 @@ namespace ERPCore.Services.Interfaces;
public interface ISalesInvoiceService
{
Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default);
/// <summary>Count and amount totals over every invoice matching the same filters as <see cref="ListAsync"/>.</summary>
Task<SalesInvoiceListTotalsDto> TotalsAsync(string? q, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default);
Task<ETagged<SalesInvoiceDto>?> GetAsync(int salesInvoiceId, CancellationToken ct = default);
Task<SalesInvoicePostingCheckDto> CheckPostingAsync(int salesInvoiceId, CancellationToken ct = default);
Task<ETagged<SalesInvoiceDto>> CreateAsync(CreateSalesInvoiceRequest request, CancellationToken ct = default);
+27 -3
View File
@@ -41,13 +41,37 @@ public sealed class SalesConfigService : ISalesConfigService
public async Task<ETagged<SalesConfigDto>> GetAsync(CancellationToken ct = default)
{
var config = await _config.Query().AsNoTracking().Include(c => c.CashCustomer).Include(c => c.DefaultWarehouse)
.FirstOrDefaultAsync(c => c.ConfigId == SalesConfig.SingletonId, ct)
?? throw new NotFoundException("Sales configuration has not been seeded.");
var config = await LoadAsync(ct);
// "Allow cash customer" can be on with no customer linked — the flag was set outside
// UpdateAsync (seed, SQL, an older build). Every sales screen then hides the cash customer
// because there's no id to offer, so link (or create) CUST-CASH here instead.
if (config.AllowCashCustomer && config.CashCustomerId is null)
{
var tracked = await _config.GetByIdAsync(SalesConfig.SingletonId, ct);
if (tracked is not null && tracked.CashCustomerId is null)
{
tracked.CashCustomerId = await ResolveCashCustomerIdAsync(ct);
try
{
await _uow.SaveChangesAsync(ct);
}
catch (DbUpdateConcurrencyException)
{
// A concurrent save got there first — whatever it stored is re-read below.
}
}
config = await LoadAsync(ct);
}
return new ETagged<SalesConfigDto>(Map(config), config.RowVersion);
}
private async Task<SalesConfig> LoadAsync(CancellationToken ct) =>
await _config.Query().AsNoTracking().Include(c => c.CashCustomer).Include(c => c.DefaultWarehouse)
.FirstOrDefaultAsync(c => c.ConfigId == SalesConfig.SingletonId, ct)
?? throw new NotFoundException("Sales configuration has not been seeded.");
public async Task<ETagged<SalesConfigDto>> UpdateAsync(
UpdateSalesConfigRequest request, uint expectedRowVersion, CancellationToken ct = default)
{
@@ -53,10 +53,44 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
public async Task<PagedResponse<SalesInvoiceSummaryDto>> ListAsync(PageQuery query, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default)
{
IQueryable<SalesInvoice> q = _invoices.Query().AsNoTracking().Include(x => x.Lines);
if (!string.IsNullOrWhiteSpace(query.Q))
var q = Filter(_invoices.Query().AsNoTracking().Include(x => x.Lines), query.Q, status, customerId, warehouseId, branchId, from, to, createdBy, paid);
var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(x => x.SalesInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
return PagedResponse<SalesInvoiceSummaryDto>.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total);
}
public async Task<SalesInvoiceListTotalsDto> TotalsAsync(string? q, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from = null, DateOnly? to = null, int? createdBy = null, bool? paid = null, CancellationToken ct = default)
{
var filtered = Filter(_invoices.Query().AsNoTracking(), q, status, customerId, warehouseId, branchId, from, to, createdBy, paid);
// Overdue = due date (InvoiceDate) before today with money still owed — the list's red "Due" rule.
var today = DateTime.UtcNow.Date;
var totals = await filtered
.GroupBy(_ => 1)
.Select(g => new
{
Count = g.Count(),
Net = g.Sum(x => x.GrandTotal),
Paid = g.Sum(x => x.PaidAmount),
Balance = g.Sum(x => x.BalanceAmount > 0 ? x.BalanceAmount : 0),
Unpaid = g.Count(x => x.BalanceAmount > 0),
Overdue = g.Count(x => x.BalanceAmount > 0 && x.InvoiceDate < today),
})
.FirstOrDefaultAsync(ct);
return totals is null
? new SalesInvoiceListTotalsDto(0, 0m, 0m, 0m, 0, 0)
: new SalesInvoiceListTotalsDto(totals.Count, totals.Net, totals.Paid, totals.Balance, totals.Unpaid, totals.Overdue);
}
/// <summary>The list's filters, shared with <see cref="TotalsAsync"/> so the totals always cover exactly the listed rows.</summary>
private static IQueryable<SalesInvoice> Filter(IQueryable<SalesInvoice> q, string? search, SalesInvoiceStatus? status, int? customerId, int? warehouseId, int? branchId, DateOnly? from, DateOnly? to, int? createdBy, bool? paid)
{
if (!string.IsNullOrWhiteSpace(search))
{
var term = query.Q.Trim();
var term = search.Trim();
q = q.Where(x => EF.Functions.Like(x.InvoiceNo, $"%{term}%") || EF.Functions.Like(x.CustomerSnapshotName, $"%{term}%"));
}
if (status is not null) q = q.Where(x => x.Status == status);
@@ -72,11 +106,7 @@ public sealed class SalesInvoiceService : ISalesInvoiceService
// paid=true: nothing left to pay; paid=false: a balance is still due.
if (paid == true) q = q.Where(x => x.BalanceAmount <= 0);
if (paid == false) q = q.Where(x => x.BalanceAmount > 0);
var total = await q.CountAsync(ct);
var rows = await q.OrderByDescending(x => x.SalesInvoiceId).Skip(query.Skip).Take(query.PageSize).ToListAsync(ct);
return PagedResponse<SalesInvoiceSummaryDto>.Create(rows.Select(MapSummary).ToList(), query.Page, query.PageSize, total);
return q;
}
public async Task<ETagged<SalesInvoiceDto>?> GetAsync(int salesInvoiceId, CancellationToken ct = default)