diff --git a/backend/Arah.Api.Bff/Journeys/BffJourneyRegistry.cs b/backend/Arah.Api.Bff/Journeys/BffJourneyRegistry.cs index 0883aec7..6bcf5f1e 100644 --- a/backend/Arah.Api.Bff/Journeys/BffJourneyRegistry.cs +++ b/backend/Arah.Api.Bff/Journeys/BffJourneyRegistry.cs @@ -202,8 +202,8 @@ public static string GetApiPathBase(string journeyName) { new("entities", "GET", "Entidades do mapa. Query: territoryId, categoryId, bounds, etc."), new("entities/paged", "GET", "Entidades paginadas. Query: territoryId, pageNumber, pageSize."), - new("pins", "GET", "Pins do mapa. Query: territoryId, bounds."), - new("pins/paged", "GET", "Pins paginados."), + new("pins", "GET", "Pins do mapa. Query: territoryId, types, assetId, assetTypes, assetSubtypes."), + new("pins/paged", "GET", "Pins paginados. Query: territoryId, types, assetId, assetTypes, assetSubtypes, pageNumber, pageSize."), new("entities", "POST", "Cria entidade no mapa. Body conforme API."), new("entities/{entityId}/confirmations", "POST", "Confirma entidade."), new("entities/{entityId}/relations", "POST", "Cria relação entre entidades.") @@ -364,8 +364,8 @@ public static string GetApiPathBase(string journeyName) { new("entities", "GET", "Entidades do mapa."), new("entities/paged", "GET", "Entidades paginadas."), - new("pins", "GET", "Pins do mapa."), - new("pins/paged", "GET", "Pins paginados.") + new("pins", "GET", "Pins do mapa. Query: territoryId, types, assetId, assetTypes, assetSubtypes."), + new("pins/paged", "GET", "Pins paginados. Query: territoryId, types, assetId, assetTypes, assetSubtypes, pageNumber, pageSize.") }, [Assets] = new List { diff --git a/backend/Arah.Api/Contracts/Map/MapPinResponse.cs b/backend/Arah.Api/Contracts/Map/MapPinResponse.cs index 2145f8c7..b412ddc8 100644 --- a/backend/Arah.Api/Contracts/Map/MapPinResponse.cs +++ b/backend/Arah.Api/Contracts/Map/MapPinResponse.cs @@ -12,4 +12,6 @@ public sealed record MapPinResponse( Guid? MediaId, Guid? EventId, Guid? EntityId, - string? Status); + string? Status, + string? AssetType = null, + string? AssetSubtype = null); diff --git a/backend/Arah.Api/Controllers/Map/MapController.cs b/backend/Arah.Api/Controllers/Map/MapController.cs index 3e096ad5..3c8b3ae2 100644 --- a/backend/Arah.Api/Controllers/Map/MapController.cs +++ b/backend/Arah.Api/Controllers/Map/MapController.cs @@ -205,6 +205,7 @@ public async Task>> GetPins( [FromQuery] string? types, [FromQuery] Guid? assetId, [FromQuery] string? assetTypes, + [FromQuery] string? assetSubtypes, CancellationToken cancellationToken) { var resolvedTerritoryId = await ResolveTerritoryIdAsync(territoryId, cancellationToken); @@ -226,7 +227,8 @@ public async Task>> GetPins( filters, assetId, ParseCsv(assetTypes), - cancellationToken); + cancellationToken, + ParseCsv(assetSubtypes)); return Ok(pins.Select(ToResponse)); } @@ -243,6 +245,7 @@ public async Task>> GetPinsPaged( [FromQuery] string? types, [FromQuery] Guid? assetId, [FromQuery] string? assetTypes, + [FromQuery] string? assetSubtypes, CancellationToken cancellationToken, [FromQuery] int pageNumber = 1, [FromQuery] int pageSize = 20) @@ -269,7 +272,8 @@ public async Task>> GetPinsPaged( assetId, ParseCsv(assetTypes), pagination, - cancellationToken); + cancellationToken, + ParseCsv(assetSubtypes)); var response = new PagedResponse( pagedResult.Items.Select(ToResponse).ToList(), @@ -294,7 +298,9 @@ private static MapPinResponse ToResponse(MapPin pin) => pin.MediaId, pin.EventId, pin.EntityId, - pin.Status); + pin.Status, + pin.AssetType, + pin.AssetSubtype); private static IReadOnlyList? ParseCsv(string? raw) { diff --git a/backend/Arah.Application/Models/MapPin.cs b/backend/Arah.Application/Models/MapPin.cs index b8f568b5..dc8b7719 100644 --- a/backend/Arah.Application/Models/MapPin.cs +++ b/backend/Arah.Application/Models/MapPin.cs @@ -13,4 +13,6 @@ public sealed record MapPin( Guid? MediaId, Guid? EventId, Guid? EntityId, - string? Status); + string? Status, + string? AssetType = null, + string? AssetSubtype = null); diff --git a/backend/Arah.Application/Services/Map/MapPinsService.cs b/backend/Arah.Application/Services/Map/MapPinsService.cs index 4936c082..f85f89a8 100644 --- a/backend/Arah.Application/Services/Map/MapPinsService.cs +++ b/backend/Arah.Application/Services/Map/MapPinsService.cs @@ -46,7 +46,8 @@ public async Task> ListPinsAsync( MapPinFilters filters, Guid? assetId, IReadOnlyList? assetTypes, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyList? assetSubtypes = null) { var pins = new List(); @@ -63,13 +64,16 @@ public async Task> ListPinsAsync( if (filters.Assets) { var assetTypeList = assetId is null ? assetTypes : null; + var assetSubtypeList = assetId is null ? assetSubtypes : null; var assets = await _assetRepository.ListAsync( territoryId, assetId, - assetTypeList, + types: null, AssetStatus.Active, null, - cancellationToken); + cancellationToken, + subtypes: assetSubtypeList, + typesOrSubtypes: assetTypeList); pins.AddRange(await BuildAssetPinsAsync(assets, cancellationToken)); } @@ -110,7 +114,8 @@ public async Task> ListPinsPagedAsync( Guid? assetId, IReadOnlyList? assetTypes, PaginationParameters pagination, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyList? assetSubtypes = null) { // Cada tipo usa query paged com a mesma paginação da request (contrato pré-refactor). // Depois: merge → order by PinType → Skip/Take final. Total = tamanho do merge, não dump completo. @@ -130,15 +135,18 @@ public async Task> ListPinsPagedAsync( if (filters.Assets) { var assetTypeList = assetId is null ? assetTypes : null; + var assetSubtypeList = assetId is null ? assetSubtypes : null; var assets = await _assetRepository.ListPagedAsync( territoryId, assetId, - assetTypeList, + types: null, AssetStatus.Active, null, pagination.Skip, pagination.Take, - cancellationToken); + cancellationToken, + subtypes: assetSubtypeList, + typesOrSubtypes: assetTypeList); pins.AddRange(await BuildAssetPinsAsync(assets, cancellationToken)); } @@ -218,7 +226,9 @@ private async Task> BuildAssetPinsAsync( null, null, null, - asset.Status.ToString().ToUpperInvariant()); + asset.Status.ToString().ToUpperInvariant(), + asset.Type, + asset.Subtype); }).ToList(); } diff --git a/backend/Arah.Infrastructure/InMemory/InMemoryAssetRepository.cs b/backend/Arah.Infrastructure/InMemory/InMemoryAssetRepository.cs index 43f21310..30ae28a2 100644 --- a/backend/Arah.Infrastructure/InMemory/InMemoryAssetRepository.cs +++ b/backend/Arah.Infrastructure/InMemory/InMemoryAssetRepository.cs @@ -18,34 +18,18 @@ public Task> ListAsync( IReadOnlyCollection? types, AssetStatus? status, string? search, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null) { - var query = _dataStore.TerritoryAssets - .Where(asset => asset.TerritoryId == territoryId) - .AsEnumerable(); - - if (assetId is not null) - { - query = query.Where(asset => asset.Id == assetId.Value); - } - - if (types is not null && types.Count > 0) - { - var normalized = types.Select(type => type.Trim().ToLowerInvariant()).ToHashSet(); - query = query.Where(asset => normalized.Contains(asset.Type.ToLowerInvariant())); - } - - if (status is not null) - { - query = query.Where(asset => asset.Status == status); - } - - if (!string.IsNullOrWhiteSpace(search)) - { - query = query.Where(asset => - asset.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || - (asset.Description is not null && asset.Description.Contains(search, StringComparison.OrdinalIgnoreCase))); - } + var query = ApplyFilters( + _dataStore.TerritoryAssets.Where(asset => asset.TerritoryId == territoryId), + assetId, + types, + status, + search, + subtypes, + typesOrSubtypes); return Task.FromResult>(query.ToList()); } @@ -94,34 +78,18 @@ public Task> ListPagedAsync( string? search, int skip, int take, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null) { - var query = _dataStore.TerritoryAssets - .Where(asset => asset.TerritoryId == territoryId) - .AsEnumerable(); - - if (assetId is not null) - { - query = query.Where(asset => asset.Id == assetId.Value); - } - - if (types is not null && types.Count > 0) - { - var normalized = types.Select(type => type.Trim().ToLowerInvariant()).ToHashSet(); - query = query.Where(asset => normalized.Contains(asset.Type.ToLowerInvariant())); - } - - if (status is not null) - { - query = query.Where(asset => asset.Status == status); - } - - if (!string.IsNullOrWhiteSpace(search)) - { - query = query.Where(asset => - asset.Name.Contains(search, StringComparison.OrdinalIgnoreCase) || - (asset.Description is not null && asset.Description.Contains(search, StringComparison.OrdinalIgnoreCase))); - } + var query = ApplyFilters( + _dataStore.TerritoryAssets.Where(asset => asset.TerritoryId == territoryId), + assetId, + types, + status, + search, + subtypes, + typesOrSubtypes); var result = query .OrderByDescending(asset => asset.CreatedAtUtc) @@ -138,21 +106,53 @@ public Task CountAsync( IReadOnlyCollection? types, AssetStatus? status, string? search, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null) { - var query = _dataStore.TerritoryAssets - .Where(asset => asset.TerritoryId == territoryId) - .AsEnumerable(); + var query = ApplyFilters( + _dataStore.TerritoryAssets.Where(asset => asset.TerritoryId == territoryId), + assetId, + types, + status, + search, + subtypes, + typesOrSubtypes); + + const int maxInt32 = int.MaxValue; + var count = query.Count(); + return Task.FromResult(count > maxInt32 ? maxInt32 : count); + } + + private static IEnumerable ApplyFilters( + IEnumerable query, + Guid? assetId, + IReadOnlyCollection? types, + AssetStatus? status, + string? search, + IReadOnlyCollection? subtypes, + IReadOnlyCollection? typesOrSubtypes) + { + var normalizedTypes = TerritoryAssetTypeMatch.NormalizeFilter(types); + var normalizedSubtypes = TerritoryAssetTypeMatch.NormalizeFilter(subtypes); + var normalizedTypesOrSubtypes = TerritoryAssetTypeMatch.NormalizeFilter(typesOrSubtypes); if (assetId is not null) { query = query.Where(asset => asset.Id == assetId.Value); } - if (types is not null && types.Count > 0) + if (normalizedTypes is not null + || normalizedSubtypes is not null + || normalizedTypesOrSubtypes is not null) { - var normalized = types.Select(type => type.Trim().ToLowerInvariant()).ToHashSet(); - query = query.Where(asset => normalized.Contains(asset.Type.ToLowerInvariant())); + query = query.Where(asset => + TerritoryAssetTypeMatch.Matches( + asset.Type, + asset.Subtype, + normalizedTypes, + normalizedSubtypes, + normalizedTypesOrSubtypes)); } if (status is not null) @@ -167,8 +167,6 @@ public Task CountAsync( (asset.Description is not null && asset.Description.Contains(search, StringComparison.OrdinalIgnoreCase))); } - const int maxInt32 = int.MaxValue; - var count = query.Count(); - return Task.FromResult(count > maxInt32 ? maxInt32 : count); + return query; } } diff --git a/backend/Arah.Modules.Assets.Infrastructure/Postgres/PostgresAssetRepository.cs b/backend/Arah.Modules.Assets.Infrastructure/Postgres/PostgresAssetRepository.cs index 489023e6..38e7e56d 100644 --- a/backend/Arah.Modules.Assets.Infrastructure/Postgres/PostgresAssetRepository.cs +++ b/backend/Arah.Modules.Assets.Infrastructure/Postgres/PostgresAssetRepository.cs @@ -20,33 +20,11 @@ public async Task> ListAsync( IReadOnlyCollection? types, AssetStatus? status, string? search, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null) { - IQueryable query = _dbContext.TerritoryAssets.AsNoTracking() - .Where(asset => asset.TerritoryId == territoryId); - - if (assetId is not null) - { - query = query.Where(asset => asset.Id == assetId); - } - - if (types is not null && types.Count > 0) - { - query = query.Where(asset => types.Contains(asset.Type)); - } - - if (status is not null) - { - query = query.Where(asset => asset.Status == status); - } - - if (!string.IsNullOrWhiteSpace(search)) - { - var pattern = $"%{search}%"; - query = query.Where(asset => EF.Functions.ILike(asset.Name, pattern) || - (asset.Description != null && EF.Functions.ILike(asset.Description, pattern))); - } - + var query = BuildQuery(territoryId, assetId, types, status, search, subtypes, typesOrSubtypes); var records = await query.ToListAsync(cancellationToken); return records.Select(record => record.ToDomain()).ToList(); } @@ -92,6 +70,7 @@ public async Task UpdateAsync(TerritoryAsset asset, CancellationToken cancellati } record.Type = asset.Type; + record.Subtype = asset.Subtype; record.Name = asset.Name; record.Description = asset.Description; record.Status = asset.Status; @@ -110,33 +89,11 @@ public async Task> ListPagedAsync( string? search, int skip, int take, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null) { - IQueryable query = _dbContext.TerritoryAssets.AsNoTracking() - .Where(asset => asset.TerritoryId == territoryId); - - if (assetId is not null) - { - query = query.Where(asset => asset.Id == assetId); - } - - if (types is not null && types.Count > 0) - { - query = query.Where(asset => types.Contains(asset.Type)); - } - - if (status is not null) - { - query = query.Where(asset => asset.Status == status); - } - - if (!string.IsNullOrWhiteSpace(search)) - { - var pattern = $"%{search}%"; - query = query.Where(asset => EF.Functions.ILike(asset.Name, pattern) || - (asset.Description != null && EF.Functions.ILike(asset.Description, pattern))); - } - + var query = BuildQuery(territoryId, assetId, types, status, search, subtypes, typesOrSubtypes); var records = await query .OrderByDescending(asset => asset.CreatedAtUtc) .Skip(skip) @@ -151,35 +108,105 @@ public async Task CountAsync( IReadOnlyCollection? types, AssetStatus? status, string? search, - CancellationToken cancellationToken) + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null) { - IQueryable query = _dbContext.TerritoryAssets + var query = BuildQuery(territoryId, assetId, types, status, search, subtypes, typesOrSubtypes); + const int maxInt32 = int.MaxValue; + var count = await query.CountAsync(cancellationToken); + return count > maxInt32 ? maxInt32 : (int)count; + } + + private IQueryable BuildQuery( + Guid territoryId, + Guid? assetId, + IReadOnlyCollection? types, + AssetStatus? status, + string? search, + IReadOnlyCollection? subtypes, + IReadOnlyCollection? typesOrSubtypes) + { + var query = _dbContext.TerritoryAssets.AsNoTracking() .Where(asset => asset.TerritoryId == territoryId); - if (assetId is not null) + query = ApplyIdentityFilter(query, assetId); + query = ApplyClassificationFilters(query, types, subtypes, typesOrSubtypes); + query = ApplyStatusFilter(query, status); + query = ApplySearchFilter(query, search); + return query; + } + + private static IQueryable ApplyIdentityFilter( + IQueryable query, + Guid? assetId) + { + if (assetId is null) + { + return query; + } + + return query.Where(asset => asset.Id == assetId); + } + + private static IQueryable ApplyClassificationFilters( + IQueryable query, + IReadOnlyCollection? types, + IReadOnlyCollection? subtypes, + IReadOnlyCollection? typesOrSubtypes) + { + var normalizedTypes = TerritoryAssetTypeMatch.NormalizeFilter(types); + var normalizedSubtypes = TerritoryAssetTypeMatch.NormalizeFilter(subtypes); + var normalizedTypesOrSubtypes = TerritoryAssetTypeMatch.NormalizeFilter(typesOrSubtypes); + + if (normalizedTypes is not null) { - query = query.Where(asset => asset.Id == assetId); + var typeList = normalizedTypes.ToList(); + query = query.Where(asset => typeList.Contains(asset.Type)); } - if (types is not null && types.Count > 0) + if (normalizedSubtypes is not null) { - query = query.Where(asset => types.Contains(asset.Type)); + var subtypeList = normalizedSubtypes.ToList(); + query = query.Where(asset => asset.Subtype != null && subtypeList.Contains(asset.Subtype)); } - if (status is not null) + if (normalizedTypesOrSubtypes is not null) { - query = query.Where(asset => asset.Status == status); + // Mapa assetTypes: legado type=river OU ponte natural+subtype. + var keys = normalizedTypesOrSubtypes.ToList(); + query = query.Where(asset => + keys.Contains(asset.Type) || + (asset.Subtype != null && keys.Contains(asset.Subtype))); } - if (!string.IsNullOrWhiteSpace(search)) + return query; + } + + private static IQueryable ApplyStatusFilter( + IQueryable query, + AssetStatus? status) + { + if (status is null) { - var pattern = $"%{search}%"; - query = query.Where(asset => EF.Functions.ILike(asset.Name, pattern) || - (asset.Description != null && EF.Functions.ILike(asset.Description, pattern))); + return query; } - const int maxInt32 = int.MaxValue; - var count = await query.CountAsync(cancellationToken); - return count > maxInt32 ? maxInt32 : (int)count; + return query.Where(asset => asset.Status == status); + } + + private static IQueryable ApplySearchFilter( + IQueryable query, + string? search) + { + if (string.IsNullOrWhiteSpace(search)) + { + return query; + } + + var pattern = $"%{search}%"; + return query.Where(asset => + EF.Functions.ILike(asset.Name, pattern) || + (asset.Description != null && EF.Functions.ILike(asset.Description, pattern))); } } diff --git a/backend/Arah.Modules.Assets/Application/Interfaces/ITerritoryAssetRepository.cs b/backend/Arah.Modules.Assets/Application/Interfaces/ITerritoryAssetRepository.cs index cb19d907..0f0a6332 100644 --- a/backend/Arah.Modules.Assets/Application/Interfaces/ITerritoryAssetRepository.cs +++ b/backend/Arah.Modules.Assets/Application/Interfaces/ITerritoryAssetRepository.cs @@ -10,7 +10,9 @@ Task> ListAsync( IReadOnlyCollection? types, AssetStatus? status, string? search, - CancellationToken cancellationToken); + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null); Task> ListByIdsAsync(IReadOnlyCollection assetIds, CancellationToken cancellationToken); Task GetByIdAsync(Guid assetId, CancellationToken cancellationToken); @@ -28,7 +30,9 @@ Task> ListPagedAsync( string? search, int skip, int take, - CancellationToken cancellationToken); + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null); /// /// Counts assets matching criteria. @@ -39,5 +43,7 @@ Task CountAsync( IReadOnlyCollection? types, AssetStatus? status, string? search, - CancellationToken cancellationToken); + CancellationToken cancellationToken, + IReadOnlyCollection? subtypes = null, + IReadOnlyCollection? typesOrSubtypes = null); } diff --git a/backend/Arah.Modules.Assets/Domain/TerritoryAssetTypeMatch.cs b/backend/Arah.Modules.Assets/Domain/TerritoryAssetTypeMatch.cs new file mode 100644 index 00000000..1204fb26 --- /dev/null +++ b/backend/Arah.Modules.Assets/Domain/TerritoryAssetTypeMatch.cs @@ -0,0 +1,61 @@ +namespace Arah.Modules.Assets.Domain; + +/// +/// Critérios de filtro Type/Subtype para listagens de TerritoryAsset. +/// +/// types — só (API assets). +/// subtypes — só . +/// typesOrSubtypes — Type ou Subtype (mapa assetTypes / legado + WA-E1). +/// +/// +public static class TerritoryAssetTypeMatch +{ + public static bool Matches( + string type, + string? subtype, + IReadOnlyCollection? types, + IReadOnlyCollection? subtypes, + IReadOnlyCollection? typesOrSubtypes = null) + { + if (types is { Count: > 0 } + && !types.Contains(type, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + + if (subtypes is { Count: > 0 }) + { + if (subtype is null || !subtypes.Contains(subtype, StringComparer.OrdinalIgnoreCase)) + { + return false; + } + } + + if (typesOrSubtypes is { Count: > 0 }) + { + var orMatch = typesOrSubtypes.Contains(type, StringComparer.OrdinalIgnoreCase) + || (subtype is not null + && typesOrSubtypes.Contains(subtype, StringComparer.OrdinalIgnoreCase)); + if (!orMatch) + { + return false; + } + } + + return true; + } + + public static IReadOnlyCollection? NormalizeFilter(IReadOnlyCollection? values) + { + if (values is null || values.Count == 0) + { + return null; + } + + return values + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Select(v => v.Trim().ToLowerInvariant()) + .Distinct(StringComparer.Ordinal) + .ToList(); + } +} diff --git a/backend/Tests/Arah.Tests/Api/ApiScenariosTests.cs b/backend/Tests/Arah.Tests/Api/ApiScenariosTests.cs index cf72531d..18913df7 100644 --- a/backend/Tests/Arah.Tests/Api/ApiScenariosTests.cs +++ b/backend/Tests/Arah.Tests/Api/ApiScenariosTests.cs @@ -1101,16 +1101,115 @@ public async Task Map_Pins_FilterAssets() null, new[] { new AssetGeoAnchorRequest(-23.376, -45.026) })); springResponse.EnsureSuccessStatusCode(); + var springAsset = await springResponse.Content.ReadFromJsonAsync(); + + var curatorToken = await LoginForTokenAsync(client, "google", "curator-external"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", curatorToken); + await CurateAssetApprovedAsync(client, riverAsset!.Id); + await CurateAssetApprovedAsync(client, springAsset!.Id); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", residentToken); var byType = await client.GetFromJsonAsync>( $"api/v1/map/pins?territoryId={ActiveTerritoryId}&types=asset&assetTypes=river"); Assert.NotNull(byType); - Assert.All(byType!, pin => Assert.Equal(riverAsset!.Id, pin.AssetId)); + Assert.NotEmpty(byType!); + Assert.All(byType, pin => Assert.Equal(riverAsset.Id, pin.AssetId)); var byId = await client.GetFromJsonAsync>( - $"api/v1/map/pins?territoryId={ActiveTerritoryId}&types=asset&assetId={riverAsset!.Id}"); + $"api/v1/map/pins?territoryId={ActiveTerritoryId}&types=asset&assetId={riverAsset.Id}"); Assert.NotNull(byId); - Assert.All(byId!, pin => Assert.Equal(riverAsset.Id, pin.AssetId)); + Assert.NotEmpty(byId!); + Assert.All(byId, pin => Assert.Equal(riverAsset.Id, pin.AssetId)); + } + + [Fact] + public async Task Map_Pins_FilterWaterBodySubtypes_WAE2() + { + using var factory = new ApiFactory(); + using var client = factory.CreateClient(); + client.DefaultRequestHeaders.Add(ApiHeaders.SessionId, "map-water-bodies"); + await SelectTerritoryAsync(client, ActiveTerritoryId); + + var residentToken = await LoginForTokenAsync(client, "google", "resident-external"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", residentToken); + + var riverResponse = await client.PostAsJsonAsync( + "api/v1/assets", + new CreateAssetRequest( + ActiveTerritoryId, + "natural", + "Rio Tipado", + null, + new[] { new AssetGeoAnchorRequest(-23.381, -45.031) }, + "river")); + riverResponse.EnsureSuccessStatusCode(); + var riverAsset = await riverResponse.Content.ReadFromJsonAsync(); + + var springResponse = await client.PostAsJsonAsync( + "api/v1/assets", + new CreateAssetRequest( + ActiveTerritoryId, + "natural", + "Nascente Tipada", + null, + new[] { new AssetGeoAnchorRequest(-23.382, -45.032) }, + "spring")); + springResponse.EnsureSuccessStatusCode(); + var springAsset = await springResponse.Content.ReadFromJsonAsync(); + + var culturalResponse = await client.PostAsJsonAsync( + "api/v1/assets", + new CreateAssetRequest( + ActiveTerritoryId, + "cultural", + "Praça Central", + null, + new[] { new AssetGeoAnchorRequest(-23.383, -45.033) })); + culturalResponse.EnsureSuccessStatusCode(); + var culturalAsset = await culturalResponse.Content.ReadFromJsonAsync(); + + var pendingPins = await client.GetFromJsonAsync>( + $"api/v1/map/pins?territoryId={ActiveTerritoryId}&types=asset&assetId={riverAsset!.Id}"); + Assert.NotNull(pendingPins); + Assert.Empty(pendingPins!); + + var curatorToken = await LoginForTokenAsync(client, "google", "curator-external"); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", curatorToken); + await CurateAssetApprovedAsync(client, riverAsset.Id); + await CurateAssetApprovedAsync(client, springAsset!.Id); + await CurateAssetApprovedAsync(client, culturalAsset!.Id); + + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", residentToken); + + var bySubtype = await client.GetFromJsonAsync>( + $"api/v1/map/pins?territoryId={ActiveTerritoryId}&types=asset&assetSubtypes=river"); + Assert.NotNull(bySubtype); + Assert.Single(bySubtype!); + Assert.Equal(riverAsset.Id, bySubtype[0].AssetId); + Assert.Equal("natural", bySubtype[0].AssetType); + Assert.Equal("river", bySubtype[0].AssetSubtype); + + var byTypesCompat = await client.GetFromJsonAsync>( + $"api/v1/map/pins?territoryId={ActiveTerritoryId}&types=asset&assetTypes=river,stream,spring,waterfall,well,potable_water"); + Assert.NotNull(byTypesCompat); + Assert.Equal(2, byTypesCompat!.Count); + Assert.DoesNotContain(byTypesCompat, pin => pin.AssetId == culturalAsset.Id); + Assert.All(byTypesCompat, pin => Assert.Equal("natural", pin.AssetType)); + + var assetsByTypeOnly = await client.GetFromJsonAsync>( + $"api/v1/assets?territoryId={ActiveTerritoryId}&types=river"); + Assert.NotNull(assetsByTypeOnly); + Assert.DoesNotContain(assetsByTypeOnly!, a => a.Id == riverAsset.Id); + Assert.All(assetsByTypeOnly, a => Assert.Equal("river", a.Type)); + } + + private static async Task CurateAssetApprovedAsync(HttpClient client, Guid assetId) + { + var response = await client.PostAsJsonAsync( + $"api/v1/assets/{assetId}/curate?territoryId={ActiveTerritoryId}", + new CurateAssetRequest("Approved", null)); + response.EnsureSuccessStatusCode(); } [Fact] diff --git a/backend/Tests/Arah.Tests/Domain/Assets/TerritoryAssetTypeMatchTests.cs b/backend/Tests/Arah.Tests/Domain/Assets/TerritoryAssetTypeMatchTests.cs new file mode 100644 index 00000000..6de21fbe --- /dev/null +++ b/backend/Tests/Arah.Tests/Domain/Assets/TerritoryAssetTypeMatchTests.cs @@ -0,0 +1,66 @@ +using Arah.Modules.Assets.Domain; +using Xunit; + +namespace Arah.Tests.Domain.Assets; + +public sealed class TerritoryAssetTypeMatchTests +{ + [Fact] + public void Matches_Types_MatchesTypeOnly() + { + var types = new[] { "river" }; + Assert.True(TerritoryAssetTypeMatch.Matches("river", null, types, null)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "river", types, null)); + Assert.False(TerritoryAssetTypeMatch.Matches("cultural", null, types, null)); + + var mixedTypes = new[] { "RiVeR" }; + Assert.True(TerritoryAssetTypeMatch.Matches("river", null, mixedTypes, null)); + Assert.True(TerritoryAssetTypeMatch.Matches("RIVER", null, mixedTypes, null)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "RiVeR", mixedTypes, null)); + } + + [Fact] + public void Matches_TypesOrSubtypes_MatchesTypeOrSubtype() + { + var keys = new[] { "river" }; + Assert.True(TerritoryAssetTypeMatch.Matches("river", null, null, null, keys)); + Assert.True(TerritoryAssetTypeMatch.Matches("natural", "river", null, null, keys)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "spring", null, null, keys)); + Assert.False(TerritoryAssetTypeMatch.Matches("cultural", null, null, null, keys)); + + var mixedKeys = new[] { "RiVeR" }; + Assert.True(TerritoryAssetTypeMatch.Matches("RIVER", null, null, null, mixedKeys)); + Assert.True(TerritoryAssetTypeMatch.Matches("natural", "river", null, null, mixedKeys)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "SPRING", null, null, mixedKeys)); + } + + [Fact] + public void Matches_Subtypes_RequiresSubtype() + { + var subtypes = new[] { "river", "stream" }; + Assert.True(TerritoryAssetTypeMatch.Matches("natural", "river", null, subtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "spring", null, subtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("river", null, null, subtypes)); + + var mixedSubtypes = new[] { "RiVeR", "STREAM" }; + Assert.True(TerritoryAssetTypeMatch.Matches("natural", "river", null, mixedSubtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "Spring", null, mixedSubtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("RIVER", null, null, mixedSubtypes)); + } + + [Fact] + public void Matches_TypesAndSubtypes_AppliesBoth() + { + var types = new[] { "natural" }; + var subtypes = new[] { "river" }; + Assert.True(TerritoryAssetTypeMatch.Matches("natural", "river", types, subtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("natural", "spring", types, subtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("cultural", "river", types, subtypes)); + + var mixedTypes = new[] { "NaTuRaL" }; + var mixedSubtypes = new[] { "RiVeR" }; + Assert.True(TerritoryAssetTypeMatch.Matches("natural", "river", mixedTypes, mixedSubtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("NATURAL", "spring", mixedTypes, mixedSubtypes)); + Assert.False(TerritoryAssetTypeMatch.Matches("cultural", "RIVER", mixedTypes, mixedSubtypes)); + } +} diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index fd8b8cee..109441cf 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,13 @@ e este projeto adere ao [Semantic Versioning](https://semver.org/lang/pt-BR/). ## [Unreleased] +### Adicionado — WA-E2 pins/filtros de corpos d'água no mapa (2026-08-09) + +- `GET /map/pins`: query `assetSubtypes`; `assetTypes` casa Type **ou** Subtype **só no mapa** (API assets permanece Type-only) +- Pin response: `assetType` / `assetSubtype`; Flutter chip "Corpos d'água" (filtro server-side via `assetTypes`) +- Sensibilidade HIGH/RESTRICTED **fora** deste slice (sem campos em TerritoryAsset; AC-WA-4 permanece pending) +- Spec-Id: `water-bodies-curation` + ### Adicionado — WA-E1 tipagem hídrica em TerritoryAsset (2026-08-08) - Campo `Subtype` em TerritoryAsset (API create/update/response) com allowlist `river|stream|spring|waterfall|well|potable_water` quando `type=natural` diff --git a/docs/STATUS_FASES.md b/docs/STATUS_FASES.md index 4513a79d..b3861926 100644 --- a/docs/STATUS_FASES.md +++ b/docs/STATUS_FASES.md @@ -50,7 +50,7 @@ Capacidade de domínio (não é trilha `kind: track` nem nova FASE*): rios, cór | Artefato | Papel | |----------|--------| -| [CORPOS_DAGUA_TERRITORIO.md](./backlog-api/CORPOS_DAGUA_TERRITORIO.md) | Realinhamento + backlog WA-E* / 24.0 · WA-E1 tipagem (parcial vs FASE24.0) | +| [CORPOS_DAGUA_TERRITORIO.md](./backlog-api/CORPOS_DAGUA_TERRITORIO.md) | Realinhamento + backlog WA-E* / 24.0 · WA-E1 tipagem ✅ · WA-E2 mapa (pins/filtros) | | [FASE24.md](./backlog-api/FASE24.md) §24.0 | Fundação NaturalAsset hídrico + curadoria | | [water-bodies-curation.spec.yaml](./specs/features/water-bodies-curation.spec.yaml) | Spec SDD draft · **Spec-Id:** `water-bodies-curation` · ponte tipada no código; AC-WA-1…6 pendentes para NaturalAsset | diff --git a/docs/_meta/PHASE_QUEUE.yaml b/docs/_meta/PHASE_QUEUE.yaml index 588f2792..5ff0395a 100644 --- a/docs/_meta/PHASE_QUEUE.yaml +++ b/docs/_meta/PHASE_QUEUE.yaml @@ -45,6 +45,7 @@ queue: # Web abertos: DSG-04 (espaçamento 8px portal), DSG-06 (glass cross), DSG-07 (syntax WCAG). # DSG-01/02/03/05/08 já corrigidos. # App×UI kit: docs/design/ANALISE_DESIGN_VS_APP_FLUTTER.md (APP-DS-01..17). + # Feito 2026-08-09: WA-E2 pins/filtros corpos d'água (mapa). # Feito 2026-08-08: APP-DS-17 saldo vendedor (Onda I); APP-DS-16 foto do produto (Onda H). # Feito 2026-08-05: APP-DS-15 produtos/QR/Em breve (Onda G); APP-DS-14 checkout PIX (Onda F); APP-DS-12 motion (Onda E). # Feito 2026-07-23: APP-DS-01..11 + 13 (Onda A–D). diff --git a/docs/api/60_06_API_MAPA.md b/docs/api/60_06_API_MAPA.md index 3e9adc2d..c8a752b5 100644 --- a/docs/api/60_06_API_MAPA.md +++ b/docs/api/60_06_API_MAPA.md @@ -85,13 +85,20 @@ **Como usar**: - Exige autenticação -- Query params: `territoryId` (opcional), `type` (filtro opcional: entity, post, asset, alert, event) +- Query params: + - `territoryId` (opcional, usa território ativo se não informado) + - `types` (CSV opcional: `entity`, `post`, `event`, `asset`, `alert`, `media`) + - `assetId` (opcional — restringe a um asset) + - `assetTypes` (CSV opcional — no **mapa** casa com `Type` **ou** `Subtype`; legado + WA-E2) + - `assetSubtypes` (CSV opcional — casa apenas com `Subtype`; ex.: `river,stream,spring`) - Header `X-Session-Id` para identificar território ativo **Regras de negócio**: - **Visibilidade**: Respeita regras de visibilidade de cada tipo de conteúdo -- **Filtros**: `type` filtra por tipo de pin -- **Retorno**: Dados mínimos para projeção no mapa (coordenadas, ID, tipo, título básico) +- **Assets no mapa**: apenas status `Active` (após curadoria) +- **Retorno**: coordenadas, IDs, tipo de pin, título; para assets também `assetType` e `assetSubtype` +- **Sensibilidade**: omitir HIGH/RESTRICTED ainda não aplica na ponte TerritoryAsset (acompanhamento FASE24.0 / AC-WA-4) +- **Nota**: `GET /api/v1/assets?types=` continua filtrando só por `Type` (não por Subtype) ### Obter Pins do Mapa Paginados (`GET /api/v1/map/pins/paged`) @@ -99,13 +106,13 @@ **Como usar**: - Exige autenticação -- Query params: `territoryId` (opcional), `type` (filtro opcional), `pageNumber` (padrão: 1), `pageSize` (padrão: 20) +- Query params: mesmos de `/pins` + `pageNumber` (padrão: 1), `pageSize` (padrão: 20) - Header `X-Session-Id` para identificar território ativo **Regras de negócio**: - **Paginação**: Padrão 20 itens por página - **Visibilidade**: Respeita regras de visibilidade de cada tipo de conteúdo -- **Filtros**: `type` filtra por tipo de pin +- **Filtros**: `types` / `assetTypes` / `assetSubtypes` como em `/pins` - **Retorno**: `PagedResponse` com metadados de paginação --- @@ -117,6 +124,7 @@ - **[Assets](./60_08_API_ASSETS.md)** - Assets aparecem como pins no mapa - **[Alertas](./60_07_API_ALERTAS.md)** - Alertas aparecem como pins no mapa - **[Regras de Visibilidade](./60_17_API_VISIBILIDADE.md)** - Visibilidade de entidades +- **[Corpos d'água](../backlog-api/CORPOS_DAGUA_TERRITORIO.md)** - WA-E2 filtros hídricos --- diff --git a/docs/backlog-api/CORPOS_DAGUA_TERRITORIO.md b/docs/backlog-api/CORPOS_DAGUA_TERRITORIO.md index 8520338d..5e60ab65 100644 --- a/docs/backlog-api/CORPOS_DAGUA_TERRITORIO.md +++ b/docs/backlog-api/CORPOS_DAGUA_TERRITORIO.md @@ -1,7 +1,7 @@ # Corpos d'água do território — rios, nascentes e fontes curáveis -**Versão**: 1.2 -**Data**: 2026-08-08 +**Versão**: 1.3 +**Data**: 2026-08-09 **Status**: ✅ Aprovado para planejamento (backlog) **Domínio dono**: `mapa-lugares` (primário) · co-ativação: `territorio-membership`, `governanca-transparencia`, `feed-conteudo`, `design-ux` · sinais externos: trilha TI **Âncora de fase**: [FASE24](./FASE24.md) (Saúde Territorial) · ponte atual: TerritoryAsset / MapEntity @@ -79,8 +79,8 @@ Refinar o que já existe em Assets/Mapa: | ID | Item | Prio | Notas | |----|------|------|-------| -| WA-E1 | Tipagem hídrica em TerritoryAsset (`natural` + subtype) | P1 | Implementado (ponte) — smoke HTTP; AC NaturalAsset ainda FASE24.0 | -| WA-E2 | Pins/filtros de mapa para corpos d'água | P1 | Flutter + BFF; filtrar HIGH/RESTRICTED server-side | +| WA-E1 | Tipagem hídrica em TerritoryAsset (`natural` + subtype) | P1 | ✅ Implementado (ponte) — smoke HTTP; AC NaturalAsset ainda FASE24.0 | +| WA-E2 | Pins/filtros de mapa para corpos d'água | P1 | ✅ Filtro `assetSubtypes` + chip Flutter; HIGH/RESTRICTED → acompanhamento | | WA-E3 | Glossário + docs funcionais alinhados | P0 | ✅ Docs iniciais; manter sync com código | | WA-E4 | Curadoria: copy/UX “cuidar do rio / da nascente” | P2 | design-ux | @@ -153,6 +153,7 @@ Refinar o que já existe em Assets/Mapa: ### Changelog +- **1.3** (2026-08-09): WA-E2 — filtro de pins por subtype + chip Flutter; sensibilidade deferida. - **1.2** (2026-08-08): WA-E1 implementado — `TerritoryAsset.Subtype` com allowlist hídrica. - **1.1** (2026-08-08): Alinhamento CodeRabbit — vocabulário canônico, MER vs implementação, typo hídrico, WaterBody alias. - **1.0** (2026-08-08): Introdução da capacidade no backlog — rios e fontes como entidades curáveis do território. diff --git a/docs/specs/features/water-bodies-curation.spec.yaml b/docs/specs/features/water-bodies-curation.spec.yaml index a6642499..b451980a 100644 --- a/docs/specs/features/water-bodies-curation.spec.yaml +++ b/docs/specs/features/water-bodies-curation.spec.yaml @@ -12,6 +12,7 @@ intent: | WaterBody é alias de produto/API (não entidade persistida separada). Fundação para observações de saúde hídrica (FASE24) e vínculo com posts/mapa. Slice WA-E1 (ponte no código): TerritoryAsset.type=natural + subtype allowlist; + Slice WA-E2: map pins filtráveis por assetSubtypes (sem HIGH/RESTRICTED ainda); não marca AC-WA-* como covered até NaturalAsset/FASE24.0. # Vocabulário canônico NATURAL_ASSET.type (UPPERCASE): diff --git a/frontend/arah.app/lib/core/theme/app_design_tokens.dart b/frontend/arah.app/lib/core/theme/app_design_tokens.dart index 529fd6c5..0f6e6d83 100644 --- a/frontend/arah.app/lib/core/theme/app_design_tokens.dart +++ b/frontend/arah.app/lib/core/theme/app_design_tokens.dart @@ -116,6 +116,10 @@ class AppDesignTokens { case 'entity': case 'waterfall': case 'spring': + case 'river': + case 'stream': + case 'well': + case 'potable_water': return water; case 'viewpoint': return territoryBoundary; diff --git a/frontend/arah.app/lib/features/map/data/models/map_pin.dart b/frontend/arah.app/lib/features/map/data/models/map_pin.dart index 7298622f..95986e1c 100644 --- a/frontend/arah.app/lib/features/map/data/models/map_pin.dart +++ b/frontend/arah.app/lib/features/map/data/models/map_pin.dart @@ -10,6 +10,8 @@ class MapPin { this.eventId, this.assetId, this.status, + this.assetType, + this.assetSubtype, }); final String pinType; @@ -21,6 +23,17 @@ class MapPin { final String? eventId; final String? assetId; final String? status; + final String? assetType; + final String? assetSubtype; + + /// Chave visual: subtype hídrico > type do asset > pinType genérico. + String get visualType { + final subtype = assetSubtype?.trim(); + if (subtype != null && subtype.isNotEmpty) return subtype; + final type = assetType?.trim(); + if (type != null && type.isNotEmpty && type != 'natural') return type; + return pinType; + } factory MapPin.fromJson(Map json) { String? guidToString(dynamic v) => @@ -36,6 +49,8 @@ class MapPin { eventId: guidToString(json['eventId']), assetId: guidToString(json['assetId']), status: json['status'] as String?, + assetType: json['assetType'] as String?, + assetSubtype: json['assetSubtype'] as String?, ); } } diff --git a/frontend/arah.app/lib/features/map/data/repositories/map_repository.dart b/frontend/arah.app/lib/features/map/data/repositories/map_repository.dart index d6576512..60d83649 100644 --- a/frontend/arah.app/lib/features/map/data/repositories/map_repository.dart +++ b/frontend/arah.app/lib/features/map/data/repositories/map_repository.dart @@ -1,21 +1,33 @@ import '../models/map_pin.dart'; import '../../../../core/network/bff_client.dart'; +/// Valores hídricos (legado em Type ou ponte WA-E1 em Subtype) — CSV para assetTypes. +const String kWaterBodySubtypesCsv = + 'river,stream,spring,waterfall,well,potable_water'; + /// Repositório da jornada BFF map (pins, entidades). Consome GET map/pins. class MapRepository { MapRepository({required BffClient client}) : _client = client; final BffClient _client; - /// GET map/pins?territoryId=...&types=... (types opcional: entity,post,event,asset,alert,media). + /// GET map/pins?territoryId=...&types=...&assetTypes=...&assetSubtypes=... Future> getPins({ required String territoryId, String? types, + String? assetTypes, + String? assetSubtypes, }) async { var path = 'pins?territoryId=$territoryId'; if (types != null && types.isNotEmpty) { path += '&types=$types'; } + if (assetTypes != null && assetTypes.isNotEmpty) { + path += '&assetTypes=$assetTypes'; + } + if (assetSubtypes != null && assetSubtypes.isNotEmpty) { + path += '&assetSubtypes=$assetSubtypes'; + } final response = await _client.get('map', path); final list = response.data is List ? response.data as List : null; if (list == null) return []; diff --git a/frontend/arah.app/lib/features/map/presentation/providers/map_pins_provider.dart b/frontend/arah.app/lib/features/map/presentation/providers/map_pins_provider.dart index fc12fe87..24650535 100644 --- a/frontend/arah.app/lib/features/map/presentation/providers/map_pins_provider.dart +++ b/frontend/arah.app/lib/features/map/presentation/providers/map_pins_provider.dart @@ -8,10 +8,45 @@ final mapRepositoryProvider = Provider((ref) { return MapRepository(client: ref.watch(bffClientProvider)); }); -/// Pins do mapa para o território. BFF map/pins. +/// Filtro de pins do mapa (WA-E2: corpos d'água via assetTypes no servidor). +enum MapPinsFilter { + all, + waterBodies, +} + +final mapPinsFilterProvider = StateProvider((ref) => MapPinsFilter.all); + +class MapPinsQuery { + const MapPinsQuery({required this.territoryId, required this.filter}); + + final String? territoryId; + final MapPinsFilter filter; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is MapPinsQuery && + runtimeType == other.runtimeType && + territoryId == other.territoryId && + filter == other.filter; + + @override + int get hashCode => Object.hash(territoryId, filter); +} + +/// Pins do mapa para o território. BFF map/pins (filtro server-side). final mapPinsProvider = - FutureProvider.autoDispose.family, String?>((ref, territoryId) async { + FutureProvider.autoDispose.family, MapPinsQuery>((ref, query) async { + final territoryId = query.territoryId; if (territoryId == null || territoryId.isEmpty) return []; final repo = ref.watch(mapRepositoryProvider); + if (query.filter == MapPinsFilter.waterBodies) { + // assetTypes casa Type OU Subtype — inclui legado (type=river) e WA-E1 (natural+subtype). + return repo.getPins( + territoryId: territoryId, + types: 'asset', + assetTypes: kWaterBodySubtypesCsv, + ); + } return repo.getPins(territoryId: territoryId); -}); +}); \ No newline at end of file diff --git a/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart b/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart index a2a189d0..50630e60 100644 --- a/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart +++ b/frontend/arah.app/lib/features/map/presentation/screens/map_screen.dart @@ -48,7 +48,11 @@ class _MapScreenState extends ConsumerState { // reflita no mapa e apenas um contorno seja exibido por vez. final territoryId = ref.watch(selectedTerritoryIdValueProvider) ?? widget.territoryId; final geo = ref.watch(geoLocationStateProvider); - final pinsAsync = ref.watch(mapPinsProvider(territoryId)); + final pinsFilter = ref.watch(mapPinsFilterProvider); + final pinsAsync = ref.watch(mapPinsProvider(MapPinsQuery( + territoryId: territoryId, + filter: pinsFilter, + ))); final territoryDetailAsync = ref.watch(territoryDetailProvider(territoryId ?? '')); final hasTerritory = territoryId != null && territoryId.isNotEmpty; @@ -94,7 +98,9 @@ class _MapScreenState extends ConsumerState { ), ), ) - : FlutterMap( + : Stack( + children: [ + FlutterMap( mapController: _mapController, options: MapOptions( initialCenter: initialCenter, @@ -151,6 +157,37 @@ class _MapScreenState extends ConsumerState { ), ], ), + Positioned( + top: AppConstants.spacingSm, + left: AppConstants.spacingMd, + right: AppConstants.spacingMd, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + FilterChip( + label: Text(l10n.mapFilterAll), + selected: pinsFilter == MapPinsFilter.all, + onSelected: (_) { + ref.read(mapPinsFilterProvider.notifier).state = + MapPinsFilter.all; + }, + ), + const SizedBox(width: AppConstants.spacingSm), + FilterChip( + label: Text(l10n.mapFilterWaterBodies), + selected: pinsFilter == MapPinsFilter.waterBodies, + onSelected: (_) { + ref.read(mapPinsFilterProvider.notifier).state = + MapPinsFilter.waterBodies; + }, + ), + ], + ), + ), + ), + ], + ), ); } @@ -206,6 +243,7 @@ class _MapScreenState extends ConsumerState { Widget _buildPinsLayer(BuildContext context, List pins, String? territoryId) { return MarkerLayer( markers: pins.map((pin) { + final visualType = pin.visualType; return Marker( point: LatLng(pin.latitude, pin.longitude), width: AppConstants.minTouchTargetSize, @@ -214,17 +252,17 @@ class _MapScreenState extends ConsumerState { // confiável na web do que GestureDetector dentro do marcador. child: Container( decoration: BoxDecoration( - color: AppDesignTokens.pinColorForType(pin.pinType).withValues(alpha: 0.25), + color: AppDesignTokens.pinColorForType(visualType).withValues(alpha: 0.25), shape: BoxShape.circle, border: Border.all( - color: AppDesignTokens.pinColorForType(pin.pinType), + color: AppDesignTokens.pinColorForType(visualType), width: 2, ), ), child: Icon( - _iconForPinType(pin.pinType), + _iconForPinType(visualType), size: 20, - color: AppDesignTokens.pinColorForType(pin.pinType), + color: AppDesignTokens.pinColorForType(visualType), ), ), ); @@ -246,6 +284,13 @@ class _MapScreenState extends ConsumerState { return Icons.warning_amber; case 'media': return Icons.perm_media; + case 'river': + case 'stream': + case 'spring': + case 'waterfall': + case 'well': + case 'potable_water': + return Icons.water_drop; default: return Icons.place; } diff --git a/frontend/arah.app/lib/l10n/app_en.arb b/frontend/arah.app/lib/l10n/app_en.arb index 166d7fca..17b7e95e 100644 --- a/frontend/arah.app/lib/l10n/app_en.arb +++ b/frontend/arah.app/lib/l10n/app_en.arb @@ -111,6 +111,8 @@ "sessionExpired": "Session expired. Please log in again.", "enterToAccess": "Sign in to access profile, post, and notifications.", "map": "Map", +"mapFilterAll": "All", +"mapFilterWaterBodies": "Water bodies", "viewOnMap": "View on map", "mapEntity": "Place / map point", "mapPost": "Post", diff --git a/frontend/arah.app/lib/l10n/app_localizations.dart b/frontend/arah.app/lib/l10n/app_localizations.dart index 53232e89..91d00abc 100644 --- a/frontend/arah.app/lib/l10n/app_localizations.dart +++ b/frontend/arah.app/lib/l10n/app_localizations.dart @@ -18,7 +18,7 @@ import 'app_localizations_pt.dart'; /// `supportedLocales` list. For example: /// /// ```dart -/// import 'gen_l10n/app_localizations.dart'; +/// import 'l10n/app_localizations.dart'; /// /// return MaterialApp( /// localizationsDelegates: AppLocalizations.localizationsDelegates, @@ -62,7 +62,8 @@ import 'app_localizations_pt.dart'; /// be consistent with the languages listed in the AppLocalizations.supportedLocales /// property. abstract class AppLocalizations { - AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); final String localeName; @@ -70,7 +71,8 @@ abstract class AppLocalizations { return Localizations.of(context, AppLocalizations); } - static const LocalizationsDelegate delegate = _AppLocalizationsDelegate(); + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); /// A list of this localizations delegate along with the default localizations /// delegates. @@ -82,7 +84,8 @@ abstract class AppLocalizations { /// Additional delegates can be added by appending to this list in /// MaterialApp. This list does not have to be used at all if a custom list /// of delegates is preferred or required. - static const List> localizationsDelegates = >[ + static const List> localizationsDelegates = + >[ delegate, GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, @@ -641,6 +644,18 @@ abstract class AppLocalizations { /// **'Mapa'** String get map; + /// No description provided for @mapFilterAll. + /// + /// In pt, this message translates to: + /// **'Todos'** + String get mapFilterAll; + + /// No description provided for @mapFilterWaterBodies. + /// + /// In pt, this message translates to: + /// **'Corpos d\'água'** + String get mapFilterWaterBodies; + /// No description provided for @viewOnMap. /// /// In pt, this message translates to: @@ -3006,7 +3021,8 @@ abstract class AppLocalizations { String get comingSoonUnderstood; } -class _AppLocalizationsDelegate extends LocalizationsDelegate { +class _AppLocalizationsDelegate + extends LocalizationsDelegate { const _AppLocalizationsDelegate(); @override @@ -3015,25 +3031,25 @@ class _AppLocalizationsDelegate extends LocalizationsDelegate } @override - bool isSupported(Locale locale) => ['en', 'pt'].contains(locale.languageCode); + bool isSupported(Locale locale) => + ['en', 'pt'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; } AppLocalizations lookupAppLocalizations(Locale locale) { - - // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'en': return AppLocalizationsEn(); - case 'pt': return AppLocalizationsPt(); + case 'en': + return AppLocalizationsEn(); + case 'pt': + return AppLocalizationsPt(); } throw FlutterError( - 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' - 'an issue with the localizations generation tool. Please file an issue ' - 'on GitHub with a reproducible sample app and the gen-l10n configuration ' - 'that was used.' - ); + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.'); } diff --git a/frontend/arah.app/lib/l10n/app_localizations_en.dart b/frontend/arah.app/lib/l10n/app_localizations_en.dart index d799beda..d5ac9d52 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_en.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_en.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -52,7 +54,8 @@ class AppLocalizationsEn extends AppLocalizations { String get territories => 'Territories'; @override - String get territoriesSubtitle => 'Tap a territory to see its feed or switch region.'; + String get territoriesSubtitle => + 'Tap a territory to see its feed or switch region.'; @override String get noTerritoryAvailable => 'No territory available'; @@ -61,7 +64,8 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingTitle => 'Choose your territory'; @override - String get onboardingDescription => 'To see the feed and join the community, choose a territory near you.'; + String get onboardingDescription => + 'To see the feed and join the community, choose a territory near you.'; @override String get useMyLocation => 'Use my location'; @@ -76,7 +80,8 @@ class AppLocalizationsEn extends AppLocalizations { String get registerMunicipalityTitle => 'Register your municipality'; @override - String get registerMunicipalityDescription => 'We use the official IBGE boundary to create your city territory so you can enter as a visitor.'; + String get registerMunicipalityDescription => + 'We use the official IBGE boundary to create your city territory so you can enter as a visitor.'; @override String registerMunicipalityButton(String city) { @@ -95,7 +100,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get registerMunicipalityFailed => 'Could not fetch IBGE boundary. Adjust the pin and draw your cell.'; + String get registerMunicipalityFailed => + 'Could not fetch IBGE boundary. Adjust the pin and draw your cell.'; @override String get proposeTerritoryButton => 'Draw my cell'; @@ -104,13 +110,16 @@ class AppLocalizationsEn extends AppLocalizations { String get proposeTerritoryTitle => 'Propose territory'; @override - String get proposeTerritoryDescription => 'Adjust the pin on the map, confirm city and state, and draw the boundary. A curator will validate before activation; you get provisional visitor access.'; + String get proposeTerritoryDescription => + 'Adjust the pin on the map, confirm city and state, and draw the boundary. A curator will validate before activation; you get provisional visitor access.'; @override - String get proposeTerritoryTapPin => 'Tap the map to adjust the territory center.'; + String get proposeTerritoryTapPin => + 'Tap the map to adjust the territory center.'; @override - String get proposeTerritoryTapPolygon => 'Tap the map to add polygon vertices (minimum 3).'; + String get proposeTerritoryTapPolygon => + 'Tap the map to add polygon vertices (minimum 3).'; @override String get proposeTerritoryCity => 'City'; @@ -125,7 +134,8 @@ class AppLocalizationsEn extends AppLocalizations { String get proposeTerritoryPolygonMode => 'Draw polygon'; @override - String get proposeTerritoryPolygonModeHint => 'Off: uses a circle with adjustable radius.'; + String get proposeTerritoryPolygonModeHint => + 'Off: uses a circle with adjustable radius.'; @override String proposeTerritoryRadiusLabel(String km) { @@ -147,16 +157,19 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get proposeTerritoryCityStateRequired => 'Enter city and state (2 letters).'; + String get proposeTerritoryCityStateRequired => + 'Enter city and state (2 letters).'; @override - String get proposeTerritoryPolygonMinPoints => 'The polygon needs at least 3 points.'; + String get proposeTerritoryPolygonMinPoints => + 'The polygon needs at least 3 points.'; @override String get territoryPendingBadge => 'Awaiting curator'; @override - String get onboardingPendingTerritoryHint => 'This territory is under validation. You can enter provisionally as a visitor.'; + String get onboardingPendingTerritoryHint => + 'This territory is under validation. You can enter provisionally as a visitor.'; @override String get onboardingNearbyTitle => 'Near you'; @@ -165,16 +178,19 @@ class AppLocalizationsEn extends AppLocalizations { String get onboardingAllTerritoriesTitle => 'All territories'; @override - String get onboardingOrChooseFromList => 'Or choose a territory from the list below'; + String get onboardingOrChooseFromList => + 'Or choose a territory from the list below'; @override String get onboardingLocationEnabled => 'Location enabled'; @override - String get onboardingLocationPrivacy => 'Your location is private and not visible to other users.'; + String get onboardingLocationPrivacy => + 'Your location is private and not visible to other users.'; @override - String get onboardingAllowLocationToCenter => 'Allow location to center the map and see nearby territories.'; + String get onboardingAllowLocationToCenter => + 'Allow location to center the map and see nearby territories.'; @override String onboardingContinueWith(Object name) { @@ -182,7 +198,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get onboardingVisitorOnContinue => 'When you continue, you will enter as a visitor in this territory and will be able to see the feed.'; + String get onboardingVisitorOnContinue => + 'When you continue, you will enter as a visitor in this territory and will be able to see the feed.'; @override String get onboardingGettingLocation => 'Getting location...'; @@ -272,7 +289,8 @@ class AppLocalizationsEn extends AppLocalizations { String get noTerritorySelected => 'No territory selected'; @override - String get chooseTerritoryInExplore => 'Tap Explore, choose a territory, and come back here to post.'; + String get chooseTerritoryInExplore => + 'Tap Explore, choose a territory, and come back here to post.'; @override String get comingSoon => 'Coming soon'; @@ -284,11 +302,18 @@ class AppLocalizationsEn extends AppLocalizations { String get sessionExpired => 'Session expired. Please log in again.'; @override - String get enterToAccess => 'Sign in to access profile, post, and notifications.'; + String get enterToAccess => + 'Sign in to access profile, post, and notifications.'; @override String get map => 'Map'; + @override + String get mapFilterAll => 'All'; + + @override + String get mapFilterWaterBodies => 'Water bodies'; + @override String get viewOnMap => 'View on map'; @@ -692,7 +717,8 @@ class AppLocalizationsEn extends AppLocalizations { String get errorSearch => 'Search error.'; @override - String get searchMinCharsHint => 'Type at least 2 characters or see suggestions above.'; + String get searchMinCharsHint => + 'Type at least 2 characters or see suggestions above.'; @override String get connectionRequestIncoming => 'Incoming request'; @@ -753,7 +779,8 @@ class AppLocalizationsEn extends AppLocalizations { String get errorLoadAlerts => 'Could not load alerts.'; @override - String get alertsRequireResidency => 'Territory alerts require residency or curator role.'; + String get alertsRequireResidency => + 'Territory alerts require residency or curator role.'; @override String get filterAll => 'All'; @@ -780,7 +807,8 @@ class AppLocalizationsEn extends AppLocalizations { String get moderationEvidencesTab => 'Evidence'; @override - String get moderationEmptyDescription => 'When there are items to curate, they will show up here.'; + String get moderationEmptyDescription => + 'When there are items to curate, they will show up here.'; @override String get moderationCaseTypeLabel => 'Moderation case'; @@ -857,7 +885,8 @@ class AppLocalizationsEn extends AppLocalizations { String get errorRequestResidency => 'Error requesting residency.'; @override - String get errorUploadProof => 'Could not upload the proof photo. Please try again.'; + String get errorUploadProof => + 'Could not upload the proof photo. Please try again.'; @override String get moderationEvidenceSuffix => 'evidence'; @@ -884,7 +913,8 @@ class AppLocalizationsEn extends AppLocalizations { String get governanceSubtitle => 'Collective decisions of the territory.'; @override - String get chooseTerritoryForGovernance => 'Choose a territory to see the votings.'; + String get chooseTerritoryForGovernance => + 'Choose a territory to see the votings.'; @override String get noVotings => 'No votings right now.'; @@ -988,7 +1018,8 @@ class AppLocalizationsEn extends AppLocalizations { String get votingTypeFeatureFlag => 'Feature flag'; @override - String get votingTypeTerritoryCharacterization => 'Territory characterization'; + String get votingTypeTerritoryCharacterization => + 'Territory characterization'; @override String get votingTypeCommunityPolicy => 'Community policy'; @@ -1063,7 +1094,8 @@ class AppLocalizationsEn extends AppLocalizations { String get services => 'Services'; @override - String get servicesHubSubtitle => 'Everything life in the territory needs — local economy, services, governance, and care for the place.'; + String get servicesHubSubtitle => + 'Everything life in the territory needs — local economy, services, governance, and care for the place.'; @override String get servicesCategoryEconomy => 'Local economy'; @@ -1138,7 +1170,8 @@ class AppLocalizationsEn extends AppLocalizations { String get visitorBannerTitle => 'You are visiting as a guest'; @override - String get visitorBannerCta => 'Confirm residency to vote and see resident-only content'; + String get visitorBannerCta => + 'Confirm residency to vote and see resident-only content'; @override String get visitor => 'Visitor'; @@ -1179,7 +1212,8 @@ class AppLocalizationsEn extends AppLocalizations { } @override - String get residencyJourneyPresenceSubtitle => 'Residents access restricted content, vote, and take part in local governance.'; + String get residencyJourneyPresenceSubtitle => + 'Residents access restricted content, vote, and take part in local governance.'; @override String get residencyJourneyPresenceTitle => 'Presence in the territory'; @@ -1191,13 +1225,15 @@ class AppLocalizationsEn extends AppLocalizations { String get residencyJourneyProofTitle => 'Send proof of residency'; @override - String get residencyJourneyProofSubtitle => 'Utility bill, lease, or association letter. Describe it and optionally attach a photo.'; + String get residencyJourneyProofSubtitle => + 'Utility bill, lease, or association letter. Describe it and optionally attach a photo.'; @override String get residencyJourneyMessageLabel => 'Message / proof'; @override - String get residencyJourneyMessageHint => 'E.g. Electricity bill in my name, Palm Street 120'; + String get residencyJourneyMessageHint => + 'E.g. Electricity bill in my name, Palm Street 120'; @override String get residencyJourneyAttachProof => 'Attach proof photo'; @@ -1257,7 +1293,8 @@ class AppLocalizationsEn extends AppLocalizations { String get residencyJourneySuccessTitle => 'Request sent!'; @override - String get residencyJourneySuccessMessage => 'Territory curators will review your proof. You will be notified once residency is confirmed.'; + String get residencyJourneySuccessMessage => + 'Territory curators will review your proof. You will be notified once residency is confirmed.'; @override String get checkoutJourneyTitle => 'Checkout'; @@ -1266,7 +1303,8 @@ class AppLocalizationsEn extends AppLocalizations { String get checkoutBagTitle => 'Your bag'; @override - String get checkoutEmptyCart => 'Your bag is empty. Go back to Marketplace to add items.'; + String get checkoutEmptyCart => + 'Your bag is empty. Go back to Marketplace to add items.'; @override String get checkoutQuantity => 'Quantity'; @@ -1275,19 +1313,22 @@ class AppLocalizationsEn extends AppLocalizations { String get checkoutFulfillmentTitle => 'Fulfillment'; @override - String get checkoutFulfillmentSubtitle => 'Choose pickup in the territory or delivery arranged with the store.'; + String get checkoutFulfillmentSubtitle => + 'Choose pickup in the territory or delivery arranged with the store.'; @override String get checkoutFulfillmentPickup => 'Pickup'; @override - String get checkoutFulfillmentPickupHint => 'Arrange place and time with the store.'; + String get checkoutFulfillmentPickupHint => + 'Arrange place and time with the store.'; @override String get checkoutFulfillmentDelivery => 'Delivery'; @override - String get checkoutFulfillmentDeliveryHint => 'Arranged locally (no automatic shipping).'; + String get checkoutFulfillmentDeliveryHint => + 'Arranged locally (no automatic shipping).'; @override String get checkoutReviewTitle => 'Review and pay'; @@ -1302,13 +1343,15 @@ class AppLocalizationsEn extends AppLocalizations { String get checkoutPayPix => 'Pay with PIX'; @override - String get checkoutPixHint => 'We generate a PIX code for you to copy and pay in your bank app.'; + String get checkoutPixHint => + 'We generate a PIX code for you to copy and pay in your bank app.'; @override String get checkoutSuccessTitle => 'PIX ready'; @override - String get checkoutSuccessMessage => 'Copy the code, pay in your bank app, then tap «I already paid».'; + String get checkoutSuccessMessage => + 'Copy the code, pay in your bank app, then tap «I already paid».'; @override String get checkoutDone => 'Done'; @@ -1332,13 +1375,15 @@ class AppLocalizationsEn extends AppLocalizations { String get pixPaymentConfirmed => 'Payment confirmed'; @override - String get pixPaymentPendingHint => 'Payment not confirmed yet. If you already paid, wait a few seconds and try again.'; + String get pixPaymentPendingHint => + 'Payment not confirmed yet. If you already paid, wait a few seconds and try again.'; @override String get openStoreEmptyTitle => 'You don\'t have a store yet'; @override - String get openStoreEmptyDescription => 'Create your store in the territory to list items and receive payments.'; + String get openStoreEmptyDescription => + 'Create your store in the territory to list items and receive payments.'; @override String get storePaymentsTitle => 'Payments'; @@ -1347,22 +1392,26 @@ class AppLocalizationsEn extends AppLocalizations { String get storePaymentsEnabled => 'Accept payments'; @override - String get storePaymentsEnabledHint => 'Customers can pay this store\'s orders via PIX.'; + String get storePaymentsEnabledHint => + 'Customers can pay this store\'s orders via PIX.'; @override - String get storePaymentsDisabledHint => 'Payments off — turn on to receive via PIX.'; + String get storePaymentsDisabledHint => + 'Payments off — turn on to receive via PIX.'; @override String get storePixKeyLabel => 'PIX key'; @override - String get storePixKeyHint => 'The store PIX key is configured in the territory payment gateway. Here you only enable or disable accepting payments.'; + String get storePixKeyHint => + 'The store PIX key is configured in the territory payment gateway. Here you only enable or disable accepting payments.'; @override String get sellerBalanceTitle => 'Your balance'; @override - String get sellerBalanceHint => 'Territory amounts after confirmed sales. Payouts are processed by the territory.'; + String get sellerBalanceHint => + 'Territory amounts after confirmed sales. Payouts are processed by the territory.'; @override String get sellerBalanceLoadError => 'Could not load balance.'; @@ -1380,7 +1429,8 @@ class AppLocalizationsEn extends AppLocalizations { String get myProductsTitle => 'My products'; @override - String get myProductsEmpty => 'No products yet. Tap Add to create the first one.'; + String get myProductsEmpty => + 'No products yet. Tap Add to create the first one.'; @override String get addProduct => 'Add'; @@ -1401,7 +1451,8 @@ class AppLocalizationsEn extends AppLocalizations { String get addProductPhotoDescriptionTitle => 'Photo and description'; @override - String get addProductDescriptionHint => 'Describe what it is, how to use it, and local conditions.'; + String get addProductDescriptionHint => + 'Describe what it is, how to use it, and local conditions.'; @override String get addProductReviewTitle => 'Review'; @@ -1419,7 +1470,8 @@ class AppLocalizationsEn extends AppLocalizations { String get addProductPhotoHint => 'Tap to choose a photo from the gallery'; @override - String get addProductPhotoOptionalHint => 'Optional — products with photos sell better'; + String get addProductPhotoOptionalHint => + 'Optional — products with photos sell better'; @override String get productPhotoLabel => 'Photo'; @@ -1467,13 +1519,15 @@ class AppLocalizationsEn extends AppLocalizations { String get archiveProduct => 'Archive product'; @override - String get archiveProductConfirm => 'The product will no longer appear in Marketplace. You can create another later.'; + String get archiveProductConfirm => + 'The product will no longer appear in Marketplace. You can create another later.'; @override String get errorSaveProduct => 'Could not save product.'; @override - String get errorUploadProductPhoto => 'Could not upload the photo. Please try again.'; + String get errorUploadProductPhoto => + 'Could not upload the photo. Please try again.'; @override String get comingSoonJourneyTitle => 'Coming soon'; @@ -1490,7 +1544,8 @@ class AppLocalizationsEn extends AppLocalizations { String get comingSoonNotifyTitle => 'Follow along'; @override - String get comingSoonNotifyMessage => 'When it goes live, you will find it here in the Services hub. No rush — collective care first.'; + String get comingSoonNotifyMessage => + 'When it goes live, you will find it here in the Services hub. No rush — collective care first.'; @override String get comingSoonUnderstood => 'Got it'; diff --git a/frontend/arah.app/lib/l10n/app_localizations_pt.dart b/frontend/arah.app/lib/l10n/app_localizations_pt.dart index cfbf0ae5..ecec350e 100644 --- a/frontend/arah.app/lib/l10n/app_localizations_pt.dart +++ b/frontend/arah.app/lib/l10n/app_localizations_pt.dart @@ -1,3 +1,5 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; import 'app_localizations.dart'; // ignore_for_file: type=lint @@ -46,13 +48,15 @@ class AppLocalizationsPt extends AppLocalizations { String get profile => 'Perfil'; @override - String get chooseTerritory => 'Escolha um território para ver o feed da região'; + String get chooseTerritory => + 'Escolha um território para ver o feed da região'; @override String get territories => 'Territórios'; @override - String get territoriesSubtitle => 'Toque em um território para ver o feed da região ou trocar de região.'; + String get territoriesSubtitle => + 'Toque em um território para ver o feed da região ou trocar de região.'; @override String get noTerritoryAvailable => 'Nenhum território disponível'; @@ -61,22 +65,26 @@ class AppLocalizationsPt extends AppLocalizations { String get onboardingTitle => 'Escolha seu território'; @override - String get onboardingDescription => 'Para ver o feed e participar da comunidade, escolha um território próximo a você.'; + String get onboardingDescription => + 'Para ver o feed e participar da comunidade, escolha um território próximo a você.'; @override String get useMyLocation => 'Usar minha localização'; @override - String get enableLocationHint => 'Ative a localização para ver territórios próximos.'; + String get enableLocationHint => + 'Ative a localização para ver territórios próximos.'; @override - String get noTerritoryInRegion => 'Nenhum território cadastrado perto de você ainda.'; + String get noTerritoryInRegion => + 'Nenhum território cadastrado perto de você ainda.'; @override String get registerMunicipalityTitle => 'Cadastrar seu município'; @override - String get registerMunicipalityDescription => 'Usamos o contorno oficial do IBGE para criar o território da sua cidade e você poder entrar como visitante.'; + String get registerMunicipalityDescription => + 'Usamos o contorno oficial do IBGE para criar o território da sua cidade e você poder entrar como visitante.'; @override String registerMunicipalityButton(String city) { @@ -95,7 +103,8 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get registerMunicipalityFailed => 'Não foi possível obter o contorno IBGE. Ajuste o pin e desenhe sua célula.'; + String get registerMunicipalityFailed => + 'Não foi possível obter o contorno IBGE. Ajuste o pin e desenhe sua célula.'; @override String get proposeTerritoryButton => 'Desenhar minha célula'; @@ -104,13 +113,16 @@ class AppLocalizationsPt extends AppLocalizations { String get proposeTerritoryTitle => 'Propor território'; @override - String get proposeTerritoryDescription => 'Ajuste o pin no mapa, confirme cidade e UF e desenhe o perímetro. Um curador validará antes da ativação; você terá acesso provisório como visitante.'; + String get proposeTerritoryDescription => + 'Ajuste o pin no mapa, confirme cidade e UF e desenhe o perímetro. Um curador validará antes da ativação; você terá acesso provisório como visitante.'; @override - String get proposeTerritoryTapPin => 'Toque no mapa para ajustar o centro do território.'; + String get proposeTerritoryTapPin => + 'Toque no mapa para ajustar o centro do território.'; @override - String get proposeTerritoryTapPolygon => 'Toque no mapa para adicionar vértices do polígono (mínimo 3).'; + String get proposeTerritoryTapPolygon => + 'Toque no mapa para adicionar vértices do polígono (mínimo 3).'; @override String get proposeTerritoryCity => 'Cidade'; @@ -125,7 +137,8 @@ class AppLocalizationsPt extends AppLocalizations { String get proposeTerritoryPolygonMode => 'Desenhar polígono'; @override - String get proposeTerritoryPolygonModeHint => 'Desligado: usa círculo com raio ajustável.'; + String get proposeTerritoryPolygonModeHint => + 'Desligado: usa círculo com raio ajustável.'; @override String proposeTerritoryRadiusLabel(String km) { @@ -147,16 +160,19 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get proposeTerritoryCityStateRequired => 'Informe cidade e UF (2 letras).'; + String get proposeTerritoryCityStateRequired => + 'Informe cidade e UF (2 letras).'; @override - String get proposeTerritoryPolygonMinPoints => 'O polígono precisa de pelo menos 3 pontos.'; + String get proposeTerritoryPolygonMinPoints => + 'O polígono precisa de pelo menos 3 pontos.'; @override String get territoryPendingBadge => 'Aguardando curador'; @override - String get onboardingPendingTerritoryHint => 'Este território está em validação. Você pode entrar provisoriamente como visitante.'; + String get onboardingPendingTerritoryHint => + 'Este território está em validação. Você pode entrar provisoriamente como visitante.'; @override String get onboardingNearbyTitle => 'Próximos a você'; @@ -165,16 +181,19 @@ class AppLocalizationsPt extends AppLocalizations { String get onboardingAllTerritoriesTitle => 'Todos os territórios'; @override - String get onboardingOrChooseFromList => 'Ou escolha um território na lista abaixo'; + String get onboardingOrChooseFromList => + 'Ou escolha um território na lista abaixo'; @override String get onboardingLocationEnabled => 'Localização ativa'; @override - String get onboardingLocationPrivacy => 'Sua localização é privada e não fica visível para outros usuários.'; + String get onboardingLocationPrivacy => + 'Sua localização é privada e não fica visível para outros usuários.'; @override - String get onboardingAllowLocationToCenter => 'Permita a localização para centralizar o mapa e ver territórios próximos.'; + String get onboardingAllowLocationToCenter => + 'Permita a localização para centralizar o mapa e ver territórios próximos.'; @override String onboardingContinueWith(Object name) { @@ -182,7 +201,8 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get onboardingVisitorOnContinue => 'Ao continuar, você entrará como visitante neste território e poderá ver o feed da região.'; + String get onboardingVisitorOnContinue => + 'Ao continuar, você entrará como visitante neste território e poderá ver o feed da região.'; @override String get onboardingGettingLocation => 'Obtendo localização...'; @@ -272,7 +292,8 @@ class AppLocalizationsPt extends AppLocalizations { String get noTerritorySelected => 'Nenhum território selecionado'; @override - String get chooseTerritoryInExplore => 'Toque em Explorar, escolha um território e volte aqui para publicar.'; + String get chooseTerritoryInExplore => + 'Toque em Explorar, escolha um território e volte aqui para publicar.'; @override String get comingSoon => 'Em breve'; @@ -284,11 +305,18 @@ class AppLocalizationsPt extends AppLocalizations { String get sessionExpired => 'Sessão expirada. Faça login novamente.'; @override - String get enterToAccess => 'Entre na sua conta para acessar perfil, publicar e notificações.'; + String get enterToAccess => + 'Entre na sua conta para acessar perfil, publicar e notificações.'; @override String get map => 'Mapa'; + @override + String get mapFilterAll => 'Todos'; + + @override + String get mapFilterWaterBodies => 'Corpos d\'água'; + @override String get viewOnMap => 'Ver no mapa'; @@ -506,7 +534,8 @@ class AppLocalizationsPt extends AppLocalizations { String get reportAlert => 'Reportar alerta'; @override - String get chooseTerritoryForAlerts => 'Escolha um território para ver alertas.'; + String get chooseTerritoryForAlerts => + 'Escolha um território para ver alertas.'; @override String get pending => 'Pendentes'; @@ -683,7 +712,8 @@ class AppLocalizationsPt extends AppLocalizations { String get errorLoadConnections => 'Não foi possível carregar conexões.'; @override - String get noConnectionsYet => 'Nenhuma conexão ainda. Toque em Adicionar para buscar pessoas.'; + String get noConnectionsYet => + 'Nenhuma conexão ainda. Toque em Adicionar para buscar pessoas.'; @override String get errorLoadSuggestions => 'Erro ao carregar sugestões.'; @@ -692,7 +722,8 @@ class AppLocalizationsPt extends AppLocalizations { String get errorSearch => 'Erro na busca.'; @override - String get searchMinCharsHint => 'Digite ao menos 2 caracteres ou veja sugestões acima.'; + String get searchMinCharsHint => + 'Digite ao menos 2 caracteres ou veja sugestões acima.'; @override String get connectionRequestIncoming => 'Solicitação recebida'; @@ -753,7 +784,8 @@ class AppLocalizationsPt extends AppLocalizations { String get errorLoadAlerts => 'Não foi possível carregar alertas.'; @override - String get alertsRequireResidency => 'Alertas do território exigem residência ou curadoria.'; + String get alertsRequireResidency => + 'Alertas do território exigem residência ou curadoria.'; @override String get filterAll => 'Todos'; @@ -762,7 +794,8 @@ class AppLocalizationsPt extends AppLocalizations { String get postDefaultTitle => 'Post'; @override - String get chooseTerritoryBeforePost => 'Escolha um território antes de publicar.'; + String get chooseTerritoryBeforePost => + 'Escolha um território antes de publicar.'; @override String get addImage => 'Adicionar imagem'; @@ -780,7 +813,8 @@ class AppLocalizationsPt extends AppLocalizations { String get moderationEvidencesTab => 'Evidências'; @override - String get moderationEmptyDescription => 'Quando houver itens para curadoria, eles aparecem aqui.'; + String get moderationEmptyDescription => + 'Quando houver itens para curadoria, eles aparecem aqui.'; @override String get moderationCaseTypeLabel => 'Caso de moderação'; @@ -857,7 +891,8 @@ class AppLocalizationsPt extends AppLocalizations { String get errorRequestResidency => 'Erro ao solicitar residência.'; @override - String get errorUploadProof => 'Não foi possível enviar a foto do comprovante. Tente novamente.'; + String get errorUploadProof => + 'Não foi possível enviar a foto do comprovante. Tente novamente.'; @override String get moderationEvidenceSuffix => 'evidência'; @@ -884,7 +919,8 @@ class AppLocalizationsPt extends AppLocalizations { String get governanceSubtitle => 'Decisões coletivas do território.'; @override - String get chooseTerritoryForGovernance => 'Escolha um território para ver as votações.'; + String get chooseTerritoryForGovernance => + 'Escolha um território para ver as votações.'; @override String get noVotings => 'Nenhuma votação no momento.'; @@ -988,7 +1024,8 @@ class AppLocalizationsPt extends AppLocalizations { String get votingTypeFeatureFlag => 'Funcionalidade'; @override - String get votingTypeTerritoryCharacterization => 'Caracterização do território'; + String get votingTypeTerritoryCharacterization => + 'Caracterização do território'; @override String get votingTypeCommunityPolicy => 'Política comunitária'; @@ -1063,7 +1100,8 @@ class AppLocalizationsPt extends AppLocalizations { String get services => 'Serviços'; @override - String get servicesHubSubtitle => 'Tudo que a vida no território precisa — economia, serviços, governança e cuidado com o lugar.'; + String get servicesHubSubtitle => + 'Tudo que a vida no território precisa — economia, serviços, governança e cuidado com o lugar.'; @override String get servicesCategoryEconomy => 'Economia local'; @@ -1138,7 +1176,8 @@ class AppLocalizationsPt extends AppLocalizations { String get visitorBannerTitle => 'Você está como visitante'; @override - String get visitorBannerCta => 'Confirme residência para votar e ver conteúdo de moradores'; + String get visitorBannerCta => + 'Confirme residência para votar e ver conteúdo de moradores'; @override String get visitor => 'Visitante'; @@ -1179,7 +1218,8 @@ class AppLocalizationsPt extends AppLocalizations { } @override - String get residencyJourneyPresenceSubtitle => 'Moradores acessam conteúdo restrito, votam e participam da gestão.'; + String get residencyJourneyPresenceSubtitle => + 'Moradores acessam conteúdo restrito, votam e participam da gestão.'; @override String get residencyJourneyPresenceTitle => 'Presença no território'; @@ -1191,13 +1231,15 @@ class AppLocalizationsPt extends AppLocalizations { String get residencyJourneyProofTitle => 'Envie um comprovante'; @override - String get residencyJourneyProofSubtitle => 'Conta de luz, água, contrato ou declaração da associação. Descreva e, se quiser, anexe uma foto.'; + String get residencyJourneyProofSubtitle => + 'Conta de luz, água, contrato ou declaração da associação. Descreva e, se quiser, anexe uma foto.'; @override String get residencyJourneyMessageLabel => 'Mensagem / comprovante'; @override - String get residencyJourneyMessageHint => 'Ex.: Conta de luz em meu nome, Rua das Palmeiras 120'; + String get residencyJourneyMessageHint => + 'Ex.: Conta de luz em meu nome, Rua das Palmeiras 120'; @override String get residencyJourneyAttachProof => 'Anexar foto do comprovante'; @@ -1257,7 +1299,8 @@ class AppLocalizationsPt extends AppLocalizations { String get residencyJourneySuccessTitle => 'Solicitação enviada!'; @override - String get residencyJourneySuccessMessage => 'A curadoria do território vai analisar seu comprovante. Você será avisado assim que sua residência for confirmada.'; + String get residencyJourneySuccessMessage => + 'A curadoria do território vai analisar seu comprovante. Você será avisado assim que sua residência for confirmada.'; @override String get checkoutJourneyTitle => 'Checkout'; @@ -1266,7 +1309,8 @@ class AppLocalizationsPt extends AppLocalizations { String get checkoutBagTitle => 'Sua sacola'; @override - String get checkoutEmptyCart => 'Sua sacola está vazia. Volte ao Mercado para adicionar itens.'; + String get checkoutEmptyCart => + 'Sua sacola está vazia. Volte ao Mercado para adicionar itens.'; @override String get checkoutQuantity => 'Quantidade'; @@ -1275,19 +1319,22 @@ class AppLocalizationsPt extends AppLocalizations { String get checkoutFulfillmentTitle => 'Como receber'; @override - String get checkoutFulfillmentSubtitle => 'Escolha retirada no território ou entrega combinada com a loja.'; + String get checkoutFulfillmentSubtitle => + 'Escolha retirada no território ou entrega combinada com a loja.'; @override String get checkoutFulfillmentPickup => 'Retirada'; @override - String get checkoutFulfillmentPickupHint => 'Combine o ponto e horário com a loja.'; + String get checkoutFulfillmentPickupHint => + 'Combine o ponto e horário com a loja.'; @override String get checkoutFulfillmentDelivery => 'Entrega'; @override - String get checkoutFulfillmentDeliveryHint => 'Combinada localmente (sem frete automático).'; + String get checkoutFulfillmentDeliveryHint => + 'Combinada localmente (sem frete automático).'; @override String get checkoutReviewTitle => 'Revisão e pagamento'; @@ -1302,13 +1349,15 @@ class AppLocalizationsPt extends AppLocalizations { String get checkoutPayPix => 'Pagar com PIX'; @override - String get checkoutPixHint => 'Geramos um código PIX para você copiar e pagar no app do banco.'; + String get checkoutPixHint => + 'Geramos um código PIX para você copiar e pagar no app do banco.'; @override String get checkoutSuccessTitle => 'PIX gerado'; @override - String get checkoutSuccessMessage => 'Copie o código, pague no banco e toque em «Já paguei» quando concluir.'; + String get checkoutSuccessMessage => + 'Copie o código, pague no banco e toque em «Já paguei» quando concluir.'; @override String get checkoutDone => 'Concluir'; @@ -1323,7 +1372,8 @@ class AppLocalizationsPt extends AppLocalizations { String get pixCodeCopied => 'Código PIX copiado'; @override - String get pixCodeUnavailable => 'Código indisponível. Tente novamente em instantes.'; + String get pixCodeUnavailable => + 'Código indisponível. Tente novamente em instantes.'; @override String get pixAlreadyPaid => 'Já paguei'; @@ -1332,13 +1382,15 @@ class AppLocalizationsPt extends AppLocalizations { String get pixPaymentConfirmed => 'Pagamento confirmado'; @override - String get pixPaymentPendingHint => 'Ainda não confirmamos o pagamento. Se já pagou, aguarde alguns segundos e tente de novo.'; + String get pixPaymentPendingHint => + 'Ainda não confirmamos o pagamento. Se já pagou, aguarde alguns segundos e tente de novo.'; @override String get openStoreEmptyTitle => 'Você ainda não tem loja'; @override - String get openStoreEmptyDescription => 'Crie sua loja no território para publicar itens e receber pagamentos.'; + String get openStoreEmptyDescription => + 'Crie sua loja no território para publicar itens e receber pagamentos.'; @override String get storePaymentsTitle => 'Pagamentos'; @@ -1347,22 +1399,26 @@ class AppLocalizationsPt extends AppLocalizations { String get storePaymentsEnabled => 'Aceitar pagamentos'; @override - String get storePaymentsEnabledHint => 'Clientes podem pagar pedidos desta loja via PIX.'; + String get storePaymentsEnabledHint => + 'Clientes podem pagar pedidos desta loja via PIX.'; @override - String get storePaymentsDisabledHint => 'Pagamentos desativados — ative para receber via PIX.'; + String get storePaymentsDisabledHint => + 'Pagamentos desativados — ative para receber via PIX.'; @override String get storePixKeyLabel => 'Chave PIX'; @override - String get storePixKeyHint => 'A chave PIX da loja é configurada no gateway de pagamento do território. Aqui você só liga ou desliga a aceitação de pagamentos.'; + String get storePixKeyHint => + 'A chave PIX da loja é configurada no gateway de pagamento do território. Aqui você só liga ou desliga a aceitação de pagamentos.'; @override String get sellerBalanceTitle => 'Seu saldo'; @override - String get sellerBalanceHint => 'Valores do território após vendas confirmadas. O saque é processado pelo território.'; + String get sellerBalanceHint => + 'Valores do território após vendas confirmadas. O saque é processado pelo território.'; @override String get sellerBalanceLoadError => 'Não foi possível carregar o saldo.'; @@ -1380,7 +1436,8 @@ class AppLocalizationsPt extends AppLocalizations { String get myProductsTitle => 'Meus produtos'; @override - String get myProductsEmpty => 'Nenhum produto publicado. Toque em Adicionar para criar o primeiro.'; + String get myProductsEmpty => + 'Nenhum produto publicado. Toque em Adicionar para criar o primeiro.'; @override String get addProduct => 'Adicionar'; @@ -1401,7 +1458,8 @@ class AppLocalizationsPt extends AppLocalizations { String get addProductPhotoDescriptionTitle => 'Foto e descrição'; @override - String get addProductDescriptionHint => 'Conte o que é, como usar e condições do território.'; + String get addProductDescriptionHint => + 'Conte o que é, como usar e condições do território.'; @override String get addProductReviewTitle => 'Revisão'; @@ -1419,7 +1477,8 @@ class AppLocalizationsPt extends AppLocalizations { String get addProductPhotoHint => 'Toque para escolher uma foto da galeria'; @override - String get addProductPhotoOptionalHint => 'Opcional — produtos com foto vendem melhor'; + String get addProductPhotoOptionalHint => + 'Opcional — produtos com foto vendem melhor'; @override String get productPhotoLabel => 'Foto'; @@ -1467,13 +1526,15 @@ class AppLocalizationsPt extends AppLocalizations { String get archiveProduct => 'Arquivar produto'; @override - String get archiveProductConfirm => 'O produto deixa de aparecer no Mercado. Você pode criar outro depois.'; + String get archiveProductConfirm => + 'O produto deixa de aparecer no Mercado. Você pode criar outro depois.'; @override String get errorSaveProduct => 'Erro ao salvar produto.'; @override - String get errorUploadProductPhoto => 'Não foi possível enviar a foto. Tente de novo.'; + String get errorUploadProductPhoto => + 'Não foi possível enviar a foto. Tente de novo.'; @override String get comingSoonJourneyTitle => 'Em breve'; @@ -1490,7 +1551,8 @@ class AppLocalizationsPt extends AppLocalizations { String get comingSoonNotifyTitle => 'Acompanhe o progresso'; @override - String get comingSoonNotifyMessage => 'Quando estiver no ar, você encontra aqui no hub Serviços. Sem pressa, com cuidado coletivo.'; + String get comingSoonNotifyMessage => + 'Quando estiver no ar, você encontra aqui no hub Serviços. Sem pressa, com cuidado coletivo.'; @override String get comingSoonUnderstood => 'Entendi'; diff --git a/frontend/arah.app/lib/l10n/app_pt.arb b/frontend/arah.app/lib/l10n/app_pt.arb index 2ab3d623..ab89d5cb 100644 --- a/frontend/arah.app/lib/l10n/app_pt.arb +++ b/frontend/arah.app/lib/l10n/app_pt.arb @@ -111,6 +111,8 @@ "sessionExpired": "Sessão expirada. Faça login novamente.", "enterToAccess": "Entre na sua conta para acessar perfil, publicar e notificações.", "map": "Mapa", +"mapFilterAll": "Todos", +"mapFilterWaterBodies": "Corpos d'água", "viewOnMap": "Ver no mapa", "mapEntity": "Estabelecimento / ponto no mapa", "mapPost": "Post",