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.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.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(); } }