using foodmarket.Application.Catalog;
using foodmarket.Application.Common;
using foodmarket.Application.Common.Tenancy;
using foodmarket.Domain.Catalog;
using foodmarket.Infrastructure.Persistence;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace foodmarket.Api.Controllers.Catalog;
/// Phase5c: единицы измерения — глобальный справочник (управляется
/// SuperAdmin'ом). Орга подключает нужные через junction org_units_of_measure.
/// Этот контроллер: read-only список + toggle включения/выключения.
/// CRUD — на /api/super-admin/units-of-measure.
[ApiController]
[Authorize]
[Route("api/catalog/units-of-measure")]
public class UnitsOfMeasureController : ControllerBase
{
private readonly AppDbContext _db;
private readonly ITenantContext _tenant;
public UnitsOfMeasureController(AppDbContext db, ITenantContext tenant)
{
_db = db;
_tenant = tenant;
}
/// Список единиц для текущей орги: только включённые active
/// globals. Для SuperAdmin без override — все active globals (чтобы UI
/// мог показывать справочник в платформенном контексте).
[HttpGet]
public async Task>> List(
[FromQuery] PagedRequest req, CancellationToken ct)
{
var orgId = _tenant.OrganizationId;
var isSuperAdminPlatform = _tenant.IsSuperAdmin && !_tenant.IsTenantOverride;
// Globals — read через IgnoreQueryFilters: фильтр иначе пропустит
// нашу же null-OrganizationId-запись только потому, что мы её специально
// ищем; явная фильтрация по OrganizationId IS NULL понятнее.
var q = _db.UnitsOfMeasure
.IgnoreQueryFilters()
.AsNoTracking()
.Where(u => u.OrganizationId == null && u.IsActive);
if (!isSuperAdminPlatform && orgId.HasValue)
{
// Org Admin / Storekeeper: только включённые в его орге.
q = q.Where(u => _db.OrgUnitsOfMeasure
.IgnoreQueryFilters()
.Any(j => j.OrganizationId == orgId.Value && j.UnitOfMeasureId == u.Id));
}
if (!string.IsNullOrWhiteSpace(req.Search))
{
var s = req.Search.Trim().ToLower();
q = q.Where(u => u.Name.ToLower().Contains(s) || u.Code.ToLower().Contains(s));
}
var total = await q.CountAsync(ct);
q = (req.Sort, req.Desc) switch
{
("code", false) => q.OrderBy(u => u.Code),
("code", true) => q.OrderByDescending(u => u.Code),
("name", true) => q.OrderByDescending(u => u.Name),
_ => q.OrderBy(u => u.Name),
};
var items = await q
.Skip(req.Skip).Take(req.Take)
.Select(u => new UnitOfMeasureDto(
u.Id, u.Code, u.Name, u.Description, u.OrganizationId, u.IsActive, true))
.ToListAsync(ct);
return new PagedResult { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
}
[HttpGet("{id:guid}")]
public async Task> Get(Guid id, CancellationToken ct)
{
var u = await _db.UnitsOfMeasure
.IgnoreQueryFilters()
.AsNoTracking()
.FirstOrDefaultAsync(x => x.Id == id && x.OrganizationId == null, ct);
if (u is null) return NotFound();
var orgId = _tenant.OrganizationId;
var enabled = !orgId.HasValue || await _db.OrgUnitsOfMeasure
.IgnoreQueryFilters()
.AnyAsync(j => j.OrganizationId == orgId.Value && j.UnitOfMeasureId == id, ct);
return new UnitOfMeasureDto(u.Id, u.Code, u.Name, u.Description, u.OrganizationId, u.IsActive, enabled);
}
/// Включить global для текущей орги. Идемпотентно: повторный
/// вызов отдаёт 204 и не плодит дубликатов junction.
[HttpPost("{id:guid}/enable"), Authorize(Roles = "Admin,SuperAdmin")]
public async Task Enable(Guid id, CancellationToken ct)
{
var orgId = _tenant.OrganizationId;
if (!orgId.HasValue) return BadRequest(new { error = "Tenant context required." });
var unit = await _db.UnitsOfMeasure
.IgnoreQueryFilters()
.FirstOrDefaultAsync(u => u.Id == id && u.OrganizationId == null && u.IsActive, ct);
if (unit is null) return NotFound();
var existing = await _db.OrgUnitsOfMeasure
.IgnoreQueryFilters()
.AnyAsync(j => j.OrganizationId == orgId.Value && j.UnitOfMeasureId == id, ct);
if (!existing)
{
_db.OrgUnitsOfMeasure.Add(new OrgUnitOfMeasure { OrganizationId = orgId.Value, UnitOfMeasureId = id });
await _db.SaveChangesAsync(ct);
}
return NoContent();
}
/// Отключить global для текущей орги. Если на эту единицу
/// ссылаются продукты орги — 409 со списком названий, чтобы админ
/// перепривязал их сначала.
[HttpDelete("{id:guid}/enable"), Authorize(Roles = "Admin,SuperAdmin")]
public async Task Disable(Guid id, CancellationToken ct)
{
var orgId = _tenant.OrganizationId;
if (!orgId.HasValue) return BadRequest(new { error = "Tenant context required." });
var productNames = await _db.Products
.Where(p => p.UnitOfMeasureId == id)
.OrderBy(p => p.Name)
.Select(p => p.Name)
.Take(10)
.ToListAsync(ct);
if (productNames.Count > 0)
{
return Conflict(new
{
error = "Единица используется в товарах. Перепривяжите товары на другую единицу прежде чем отключать.",
products = productNames,
});
}
var link = await _db.OrgUnitsOfMeasure
.IgnoreQueryFilters()
.FirstOrDefaultAsync(j => j.OrganizationId == orgId.Value && j.UnitOfMeasureId == id, ct);
if (link is not null)
{
_db.OrgUnitsOfMeasure.Remove(link);
await _db.SaveChangesAsync(ct);
}
return NoContent();
}
}