Commit 1b68006dd4beff00099b4172b8240be6c0d00330
1 parent
3b0c279a
20260708
Showing
51 changed files
with
2430 additions
and
541 deletions
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Common/ILabelEntityPartnerScopeInput.cs
0 → 100644
| 1 | +namespace FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | + | |
| 3 | +/// <summary> | |
| 4 | +/// 标签类型 / 分类 / 多选项创建编辑入参中的 Company 适用范围字段。 | |
| 5 | +/// </summary> | |
| 6 | +public interface ILabelEntityPartnerScopeInput | |
| 7 | +{ | |
| 8 | + /// <summary>适用 Company:ALL / SPECIFIED</summary> | |
| 9 | + string? AppliedPartnerType { get; } | |
| 10 | + | |
| 11 | + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary> | |
| 12 | + List<string>? PartnerIds { get; } | |
| 13 | + | |
| 14 | + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary> | |
| 15 | + List<string>? CompanyIds { get; } | |
| 16 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelCreateInputVo.cs
| ... | ... | @@ -45,7 +45,8 @@ public class LabelCreateInputVo |
| 45 | 45 | |
| 46 | 46 | public string LabelCategoryId { get; set; } = string.Empty; |
| 47 | 47 | |
| 48 | - public string LabelTypeId { get; set; } = string.Empty; | |
| 48 | + /// <summary>标签类型 Id(<c>fl_label_type.Id</c>);可选,未传时标签不绑定类型</summary> | |
| 49 | + public string? LabelTypeId { get; set; } | |
| 49 | 50 | |
| 50 | 51 | public List<string> ProductIds { get; set; } = new(); |
| 51 | 52 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListInputVo.cs
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs
| ... | ... | @@ -31,7 +31,8 @@ public class LabelUpdateInputVo |
| 31 | 31 | |
| 32 | 32 | public string LabelCategoryId { get; set; } = string.Empty; |
| 33 | 33 | |
| 34 | - public string LabelTypeId { get; set; } = string.Empty; | |
| 34 | + /// <summary>标签类型 Id(<c>fl_label_type.Id</c>);可选,未传时清空类型绑定</summary> | |
| 35 | + public string? LabelTypeId { get; set; } | |
| 35 | 36 | |
| 36 | 37 | public List<string> ProductIds { get; set; } = new(); |
| 37 | 38 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryCreateInputVo.cs
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | + | |
| 1 | 3 | namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory; |
| 2 | 4 | |
| 3 | -public class LabelCategoryCreateInputVo | |
| 5 | +public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput | |
| 4 | 6 | { |
| 5 | 7 | public string CategoryCode { get; set; } = string.Empty; |
| 6 | 8 | |
| ... | ... | @@ -24,6 +26,17 @@ public class LabelCategoryCreateInputVo |
| 24 | 26 | public string ButtonAppearance { get; set; } = "TEXT"; |
| 25 | 27 | |
| 26 | 28 | /// <summary> |
| 29 | + /// 适用 Company:ALL / SPECIFIED | |
| 30 | + /// </summary> | |
| 31 | + public string? AppliedPartnerType { get; set; } | |
| 32 | + | |
| 33 | + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary> | |
| 34 | + public List<string>? PartnerIds { get; set; } | |
| 35 | + | |
| 36 | + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary> | |
| 37 | + public List<string>? CompanyIds { get; set; } | |
| 38 | + | |
| 39 | + /// <summary> | |
| 27 | 40 | /// 门店可用范围:ALL / SPECIFIED |
| 28 | 41 | /// </summary> |
| 29 | 42 | public string AvailabilityType { get; set; } = "ALL"; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListInputVo.cs
| ... | ... | @@ -8,6 +8,9 @@ public class LabelCategoryGetListInputVo : PagedAndSortedResultRequestDto |
| 8 | 8 | |
| 9 | 9 | public bool? State { get; set; } |
| 10 | 10 | |
| 11 | + /// <summary>Company 筛选(<c>fl_partner.Id</c>)</summary> | |
| 12 | + public string? PartnerId { get; set; } | |
| 13 | + | |
| 11 | 14 | /// <summary> |
| 12 | 15 | /// Region 筛选(<c>fl_group.Id</c>);含 <c>availabilityType=ALL</c> 的分类 |
| 13 | 16 | /// </summary> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetListOutputDto.cs
| ... | ... | @@ -18,6 +18,16 @@ public class LabelCategoryGetListOutputDto |
| 18 | 18 | |
| 19 | 19 | public string AvailabilityType { get; set; } = "ALL"; |
| 20 | 20 | |
| 21 | + public string AppliedPartnerType { get; set; } = "ALL"; | |
| 22 | + | |
| 23 | + /// <summary>适用 Company 展示(ALL 时为 All Companies)</summary> | |
| 24 | + public string Company { get; set; } = string.Empty; | |
| 25 | + | |
| 26 | + public List<string> PartnerIds { get; set; } = new(); | |
| 27 | + | |
| 28 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | |
| 29 | + public List<string> CompanyIds { get; set; } = new(); | |
| 30 | + | |
| 21 | 31 | /// <summary>适用 Region 展示(ALL 时为 All Regions)</summary> |
| 22 | 32 | public string Region { get; set; } = string.Empty; |
| 23 | 33 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelCategory/LabelCategoryGetOutputDto.cs
| ... | ... | @@ -18,6 +18,16 @@ public class LabelCategoryGetOutputDto |
| 18 | 18 | |
| 19 | 19 | public string AvailabilityType { get; set; } = "ALL"; |
| 20 | 20 | |
| 21 | + public string AppliedPartnerType { get; set; } = "ALL"; | |
| 22 | + | |
| 23 | + /// <summary>列表/详情 Company 展示</summary> | |
| 24 | + public string Company { get; set; } = string.Empty; | |
| 25 | + | |
| 26 | + public List<string> PartnerIds { get; set; } = new(); | |
| 27 | + | |
| 28 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | |
| 29 | + public List<string> CompanyIds { get; set; } = new(); | |
| 30 | + | |
| 21 | 31 | /// <summary>适用 Region Id 列表(多选;<c>ALL</c> 时为空)</summary> |
| 22 | 32 | public List<string> RegionIds { get; set; } = new(); |
| 23 | 33 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionCreateInputVo.cs
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | + | |
| 1 | 3 | namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; |
| 2 | 4 | |
| 3 | -public class LabelMultipleOptionCreateInputVo | |
| 5 | +public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput | |
| 4 | 6 | { |
| 5 | 7 | /// <summary>多选项编码(可选;未传或空字符串时存空,列表/详情出参为「无」)</summary> |
| 6 | 8 | public string? OptionCode { get; set; } |
| ... | ... | @@ -12,6 +14,17 @@ public class LabelMultipleOptionCreateInputVo |
| 12 | 14 | public bool State { get; set; } = true; |
| 13 | 15 | |
| 14 | 16 | /// <summary> |
| 17 | + /// 适用 Company:ALL / SPECIFIED | |
| 18 | + /// </summary> | |
| 19 | + public string? AppliedPartnerType { get; set; } | |
| 20 | + | |
| 21 | + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary> | |
| 22 | + public List<string>? PartnerIds { get; set; } | |
| 23 | + | |
| 24 | + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary> | |
| 25 | + public List<string>? CompanyIds { get; set; } | |
| 26 | + | |
| 27 | + /// <summary> | |
| 15 | 28 | /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED |
| 16 | 29 | /// </summary> |
| 17 | 30 | public string AvailabilityType { get; set; } = "ALL"; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListInputVo.cs
| ... | ... | @@ -8,6 +8,9 @@ public class LabelMultipleOptionGetListInputVo : PagedAndSortedResultRequestDto |
| 8 | 8 | |
| 9 | 9 | public bool? State { get; set; } |
| 10 | 10 | |
| 11 | + /// <summary>Company 筛选(<c>fl_partner.Id</c>)</summary> | |
| 12 | + public string? PartnerId { get; set; } | |
| 13 | + | |
| 11 | 14 | /// <summary> |
| 12 | 15 | /// Region 筛选(<c>fl_group.Id</c>) |
| 13 | 16 | /// </summary> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetListOutputDto.cs
| ... | ... | @@ -14,6 +14,14 @@ public class LabelMultipleOptionGetListOutputDto |
| 14 | 14 | |
| 15 | 15 | public string AvailabilityType { get; set; } = "ALL"; |
| 16 | 16 | |
| 17 | + public string AppliedPartnerType { get; set; } = "ALL"; | |
| 18 | + | |
| 19 | + public string Company { get; set; } = string.Empty; | |
| 20 | + | |
| 21 | + public List<string> PartnerIds { get; set; } = new(); | |
| 22 | + | |
| 23 | + public List<string> CompanyIds { get; set; } = new(); | |
| 24 | + | |
| 17 | 25 | public int OrderNum { get; set; } |
| 18 | 26 | |
| 19 | 27 | public DateTime LastEdited { get; set; } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelMultipleOption/LabelMultipleOptionGetOutputDto.cs
| ... | ... | @@ -16,6 +16,14 @@ public class LabelMultipleOptionGetOutputDto |
| 16 | 16 | |
| 17 | 17 | public string AvailabilityType { get; set; } = "ALL"; |
| 18 | 18 | |
| 19 | + public string AppliedPartnerType { get; set; } = "ALL"; | |
| 20 | + | |
| 21 | + public string Company { get; set; } = string.Empty; | |
| 22 | + | |
| 23 | + public List<string> PartnerIds { get; set; } = new(); | |
| 24 | + | |
| 25 | + public List<string> CompanyIds { get; set; } = new(); | |
| 26 | + | |
| 19 | 27 | public List<string> RegionIds { get; set; } = new(); |
| 20 | 28 | |
| 21 | 29 | public List<string> GroupIds { get; set; } = new(); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeCreateInputVo.cs
| 1 | +using FoodLabeling.Application.Contracts.Dtos.Common; | |
| 2 | + | |
| 1 | 3 | namespace FoodLabeling.Application.Contracts.Dtos.LabelType; |
| 2 | 4 | |
| 3 | -public class LabelTypeCreateInputVo | |
| 5 | +public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput | |
| 4 | 6 | { |
| 5 | 7 | public string TypeCode { get; set; } = string.Empty; |
| 6 | 8 | |
| ... | ... | @@ -9,6 +11,17 @@ public class LabelTypeCreateInputVo |
| 9 | 11 | public bool State { get; set; } = true; |
| 10 | 12 | |
| 11 | 13 | /// <summary> |
| 14 | + /// 适用 Company:ALL / SPECIFIED | |
| 15 | + /// </summary> | |
| 16 | + public string? AppliedPartnerType { get; set; } | |
| 17 | + | |
| 18 | + /// <summary>适用 Company(<c>fl_partner.Id</c>)</summary> | |
| 19 | + public List<string>? PartnerIds { get; set; } | |
| 20 | + | |
| 21 | + /// <summary>与 <see cref="PartnerIds"/> 相同(兼容字段)</summary> | |
| 22 | + public List<string>? CompanyIds { get; set; } | |
| 23 | + | |
| 24 | + /// <summary> | |
| 12 | 25 | /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED |
| 13 | 26 | /// </summary> |
| 14 | 27 | public string AvailabilityType { get; set; } = "ALL"; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListInputVo.cs
| ... | ... | @@ -8,6 +8,9 @@ public class LabelTypeGetListInputVo : PagedAndSortedResultRequestDto |
| 8 | 8 | |
| 9 | 9 | public bool? State { get; set; } |
| 10 | 10 | |
| 11 | + /// <summary>Company 筛选(<c>fl_partner.Id</c>)</summary> | |
| 12 | + public string? PartnerId { get; set; } | |
| 13 | + | |
| 11 | 14 | /// <summary> |
| 12 | 15 | /// Region 筛选(<c>fl_group.Id</c>);仅返回在该 Region 下存在标签实例的类型 |
| 13 | 16 | /// </summary> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetListOutputDto.cs
| ... | ... | @@ -12,6 +12,16 @@ public class LabelTypeGetListOutputDto |
| 12 | 12 | |
| 13 | 13 | public string AvailabilityType { get; set; } = "ALL"; |
| 14 | 14 | |
| 15 | + public string AppliedPartnerType { get; set; } = "ALL"; | |
| 16 | + | |
| 17 | + /// <summary>适用 Company 展示(ALL 时为 All Companies)</summary> | |
| 18 | + public string Company { get; set; } = string.Empty; | |
| 19 | + | |
| 20 | + public List<string> PartnerIds { get; set; } = new(); | |
| 21 | + | |
| 22 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | |
| 23 | + public List<string> CompanyIds { get; set; } = new(); | |
| 24 | + | |
| 15 | 25 | public int OrderNum { get; set; } |
| 16 | 26 | |
| 17 | 27 | /// <summary>列表列 No. of Labels:该类型下未删除标签数(统计 <c>fl_label</c>,非库表物理列)</summary> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/LabelType/LabelTypeGetOutputDto.cs
| ... | ... | @@ -14,6 +14,16 @@ public class LabelTypeGetOutputDto |
| 14 | 14 | |
| 15 | 15 | public string AvailabilityType { get; set; } = "ALL"; |
| 16 | 16 | |
| 17 | + public string AppliedPartnerType { get; set; } = "ALL"; | |
| 18 | + | |
| 19 | + /// <summary>列表/详情 Company 展示</summary> | |
| 20 | + public string Company { get; set; } = string.Empty; | |
| 21 | + | |
| 22 | + public List<string> PartnerIds { get; set; } = new(); | |
| 23 | + | |
| 24 | + /// <summary>与 <see cref="PartnerIds"/> 相同</summary> | |
| 25 | + public List<string> CompanyIds { get; set; } = new(); | |
| 26 | + | |
| 17 | 27 | public List<string> RegionIds { get; set; } = new(); |
| 18 | 28 | |
| 19 | 29 | public List<string> GroupIds { get; set; } = new(); | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/ProductCategory/ProductCategoryGetListInputVo.cs
| ... | ... | @@ -18,11 +18,16 @@ public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto |
| 18 | 18 | public bool? State { get; set; } |
| 19 | 19 | |
| 20 | 20 | /// <summary> |
| 21 | - /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类,以及 <c>availabilityType=ALL</c> 的分类 | |
| 21 | + /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类 | |
| 22 | 22 | /// </summary> |
| 23 | 23 | public string? GroupId { get; set; } |
| 24 | 24 | |
| 25 | 25 | /// <summary> |
| 26 | + /// 按 Company 筛选(<c>fl_partner.Id</c>) | |
| 27 | + /// </summary> | |
| 28 | + public string? PartnerId { get; set; } | |
| 29 | + | |
| 30 | + /// <summary> | |
| 26 | 31 | /// 按门店筛选(<c>location.Id</c>,Guid 字符串);优先于 <see cref="GroupId"/> |
| 27 | 32 | /// </summary> |
| 28 | 33 | public string? LocationId { get; set; } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/ILabelAppService.cs
| ... | ... | @@ -11,7 +11,7 @@ namespace FoodLabeling.Application.Contracts.IServices; |
| 11 | 11 | public interface ILabelAppService : IApplicationService |
| 12 | 12 | { |
| 13 | 13 | /// <summary> |
| 14 | - /// 按产品分页列表(一个产品展示多个标签)。支持 <c>GroupId</c> / <c>LocationId</c> 筛选(命中 <c>fl_label_region</c>、<c>fl_label_location</c> 或 <c>AppliedRegionType=ALL</c>);出参含 <c>locationIds</c>、<c>region</c>、<c>regionIds</c>、<c>appliedRegionType</c>。 | |
| 14 | + /// 按产品分页列表(一个产品展示多个标签)。支持 <c>GroupId</c> / <c>LocationId</c> 筛选(命中 <c>fl_label_region</c>、<c>fl_label_location</c> 或 <c>AppliedRegionType=ALL</c>);<c>labelTypeId</c> 为可选筛选;出参含 <c>locationIds</c>、<c>region</c>、<c>regionIds</c>、<c>appliedRegionType</c>。 | |
| 15 | 15 | /// </summary> |
| 16 | 16 | Task<PagedResultWithPageDto<LabelGetListOutputDto>> GetListAsync(LabelGetListInputVo input); |
| 17 | 17 | |
| ... | ... | @@ -21,7 +21,7 @@ public interface ILabelAppService : IApplicationService |
| 21 | 21 | Task<LabelGetOutputDto> GetAsync(string id); |
| 22 | 22 | |
| 23 | 23 | /// <summary> |
| 24 | - /// 新增标签。Body 支持 <c>appliedRegionType</c>(ALL/SPECIFIED)、<c>regionIds</c> / <c>groupIds</c>(落库 <c>fl_label_region</c>)、<c>locationIds</c>(主字段,落库 <c>fl_label_location</c>);<c>locationId</c> 与 <c>locationIds</c> 合并。 | |
| 24 | + /// 新增标签。Body 支持 <c>appliedRegionType</c>(ALL/SPECIFIED)、<c>regionIds</c> / <c>groupIds</c>(落库 <c>fl_label_region</c>)、<c>locationIds</c>(主字段,落库 <c>fl_label_location</c>);<c>locationId</c> 与 <c>locationIds</c> 合并;<c>labelTypeId</c> 可选。 | |
| 25 | 25 | /// </summary> |
| 26 | 26 | Task<LabelGetOutputDto> CreateAsync(LabelCreateInputVo input); |
| 27 | 27 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/IServices/IUsAppAuthAppService.cs
| ... | ... | @@ -11,12 +11,12 @@ namespace FoodLabeling.Application.Contracts.IServices; |
| 11 | 11 | public interface IUsAppAuthAppService : IApplicationService |
| 12 | 12 | { |
| 13 | 13 | /// <summary> |
| 14 | - /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回 userlocation 绑定的门店 | |
| 14 | + /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回可选门店(Company Admin 为绑定 Company 下全部门店) | |
| 15 | 15 | /// </summary> |
| 16 | 16 | Task<UsAppLoginOutputDto> LoginAsync(UsAppLoginInputVo input); |
| 17 | 17 | |
| 18 | 18 | /// <summary> |
| 19 | - /// 获取当前登录账号已绑定的门店(用于切换门店等场景) | |
| 19 | + /// 获取当前登录账号可选门店(Company Admin 返回绑定 Company 下全部门店,用于切换门店等场景) | |
| 20 | 20 | /// </summary> |
| 21 | 21 | Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync(); |
| 22 | 22 | |
| ... | ... | @@ -62,7 +62,7 @@ public interface IUsAppAuthAppService : IApplicationService |
| 62 | 62 | Task ChangePasswordAsync(UsAppChangePasswordInputVo input); |
| 63 | 63 | |
| 64 | 64 | /// <summary> |
| 65 | - /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) | |
| 65 | + /// 按门店 Id 查询 Location 详情(须为当前账号可选门店;Company Admin 含绑定 Company 下任意门店) | |
| 66 | 66 | /// </summary> |
| 67 | 67 | /// <param name="locationId">门店 Guid 字符串</param> |
| 68 | 68 | /// <returns>店名、地址、电话、经营时间(operatingHours)、店长信息</returns> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LabelEntityPartnerScopeHelper.cs
0 → 100644
| 1 | +using FoodLabeling.Application.Services.DbModels; | |
| 2 | +using SqlSugar; | |
| 3 | +using Volo.Abp; | |
| 4 | +using Volo.Abp.Guids; | |
| 5 | + | |
| 6 | +namespace FoodLabeling.Application.Helpers; | |
| 7 | + | |
| 8 | +/// <summary> | |
| 9 | +/// 标签类型 / 分类 / 多选项适用 Company:ALL/SPECIFIED + 多选落库(对齐 <see cref="LabelTemplateScopeHelper"/> 的 Company 维度)。 | |
| 10 | +/// </summary> | |
| 11 | +public static class LabelEntityPartnerScopeHelper | |
| 12 | +{ | |
| 13 | + public const string ScopeAll = "ALL"; | |
| 14 | + public const string ScopeSpecified = "SPECIFIED"; | |
| 15 | + | |
| 16 | + public const string AllCompaniesDisplay = "All Companies"; | |
| 17 | + public const string EmptyDisplay = "无"; | |
| 18 | + | |
| 19 | + /// <summary>标签实体种类(对应主表与 partner 关联表)。</summary> | |
| 20 | + public enum LabelEntityPartnerKind | |
| 21 | + { | |
| 22 | + /// <summary>标签类型 <c>fl_label_type</c></summary> | |
| 23 | + Type, | |
| 24 | + | |
| 25 | + /// <summary>标签分类 <c>fl_label_category</c></summary> | |
| 26 | + Category, | |
| 27 | + | |
| 28 | + /// <summary>标签多选项 <c>fl_label_multiple_option</c></summary> | |
| 29 | + MultipleOption | |
| 30 | + } | |
| 31 | + | |
| 32 | + public sealed class LabelEntityPartnerScopeSaveResult | |
| 33 | + { | |
| 34 | + public string AppliedPartnerType { get; init; } = ScopeAll; | |
| 35 | + | |
| 36 | + public List<string> PartnerIds { get; init; } = new(); | |
| 37 | + } | |
| 38 | + | |
| 39 | + public sealed class LabelEntityPartnerScopeDisplay | |
| 40 | + { | |
| 41 | + public string Company { get; init; } = AllCompaniesDisplay; | |
| 42 | + | |
| 43 | + public string AppliedPartnerType { get; init; } = ScopeAll; | |
| 44 | + | |
| 45 | + public List<string> PartnerIds { get; init; } = new(); | |
| 46 | + } | |
| 47 | + | |
| 48 | + private static LabelEntityPartnerScopeSchemaStatus? _cachedSchema; | |
| 49 | + | |
| 50 | + private sealed class LabelEntityPartnerScopeSchemaStatus | |
| 51 | + { | |
| 52 | + public bool HasTypePartnerColumn { get; init; } | |
| 53 | + | |
| 54 | + public bool HasCategoryPartnerColumn { get; init; } | |
| 55 | + | |
| 56 | + public bool HasMultipleOptionPartnerColumn { get; init; } | |
| 57 | + | |
| 58 | + public bool HasTypePartnerTable { get; init; } | |
| 59 | + | |
| 60 | + public bool HasCategoryPartnerTable { get; init; } | |
| 61 | + | |
| 62 | + public bool HasMultipleOptionPartnerTable { get; init; } | |
| 63 | + } | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// 合并 <paramref name="partnerIds"/> 与 <paramref name="companyIds"/> 并去重排序。 | |
| 67 | + /// </summary> | |
| 68 | + public static List<string> NormalizePartnerIds(IReadOnlyList<string>? partnerIds, IReadOnlyList<string>? companyIds) | |
| 69 | + { | |
| 70 | + var merged = new HashSet<string>(StringComparer.Ordinal); | |
| 71 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(partnerIds)) | |
| 72 | + { | |
| 73 | + merged.Add(id); | |
| 74 | + } | |
| 75 | + | |
| 76 | + foreach (var id in LocationScopeBindingHelper.NormalizeIds(companyIds)) | |
| 77 | + { | |
| 78 | + merged.Add(id); | |
| 79 | + } | |
| 80 | + | |
| 81 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | |
| 82 | + } | |
| 83 | + | |
| 84 | + /// <summary> | |
| 85 | + /// 解析适用 Company 范围类型(ALL/SPECIFIED)。 | |
| 86 | + /// </summary> | |
| 87 | + public static string ResolveAppliedPartnerType(string? type, List<string> partnerIds, bool hasArrayInPayload) | |
| 88 | + { | |
| 89 | + if (partnerIds.Count > 0) | |
| 90 | + { | |
| 91 | + return ScopeSpecified; | |
| 92 | + } | |
| 93 | + | |
| 94 | + var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant(); | |
| 95 | + if (hasArrayInPayload && string.Equals(normalized, ScopeAll, StringComparison.OrdinalIgnoreCase)) | |
| 96 | + { | |
| 97 | + return ScopeAll; | |
| 98 | + } | |
| 99 | + | |
| 100 | + return string.Equals(normalized, ScopeSpecified, StringComparison.OrdinalIgnoreCase) | |
| 101 | + ? ScopeSpecified | |
| 102 | + : ScopeAll; | |
| 103 | + } | |
| 104 | + | |
| 105 | + /// <summary> | |
| 106 | + /// 解析新增/编辑入参中的 Company 范围并校验 partner Id 有效性。 | |
| 107 | + /// </summary> | |
| 108 | + public static async Task<LabelEntityPartnerScopeSaveResult> ResolvePartnerScopeForSaveAsync( | |
| 109 | + ISqlSugarClient db, | |
| 110 | + string? appliedPartnerType, | |
| 111 | + IReadOnlyList<string>? partnerIds, | |
| 112 | + IReadOnlyList<string>? companyIds) | |
| 113 | + { | |
| 114 | + var normalizedPartnerIds = NormalizePartnerIds(partnerIds, companyIds); | |
| 115 | + var partnerType = ResolveAppliedPartnerType( | |
| 116 | + appliedPartnerType, | |
| 117 | + normalizedPartnerIds, | |
| 118 | + partnerIds is not null || companyIds is not null); | |
| 119 | + | |
| 120 | + ValidatePartnerType(partnerType); | |
| 121 | + | |
| 122 | + if (string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) | |
| 123 | + && normalizedPartnerIds.Count == 0) | |
| 124 | + { | |
| 125 | + throw new UserFriendlyException("指定适用 Company 时,partnerIds/companyIds 不能为空"); | |
| 126 | + } | |
| 127 | + | |
| 128 | + if (normalizedPartnerIds.Count > 0) | |
| 129 | + { | |
| 130 | + await ValidatePartnerIdsExistAsync(db, normalizedPartnerIds); | |
| 131 | + } | |
| 132 | + | |
| 133 | + return new LabelEntityPartnerScopeSaveResult | |
| 134 | + { | |
| 135 | + AppliedPartnerType = partnerType, | |
| 136 | + PartnerIds = normalizedPartnerIds | |
| 137 | + }; | |
| 138 | + } | |
| 139 | + | |
| 140 | + /// <summary> | |
| 141 | + /// 保存 Company 适用范围(关联表 + <c>AppliedPartnerType</c> 列);未执行迁移脚本时 no-op。 | |
| 142 | + /// </summary> | |
| 143 | + public static async Task SavePartnerScopeAsync( | |
| 144 | + ISqlSugarClient db, | |
| 145 | + IGuidGenerator guidGenerator, | |
| 146 | + LabelEntityPartnerKind kind, | |
| 147 | + string entityId, | |
| 148 | + LabelEntityPartnerScopeSaveResult scope, | |
| 149 | + string? currentUserId, | |
| 150 | + DateTime now) | |
| 151 | + { | |
| 152 | + if (string.IsNullOrWhiteSpace(entityId)) | |
| 153 | + { | |
| 154 | + return; | |
| 155 | + } | |
| 156 | + | |
| 157 | + var schema = await GetSchemaStatusAsync(db); | |
| 158 | + if (!HasPartnerScopeForKind(schema, kind)) | |
| 159 | + { | |
| 160 | + return; | |
| 161 | + } | |
| 162 | + | |
| 163 | + await DeletePartnerScopeRowsAsync(db, kind, entityId); | |
| 164 | + | |
| 165 | + if (string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) | |
| 166 | + && scope.PartnerIds.Count > 0) | |
| 167 | + { | |
| 168 | + await InsertPartnerRowsAsync(db, guidGenerator, kind, entityId, scope.PartnerIds, currentUserId, now); | |
| 169 | + } | |
| 170 | + | |
| 171 | + await SetAppliedPartnerTypeAsync(db, kind, entityId, scope.AppliedPartnerType); | |
| 172 | + } | |
| 173 | + | |
| 174 | + /// <summary> | |
| 175 | + /// 删除实体下 Company 关联行;未迁移 partner 表时跳过。 | |
| 176 | + /// </summary> | |
| 177 | + public static async Task DeletePartnerScopeRowsAsync( | |
| 178 | + ISqlSugarClient db, | |
| 179 | + LabelEntityPartnerKind kind, | |
| 180 | + string entityId) | |
| 181 | + { | |
| 182 | + if (string.IsNullOrWhiteSpace(entityId)) | |
| 183 | + { | |
| 184 | + return; | |
| 185 | + } | |
| 186 | + | |
| 187 | + var schema = await GetSchemaStatusAsync(db); | |
| 188 | + if (!HasPartnerTableForKind(schema, kind)) | |
| 189 | + { | |
| 190 | + return; | |
| 191 | + } | |
| 192 | + | |
| 193 | + switch (kind) | |
| 194 | + { | |
| 195 | + case LabelEntityPartnerKind.Type: | |
| 196 | + await db.Deleteable<FlLabelTypePartnerDbEntity>() | |
| 197 | + .Where(x => x.LabelTypeId == entityId) | |
| 198 | + .ExecuteCommandAsync(); | |
| 199 | + break; | |
| 200 | + case LabelEntityPartnerKind.Category: | |
| 201 | + await db.Deleteable<FlLabelCategoryPartnerDbEntity>() | |
| 202 | + .Where(x => x.CategoryId == entityId) | |
| 203 | + .ExecuteCommandAsync(); | |
| 204 | + break; | |
| 205 | + case LabelEntityPartnerKind.MultipleOption: | |
| 206 | + await db.Deleteable<FlLabelMultipleOptionPartnerDbEntity>() | |
| 207 | + .Where(x => x.MultipleOptionId == entityId) | |
| 208 | + .ExecuteCommandAsync(); | |
| 209 | + break; | |
| 210 | + } | |
| 211 | + } | |
| 212 | + | |
| 213 | + /// <summary> | |
| 214 | + /// 批量加载 Company 展示与 Id 数组(详情/列表)。 | |
| 215 | + /// </summary> | |
| 216 | + public static async Task<Dictionary<string, LabelEntityPartnerScopeDisplay>> BuildPartnerScopeDisplayMapAsync( | |
| 217 | + ISqlSugarClient db, | |
| 218 | + LabelEntityPartnerKind kind, | |
| 219 | + IReadOnlyList<string> entityIds) | |
| 220 | + { | |
| 221 | + var result = new Dictionary<string, LabelEntityPartnerScopeDisplay>(StringComparer.Ordinal); | |
| 222 | + if (entityIds.Count == 0) | |
| 223 | + { | |
| 224 | + return result; | |
| 225 | + } | |
| 226 | + | |
| 227 | + var ids = entityIds.Where(x => !string.IsNullOrWhiteSpace(x)) | |
| 228 | + .Select(x => x.Trim()) | |
| 229 | + .Distinct(StringComparer.Ordinal) | |
| 230 | + .ToList(); | |
| 231 | + if (ids.Count == 0) | |
| 232 | + { | |
| 233 | + return result; | |
| 234 | + } | |
| 235 | + | |
| 236 | + var schema = await GetSchemaStatusAsync(db); | |
| 237 | + var hasScope = HasPartnerScopeForKind(schema, kind); | |
| 238 | + | |
| 239 | + var partnerLinks = hasScope | |
| 240 | + ? await LoadPartnerLinksAsync(db, kind, ids) | |
| 241 | + : new List<(string EntityId, string PartnerId)>(); | |
| 242 | + | |
| 243 | + var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList(); | |
| 244 | + var partnerNameById = await LoadPartnerNameMapAsync(db, partnerIdSet); | |
| 245 | + | |
| 246 | + foreach (var entityId in ids) | |
| 247 | + { | |
| 248 | + var pIds = LocationScopeBindingHelper.NormalizeIds( | |
| 249 | + partnerLinks.Where(x => x.EntityId == entityId).Select(x => x.PartnerId).ToList()); | |
| 250 | + | |
| 251 | + var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; | |
| 252 | + if (hasScope) | |
| 253 | + { | |
| 254 | + var storedType = await GetAppliedPartnerTypeAsync(db, kind, entityId); | |
| 255 | + if (!string.IsNullOrWhiteSpace(storedType)) | |
| 256 | + { | |
| 257 | + partnerType = NormalizeScopeType(storedType, ScopeAll); | |
| 258 | + } | |
| 259 | + else if (pIds.Count > 0) | |
| 260 | + { | |
| 261 | + partnerType = ScopeSpecified; | |
| 262 | + } | |
| 263 | + } | |
| 264 | + | |
| 265 | + var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase) | |
| 266 | + ? AllCompaniesDisplay | |
| 267 | + : BuildCompanyDisplay(pIds, partnerNameById); | |
| 268 | + | |
| 269 | + result[entityId] = new LabelEntityPartnerScopeDisplay | |
| 270 | + { | |
| 271 | + Company = companyDisplay, | |
| 272 | + AppliedPartnerType = partnerType, | |
| 273 | + PartnerIds = pIds | |
| 274 | + }; | |
| 275 | + } | |
| 276 | + | |
| 277 | + return result; | |
| 278 | + } | |
| 279 | + | |
| 280 | + /// <summary> | |
| 281 | + /// 列表 Company 列展示文案。 | |
| 282 | + /// </summary> | |
| 283 | + public static string BuildCompanyDisplay( | |
| 284 | + IReadOnlyList<string> partnerIds, | |
| 285 | + IReadOnlyDictionary<string, string> partnerNameById) | |
| 286 | + { | |
| 287 | + if (partnerIds.Count == 0) | |
| 288 | + { | |
| 289 | + return EmptyDisplay; | |
| 290 | + } | |
| 291 | + | |
| 292 | + var names = partnerIds | |
| 293 | + .Select(id => partnerNameById.TryGetValue(id, out var name) ? name : id) | |
| 294 | + .Select(x => x?.Trim()) | |
| 295 | + .Where(x => !string.IsNullOrEmpty(x)) | |
| 296 | + .Distinct(StringComparer.OrdinalIgnoreCase) | |
| 297 | + .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) | |
| 298 | + .ToList(); | |
| 299 | + | |
| 300 | + return names.Count > 0 ? string.Join(", ", names) : EmptyDisplay; | |
| 301 | + } | |
| 302 | + | |
| 303 | + /// <summary> | |
| 304 | + /// 列表筛选:按可见 Company 过滤标签类型(与 Region/Location 筛选 AND)。 | |
| 305 | + /// </summary> | |
| 306 | + public static async Task<ISugarQueryable<FlLabelTypeDbEntity>> ApplyTypePartnerListFilterAsync( | |
| 307 | + ISqlSugarClient db, | |
| 308 | + ISugarQueryable<FlLabelTypeDbEntity> query, | |
| 309 | + IReadOnlyList<string>? scopedPartnerIds) | |
| 310 | + { | |
| 311 | + if (scopedPartnerIds is null) | |
| 312 | + { | |
| 313 | + return query; | |
| 314 | + } | |
| 315 | + | |
| 316 | + var schema = await GetSchemaStatusAsync(db); | |
| 317 | + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.Type)) | |
| 318 | + { | |
| 319 | + return query; | |
| 320 | + } | |
| 321 | + | |
| 322 | + if (scopedPartnerIds.Count == 0) | |
| 323 | + { | |
| 324 | + return query.Where(t => | |
| 325 | + !SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>() | |
| 326 | + .Where(p => p.LabelTypeId == t.Id) | |
| 327 | + .Any()); | |
| 328 | + } | |
| 329 | + | |
| 330 | + return query.Where(t => | |
| 331 | + !SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>() | |
| 332 | + .Where(p => p.LabelTypeId == t.Id) | |
| 333 | + .Any() | |
| 334 | + || SqlFunc.Subqueryable<FlLabelTypePartnerDbEntity>() | |
| 335 | + .Where(p => p.LabelTypeId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) | |
| 336 | + .Any()); | |
| 337 | + } | |
| 338 | + | |
| 339 | + /// <summary> | |
| 340 | + /// 列表筛选:按可见 Company 过滤标签分类。 | |
| 341 | + /// </summary> | |
| 342 | + public static async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryPartnerListFilterAsync( | |
| 343 | + ISqlSugarClient db, | |
| 344 | + ISugarQueryable<FlLabelCategoryDbEntity> query, | |
| 345 | + IReadOnlyList<string>? scopedPartnerIds) | |
| 346 | + { | |
| 347 | + if (scopedPartnerIds is null) | |
| 348 | + { | |
| 349 | + return query; | |
| 350 | + } | |
| 351 | + | |
| 352 | + var schema = await GetSchemaStatusAsync(db); | |
| 353 | + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.Category)) | |
| 354 | + { | |
| 355 | + return query; | |
| 356 | + } | |
| 357 | + | |
| 358 | + if (scopedPartnerIds.Count == 0) | |
| 359 | + { | |
| 360 | + return query.Where(c => | |
| 361 | + !SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>() | |
| 362 | + .Where(p => p.CategoryId == c.Id) | |
| 363 | + .Any()); | |
| 364 | + } | |
| 365 | + | |
| 366 | + return query.Where(c => | |
| 367 | + !SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>() | |
| 368 | + .Where(p => p.CategoryId == c.Id) | |
| 369 | + .Any() | |
| 370 | + || SqlFunc.Subqueryable<FlLabelCategoryPartnerDbEntity>() | |
| 371 | + .Where(p => p.CategoryId == c.Id && scopedPartnerIds.Contains(p.PartnerId)) | |
| 372 | + .Any()); | |
| 373 | + } | |
| 374 | + | |
| 375 | + /// <summary> | |
| 376 | + /// 列表筛选:按可见 Company 过滤标签多选项。 | |
| 377 | + /// </summary> | |
| 378 | + public static async Task<ISugarQueryable<FlLabelMultipleOptionDbEntity>> ApplyMultipleOptionPartnerListFilterAsync( | |
| 379 | + ISqlSugarClient db, | |
| 380 | + ISugarQueryable<FlLabelMultipleOptionDbEntity> query, | |
| 381 | + IReadOnlyList<string>? scopedPartnerIds) | |
| 382 | + { | |
| 383 | + if (scopedPartnerIds is null) | |
| 384 | + { | |
| 385 | + return query; | |
| 386 | + } | |
| 387 | + | |
| 388 | + var schema = await GetSchemaStatusAsync(db); | |
| 389 | + if (!HasPartnerTableForKind(schema, LabelEntityPartnerKind.MultipleOption)) | |
| 390 | + { | |
| 391 | + return query; | |
| 392 | + } | |
| 393 | + | |
| 394 | + if (scopedPartnerIds.Count == 0) | |
| 395 | + { | |
| 396 | + return query.Where(o => | |
| 397 | + !SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>() | |
| 398 | + .Where(p => p.MultipleOptionId == o.Id) | |
| 399 | + .Any()); | |
| 400 | + } | |
| 401 | + | |
| 402 | + return query.Where(o => | |
| 403 | + !SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>() | |
| 404 | + .Where(p => p.MultipleOptionId == o.Id) | |
| 405 | + .Any() | |
| 406 | + || SqlFunc.Subqueryable<FlLabelMultipleOptionPartnerDbEntity>() | |
| 407 | + .Where(p => p.MultipleOptionId == o.Id && scopedPartnerIds.Contains(p.PartnerId)) | |
| 408 | + .Any()); | |
| 409 | + } | |
| 410 | + | |
| 411 | + /// <summary> | |
| 412 | + /// 由 Query <c>partnerId</c> 解析列表 Company 筛选 Id;未传则返回 <c>null</c>。 | |
| 413 | + /// </summary> | |
| 414 | + public static async Task<List<string>?> ResolveScopedPartnerIdsForListAsync( | |
| 415 | + ISqlSugarClient db, | |
| 416 | + string? partnerId) | |
| 417 | + { | |
| 418 | + var pid = partnerId?.Trim(); | |
| 419 | + if (string.IsNullOrWhiteSpace(pid)) | |
| 420 | + { | |
| 421 | + return null; | |
| 422 | + } | |
| 423 | + | |
| 424 | + var exists = await db.Queryable<FlPartnerDbEntity>() | |
| 425 | + .AnyAsync(x => !x.IsDeleted && x.Id == pid); | |
| 426 | + return exists ? new List<string> { pid } : new List<string>(); | |
| 427 | + } | |
| 428 | + | |
| 429 | + /// <summary> | |
| 430 | + /// 由 Query partnerId 或可见门店范围解析列表 Company 筛选 Id;均未限时返回 <c>null</c>。 | |
| 431 | + /// </summary> | |
| 432 | + public static async Task<List<string>?> ResolveListPartnerIdsAsync( | |
| 433 | + ISqlSugarClient db, | |
| 434 | + string? partnerId, | |
| 435 | + List<string>? scopedLocationIds) | |
| 436 | + { | |
| 437 | + var fromQuery = await ResolveScopedPartnerIdsForListAsync(db, partnerId); | |
| 438 | + if (fromQuery is not null) | |
| 439 | + { | |
| 440 | + return fromQuery; | |
| 441 | + } | |
| 442 | + | |
| 443 | + if (scopedLocationIds is null) | |
| 444 | + { | |
| 445 | + return null; | |
| 446 | + } | |
| 447 | + | |
| 448 | + if (scopedLocationIds.Count == 0) | |
| 449 | + { | |
| 450 | + return new List<string>(); | |
| 451 | + } | |
| 452 | + | |
| 453 | + return await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, scopedLocationIds); | |
| 454 | + } | |
| 455 | + | |
| 456 | + /// <summary>单元测试或切换库后调用。</summary> | |
| 457 | + public static void ResetSchemaCacheForTests() => _cachedSchema = null; | |
| 458 | + | |
| 459 | + private static void ValidatePartnerType(string type) | |
| 460 | + { | |
| 461 | + if (type != ScopeAll && type != ScopeSpecified) | |
| 462 | + { | |
| 463 | + throw new UserFriendlyException("适用 Company 范围不合法(ALL/SPECIFIED)"); | |
| 464 | + } | |
| 465 | + } | |
| 466 | + | |
| 467 | + private static string NormalizeScopeType(string? type, string fallback) | |
| 468 | + { | |
| 469 | + var t = (type ?? fallback).Trim().ToUpperInvariant(); | |
| 470 | + return t == ScopeSpecified ? ScopeSpecified : ScopeAll; | |
| 471 | + } | |
| 472 | + | |
| 473 | + private static async Task ValidatePartnerIdsExistAsync(ISqlSugarClient db, List<string> partnerIds) | |
| 474 | + { | |
| 475 | + if (partnerIds.Count == 0) | |
| 476 | + { | |
| 477 | + return; | |
| 478 | + } | |
| 479 | + | |
| 480 | + var count = await db.Queryable<FlPartnerDbEntity>() | |
| 481 | + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) | |
| 482 | + .CountAsync(); | |
| 483 | + if (count != partnerIds.Count) | |
| 484 | + { | |
| 485 | + throw new UserFriendlyException("存在无效的 Company(partnerIds/companyIds),请刷新后重试"); | |
| 486 | + } | |
| 487 | + } | |
| 488 | + | |
| 489 | + private static async Task<LabelEntityPartnerScopeSchemaStatus> GetSchemaStatusAsync(ISqlSugarClient db) | |
| 490 | + { | |
| 491 | + if (_cachedSchema is not null) | |
| 492 | + { | |
| 493 | + return _cachedSchema; | |
| 494 | + } | |
| 495 | + | |
| 496 | + try | |
| 497 | + { | |
| 498 | + _cachedSchema = new LabelEntityPartnerScopeSchemaStatus | |
| 499 | + { | |
| 500 | + HasTypePartnerColumn = await ColumnExistsAsync(db, "fl_label_type", "AppliedPartnerType"), | |
| 501 | + HasCategoryPartnerColumn = await ColumnExistsAsync(db, "fl_label_category", "AppliedPartnerType"), | |
| 502 | + HasMultipleOptionPartnerColumn = await ColumnExistsAsync( | |
| 503 | + db, "fl_label_multiple_option", "AppliedPartnerType"), | |
| 504 | + HasTypePartnerTable = await TableExistsAsync(db, "fl_label_type_partner"), | |
| 505 | + HasCategoryPartnerTable = await TableExistsAsync(db, "fl_label_category_partner"), | |
| 506 | + HasMultipleOptionPartnerTable = await TableExistsAsync(db, "fl_label_multiple_option_partner") | |
| 507 | + }; | |
| 508 | + } | |
| 509 | + catch | |
| 510 | + { | |
| 511 | + _cachedSchema = new LabelEntityPartnerScopeSchemaStatus(); | |
| 512 | + } | |
| 513 | + | |
| 514 | + return _cachedSchema; | |
| 515 | + } | |
| 516 | + | |
| 517 | + private static bool HasPartnerScopeForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) => | |
| 518 | + HasPartnerTableForKind(schema, kind) && HasPartnerColumnForKind(schema, kind); | |
| 519 | + | |
| 520 | + private static bool HasPartnerTableForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) => | |
| 521 | + kind switch | |
| 522 | + { | |
| 523 | + LabelEntityPartnerKind.Type => schema.HasTypePartnerTable, | |
| 524 | + LabelEntityPartnerKind.Category => schema.HasCategoryPartnerTable, | |
| 525 | + LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerTable, | |
| 526 | + _ => false | |
| 527 | + }; | |
| 528 | + | |
| 529 | + private static bool HasPartnerColumnForKind(LabelEntityPartnerScopeSchemaStatus schema, LabelEntityPartnerKind kind) => | |
| 530 | + kind switch | |
| 531 | + { | |
| 532 | + LabelEntityPartnerKind.Type => schema.HasTypePartnerColumn, | |
| 533 | + LabelEntityPartnerKind.Category => schema.HasCategoryPartnerColumn, | |
| 534 | + LabelEntityPartnerKind.MultipleOption => schema.HasMultipleOptionPartnerColumn, | |
| 535 | + _ => false | |
| 536 | + }; | |
| 537 | + | |
| 538 | + private static async Task<bool> ColumnExistsAsync(ISqlSugarClient db, string tableName, string columnName) | |
| 539 | + { | |
| 540 | + var count = await db.Ado.GetIntAsync( | |
| 541 | + """ | |
| 542 | + SELECT COUNT(*) | |
| 543 | + FROM information_schema.COLUMNS | |
| 544 | + WHERE TABLE_SCHEMA = DATABASE() | |
| 545 | + AND TABLE_NAME = @tableName | |
| 546 | + AND COLUMN_NAME = @columnName | |
| 547 | + """, | |
| 548 | + new { tableName, columnName }); | |
| 549 | + return count > 0; | |
| 550 | + } | |
| 551 | + | |
| 552 | + private static async Task<bool> TableExistsAsync(ISqlSugarClient db, string tableName) | |
| 553 | + { | |
| 554 | + var count = await db.Ado.GetIntAsync( | |
| 555 | + """ | |
| 556 | + SELECT COUNT(*) | |
| 557 | + FROM information_schema.TABLES | |
| 558 | + WHERE TABLE_SCHEMA = DATABASE() | |
| 559 | + AND TABLE_NAME = @tableName | |
| 560 | + """, | |
| 561 | + new { tableName }); | |
| 562 | + return count > 0; | |
| 563 | + } | |
| 564 | + | |
| 565 | + private static async Task SetAppliedPartnerTypeAsync( | |
| 566 | + ISqlSugarClient db, | |
| 567 | + LabelEntityPartnerKind kind, | |
| 568 | + string entityId, | |
| 569 | + string appliedPartnerType) | |
| 570 | + { | |
| 571 | + var schema = await GetSchemaStatusAsync(db); | |
| 572 | + if (!HasPartnerColumnForKind(schema, kind)) | |
| 573 | + { | |
| 574 | + return; | |
| 575 | + } | |
| 576 | + | |
| 577 | + var tableName = kind switch | |
| 578 | + { | |
| 579 | + LabelEntityPartnerKind.Type => "fl_label_type", | |
| 580 | + LabelEntityPartnerKind.Category => "fl_label_category", | |
| 581 | + LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", | |
| 582 | + _ => string.Empty | |
| 583 | + }; | |
| 584 | + | |
| 585 | + if (string.IsNullOrEmpty(tableName)) | |
| 586 | + { | |
| 587 | + return; | |
| 588 | + } | |
| 589 | + | |
| 590 | + await db.Ado.ExecuteCommandAsync( | |
| 591 | + $"UPDATE `{tableName}` SET AppliedPartnerType = @type WHERE Id = @id", | |
| 592 | + new { type = appliedPartnerType.Trim(), id = entityId.Trim() }); | |
| 593 | + } | |
| 594 | + | |
| 595 | + private static async Task<string?> GetAppliedPartnerTypeAsync( | |
| 596 | + ISqlSugarClient db, | |
| 597 | + LabelEntityPartnerKind kind, | |
| 598 | + string entityId) | |
| 599 | + { | |
| 600 | + var schema = await GetSchemaStatusAsync(db); | |
| 601 | + if (!HasPartnerColumnForKind(schema, kind)) | |
| 602 | + { | |
| 603 | + return null; | |
| 604 | + } | |
| 605 | + | |
| 606 | + var tableName = kind switch | |
| 607 | + { | |
| 608 | + LabelEntityPartnerKind.Type => "fl_label_type", | |
| 609 | + LabelEntityPartnerKind.Category => "fl_label_category", | |
| 610 | + LabelEntityPartnerKind.MultipleOption => "fl_label_multiple_option", | |
| 611 | + _ => string.Empty | |
| 612 | + }; | |
| 613 | + | |
| 614 | + if (string.IsNullOrEmpty(tableName)) | |
| 615 | + { | |
| 616 | + return null; | |
| 617 | + } | |
| 618 | + | |
| 619 | + var value = await db.Ado.GetScalarAsync( | |
| 620 | + $"SELECT AppliedPartnerType FROM `{tableName}` WHERE Id = @id LIMIT 1", | |
| 621 | + new { id = entityId.Trim() }); | |
| 622 | + | |
| 623 | + return value?.ToString()?.Trim(); | |
| 624 | + } | |
| 625 | + | |
| 626 | + private static async Task<List<(string EntityId, string PartnerId)>> LoadPartnerLinksAsync( | |
| 627 | + ISqlSugarClient db, | |
| 628 | + LabelEntityPartnerKind kind, | |
| 629 | + IReadOnlyList<string> entityIds) | |
| 630 | + { | |
| 631 | + switch (kind) | |
| 632 | + { | |
| 633 | + case LabelEntityPartnerKind.Type: | |
| 634 | + { | |
| 635 | + var rows = await db.Queryable<FlLabelTypePartnerDbEntity>() | |
| 636 | + .Where(x => entityIds.Contains(x.LabelTypeId)) | |
| 637 | + .ToListAsync(); | |
| 638 | + return rows.Select(x => (x.LabelTypeId, x.PartnerId)).ToList(); | |
| 639 | + } | |
| 640 | + case LabelEntityPartnerKind.Category: | |
| 641 | + { | |
| 642 | + var rows = await db.Queryable<FlLabelCategoryPartnerDbEntity>() | |
| 643 | + .Where(x => entityIds.Contains(x.CategoryId)) | |
| 644 | + .ToListAsync(); | |
| 645 | + return rows.Select(x => (x.CategoryId, x.PartnerId)).ToList(); | |
| 646 | + } | |
| 647 | + case LabelEntityPartnerKind.MultipleOption: | |
| 648 | + { | |
| 649 | + var rows = await db.Queryable<FlLabelMultipleOptionPartnerDbEntity>() | |
| 650 | + .Where(x => entityIds.Contains(x.MultipleOptionId)) | |
| 651 | + .ToListAsync(); | |
| 652 | + return rows.Select(x => (x.MultipleOptionId, x.PartnerId)).ToList(); | |
| 653 | + } | |
| 654 | + default: | |
| 655 | + return new List<(string EntityId, string PartnerId)>(); | |
| 656 | + } | |
| 657 | + } | |
| 658 | + | |
| 659 | + private static async Task InsertPartnerRowsAsync( | |
| 660 | + ISqlSugarClient db, | |
| 661 | + IGuidGenerator guidGenerator, | |
| 662 | + LabelEntityPartnerKind kind, | |
| 663 | + string entityId, | |
| 664 | + IReadOnlyList<string> partnerIds, | |
| 665 | + string? currentUserId, | |
| 666 | + DateTime now) | |
| 667 | + { | |
| 668 | + switch (kind) | |
| 669 | + { | |
| 670 | + case LabelEntityPartnerKind.Type: | |
| 671 | + { | |
| 672 | + var rows = partnerIds.Select(pid => new FlLabelTypePartnerDbEntity | |
| 673 | + { | |
| 674 | + Id = guidGenerator.Create().ToString(), | |
| 675 | + LabelTypeId = entityId, | |
| 676 | + PartnerId = pid, | |
| 677 | + CreationTime = now, | |
| 678 | + CreatorId = currentUserId | |
| 679 | + }).ToList(); | |
| 680 | + await db.Insertable(rows).ExecuteCommandAsync(); | |
| 681 | + break; | |
| 682 | + } | |
| 683 | + case LabelEntityPartnerKind.Category: | |
| 684 | + { | |
| 685 | + var rows = partnerIds.Select(pid => new FlLabelCategoryPartnerDbEntity | |
| 686 | + { | |
| 687 | + Id = guidGenerator.Create().ToString(), | |
| 688 | + CategoryId = entityId, | |
| 689 | + PartnerId = pid, | |
| 690 | + CreationTime = now, | |
| 691 | + CreatorId = currentUserId | |
| 692 | + }).ToList(); | |
| 693 | + await db.Insertable(rows).ExecuteCommandAsync(); | |
| 694 | + break; | |
| 695 | + } | |
| 696 | + case LabelEntityPartnerKind.MultipleOption: | |
| 697 | + { | |
| 698 | + var rows = partnerIds.Select(pid => new FlLabelMultipleOptionPartnerDbEntity | |
| 699 | + { | |
| 700 | + Id = guidGenerator.Create().ToString(), | |
| 701 | + MultipleOptionId = entityId, | |
| 702 | + PartnerId = pid, | |
| 703 | + CreationTime = now, | |
| 704 | + CreatorId = currentUserId | |
| 705 | + }).ToList(); | |
| 706 | + await db.Insertable(rows).ExecuteCommandAsync(); | |
| 707 | + break; | |
| 708 | + } | |
| 709 | + } | |
| 710 | + } | |
| 711 | + | |
| 712 | + private static async Task<Dictionary<string, string>> LoadPartnerNameMapAsync( | |
| 713 | + ISqlSugarClient db, | |
| 714 | + IReadOnlyList<string> partnerIds) | |
| 715 | + { | |
| 716 | + var result = new Dictionary<string, string>(StringComparer.Ordinal); | |
| 717 | + if (partnerIds.Count == 0) | |
| 718 | + { | |
| 719 | + return result; | |
| 720 | + } | |
| 721 | + | |
| 722 | + var partners = await db.Queryable<FlPartnerDbEntity>() | |
| 723 | + .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) | |
| 724 | + .ToListAsync(); | |
| 725 | + foreach (var p in partners) | |
| 726 | + { | |
| 727 | + var name = p.PartnerName?.Trim(); | |
| 728 | + result[p.Id] = string.IsNullOrEmpty(name) ? p.Id : name; | |
| 729 | + } | |
| 730 | + | |
| 731 | + return result; | |
| 732 | + } | |
| 733 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/LocationScopeBindingHelper.cs
| ... | ... | @@ -54,6 +54,61 @@ public static class LocationScopeBindingHelper |
| 54 | 54 | return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList(); |
| 55 | 55 | } |
| 56 | 56 | |
| 57 | + /// <summary> | |
| 58 | + /// App 可选门店 Id:Company Admin 展开为绑定 Company 下全部门店;其它角色为 <c>userlocation</c> 绑定。 | |
| 59 | + /// </summary> | |
| 60 | + public static Task<List<string>> ResolveAppAccessibleLocationIdsForUserAsync( | |
| 61 | + ISqlSugarClient db, | |
| 62 | + Guid userId) => | |
| 63 | + ResolveAppAccessibleLocationIdsForUserAsync(db, userId, null); | |
| 64 | + | |
| 65 | + /// <summary> | |
| 66 | + /// App 可选门店 Id;传入 <paramref name="currentUser"/> 时优先 JWT 声明识别 Company Admin,避免 ORM 查 Role 触发数据权限过滤器。 | |
| 67 | + /// </summary> | |
| 68 | + public static async Task<List<string>> ResolveAppAccessibleLocationIdsForUserAsync( | |
| 69 | + ISqlSugarClient db, | |
| 70 | + Guid userId, | |
| 71 | + ICurrentUser? currentUser) | |
| 72 | + { | |
| 73 | + var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); | |
| 74 | + if (locationIds.Count == 0) | |
| 75 | + { | |
| 76 | + return locationIds; | |
| 77 | + } | |
| 78 | + | |
| 79 | + var isCompanyAdmin = currentUser?.Id == userId | |
| 80 | + ? await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) | |
| 81 | + : await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId); | |
| 82 | + if (!isCompanyAdmin) | |
| 83 | + { | |
| 84 | + return locationIds; | |
| 85 | + } | |
| 86 | + | |
| 87 | + var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); | |
| 88 | + if (partnerIds.Count == 0) | |
| 89 | + { | |
| 90 | + return locationIds; | |
| 91 | + } | |
| 92 | + | |
| 93 | + var expanded = await ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds); | |
| 94 | + if (expanded.Count == 0) | |
| 95 | + { | |
| 96 | + return locationIds; | |
| 97 | + } | |
| 98 | + | |
| 99 | + var merged = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| 100 | + foreach (var id in expanded) | |
| 101 | + { | |
| 102 | + var key = TeamMemberListScopeHelper.NormalizeScopeKey(id); | |
| 103 | + if (!string.IsNullOrEmpty(key)) | |
| 104 | + { | |
| 105 | + merged.Add(key); | |
| 106 | + } | |
| 107 | + } | |
| 108 | + | |
| 109 | + return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); | |
| 110 | + } | |
| 111 | + | |
| 57 | 112 | public static async Task<List<string>> ResolveDisplayRegionIdsForCurrentUserAsync( |
| 58 | 113 | ISqlSugarClient db, |
| 59 | 114 | ICurrentUser currentUser) |
| ... | ... | @@ -63,12 +118,7 @@ public static class LocationScopeBindingHelper |
| 63 | 118 | return new List<string>(); |
| 64 | 119 | } |
| 65 | 120 | |
| 66 | - var roleId = await db.Queryable<UserRoleEntity>() | |
| 67 | - .Where(ur => ur.UserId == currentUser.Id.Value) | |
| 68 | - .Select(ur => (Guid?)ur.RoleId) | |
| 69 | - .FirstAsync(); | |
| 70 | - | |
| 71 | - return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, roleId); | |
| 121 | + return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, null); | |
| 72 | 122 | } |
| 73 | 123 | |
| 74 | 124 | /// <summary> |
| ... | ... | @@ -93,9 +143,7 @@ public static class LocationScopeBindingHelper |
| 93 | 143 | var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); |
| 94 | 144 | var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); |
| 95 | 145 | |
| 96 | - var isCompanyAdmin = await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) | |
| 97 | - || await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId); | |
| 98 | - if (isCompanyAdmin && partnerIds.Count > 0) | |
| 146 | + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) && partnerIds.Count > 0) | |
| 99 | 147 | { |
| 100 | 148 | regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); |
| 101 | 149 | } |
| ... | ... | @@ -120,7 +168,7 @@ public static class LocationScopeBindingHelper |
| 120 | 168 | var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); |
| 121 | 169 | var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); |
| 122 | 170 | |
| 123 | - if (await TeamMemberRoleHelper.IsCompanyAdminRoleAsync(db, roleId) && partnerIds.Count > 0) | |
| 171 | + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId) && partnerIds.Count > 0) | |
| 124 | 172 | { |
| 125 | 173 | regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); |
| 126 | 174 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsDateTimeDisplayHelper.cs
| ... | ... | @@ -409,9 +409,29 @@ public static class ReportsDateTimeDisplayHelper |
| 409 | 409 | |
| 410 | 410 | |
| 411 | 411 | return false; |
| 412 | + } | |
| 412 | 413 | |
| 414 | + /// <summary>将已解析的到期时刻格式化为 Print Log 展示文案。</summary> | |
| 415 | + public static string FormatExpiryDateTime(DateTime dt) | |
| 416 | + { | |
| 417 | + if (dt == DateTime.MinValue) | |
| 418 | + { | |
| 419 | + return "无"; | |
| 420 | + } | |
| 421 | + | |
| 422 | + return dt.ToString(DisplayFormat, CultureInfo.InvariantCulture); | |
| 413 | 423 | } |
| 414 | 424 | |
| 425 | + /// <summary>解析到期字面量(含 anchor 补全年份/时间)。</summary> | |
| 426 | + public static bool TryParseExpiryDateTime(string s, DateTime anchor, out DateTime dt) | |
| 427 | + { | |
| 428 | + if (TryParseToDateTime(s, out dt)) | |
| 429 | + { | |
| 430 | + return true; | |
| 431 | + } | |
| 432 | + | |
| 433 | + return TryParsePartialDateTime(s, anchor, out dt); | |
| 434 | + } | |
| 415 | 435 | } |
| 416 | 436 | |
| 417 | 437 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsLocationScopeHelper.cs
| ... | ... | @@ -8,7 +8,7 @@ namespace FoodLabeling.Application.Helpers; |
| 8 | 8 | |
| 9 | 9 | /// <summary> |
| 10 | 10 | /// Reports 门店数据范围:管理员可查全部(可按 Company/Region/Location 入参收窄); |
| 11 | -/// 非管理员仅统计 <c>userlocation</c> 绑定门店的打印任务。 | |
| 11 | +/// Company Admin 为绑定 Company 下全部门店;其它非管理员为 <c>userlocation</c> 绑定门店。 | |
| 12 | 12 | /// </summary> |
| 13 | 13 | public static class ReportsLocationScopeHelper |
| 14 | 14 | { |
| ... | ... | @@ -16,7 +16,7 @@ public static class ReportsLocationScopeHelper |
| 16 | 16 | /// 解析报表统计适用的门店 Id 列表。 |
| 17 | 17 | /// <list type="bullet"> |
| 18 | 18 | /// <item>返回 <c>null</c>:不限制门店(管理员且未传 Company/Region/Location 筛选);</item> |
| 19 | - /// <item>返回空列表:无绑定门店或筛选后无交集;</item> | |
| 19 | + /// <item>返回空列表:无可访问门店或筛选后无交集;</item> | |
| 20 | 20 | /// <item>返回非空列表:仅统计这些 <c>fl_label_print_task.LocationId</c>。</item> |
| 21 | 21 | /// </list> |
| 22 | 22 | /// </summary> |
| ... | ... | @@ -39,22 +39,16 @@ public static class ReportsLocationScopeHelper |
| 39 | 39 | return new List<string>(); |
| 40 | 40 | } |
| 41 | 41 | |
| 42 | - var userId = currentUser.Id.Value.ToString(); | |
| 43 | - var boundIds = LocationScopeBindingHelper.NormalizeIds( | |
| 44 | - await db.Queryable<UserLocationDbEntity>() | |
| 45 | - .Where(x => !x.IsDeleted && x.UserId == userId) | |
| 46 | - .Select(x => x.LocationId) | |
| 47 | - .Distinct() | |
| 48 | - .ToListAsync()); | |
| 49 | - | |
| 50 | - if (boundIds.Count == 0) | |
| 42 | + var accessibleIds = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync( | |
| 43 | + db, currentUser.Id.Value, currentUser); | |
| 44 | + if (accessibleIds.Count == 0) | |
| 51 | 45 | { |
| 52 | 46 | return new List<string>(); |
| 53 | 47 | } |
| 54 | 48 | |
| 55 | 49 | if (filterIds is null) |
| 56 | 50 | { |
| 57 | - return boundIds; | |
| 51 | + return accessibleIds; | |
| 58 | 52 | } |
| 59 | 53 | |
| 60 | 54 | if (filterIds.Count == 0) |
| ... | ... | @@ -62,8 +56,8 @@ public static class ReportsLocationScopeHelper |
| 62 | 56 | return new List<string>(); |
| 63 | 57 | } |
| 64 | 58 | |
| 65 | - var boundSet = new HashSet<string>(boundIds, StringComparer.OrdinalIgnoreCase); | |
| 66 | - return filterIds.Where(id => boundSet.Contains(id)).ToList(); | |
| 59 | + var accessibleSet = new HashSet<string>(accessibleIds, StringComparer.OrdinalIgnoreCase); | |
| 60 | + return filterIds.Where(id => accessibleSet.Contains(id)).ToList(); | |
| 67 | 61 | } |
| 68 | 62 | |
| 69 | 63 | /// <summary> | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/ReportsPrintLogExpiryHelper.cs
| ... | ... | @@ -53,10 +53,10 @@ public static class ReportsPrintLogExpiryHelper |
| 53 | 53 | return ReportsDateTimeDisplayHelper.FormatExpiryDateTime(resolved); |
| 54 | 54 | } |
| 55 | 55 | |
| 56 | - var raw = ExtractExpiryText(printInputJson); | |
| 56 | + var raw = ExtractExpiryText(printInputJson, reference); | |
| 57 | 57 | if (string.Equals(raw, "无", StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(renderTemplateJson)) |
| 58 | 58 | { |
| 59 | - raw = ExtractExpiryText(renderTemplateJson); | |
| 59 | + raw = ExtractExpiryText(renderTemplateJson, reference); | |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | 62 | return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, reference); |
| ... | ... | @@ -185,7 +185,7 @@ public static class ReportsPrintLogExpiryHelper |
| 185 | 185 | return false; |
| 186 | 186 | } |
| 187 | 187 | |
| 188 | - return ReportsDateTimeDisplayHelper.TryParseToDateTime(display.Trim(), reference, out result); | |
| 188 | + return ReportsDateTimeDisplayHelper.TryParseExpiryDateTime(display.Trim(), reference, out result); | |
| 189 | 189 | } |
| 190 | 190 | |
| 191 | 191 | private static string? ResolveElementDisplayText(JsonElement el, DateTime reference) |
| ... | ... | @@ -453,35 +453,6 @@ public static class ReportsPrintLogExpiryHelper |
| 453 | 453 | dt.Hour != 0 || dt.Minute != 0; |
| 454 | 454 | |
| 455 | 455 | /// <summary> |
| 456 | - /// 解析并格式化为 Print Log Expiration 列展示。 | |
| 457 | - /// </summary> | |
| 458 | - public static string ExtractFormattedExpiryText( | |
| 459 | - string? printInputJson, | |
| 460 | - string? renderTemplateJson = null, | |
| 461 | - DateTime? baseTime = null, | |
| 462 | - DateTime? printedAt = null) | |
| 463 | - { | |
| 464 | - var anchor = ResolveAnchor(baseTime, printedAt, printInputJson); | |
| 465 | - var raw = ExtractExpiryText(printInputJson, anchor); | |
| 466 | - if (IsMissingExpiry(raw) && !string.IsNullOrWhiteSpace(renderTemplateJson)) | |
| 467 | - { | |
| 468 | - raw = ExtractExpiryText(renderTemplateJson, anchor); | |
| 469 | - } | |
| 470 | - | |
| 471 | - if (TryParseOffsetPayload(raw.Trim(), out var amount, out var unit)) | |
| 472 | - { | |
| 473 | - raw = ApplyOffset(anchor, amount, unit) | |
| 474 | - .ToString(ReportsDateTimeDisplayHelper.DisplayFormat, CultureInfo.InvariantCulture); | |
| 475 | - } | |
| 476 | - | |
| 477 | - return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, anchor); | |
| 478 | - } | |
| 479 | - | |
| 480 | - /// <summary>兼容旧调用:仅 PrintInputJson。</summary> | |
| 481 | - public static string ExtractFormattedExpiryText(string? printInputJson) => | |
| 482 | - ExtractFormattedExpiryText(printInputJson, null, null, null); | |
| 483 | - | |
| 484 | - /// <summary> | |
| 485 | 456 | /// 解析保质期展示文本;无法解析时返回「无」。 |
| 486 | 457 | /// </summary> |
| 487 | 458 | public static string ExtractExpiryText(string? printInputJson, DateTime anchor) |
| ... | ... | @@ -830,7 +801,7 @@ public static class ReportsPrintLogExpiryHelper |
| 830 | 801 | } |
| 831 | 802 | |
| 832 | 803 | var t = raw.Trim(); |
| 833 | - if (!t.StartsWith('{', StringComparison.Ordinal)) | |
| 804 | + if (!t.StartsWith("{")) | |
| 834 | 805 | { |
| 835 | 806 | if (Regex.IsMatch(t, @"^\d+$")) |
| 836 | 807 | { | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/TeamMemberRoleHelper.cs
| 1 | 1 | using SqlSugar; |
| 2 | 2 | using Volo.Abp.Users; |
| 3 | -using Yi.Framework.Rbac.Domain.Entities; | |
| 4 | 3 | using Yi.Framework.Rbac.Domain.Shared.Consts; |
| 5 | 4 | |
| 6 | 5 | namespace FoodLabeling.Application.Helpers; |
| ... | ... | @@ -55,12 +54,17 @@ public static class TeamMemberRoleHelper |
| 55 | 54 | return false; |
| 56 | 55 | } |
| 57 | 56 | |
| 58 | - var roleId = await db.Queryable<UserRoleEntity>() | |
| 59 | - .Where(ur => ur.UserId == currentUser.Id.Value) | |
| 60 | - .Select(ur => (Guid?)ur.RoleId) | |
| 61 | - .FirstAsync(); | |
| 57 | + return await IsCompanyAdminUserAsync(db, currentUser.Id.Value); | |
| 58 | + } | |
| 62 | 59 | |
| 63 | - return await IsCompanyAdminRoleAsync(db, roleId); | |
| 60 | + /// <summary> | |
| 61 | + /// 指定用户是否为 Company Admin 类角色(按库内 Role 判定,用于登录等无 JWT 声明场景)。 | |
| 62 | + /// </summary> | |
| 63 | + public static async Task<bool> IsCompanyAdminUserAsync(ISqlSugarClient db, Guid userId) | |
| 64 | + { | |
| 65 | + var roleRows = await LoadUserRoleRowsAsync(db, userId); | |
| 66 | + return roleRows.Any(r => | |
| 67 | + IsCompanyAdminRoleName(r.RoleName) || IsCompanyAdminRoleToken(r.RoleCode)); | |
| 64 | 68 | } |
| 65 | 69 | |
| 66 | 70 | /// <summary> |
| ... | ... | @@ -73,8 +77,16 @@ public static class TeamMemberRoleHelper |
| 73 | 77 | return false; |
| 74 | 78 | } |
| 75 | 79 | |
| 76 | - var role = await db.Queryable<RoleAggregateRoot>() | |
| 77 | - .FirstAsync(x => !x.IsDeleted && x.Id == roleId.Value); | |
| 80 | + var roleRows = await db.Ado.SqlQueryAsync<UserRoleNameRow>( | |
| 81 | + """ | |
| 82 | + SELECT RoleCode, RoleName | |
| 83 | + FROM Role | |
| 84 | + WHERE IsDeleted = 0 AND Id = @RoleId | |
| 85 | + LIMIT 1 | |
| 86 | + """, | |
| 87 | + new { RoleId = roleId.Value }); | |
| 88 | + | |
| 89 | + var role = roleRows.FirstOrDefault(); | |
| 78 | 90 | if (role is null) |
| 79 | 91 | { |
| 80 | 92 | return false; |
| ... | ... | @@ -84,6 +96,19 @@ public static class TeamMemberRoleHelper |
| 84 | 96 | } |
| 85 | 97 | |
| 86 | 98 | /// <summary> |
| 99 | + /// 读取用户角色(raw SQL,避免 RBAC 数据权限过滤器中 Select 表达式导致 SqlSugar 解析失败)。 | |
| 100 | + /// </summary> | |
| 101 | + private static Task<List<UserRoleNameRow>> LoadUserRoleRowsAsync(ISqlSugarClient db, Guid userId) => | |
| 102 | + db.Ado.SqlQueryAsync<UserRoleNameRow>( | |
| 103 | + """ | |
| 104 | + SELECT r.RoleCode, r.RoleName | |
| 105 | + FROM UserRole ur | |
| 106 | + INNER JOIN Role r ON ur.RoleId = r.Id | |
| 107 | + WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1 | |
| 108 | + """, | |
| 109 | + new { UserId = userId }); | |
| 110 | + | |
| 111 | + /// <summary> | |
| 87 | 112 | /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。 |
| 88 | 113 | /// </summary> |
| 89 | 114 | public static string? FormatDisplayRoleName(string? roleName) |
| ... | ... | @@ -148,4 +173,11 @@ public static class TeamMemberRoleHelper |
| 148 | 173 | |
| 149 | 174 | private static string NormalizeRoleKey(string value) => |
| 150 | 175 | new string(value.Trim().ToLowerInvariant().Where(c => char.IsLetterOrDigit(c)).ToArray()); |
| 176 | + | |
| 177 | + private sealed class UserRoleNameRow | |
| 178 | + { | |
| 179 | + public string? RoleCode { get; set; } | |
| 180 | + | |
| 181 | + public string? RoleName { get; set; } | |
| 182 | + } | |
| 151 | 183 | } | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Helpers/UsAppPrintLogScopeHelper.cs
| ... | ... | @@ -13,7 +13,7 @@ namespace FoodLabeling.Application.Helpers; |
| 13 | 13 | public static class UsAppPrintLogScopeHelper |
| 14 | 14 | { |
| 15 | 15 | /// <summary> |
| 16 | - /// 管理员(<see cref="ReportsRoleHelper.IsAdminRole"/>)或角色码/名含 <c>partner</c>(忽略大小写,如 Partner Admin)。 | |
| 16 | + /// 管理员、Company Admin,或角色码/名含 <c>partner</c>(如 Partner Admin):可查看门店下全部打印记录。 | |
| 17 | 17 | /// </summary> |
| 18 | 18 | public static async Task<bool> CanViewAllPrintsAtLocationAsync( |
| 19 | 19 | ICurrentUser currentUser, |
| ... | ... | @@ -29,6 +29,11 @@ public static class UsAppPrintLogScopeHelper |
| 29 | 29 | return false; |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)) | |
| 33 | + { | |
| 34 | + return true; | |
| 35 | + } | |
| 36 | + | |
| 32 | 37 | var roleRows = await db.Ado.SqlQueryAsync<UserRoleRow>( |
| 33 | 38 | @"SELECT r.RoleCode, r.RoleName |
| 34 | 39 | FROM UserRole ur |
| ... | ... | @@ -41,30 +46,18 @@ public static class UsAppPrintLogScopeHelper |
| 41 | 46 | } |
| 42 | 47 | |
| 43 | 48 | /// <summary> |
| 44 | - /// 校验当前用户是否绑定指定门店。 | |
| 49 | + /// 校验当前用户是否可访问指定门店(含 Company Admin 公司下全部门店)。 | |
| 45 | 50 | /// </summary> |
| 46 | 51 | public static async Task EnsureUserBoundToLocationAsync( |
| 47 | 52 | ICurrentUser currentUser, |
| 48 | 53 | ISqlSugarClient db, |
| 49 | 54 | string locationId) |
| 50 | 55 | { |
| 51 | - if (currentUser.Id is null) | |
| 52 | - { | |
| 53 | - throw new UserFriendlyException("用户未登录"); | |
| 54 | - } | |
| 55 | - | |
| 56 | - var lid = locationId.Trim(); | |
| 57 | - var userIdStr = currentUser.Id.Value.ToString(); | |
| 58 | - var bound = await db.Queryable<UserLocationDbEntity>() | |
| 59 | - .AnyAsync(x => !x.IsDeleted && x.UserId == userIdStr && x.LocationId == lid); | |
| 60 | - if (!bound) | |
| 61 | - { | |
| 62 | - throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); | |
| 63 | - } | |
| 56 | + await EnsureUserCanAccessLocationAsync(currentUser, db, locationId); | |
| 64 | 57 | } |
| 65 | 58 | |
| 66 | 59 | /// <summary> |
| 67 | - /// 校验是否可访问指定门店:平台管理员(<see cref="ReportsRoleHelper.IsAdminRole"/>)不校验 userlocation;其它账号须绑定。 | |
| 60 | + /// 校验是否可访问指定门店:平台管理员不校验;Company Admin 可访问绑定 Company 下任意门店;其它账号须 userlocation 绑定。 | |
| 68 | 61 | /// </summary> |
| 69 | 62 | public static async Task EnsureUserCanAccessLocationAsync( |
| 70 | 63 | ICurrentUser currentUser, |
| ... | ... | @@ -76,7 +69,23 @@ public static class UsAppPrintLogScopeHelper |
| 76 | 69 | return; |
| 77 | 70 | } |
| 78 | 71 | |
| 79 | - await EnsureUserBoundToLocationAsync(currentUser, db, locationId); | |
| 72 | + if (currentUser.Id is null) | |
| 73 | + { | |
| 74 | + throw new UserFriendlyException("用户未登录"); | |
| 75 | + } | |
| 76 | + | |
| 77 | + var lid = TeamMemberListScopeHelper.NormalizeScopeKey(locationId); | |
| 78 | + if (string.IsNullOrEmpty(lid)) | |
| 79 | + { | |
| 80 | + throw new UserFriendlyException("门店参数无效"); | |
| 81 | + } | |
| 82 | + | |
| 83 | + var accessible = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync( | |
| 84 | + db, currentUser.Id.Value, currentUser); | |
| 85 | + if (!accessible.Contains(lid, StringComparer.OrdinalIgnoreCase)) | |
| 86 | + { | |
| 87 | + throw new UserFriendlyException("当前账号未绑定该门店,无法查看"); | |
| 88 | + } | |
| 80 | 89 | } |
| 81 | 90 | |
| 82 | 91 | /// <summary> |
| ... | ... | @@ -107,10 +116,9 @@ public static class UsAppPrintLogScopeHelper |
| 107 | 116 | var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); |
| 108 | 117 | var guids = userIdStrings |
| 109 | 118 | .Where(x => !string.IsNullOrWhiteSpace(x)) |
| 110 | - .Select(x => x!.Trim()) | |
| 111 | - .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) | |
| 112 | - .Where(x => x.HasValue) | |
| 113 | - .Select(x => x!.Value) | |
| 119 | + .Select(x => TeamMemberListScopeHelper.NormalizeScopeKey(x!)) | |
| 120 | + .Where(x => Guid.TryParse(x, out _)) | |
| 121 | + .Select(Guid.Parse) | |
| 114 | 122 | .Distinct() |
| 115 | 123 | .ToList(); |
| 116 | 124 | if (guids.Count == 0) |
| ... | ... | @@ -118,19 +126,27 @@ public static class UsAppPrintLogScopeHelper |
| 118 | 126 | return map; |
| 119 | 127 | } |
| 120 | 128 | |
| 121 | - var users = await db.Queryable<UserAggregateRoot>() | |
| 122 | - .Where(u => !u.IsDeleted && guids.Contains(u.Id)) | |
| 123 | - .Select(u => new { u.Id, u.Name, u.Nick, u.UserName }) | |
| 124 | - .ToListAsync(); | |
| 129 | + var parameters = new List<SugarParameter>(guids.Count); | |
| 130 | + var inClause = new List<string>(guids.Count); | |
| 131 | + for (var i = 0; i < guids.Count; i++) | |
| 132 | + { | |
| 133 | + var paramName = $"@uid{i}"; | |
| 134 | + inClause.Add(paramName); | |
| 135 | + parameters.Add(new SugarParameter(paramName, guids[i])); | |
| 136 | + } | |
| 137 | + | |
| 138 | + var users = await db.Ado.SqlQueryAsync<OperatorUserRow>( | |
| 139 | + $""" | |
| 140 | + SELECT Id, UserName, Name, Nick | |
| 141 | + FROM User | |
| 142 | + WHERE IsDeleted = 0 AND Id IN ({string.Join(", ", inClause)}) | |
| 143 | + """, | |
| 144 | + parameters); | |
| 125 | 145 | |
| 126 | 146 | foreach (var u in users) |
| 127 | 147 | { |
| 128 | - var display = !string.IsNullOrWhiteSpace(u.Name) | |
| 129 | - ? u.Name!.Trim() | |
| 130 | - : !string.IsNullOrWhiteSpace(u.Nick) | |
| 131 | - ? u.Nick!.Trim() | |
| 132 | - : u.UserName?.Trim() ?? string.Empty; | |
| 133 | - map[u.Id.ToString()] = string.IsNullOrWhiteSpace(display) ? "无" : display; | |
| 148 | + var display = BuildOperatorDisplayName(u.Name, u.Nick, u.UserName); | |
| 149 | + map[TeamMemberListScopeHelper.UserKey(u.Id)] = display; | |
| 134 | 150 | } |
| 135 | 151 | |
| 136 | 152 | return map; |
| ... | ... | @@ -143,7 +159,35 @@ public static class UsAppPrintLogScopeHelper |
| 143 | 159 | return "无"; |
| 144 | 160 | } |
| 145 | 161 | |
| 146 | - return map.TryGetValue(createdBy.Trim(), out var name) ? name : "无"; | |
| 162 | + var key = TeamMemberListScopeHelper.NormalizeScopeKey(createdBy); | |
| 163 | + return map.TryGetValue(key, out var name) ? name : "无"; | |
| 164 | + } | |
| 165 | + | |
| 166 | + private static string BuildOperatorDisplayName(string? name, string? nick, string? userName) | |
| 167 | + { | |
| 168 | + if (!string.IsNullOrWhiteSpace(name)) | |
| 169 | + { | |
| 170 | + return name.Trim(); | |
| 171 | + } | |
| 172 | + | |
| 173 | + if (!string.IsNullOrWhiteSpace(nick)) | |
| 174 | + { | |
| 175 | + return nick.Trim(); | |
| 176 | + } | |
| 177 | + | |
| 178 | + var user = userName?.Trim(); | |
| 179 | + return string.IsNullOrWhiteSpace(user) ? "无" : user; | |
| 180 | + } | |
| 181 | + | |
| 182 | + private sealed class OperatorUserRow | |
| 183 | + { | |
| 184 | + public Guid Id { get; set; } | |
| 185 | + | |
| 186 | + public string? UserName { get; set; } | |
| 187 | + | |
| 188 | + public string? Name { get; set; } | |
| 189 | + | |
| 190 | + public string? Nick { get; set; } | |
| 147 | 191 | } |
| 148 | 192 | |
| 149 | 193 | private static bool ContainsPartnerKeyword(string? value) => | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelCategoryPartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +[SugarTable("fl_label_category_partner")] | |
| 6 | +public class FlLabelCategoryPartnerDbEntity | |
| 7 | +{ | |
| 8 | + [SugarColumn(IsPrimaryKey = true)] | |
| 9 | + public string Id { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + public string CategoryId { get; set; } = string.Empty; | |
| 12 | + | |
| 13 | + public string PartnerId { get; set; } = string.Empty; | |
| 14 | + | |
| 15 | + public DateTime CreationTime { get; set; } | |
| 16 | + | |
| 17 | + public string? CreatorId { get; set; } | |
| 18 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelMultipleOptionPartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +[SugarTable("fl_label_multiple_option_partner")] | |
| 6 | +public class FlLabelMultipleOptionPartnerDbEntity | |
| 7 | +{ | |
| 8 | + [SugarColumn(IsPrimaryKey = true)] | |
| 9 | + public string Id { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + public string MultipleOptionId { get; set; } = string.Empty; | |
| 12 | + | |
| 13 | + public string PartnerId { get; set; } = string.Empty; | |
| 14 | + | |
| 15 | + public DateTime CreationTime { get; set; } | |
| 16 | + | |
| 17 | + public string? CreatorId { get; set; } | |
| 18 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/DbModels/FlLabelTypePartnerDbEntity.cs
0 → 100644
| 1 | +using SqlSugar; | |
| 2 | + | |
| 3 | +namespace FoodLabeling.Application.Services.DbModels; | |
| 4 | + | |
| 5 | +[SugarTable("fl_label_type_partner")] | |
| 6 | +public class FlLabelTypePartnerDbEntity | |
| 7 | +{ | |
| 8 | + [SugarColumn(IsPrimaryKey = true)] | |
| 9 | + public string Id { get; set; } = string.Empty; | |
| 10 | + | |
| 11 | + public string LabelTypeId { get; set; } = string.Empty; | |
| 12 | + | |
| 13 | + public string PartnerId { get; set; } = string.Empty; | |
| 14 | + | |
| 15 | + public DateTime CreationTime { get; set; } | |
| 16 | + | |
| 17 | + public string? CreatorId { get; set; } | |
| 18 | +} | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelAppService.cs
| ... | ... | @@ -151,12 +151,15 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 151 | 151 | }; |
| 152 | 152 | } |
| 153 | 153 | |
| 154 | - // 查询标签基础信息(分类/类型/模板) | |
| 154 | + // 查询标签基础信息(分类/类型/模板);类型 LeftJoin,允许未绑定 labelTypeId 的标签出现在列表 | |
| 155 | 155 | var labelRows = await _dbContext.SqlSugarClient |
| 156 | - .Queryable<FlLabelDbEntity, FlLabelCategoryDbEntity, FlLabelTypeDbEntity, FlLabelTemplateDbEntity>( | |
| 157 | - (l, c, t, tpl) => l.LabelCategoryId == c.Id && l.LabelTypeId == t.Id && l.TemplateId == tpl.Id) | |
| 156 | + .Queryable<FlLabelDbEntity>() | |
| 157 | + .InnerJoin<FlLabelCategoryDbEntity>((l, c) => l.LabelCategoryId == c.Id) | |
| 158 | + .LeftJoin<FlLabelTypeDbEntity>((l, c, t) => l.LabelTypeId == t.Id) | |
| 159 | + .InnerJoin<FlLabelTemplateDbEntity>((l, c, t, tpl) => l.TemplateId == tpl.Id) | |
| 158 | 160 | .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id)) |
| 159 | - .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !t.IsDeleted && !tpl.IsDeleted) | |
| 161 | + .Where((l, c, t, tpl) => !l.IsDeleted && !c.IsDeleted && !tpl.IsDeleted) | |
| 162 | + .Where((l, c, t, tpl) => t.Id == null || !t.IsDeleted) | |
| 160 | 163 | .Select((l, c, t, tpl) => new |
| 161 | 164 | { |
| 162 | 165 | l.Id, |
| ... | ... | @@ -248,7 +251,7 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 248 | 251 | Products = products, |
| 249 | 252 | TemplateName = x.TemplateName ?? string.Empty, |
| 250 | 253 | TemplateCode = x.TemplateCode ?? string.Empty, |
| 251 | - LabelTypeName = x.LabelTypeName ?? string.Empty, | |
| 254 | + LabelTypeName = string.IsNullOrWhiteSpace(x.LabelTypeName) ? "无" : x.LabelTypeName, | |
| 252 | 255 | State = x.State, |
| 253 | 256 | LastEdited = x.LastEdited, |
| 254 | 257 | HasError = false |
| ... | ... | @@ -289,8 +292,12 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 289 | 292 | .FirstAsync(x => x.Id == label.TemplateId); |
| 290 | 293 | var category = await db.Queryable<FlLabelCategoryDbEntity>() |
| 291 | 294 | .FirstAsync(x => x.Id == label.LabelCategoryId); |
| 292 | - var type = await db.Queryable<FlLabelTypeDbEntity>() | |
| 293 | - .FirstAsync(x => x.Id == label.LabelTypeId); | |
| 295 | + FlLabelTypeDbEntity? type = null; | |
| 296 | + if (!string.IsNullOrWhiteSpace(label.LabelTypeId)) | |
| 297 | + { | |
| 298 | + type = await db.Queryable<FlLabelTypeDbEntity>() | |
| 299 | + .FirstAsync(x => x.Id == label.LabelTypeId); | |
| 300 | + } | |
| 294 | 301 | |
| 295 | 302 | var productIds = await db.Queryable<FlLabelProductDbEntity>() |
| 296 | 303 | .Where(x => x.LabelId == label.Id) |
| ... | ... | @@ -368,10 +375,8 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 368 | 375 | { |
| 369 | 376 | throw new UserFriendlyException("标签分类Id不能为空"); |
| 370 | 377 | } |
| 371 | - if (string.IsNullOrWhiteSpace(input.LabelTypeId)) | |
| 372 | - { | |
| 373 | - throw new UserFriendlyException("标签类型Id不能为空"); | |
| 374 | - } | |
| 378 | + | |
| 379 | + await EnsureLabelTypeExistsIfProvidedAsync(input.LabelTypeId); | |
| 375 | 380 | |
| 376 | 381 | var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync( |
| 377 | 382 | _dbContext.SqlSugarClient, |
| ... | ... | @@ -413,7 +418,7 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 413 | 418 | TemplateId = template.Id, |
| 414 | 419 | LocationId = scope.PrimaryLocationId, |
| 415 | 420 | LabelCategoryId = input.LabelCategoryId?.Trim(), |
| 416 | - LabelTypeId = input.LabelTypeId?.Trim(), | |
| 421 | + LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId), | |
| 417 | 422 | State = input.State, |
| 418 | 423 | LabelType = null, |
| 419 | 424 | LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson) |
| ... | ... | @@ -484,10 +489,8 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 484 | 489 | { |
| 485 | 490 | throw new UserFriendlyException("标签分类Id不能为空"); |
| 486 | 491 | } |
| 487 | - if (string.IsNullOrWhiteSpace(input.LabelTypeId)) | |
| 488 | - { | |
| 489 | - throw new UserFriendlyException("标签类型Id不能为空"); | |
| 490 | - } | |
| 492 | + | |
| 493 | + await EnsureLabelTypeExistsIfProvidedAsync(input.LabelTypeId); | |
| 491 | 494 | |
| 492 | 495 | var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync( |
| 493 | 496 | _dbContext.SqlSugarClient, |
| ... | ... | @@ -510,7 +513,7 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 510 | 513 | label.TemplateId = template.Id; |
| 511 | 514 | label.LocationId = scope.PrimaryLocationId; |
| 512 | 515 | label.LabelCategoryId = input.LabelCategoryId?.Trim(); |
| 513 | - label.LabelTypeId = input.LabelTypeId?.Trim(); | |
| 516 | + label.LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId); | |
| 514 | 517 | label.State = input.State; |
| 515 | 518 | label.LastModifierId = CurrentUser?.Id?.ToString(); |
| 516 | 519 | label.LastModificationTime = now; |
| ... | ... | @@ -873,5 +876,27 @@ public class LabelAppService : ApplicationService, ILabelAppService |
| 873 | 876 | var v = (raw ?? string.Empty).Trim().ToLowerInvariant(); |
| 874 | 877 | return v is "line" or "dotted" ? v : "none"; |
| 875 | 878 | } |
| 879 | + | |
| 880 | + private static string? NormalizeOptionalLabelTypeId(string? labelTypeId) | |
| 881 | + { | |
| 882 | + var id = labelTypeId?.Trim(); | |
| 883 | + return string.IsNullOrWhiteSpace(id) ? null : id; | |
| 884 | + } | |
| 885 | + | |
| 886 | + private async Task EnsureLabelTypeExistsIfProvidedAsync(string? labelTypeId) | |
| 887 | + { | |
| 888 | + var id = NormalizeOptionalLabelTypeId(labelTypeId); | |
| 889 | + if (id is null) | |
| 890 | + { | |
| 891 | + return; | |
| 892 | + } | |
| 893 | + | |
| 894 | + var exists = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() | |
| 895 | + .AnyAsync(x => !x.IsDeleted && x.Id == id); | |
| 896 | + if (!exists) | |
| 897 | + { | |
| 898 | + throw new UserFriendlyException("标签类型不存在"); | |
| 899 | + } | |
| 900 | + } | |
| 876 | 901 | } |
| 877 | 902 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelCategoryAppService.cs
| ... | ... | @@ -35,7 +35,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 35 | 35 | (x.DisplayText != null && x.DisplayText.Contains(keyword!))) |
| 36 | 36 | .WhereIF(input.State != null, x => x.State == input.State); |
| 37 | 37 | |
| 38 | - query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId); | |
| 38 | + query = await ApplyCategoryScopeFilterAsync(query, input.PartnerId, input.GroupId, input.LocationId); | |
| 39 | 39 | |
| 40 | 40 | // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 |
| 41 | 41 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| ... | ... | @@ -72,10 +72,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 72 | 72 | |
| 73 | 73 | var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids); |
| 74 | 74 | var scopeMap = await BuildCategoryScopeMapAsync(entities); |
| 75 | + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( | |
| 76 | + _dbContext.SqlSugarClient, | |
| 77 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, | |
| 78 | + ids); | |
| 75 | 79 | |
| 76 | 80 | var items = entities.Select(x => |
| 77 | 81 | { |
| 78 | 82 | scopeMap.TryGetValue(x.Id, out var scope); |
| 83 | + partnerScopeMap.TryGetValue(x.Id, out var partnerScope); | |
| 79 | 84 | labelStatsMap.TryGetValue(x.Id, out var labelStats); |
| 80 | 85 | var creationTime = ResolveCategoryCreationTime(x); |
| 81 | 86 | return new LabelCategoryGetListOutputDto |
| ... | ... | @@ -88,6 +93,10 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 88 | 93 | State = x.State, |
| 89 | 94 | ButtonAppearance = x.ButtonAppearance, |
| 90 | 95 | AvailabilityType = x.AvailabilityType, |
| 96 | + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll, | |
| 97 | + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay, | |
| 98 | + PartnerIds = partnerScope?.PartnerIds ?? new List<string>(), | |
| 99 | + CompanyIds = partnerScope?.PartnerIds ?? new List<string>(), | |
| 91 | 100 | OrderNum = x.OrderNum, |
| 92 | 101 | NoOfLabels = labelStats?.Count ?? 0, |
| 93 | 102 | CreationTime = creationTime, |
| ... | ... | @@ -112,6 +121,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 112 | 121 | } |
| 113 | 122 | |
| 114 | 123 | var dto = MapToGetOutput(entity); |
| 124 | + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id); | |
| 115 | 125 | if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) |
| 116 | 126 | { |
| 117 | 127 | var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>() |
| ... | ... | @@ -139,7 +149,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 139 | 149 | |
| 140 | 150 | var displayText = input.DisplayText?.Trim(); |
| 141 | 151 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); |
| 142 | - var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); | |
| 152 | + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); | |
| 143 | 153 | |
| 144 | 154 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 145 | 155 | .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); |
| ... | ... | @@ -170,6 +180,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 170 | 180 | }; |
| 171 | 181 | |
| 172 | 182 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 183 | + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now); | |
| 173 | 184 | await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); |
| 174 | 185 | return await GetAsync(entity.Id); |
| 175 | 186 | } |
| ... | ... | @@ -192,7 +203,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 192 | 203 | |
| 193 | 204 | var displayText = input.DisplayText?.Trim(); |
| 194 | 205 | var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); |
| 195 | - var (availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); | |
| 206 | + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveCategoryScopeForSaveAsync(input); | |
| 196 | 207 | |
| 197 | 208 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() |
| 198 | 209 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); |
| ... | ... | @@ -213,6 +224,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 213 | 224 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 214 | 225 | |
| 215 | 226 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 227 | + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId, | |
| 228 | + entity.LastModificationTime ?? DateTime.Now); | |
| 216 | 229 | await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, |
| 217 | 230 | entity.LastModificationTime ?? DateTime.Now); |
| 218 | 231 | return await GetAsync(id); |
| ... | ... | @@ -234,6 +247,14 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 234 | 247 | throw new UserFriendlyException("该标签分类已被标签引用,无法删除"); |
| 235 | 248 | } |
| 236 | 249 | |
| 250 | + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync( | |
| 251 | + _dbContext.SqlSugarClient, | |
| 252 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, | |
| 253 | + id); | |
| 254 | + await _dbContext.SqlSugarClient.Deleteable<FlLabelCategoryLocationDbEntity>() | |
| 255 | + .Where(x => x.CategoryId == id) | |
| 256 | + .ExecuteCommandAsync(); | |
| 257 | + | |
| 237 | 258 | entity.IsDeleted = true; |
| 238 | 259 | entity.LastModificationTime = DateTime.Now; |
| 239 | 260 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| ... | ... | @@ -256,9 +277,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 256 | 277 | }; |
| 257 | 278 | } |
| 258 | 279 | |
| 259 | - private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveCategoryScopeForSaveAsync( | |
| 280 | + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List<string> LocationIds)> ResolveCategoryScopeForSaveAsync( | |
| 260 | 281 | LabelCategoryCreateInputVo input) |
| 261 | 282 | { |
| 283 | + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( | |
| 284 | + _dbContext.SqlSugarClient, | |
| 285 | + input.AppliedPartnerType, | |
| 286 | + input.PartnerIds, | |
| 287 | + input.CompanyIds); | |
| 288 | + | |
| 262 | 289 | var regionIds = NormalizeRegionIds(input); |
| 263 | 290 | var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); |
| 264 | 291 | var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); |
| ... | ... | @@ -278,20 +305,37 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 278 | 305 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); |
| 279 | 306 | } |
| 280 | 307 | |
| 281 | - if (availabilityType == "ALL") | |
| 308 | + var partnerSpecified = string.Equals( | |
| 309 | + partnerScope.AppliedPartnerType, | |
| 310 | + LabelEntityPartnerScopeHelper.ScopeSpecified, | |
| 311 | + StringComparison.OrdinalIgnoreCase); | |
| 312 | + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase); | |
| 313 | + | |
| 314 | + if (partnerSpecified || locationSpecified) | |
| 282 | 315 | { |
| 283 | - return ("ALL", new List<string>()); | |
| 316 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 317 | + _dbContext.SqlSugarClient, | |
| 318 | + partnerSpecified ? partnerScope.PartnerIds : null, | |
| 319 | + locationSpecified ? regionIds : null, | |
| 320 | + locationSpecified ? explicitLocationIds : null); | |
| 321 | + if (merged.Count == 0) | |
| 322 | + { | |
| 323 | + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); | |
| 324 | + } | |
| 325 | + | |
| 326 | + if (locationSpecified) | |
| 327 | + { | |
| 328 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 329 | + return (partnerScope, "SPECIFIED", merged); | |
| 330 | + } | |
| 284 | 331 | } |
| 285 | 332 | |
| 286 | - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 287 | - _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | |
| 288 | - if (merged.Count == 0) | |
| 333 | + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | |
| 289 | 334 | { |
| 290 | - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 335 | + return (partnerScope, "ALL", new List<string>()); | |
| 291 | 336 | } |
| 292 | 337 | |
| 293 | - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 294 | - return ("SPECIFIED", merged); | |
| 338 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 295 | 339 | } |
| 296 | 340 | |
| 297 | 341 | private static List<string> NormalizeRegionIds(LabelCategoryCreateInputVo input) |
| ... | ... | @@ -312,17 +356,24 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 312 | 356 | |
| 313 | 357 | private async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryScopeFilterAsync( |
| 314 | 358 | ISugarQueryable<FlLabelCategoryDbEntity> query, |
| 359 | + string? partnerId, | |
| 315 | 360 | string? groupId, |
| 316 | 361 | string? locationId) |
| 317 | 362 | { |
| 318 | - var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | |
| 319 | - _dbContext.SqlSugarClient, groupId, locationId); | |
| 320 | - if (scopeLocIds is null) | |
| 363 | + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( | |
| 364 | + CurrentUser, _dbContext, partnerId, groupId, locationId); | |
| 365 | + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync( | |
| 366 | + _dbContext.SqlSugarClient, partnerId, scopedLocationIds); | |
| 367 | + | |
| 368 | + query = await LabelEntityPartnerScopeHelper.ApplyCategoryPartnerListFilterAsync( | |
| 369 | + _dbContext.SqlSugarClient, query, scopedPartnerIds); | |
| 370 | + | |
| 371 | + if (scopedLocationIds is null) | |
| 321 | 372 | { |
| 322 | 373 | return query; |
| 323 | 374 | } |
| 324 | 375 | |
| 325 | - if (scopeLocIds.Count == 0) | |
| 376 | + if (scopedLocationIds.Count == 0) | |
| 326 | 377 | { |
| 327 | 378 | return query.Where(c => c.AvailabilityType == "ALL"); |
| 328 | 379 | } |
| ... | ... | @@ -330,10 +381,43 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ |
| 330 | 381 | return query.Where(c => |
| 331 | 382 | c.AvailabilityType == "ALL" || |
| 332 | 383 | SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() |
| 333 | - .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) | |
| 384 | + .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId)) | |
| 334 | 385 | .Any()); |
| 335 | 386 | } |
| 336 | 387 | |
| 388 | + private async Task SaveCategoryPartnerScopeAsync( | |
| 389 | + string categoryId, | |
| 390 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope, | |
| 391 | + string? currentUserId, | |
| 392 | + DateTime now) | |
| 393 | + { | |
| 394 | + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync( | |
| 395 | + _dbContext.SqlSugarClient, | |
| 396 | + _guidGenerator, | |
| 397 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, | |
| 398 | + categoryId, | |
| 399 | + partnerScope, | |
| 400 | + currentUserId, | |
| 401 | + now); | |
| 402 | + } | |
| 403 | + | |
| 404 | + private async Task ApplyPartnerScopeToGetOutputAsync(LabelCategoryGetOutputDto dto, string entityId) | |
| 405 | + { | |
| 406 | + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( | |
| 407 | + _dbContext.SqlSugarClient, | |
| 408 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category, | |
| 409 | + new[] { entityId }); | |
| 410 | + if (!map.TryGetValue(entityId, out var scope)) | |
| 411 | + { | |
| 412 | + return; | |
| 413 | + } | |
| 414 | + | |
| 415 | + dto.AppliedPartnerType = scope.AppliedPartnerType; | |
| 416 | + dto.Company = scope.Company; | |
| 417 | + dto.PartnerIds = scope.PartnerIds; | |
| 418 | + dto.CompanyIds = scope.PartnerIds; | |
| 419 | + } | |
| 420 | + | |
| 337 | 421 | private const string AllRegionsDisplay = "All Regions"; |
| 338 | 422 | private const string AllLocationsDisplay = "All Locations"; |
| 339 | 423 | private const string EmptyDisplay = "无"; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelMultipleOptionAppService.cs
| ... | ... | @@ -27,14 +27,22 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 27 | 27 | { |
| 28 | 28 | RefAsync<int> total = 0; |
| 29 | 29 | var keyword = input.Keyword?.Trim(); |
| 30 | - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | |
| 31 | - _dbContext.SqlSugarClient, input.GroupId, input.LocationId); | |
| 30 | + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( | |
| 31 | + CurrentUser, | |
| 32 | + _dbContext, | |
| 33 | + input.PartnerId, | |
| 34 | + input.GroupId, | |
| 35 | + input.LocationId); | |
| 36 | + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync( | |
| 37 | + _dbContext.SqlSugarClient, input.PartnerId, scopedLocationIds); | |
| 32 | 38 | |
| 33 | 39 | var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() |
| 34 | 40 | .Where(x => !x.IsDeleted) |
| 35 | 41 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!)) |
| 36 | 42 | .WhereIF(input.State != null, x => x.State == input.State); |
| 37 | 43 | |
| 44 | + query = await LabelEntityPartnerScopeHelper.ApplyMultipleOptionPartnerListFilterAsync( | |
| 45 | + _dbContext.SqlSugarClient, query, scopedPartnerIds); | |
| 38 | 46 | query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds); |
| 39 | 47 | |
| 40 | 48 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| ... | ... | @@ -48,10 +56,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 48 | 56 | |
| 49 | 57 | var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 50 | 58 | var scopeMap = await BuildMultipleOptionScopeMapAsync(entities); |
| 59 | + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( | |
| 60 | + _dbContext.SqlSugarClient, | |
| 61 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, | |
| 62 | + entities.Select(x => x.Id).ToList()); | |
| 51 | 63 | |
| 52 | 64 | var items = entities.Select(x => |
| 53 | 65 | { |
| 54 | 66 | scopeMap.TryGetValue(x.Id, out var scope); |
| 67 | + partnerScopeMap.TryGetValue(x.Id, out var partnerScope); | |
| 55 | 68 | return new LabelMultipleOptionGetListOutputDto |
| 56 | 69 | { |
| 57 | 70 | Id = x.Id, |
| ... | ... | @@ -60,6 +73,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 60 | 73 | OptionValuesJson = x.OptionValuesJson, |
| 61 | 74 | State = x.State, |
| 62 | 75 | AvailabilityType = x.AvailabilityType, |
| 76 | + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll, | |
| 77 | + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay, | |
| 78 | + PartnerIds = partnerScope?.PartnerIds ?? new List<string>(), | |
| 79 | + CompanyIds = partnerScope?.PartnerIds ?? new List<string>(), | |
| 63 | 80 | OrderNum = x.OrderNum, |
| 64 | 81 | LastEdited = x.LastModificationTime ?? x.CreationTime, |
| 65 | 82 | Region = scope?.Region ?? EmptyDisplay, |
| ... | ... | @@ -82,6 +99,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 82 | 99 | } |
| 83 | 100 | |
| 84 | 101 | var dto = MapToGetOutput(entity); |
| 102 | + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id); | |
| 85 | 103 | if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) |
| 86 | 104 | { |
| 87 | 105 | var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionLocationDbEntity>() |
| ... | ... | @@ -107,7 +125,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 107 | 125 | throw new UserFriendlyException("多选项名称不能为空"); |
| 108 | 126 | } |
| 109 | 127 | |
| 110 | - var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); | |
| 128 | + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); | |
| 111 | 129 | |
| 112 | 130 | if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: null)) |
| 113 | 131 | { |
| ... | ... | @@ -134,6 +152,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 134 | 152 | }; |
| 135 | 153 | |
| 136 | 154 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 155 | + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now); | |
| 137 | 156 | await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); |
| 138 | 157 | return await GetAsync(entity.Id); |
| 139 | 158 | } |
| ... | ... | @@ -154,7 +173,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 154 | 173 | throw new UserFriendlyException("多选项名称不能为空"); |
| 155 | 174 | } |
| 156 | 175 | |
| 157 | - var (availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); | |
| 176 | + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveMultipleOptionScopeForSaveAsync(input); | |
| 158 | 177 | |
| 159 | 178 | if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: id)) |
| 160 | 179 | { |
| ... | ... | @@ -171,6 +190,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 171 | 190 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 172 | 191 | |
| 173 | 192 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 193 | + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId, | |
| 194 | + entity.LastModificationTime ?? DateTime.Now); | |
| 174 | 195 | await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, |
| 175 | 196 | entity.LastModificationTime ?? DateTime.Now); |
| 176 | 197 | return await GetAsync(id); |
| ... | ... | @@ -185,6 +206,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 185 | 206 | return; |
| 186 | 207 | } |
| 187 | 208 | |
| 209 | + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync( | |
| 210 | + _dbContext.SqlSugarClient, | |
| 211 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, | |
| 212 | + id); | |
| 188 | 213 | await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>() |
| 189 | 214 | .Where(x => x.MultipleOptionId == id) |
| 190 | 215 | .ExecuteCommandAsync(); |
| ... | ... | @@ -199,9 +224,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 199 | 224 | private const string AllRegionsDisplay = "All Regions"; |
| 200 | 225 | private const string AllLocationsDisplay = "All Locations"; |
| 201 | 226 | |
| 202 | - private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveMultipleOptionScopeForSaveAsync( | |
| 227 | + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List<string> LocationIds)> ResolveMultipleOptionScopeForSaveAsync( | |
| 203 | 228 | LabelMultipleOptionCreateInputVo input) |
| 204 | 229 | { |
| 230 | + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( | |
| 231 | + _dbContext.SqlSugarClient, | |
| 232 | + input.AppliedPartnerType, | |
| 233 | + input.PartnerIds, | |
| 234 | + input.CompanyIds); | |
| 235 | + | |
| 205 | 236 | var regionIds = NormalizeRegionIds(input); |
| 206 | 237 | var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); |
| 207 | 238 | var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); |
| ... | ... | @@ -221,20 +252,70 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO |
| 221 | 252 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); |
| 222 | 253 | } |
| 223 | 254 | |
| 224 | - if (availabilityType == "ALL") | |
| 255 | + var partnerSpecified = string.Equals( | |
| 256 | + partnerScope.AppliedPartnerType, | |
| 257 | + LabelEntityPartnerScopeHelper.ScopeSpecified, | |
| 258 | + StringComparison.OrdinalIgnoreCase); | |
| 259 | + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase); | |
| 260 | + | |
| 261 | + if (partnerSpecified || locationSpecified) | |
| 225 | 262 | { |
| 226 | - return ("ALL", new List<string>()); | |
| 263 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 264 | + _dbContext.SqlSugarClient, | |
| 265 | + partnerSpecified ? partnerScope.PartnerIds : null, | |
| 266 | + locationSpecified ? regionIds : null, | |
| 267 | + locationSpecified ? explicitLocationIds : null); | |
| 268 | + if (merged.Count == 0) | |
| 269 | + { | |
| 270 | + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); | |
| 271 | + } | |
| 272 | + | |
| 273 | + if (locationSpecified) | |
| 274 | + { | |
| 275 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 276 | + return (partnerScope, "SPECIFIED", merged); | |
| 277 | + } | |
| 227 | 278 | } |
| 228 | 279 | |
| 229 | - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 230 | - _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | |
| 231 | - if (merged.Count == 0) | |
| 280 | + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | |
| 232 | 281 | { |
| 233 | - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 282 | + return (partnerScope, "ALL", new List<string>()); | |
| 283 | + } | |
| 284 | + | |
| 285 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 286 | + } | |
| 287 | + | |
| 288 | + private async Task SaveMultipleOptionPartnerScopeAsync( | |
| 289 | + string multipleOptionId, | |
| 290 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope, | |
| 291 | + string? currentUserId, | |
| 292 | + DateTime now) | |
| 293 | + { | |
| 294 | + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync( | |
| 295 | + _dbContext.SqlSugarClient, | |
| 296 | + _guidGenerator, | |
| 297 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, | |
| 298 | + multipleOptionId, | |
| 299 | + partnerScope, | |
| 300 | + currentUserId, | |
| 301 | + now); | |
| 302 | + } | |
| 303 | + | |
| 304 | + private async Task ApplyPartnerScopeToGetOutputAsync(LabelMultipleOptionGetOutputDto dto, string entityId) | |
| 305 | + { | |
| 306 | + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( | |
| 307 | + _dbContext.SqlSugarClient, | |
| 308 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption, | |
| 309 | + new[] { entityId }); | |
| 310 | + if (!map.TryGetValue(entityId, out var scope)) | |
| 311 | + { | |
| 312 | + return; | |
| 234 | 313 | } |
| 235 | 314 | |
| 236 | - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 237 | - return ("SPECIFIED", merged); | |
| 315 | + dto.AppliedPartnerType = scope.AppliedPartnerType; | |
| 316 | + dto.Company = scope.Company; | |
| 317 | + dto.PartnerIds = scope.PartnerIds; | |
| 318 | + dto.CompanyIds = scope.PartnerIds; | |
| 238 | 319 | } |
| 239 | 320 | |
| 240 | 321 | private static List<string> NormalizeRegionIds(LabelMultipleOptionCreateInputVo input) | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/LabelTypeAppService.cs
| ... | ... | @@ -27,14 +27,22 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 27 | 27 | { |
| 28 | 28 | RefAsync<int> total = 0; |
| 29 | 29 | var keyword = input.Keyword?.Trim(); |
| 30 | - var scopedLocationIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | |
| 31 | - _dbContext.SqlSugarClient, input.GroupId, input.LocationId); | |
| 30 | + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( | |
| 31 | + CurrentUser, | |
| 32 | + _dbContext, | |
| 33 | + input.PartnerId, | |
| 34 | + input.GroupId, | |
| 35 | + input.LocationId); | |
| 36 | + var scopedPartnerIds = await LabelEntityPartnerScopeHelper.ResolveListPartnerIdsAsync( | |
| 37 | + _dbContext.SqlSugarClient, input.PartnerId, scopedLocationIds); | |
| 32 | 38 | |
| 33 | 39 | var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() |
| 34 | 40 | .Where(x => !x.IsDeleted) |
| 35 | 41 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!)) |
| 36 | 42 | .WhereIF(input.State != null, x => x.State == input.State); |
| 37 | 43 | |
| 44 | + query = await LabelEntityPartnerScopeHelper.ApplyTypePartnerListFilterAsync( | |
| 45 | + _dbContext.SqlSugarClient, query, scopedPartnerIds); | |
| 38 | 46 | query = ApplyLabelTypeScopeFilter(query, scopedLocationIds); |
| 39 | 47 | |
| 40 | 48 | if (!string.IsNullOrWhiteSpace(input.Sorting)) |
| ... | ... | @@ -51,10 +59,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 51 | 59 | |
| 52 | 60 | var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds); |
| 53 | 61 | var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities); |
| 62 | + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( | |
| 63 | + _dbContext.SqlSugarClient, | |
| 64 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, | |
| 65 | + ids); | |
| 54 | 66 | |
| 55 | 67 | var items = entities.Select(x => |
| 56 | 68 | { |
| 57 | 69 | scopeMap.TryGetValue(x.Id, out var scope); |
| 70 | + partnerScopeMap.TryGetValue(x.Id, out var partnerScope); | |
| 58 | 71 | countMap.TryGetValue(x.Id, out var stats); |
| 59 | 72 | var locationDisplay = scope?.Location ?? EmptyDisplay; |
| 60 | 73 | return new LabelTypeGetListOutputDto |
| ... | ... | @@ -64,6 +77,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 64 | 77 | TypeName = x.TypeName, |
| 65 | 78 | State = x.State, |
| 66 | 79 | AvailabilityType = x.AvailabilityType, |
| 80 | + AppliedPartnerType = partnerScope?.AppliedPartnerType ?? LabelEntityPartnerScopeHelper.ScopeAll, | |
| 81 | + Company = partnerScope?.Company ?? LabelEntityPartnerScopeHelper.AllCompaniesDisplay, | |
| 82 | + PartnerIds = partnerScope?.PartnerIds ?? new List<string>(), | |
| 83 | + CompanyIds = partnerScope?.PartnerIds ?? new List<string>(), | |
| 67 | 84 | OrderNum = x.OrderNum, |
| 68 | 85 | NoOfLabels = stats?.Count ?? 0, |
| 69 | 86 | LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime, |
| ... | ... | @@ -88,6 +105,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 88 | 105 | } |
| 89 | 106 | |
| 90 | 107 | var dto = MapToGetOutput(entity); |
| 108 | + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id); | |
| 91 | 109 | if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) |
| 92 | 110 | { |
| 93 | 111 | var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeLocationDbEntity>() |
| ... | ... | @@ -113,7 +131,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 113 | 131 | throw new UserFriendlyException("类型编码和名称不能为空"); |
| 114 | 132 | } |
| 115 | 133 | |
| 116 | - var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); | |
| 134 | + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); | |
| 117 | 135 | |
| 118 | 136 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() |
| 119 | 137 | .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name)); |
| ... | ... | @@ -141,6 +159,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 141 | 159 | }; |
| 142 | 160 | |
| 143 | 161 | await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); |
| 162 | + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, currentUserId, now); | |
| 144 | 163 | await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); |
| 145 | 164 | return await GetAsync(entity.Id); |
| 146 | 165 | } |
| ... | ... | @@ -161,7 +180,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 161 | 180 | throw new UserFriendlyException("类型编码和名称不能为空"); |
| 162 | 181 | } |
| 163 | 182 | |
| 164 | - var (availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); | |
| 183 | + var (partnerScope, availabilityType, mergedLocationIds) = await ResolveTypeScopeForSaveAsync(input); | |
| 165 | 184 | |
| 166 | 185 | var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() |
| 167 | 186 | .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name)); |
| ... | ... | @@ -179,6 +198,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 179 | 198 | entity.LastModifierId = CurrentUser?.Id?.ToString(); |
| 180 | 199 | |
| 181 | 200 | await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); |
| 201 | + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId, | |
| 202 | + entity.LastModificationTime ?? DateTime.Now); | |
| 182 | 203 | await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, |
| 183 | 204 | entity.LastModificationTime ?? DateTime.Now); |
| 184 | 205 | return await GetAsync(id); |
| ... | ... | @@ -200,6 +221,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 200 | 221 | throw new UserFriendlyException("该标签类型已被标签引用,无法删除"); |
| 201 | 222 | } |
| 202 | 223 | |
| 224 | + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync( | |
| 225 | + _dbContext.SqlSugarClient, | |
| 226 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, | |
| 227 | + id); | |
| 203 | 228 | await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>() |
| 204 | 229 | .Where(x => x.LabelTypeId == id) |
| 205 | 230 | .ExecuteCommandAsync(); |
| ... | ... | @@ -214,9 +239,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 214 | 239 | private const string AllRegionsDisplay = "All Regions"; |
| 215 | 240 | private const string AllLocationsDisplay = "All Locations"; |
| 216 | 241 | |
| 217 | - private async Task<(string AvailabilityType, List<string> LocationIds)> ResolveTypeScopeForSaveAsync( | |
| 242 | + private async Task<(LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult PartnerScope, string AvailabilityType, List<string> LocationIds)> ResolveTypeScopeForSaveAsync( | |
| 218 | 243 | LabelTypeCreateInputVo input) |
| 219 | 244 | { |
| 245 | + var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( | |
| 246 | + _dbContext.SqlSugarClient, | |
| 247 | + input.AppliedPartnerType, | |
| 248 | + input.PartnerIds, | |
| 249 | + input.CompanyIds); | |
| 250 | + | |
| 220 | 251 | var regionIds = NormalizeRegionIds(input); |
| 221 | 252 | var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); |
| 222 | 253 | var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); |
| ... | ... | @@ -236,20 +267,70 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService |
| 236 | 267 | throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); |
| 237 | 268 | } |
| 238 | 269 | |
| 239 | - if (availabilityType == "ALL") | |
| 270 | + var partnerSpecified = string.Equals( | |
| 271 | + partnerScope.AppliedPartnerType, | |
| 272 | + LabelEntityPartnerScopeHelper.ScopeSpecified, | |
| 273 | + StringComparison.OrdinalIgnoreCase); | |
| 274 | + var locationSpecified = string.Equals(availabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase); | |
| 275 | + | |
| 276 | + if (partnerSpecified || locationSpecified) | |
| 240 | 277 | { |
| 241 | - return ("ALL", new List<string>()); | |
| 278 | + var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 279 | + _dbContext.SqlSugarClient, | |
| 280 | + partnerSpecified ? partnerScope.PartnerIds : null, | |
| 281 | + locationSpecified ? regionIds : null, | |
| 282 | + locationSpecified ? explicitLocationIds : null); | |
| 283 | + if (merged.Count == 0) | |
| 284 | + { | |
| 285 | + throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); | |
| 286 | + } | |
| 287 | + | |
| 288 | + if (locationSpecified) | |
| 289 | + { | |
| 290 | + await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 291 | + return (partnerScope, "SPECIFIED", merged); | |
| 292 | + } | |
| 242 | 293 | } |
| 243 | 294 | |
| 244 | - var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( | |
| 245 | - _dbContext.SqlSugarClient, (IReadOnlyList<string>?)null, regionIds, explicitLocationIds); | |
| 246 | - if (merged.Count == 0) | |
| 295 | + if (string.Equals(availabilityType, "ALL", StringComparison.OrdinalIgnoreCase)) | |
| 247 | 296 | { |
| 248 | - throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 297 | + return (partnerScope, "ALL", new List<string>()); | |
| 298 | + } | |
| 299 | + | |
| 300 | + throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); | |
| 301 | + } | |
| 302 | + | |
| 303 | + private async Task SaveTypePartnerScopeAsync( | |
| 304 | + string labelTypeId, | |
| 305 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerScopeSaveResult partnerScope, | |
| 306 | + string? currentUserId, | |
| 307 | + DateTime now) | |
| 308 | + { | |
| 309 | + await LabelEntityPartnerScopeHelper.SavePartnerScopeAsync( | |
| 310 | + _dbContext.SqlSugarClient, | |
| 311 | + _guidGenerator, | |
| 312 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, | |
| 313 | + labelTypeId, | |
| 314 | + partnerScope, | |
| 315 | + currentUserId, | |
| 316 | + now); | |
| 317 | + } | |
| 318 | + | |
| 319 | + private async Task ApplyPartnerScopeToGetOutputAsync(LabelTypeGetOutputDto dto, string entityId) | |
| 320 | + { | |
| 321 | + var map = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync( | |
| 322 | + _dbContext.SqlSugarClient, | |
| 323 | + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type, | |
| 324 | + new[] { entityId }); | |
| 325 | + if (!map.TryGetValue(entityId, out var scope)) | |
| 326 | + { | |
| 327 | + return; | |
| 249 | 328 | } |
| 250 | 329 | |
| 251 | - await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(_dbContext.SqlSugarClient, merged); | |
| 252 | - return ("SPECIFIED", merged); | |
| 330 | + dto.AppliedPartnerType = scope.AppliedPartnerType; | |
| 331 | + dto.Company = scope.Company; | |
| 332 | + dto.PartnerIds = scope.PartnerIds; | |
| 333 | + dto.CompanyIds = scope.PartnerIds; | |
| 253 | 334 | } |
| 254 | 335 | |
| 255 | 336 | private static List<string> NormalizeRegionIds(LabelTypeCreateInputVo input) | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ProductCategoryAppService.cs
| ... | ... | @@ -41,7 +41,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 41 | 41 | (x.DisplayText != null && x.DisplayText.Contains(keyword!))) |
| 42 | 42 | .WhereIF(input.State != null, x => x.State == input.State); |
| 43 | 43 | |
| 44 | - query = await ApplyCategoryScopeFilterAsync(query, input.GroupId, input.LocationId); | |
| 44 | + query = await ApplyCategoryScopeFilterAsync(query, input.PartnerId, input.GroupId, input.LocationId); | |
| 45 | 45 | |
| 46 | 46 | // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column |
| 47 | 47 | // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) |
| ... | ... | @@ -116,6 +116,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 116 | 116 | throw new UserFriendlyException("类别不存在"); |
| 117 | 117 | } |
| 118 | 118 | |
| 119 | + await EnsureCategoryVisibleToCurrentUserAsync(entity); | |
| 120 | + | |
| 119 | 121 | var dto = MapToGetOutput(entity); |
| 120 | 122 | if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) |
| 121 | 123 | { |
| ... | ... | @@ -189,6 +191,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 189 | 191 | throw new UserFriendlyException("类别不存在"); |
| 190 | 192 | } |
| 191 | 193 | |
| 194 | + await EnsureCategoryVisibleToCurrentUserAsync(entity); | |
| 195 | + | |
| 192 | 196 | var code = NormalizeCategoryCode(input.CategoryCode); |
| 193 | 197 | var name = input.CategoryName?.Trim(); |
| 194 | 198 | if (string.IsNullOrWhiteSpace(name)) |
| ... | ... | @@ -231,6 +235,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 231 | 235 | return; |
| 232 | 236 | } |
| 233 | 237 | |
| 238 | + await EnsureCategoryVisibleToCurrentUserAsync(entity); | |
| 239 | + | |
| 234 | 240 | // 若被产品引用则不允许删除 |
| 235 | 241 | var usedByProduct = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() |
| 236 | 242 | .AnyAsync(x => !x.IsDeleted && x.CategoryId == id); |
| ... | ... | @@ -321,28 +327,59 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp |
| 321 | 327 | |
| 322 | 328 | private async Task<ISugarQueryable<FlProductCategoryDbEntity>> ApplyCategoryScopeFilterAsync( |
| 323 | 329 | ISugarQueryable<FlProductCategoryDbEntity> query, |
| 330 | + string? partnerId, | |
| 324 | 331 | string? groupId, |
| 325 | 332 | string? locationId) |
| 326 | 333 | { |
| 327 | - var scopeLocIds = await LocationScopeBindingHelper.ResolveScopedLocationIdsAsync( | |
| 328 | - _dbContext.SqlSugarClient, groupId, locationId); | |
| 329 | - if (scopeLocIds is null) | |
| 334 | + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( | |
| 335 | + CurrentUser, _dbContext, partnerId, groupId, locationId); | |
| 336 | + if (scopedLocationIds is null) | |
| 330 | 337 | { |
| 331 | 338 | return query; |
| 332 | 339 | } |
| 333 | 340 | |
| 334 | - if (scopeLocIds.Count == 0) | |
| 341 | + if (scopedLocationIds.Count == 0) | |
| 335 | 342 | { |
| 336 | - return query.Where(c => c.AvailabilityType == "ALL"); | |
| 343 | + return query.Where(_ => false); | |
| 337 | 344 | } |
| 338 | 345 | |
| 346 | + // 非平台管理员:仅返回绑定门店与可见范围有交集的分类(不再因 AvailabilityType=ALL 暴露其它公司数据) | |
| 339 | 347 | return query.Where(c => |
| 340 | - c.AvailabilityType == "ALL" || | |
| 341 | 348 | SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>() |
| 342 | - .Where(cl => cl.CategoryId == c.Id && scopeLocIds.Contains(cl.LocationId)) | |
| 349 | + .Where(cl => cl.CategoryId == c.Id && scopedLocationIds.Contains(cl.LocationId)) | |
| 343 | 350 | .Any()); |
| 344 | 351 | } |
| 345 | 352 | |
| 353 | + /// <summary> | |
| 354 | + /// 详情/编辑/删除:非管理员须与绑定门店范围有交集。 | |
| 355 | + /// </summary> | |
| 356 | + private async Task EnsureCategoryVisibleToCurrentUserAsync(FlProductCategoryDbEntity entity) | |
| 357 | + { | |
| 358 | + if (ReportsRoleHelper.IsAdminRole(CurrentUser)) | |
| 359 | + { | |
| 360 | + return; | |
| 361 | + } | |
| 362 | + | |
| 363 | + var scopedLocationIds = await TeamMemberListScopeHelper.ResolveListLocationIdsAsync( | |
| 364 | + CurrentUser, _dbContext, null, null, null); | |
| 365 | + if (scopedLocationIds is null) | |
| 366 | + { | |
| 367 | + return; | |
| 368 | + } | |
| 369 | + | |
| 370 | + if (scopedLocationIds.Count == 0) | |
| 371 | + { | |
| 372 | + throw new UserFriendlyException("无权访问该类别"); | |
| 373 | + } | |
| 374 | + | |
| 375 | + var visible = await _dbContext.SqlSugarClient.Queryable<FlProductCategoryLocationDbEntity>() | |
| 376 | + .AnyAsync(cl => cl.CategoryId == entity.Id && scopedLocationIds.Contains(cl.LocationId)); | |
| 377 | + if (!visible) | |
| 378 | + { | |
| 379 | + throw new UserFriendlyException("无权访问该类别"); | |
| 380 | + } | |
| 381 | + } | |
| 382 | + | |
| 346 | 383 | private async Task SaveCategoryLocationsAsync( |
| 347 | 384 | string categoryId, |
| 348 | 385 | string availabilityType, | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/ReportsAppService.cs
| ... | ... | @@ -51,20 +51,25 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 51 | 51 | throw new UserFriendlyException("用户未登录"); |
| 52 | 52 | } |
| 53 | 53 | |
| 54 | - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 54 | + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( | |
| 55 | + CurrentUser, | |
| 56 | + _dbContext.SqlSugarClient, | |
| 57 | + input.PartnerId, | |
| 58 | + input.GroupId, | |
| 59 | + input.LocationId); | |
| 55 | 60 | if (locationIds is not null && locationIds.Count == 0) |
| 56 | 61 | { |
| 57 | 62 | return EmptyPrintLogPage(input); |
| 58 | 63 | } |
| 59 | 64 | |
| 60 | 65 | var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); |
| 61 | - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 66 | + var viewAllPrints = await ResolveViewAllPrintsAsync(); | |
| 62 | 67 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 63 | 68 | var keyword = input.Keyword?.Trim(); |
| 64 | 69 | |
| 65 | 70 | RefAsync<int> total = 0; |
| 66 | 71 | |
| 67 | - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 72 | + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword) | |
| 68 | 73 | .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) |
| 69 | 74 | .Where((t, l, p, lc, pc, loc, tpl) => |
| 70 | 75 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && |
| ... | ... | @@ -166,18 +171,23 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 166 | 171 | throw new UserFriendlyException("用户未登录"); |
| 167 | 172 | } |
| 168 | 173 | |
| 169 | - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 174 | + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( | |
| 175 | + CurrentUser, | |
| 176 | + _dbContext.SqlSugarClient, | |
| 177 | + input.PartnerId, | |
| 178 | + input.GroupId, | |
| 179 | + input.LocationId); | |
| 170 | 180 | if (locationIds is not null && locationIds.Count == 0) |
| 171 | 181 | { |
| 172 | 182 | return BuildEmptyPdf("print-log-empty.pdf"); |
| 173 | 183 | } |
| 174 | 184 | |
| 175 | 185 | var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); |
| 176 | - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 186 | + var viewAllPrints = await ResolveViewAllPrintsAsync(); | |
| 177 | 187 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 178 | 188 | var keyword = input.Keyword?.Trim(); |
| 179 | 189 | |
| 180 | - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 190 | + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword) | |
| 181 | 191 | .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) |
| 182 | 192 | .Where((t, l, p, lc, pc, loc, tpl) => |
| 183 | 193 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && |
| ... | ... | @@ -305,7 +315,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 305 | 315 | throw new UserFriendlyException("用户未登录"); |
| 306 | 316 | } |
| 307 | 317 | |
| 308 | - var locationIds = await ResolveFilteredLocationIdsAsync(input.PartnerId, input.GroupId, input.LocationId); | |
| 318 | + var locationIds = await ReportsLocationScopeHelper.ResolveReportLocationIdsAsync( | |
| 319 | + CurrentUser, | |
| 320 | + _dbContext.SqlSugarClient, | |
| 321 | + input.PartnerId, | |
| 322 | + input.GroupId, | |
| 323 | + input.LocationId); | |
| 309 | 324 | if (locationIds is not null && locationIds.Count == 0) |
| 310 | 325 | { |
| 311 | 326 | var emptyMs = ReportsPrintLogExcelHelper.BuildWorkbook(Array.Empty<ReportsPrintLogListItemDto>()); |
| ... | ... | @@ -315,11 +330,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 315 | 330 | } |
| 316 | 331 | |
| 317 | 332 | var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); |
| 318 | - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 333 | + var viewAllPrints = await ResolveViewAllPrintsAsync(); | |
| 319 | 334 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 320 | 335 | var keyword = input.Keyword?.Trim(); |
| 321 | 336 | |
| 322 | - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) | |
| 337 | + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword) | |
| 323 | 338 | .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) |
| 324 | 339 | .Where((t, l, p, lc, pc, loc, tpl) => |
| 325 | 340 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && |
| ... | ... | @@ -414,11 +429,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 414 | 429 | } |
| 415 | 430 | |
| 416 | 431 | var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); |
| 417 | - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 432 | + var viewAllPrints = await ResolveViewAllPrintsAsync(); | |
| 418 | 433 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 419 | 434 | var templateKeyword = input.Keyword?.Trim(); |
| 420 | 435 | |
| 421 | - var groupedRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword: null, | |
| 436 | + var groupedRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword: null, | |
| 422 | 437 | restrictToCreator: false) |
| 423 | 438 | .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) |
| 424 | 439 | .Where((t, l, p, lc, pc, loc, tpl) => |
| ... | ... | @@ -499,17 +514,17 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 499 | 514 | |
| 500 | 515 | var prevEndExcl = curStart; |
| 501 | 516 | var prevStart = curStart - span; |
| 502 | - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); | |
| 517 | + var viewAllPrints = await ResolveViewAllPrintsAsync(); | |
| 503 | 518 | var currentUserIdStr = CurrentUser.Id.Value.ToString(); |
| 504 | 519 | var keyword = input.Keyword?.Trim(); |
| 505 | 520 | |
| 506 | - var totalCur = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 521 | + var totalCur = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) | |
| 507 | 522 | .Where((t, l, p, lc, pc, loc) => |
| 508 | 523 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && |
| 509 | 524 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) |
| 510 | 525 | .CountAsync(); |
| 511 | 526 | |
| 512 | - var totalPrev = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 527 | + var totalPrev = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) | |
| 513 | 528 | .Where((t, l, p, lc, pc, loc) => |
| 514 | 529 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && |
| 515 | 530 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) |
| ... | ... | @@ -520,7 +535,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 520 | 535 | var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); |
| 521 | 536 | var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); |
| 522 | 537 | |
| 523 | - var categoryRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 538 | + var categoryRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) | |
| 524 | 539 | .Where((t, l, p, lc, pc, loc) => |
| 525 | 540 | l.LabelCategoryId != null && |
| 526 | 541 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && |
| ... | ... | @@ -531,7 +546,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 531 | 546 | |
| 532 | 547 | var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); |
| 533 | 548 | |
| 534 | - var productRows = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 549 | + var productRows = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) | |
| 535 | 550 | .Where((t, l, p, lc, pc, loc) => |
| 536 | 551 | !string.IsNullOrEmpty(p.Id) && |
| 537 | 552 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && |
| ... | ... | @@ -552,7 +567,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 552 | 567 | |
| 553 | 568 | var trendEndExcl = trendEndDay.AddDays(1); |
| 554 | 569 | |
| 555 | - var trendRaw = await BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword, restrictToCreator: false) | |
| 570 | + var trendRaw = await BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword, restrictToCreator: false) | |
| 556 | 571 | .Where((t, l, p, lc, pc, loc) => |
| 557 | 572 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && |
| 558 | 573 | SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) |
| ... | ... | @@ -704,7 +719,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 704 | 719 | FlProductCategoryDbEntity, LocationAggregateRoot> |
| 705 | 720 | BuildReportTaskCore( |
| 706 | 721 | List<string>? locationIds, |
| 707 | - bool isAdmin, | |
| 722 | + bool viewAllPrints, | |
| 708 | 723 | string currentUserIdStr, |
| 709 | 724 | string? keyword, |
| 710 | 725 | bool restrictToCreator = true) |
| ... | ... | @@ -717,7 +732,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 717 | 732 | .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) => |
| 718 | 733 | t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) |
| 719 | 734 | .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) |
| 720 | - .WhereIF(restrictToCreator && !isAdmin, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) | |
| 735 | + .WhereIF(restrictToCreator && !viewAllPrints, (t, l, p, lc, pc, loc) => t.CreatedBy == currentUserIdStr) | |
| 721 | 736 | .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) |
| 722 | 737 | .WhereIF(!string.IsNullOrWhiteSpace(keyword), |
| 723 | 738 | (t, l, p, lc, pc, loc) => |
| ... | ... | @@ -739,56 +754,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService |
| 739 | 754 | private static decimal CalcChangeRate(int current, int previous) => |
| 740 | 755 | CalcChangeRate((decimal)current, (decimal)previous); |
| 741 | 756 | |
| 742 | - private async Task<List<string>?> ResolveFilteredLocationIdsAsync(string? partnerId, string? groupId, | |
| 743 | - string? locationId) | |
| 744 | - { | |
| 745 | - var locId = locationId?.Trim(); | |
| 746 | - if (!string.IsNullOrWhiteSpace(locId)) | |
| 747 | - { | |
| 748 | - return new List<string> { locId }; | |
| 749 | - } | |
| 750 | - | |
| 751 | - var gid = groupId?.Trim(); | |
| 752 | - var pid = partnerId?.Trim(); | |
| 753 | - | |
| 754 | - if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) | |
| 755 | - { | |
| 756 | - return null; | |
| 757 | - } | |
| 758 | - | |
| 759 | - var q = _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>().Where(x => !x.IsDeleted); | |
| 760 | - | |
| 761 | - if (!string.IsNullOrWhiteSpace(gid)) | |
| 762 | - { | |
| 763 | - var g = await _dbContext.SqlSugarClient.Queryable<FlGroupDbEntity>() | |
| 764 | - .FirstAsync(x => !x.IsDeleted && x.Id == gid); | |
| 765 | - if (g is null) | |
| 766 | - { | |
| 767 | - return new List<string>(); | |
| 768 | - } | |
| 769 | - | |
| 770 | - var gName = g.GroupName?.Trim() ?? string.Empty; | |
| 771 | - var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 772 | - .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); | |
| 773 | - var pName = partner?.PartnerName?.Trim() ?? string.Empty; | |
| 774 | - q = q.Where(x => x.GroupName == gName && x.Partner == pName); | |
| 775 | - } | |
| 776 | - else if (!string.IsNullOrWhiteSpace(pid)) | |
| 777 | - { | |
| 778 | - var partner = await _dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>() | |
| 779 | - .FirstAsync(x => !x.IsDeleted && x.Id == pid); | |
| 780 | - if (partner is null) | |
| 781 | - { | |
| 782 | - return new List<string>(); | |
| 783 | - } | |
| 784 | - | |
| 785 | - var pName = partner.PartnerName?.Trim() ?? string.Empty; | |
| 786 | - q = q.Where(x => x.Partner == pName); | |
| 787 | - } | |
| 788 | - | |
| 789 | - var ids = await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); | |
| 790 | - return ids; | |
| 791 | - } | |
| 757 | + private Task<bool> ResolveViewAllPrintsAsync() => | |
| 758 | + UsAppPrintLogScopeHelper.CanViewAllPrintsAtLocationAsync(CurrentUser, _dbContext.SqlSugarClient); | |
| 792 | 759 | |
| 793 | 760 | private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, |
| 794 | 761 | DateTime? endDate) | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppAuthAppService.cs
| ... | ... | @@ -85,7 +85,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 85 | 85 | /// </summary> |
| 86 | 86 | /// <remarks> |
| 87 | 87 | /// 行为与系统 <c>AccountService.PostLoginAsync</c> 一致(含验证码、登录日志事件)。 |
| 88 | - /// 门店数据来自 <c>userlocation</c> 与 <c>location</c> 表。 | |
| 88 | + /// 门店来自 <c>userlocation</c>;Company Admin 展开为绑定 Company 下全部门店。 | |
| 89 | 89 | /// </remarks> |
| 90 | 90 | /// <param name="input">邮箱、密码;若系统开启验证码则需传 Uuid、Code</param> |
| 91 | 91 | /// <returns>Token、RefreshToken 与绑定门店</returns> |
| ... | ... | @@ -150,7 +150,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 150 | 150 | _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); |
| 151 | 151 | |
| 152 | 152 | /// <summary> |
| 153 | - /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) | |
| 153 | + /// 获取当前登录用户可选门店(切换门店时可重新拉取);Company Admin 返回绑定 Company 下全部门店。 | |
| 154 | 154 | /// </summary> |
| 155 | 155 | [Authorize] |
| 156 | 156 | public virtual async Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync() |
| ... | ... | @@ -549,21 +549,25 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService |
| 549 | 549 | |
| 550 | 550 | private async Task<List<UsAppBoundLocationDto>> LoadBoundLocationsAsync(Guid userId) |
| 551 | 551 | { |
| 552 | - var userIdStr = userId.ToString(); | |
| 553 | - var links = await _dbContext.SqlSugarClient.Queryable<UserLocationDbEntity>() | |
| 554 | - .Where(x => !x.IsDeleted && x.UserId == userIdStr) | |
| 555 | - .Select(x => x.LocationId) | |
| 556 | - .ToListAsync(); | |
| 552 | + var accessibleIds = await LocationScopeBindingHelper.ResolveAppAccessibleLocationIdsForUserAsync( | |
| 553 | + _dbContext.SqlSugarClient, userId); | |
| 554 | + if (accessibleIds.Count == 0) | |
| 555 | + { | |
| 556 | + return new List<UsAppBoundLocationDto>(); | |
| 557 | + } | |
| 557 | 558 | |
| 558 | - if (links.Count == 0) | |
| 559 | + var guidList = accessibleIds | |
| 560 | + .Select(x => Guid.TryParse(x, out var g) ? g : (Guid?)null) | |
| 561 | + .Where(x => x.HasValue) | |
| 562 | + .Select(x => x!.Value) | |
| 563 | + .ToList(); | |
| 564 | + if (guidList.Count == 0) | |
| 559 | 565 | { |
| 560 | 566 | return new List<UsAppBoundLocationDto>(); |
| 561 | 567 | } |
| 562 | 568 | |
| 563 | - var wanted = links.Distinct().ToList(); | |
| 564 | 569 | var locations = (await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() |
| 565 | - .Where(x => !x.IsDeleted) | |
| 566 | - .Where(x => wanted.Contains(x.Id.ToString())) | |
| 570 | + .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) | |
| 567 | 571 | .ToListAsync()) |
| 568 | 572 | .OrderBy(x => x.OrderNum) |
| 569 | 573 | .ThenBy(x => x.LocationName) | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application/Services/UsAppLabelingAppService.cs
| ... | ... | @@ -945,9 +945,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 945 | 945 | }) |
| 946 | 946 | .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); |
| 947 | 947 | |
| 948 | + var pageTaskIds = pageRows.Select(x => x.Id).ToList(); | |
| 949 | + var createdByByTaskId = pageTaskIds.Count == 0 | |
| 950 | + ? new Dictionary<string, string?>(StringComparer.Ordinal) | |
| 951 | + : (await _dbContext.SqlSugarClient.Queryable<FlLabelPrintTaskDbEntity>() | |
| 952 | + .Where(t => pageTaskIds.Contains(t.Id)) | |
| 953 | + .Select(t => new { t.Id, t.CreatedBy }) | |
| 954 | + .ToListAsync()) | |
| 955 | + .ToDictionary(x => x.Id, x => x.CreatedBy, StringComparer.Ordinal); | |
| 956 | + | |
| 948 | 957 | var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync( |
| 949 | 958 | _dbContext.SqlSugarClient, |
| 950 | - pageRows.Select(x => x.CreatedBy)); | |
| 959 | + createdByByTaskId.Values.Concat(pageRows.Select(x => x.CreatedBy))); | |
| 951 | 960 | |
| 952 | 961 | var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( |
| 953 | 962 | _dbContext.SqlSugarClient, |
| ... | ... | @@ -968,7 +977,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ |
| 968 | 977 | LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), |
| 969 | 978 | PrintInputJson = x.PrintInputJson, |
| 970 | 979 | PrintedAt = x.PrintedAt ?? x.CreationTime, |
| 971 | - OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName(operatorMap, x.CreatedBy), | |
| 980 | + OperatorName = UsAppPrintLogScopeHelper.ResolveOperatorName( | |
| 981 | + operatorMap, | |
| 982 | + createdByByTaskId.TryGetValue(x.Id, out var createdBy) ? createdBy : x.CreatedBy), | |
| 972 | 983 | LocationName = locationName |
| 973 | 984 | }).ToList(); |
| 974 | 985 | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/scripts/fl_label_entity_partner_scope.sql
0 → 100644
| 1 | +-- 标签类型 / 分类 / 多选项适用 Company 多选(ALL/SPECIFIED) | |
| 2 | +-- 执行前请备份;可重复执行 | |
| 3 | + | |
| 4 | +SET @db := DATABASE(); | |
| 5 | + | |
| 6 | +-- fl_label_type.AppliedPartnerType | |
| 7 | +SET @col_type_partner := ( | |
| 8 | + SELECT COUNT(*) FROM information_schema.COLUMNS | |
| 9 | + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_type' AND COLUMN_NAME = 'AppliedPartnerType' | |
| 10 | +); | |
| 11 | +SET @ddl_type_partner := IF( | |
| 12 | + @col_type_partner = 0, | |
| 13 | + 'ALTER TABLE `fl_label_type` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`', | |
| 14 | + 'SELECT 1' | |
| 15 | +); | |
| 16 | +PREPARE stmt_type_partner FROM @ddl_type_partner; | |
| 17 | +EXECUTE stmt_type_partner; | |
| 18 | +DEALLOCATE PREPARE stmt_type_partner; | |
| 19 | + | |
| 20 | +-- fl_label_category.AppliedPartnerType | |
| 21 | +SET @col_cat_partner := ( | |
| 22 | + SELECT COUNT(*) FROM information_schema.COLUMNS | |
| 23 | + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_category' AND COLUMN_NAME = 'AppliedPartnerType' | |
| 24 | +); | |
| 25 | +SET @ddl_cat_partner := IF( | |
| 26 | + @col_cat_partner = 0, | |
| 27 | + 'ALTER TABLE `fl_label_category` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`', | |
| 28 | + 'SELECT 1' | |
| 29 | +); | |
| 30 | +PREPARE stmt_cat_partner FROM @ddl_cat_partner; | |
| 31 | +EXECUTE stmt_cat_partner; | |
| 32 | +DEALLOCATE PREPARE stmt_cat_partner; | |
| 33 | + | |
| 34 | +-- fl_label_multiple_option.AppliedPartnerType | |
| 35 | +SET @col_opt_partner := ( | |
| 36 | + SELECT COUNT(*) FROM information_schema.COLUMNS | |
| 37 | + WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'fl_label_multiple_option' AND COLUMN_NAME = 'AppliedPartnerType' | |
| 38 | +); | |
| 39 | +SET @ddl_opt_partner := IF( | |
| 40 | + @col_opt_partner = 0, | |
| 41 | + 'ALTER TABLE `fl_label_multiple_option` ADD COLUMN `AppliedPartnerType` varchar(20) NOT NULL DEFAULT ''ALL'' COMMENT ''适用Company:ALL/SPECIFIED'' AFTER `AvailabilityType`', | |
| 42 | + 'SELECT 1' | |
| 43 | +); | |
| 44 | +PREPARE stmt_opt_partner FROM @ddl_opt_partner; | |
| 45 | +EXECUTE stmt_opt_partner; | |
| 46 | +DEALLOCATE PREPARE stmt_opt_partner; | |
| 47 | + | |
| 48 | +CREATE TABLE IF NOT EXISTS `fl_label_type_partner` ( | |
| 49 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 50 | + `LabelTypeId` varchar(36) NOT NULL COMMENT 'fl_label_type.Id', | |
| 51 | + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id', | |
| 52 | + `CreationTime` datetime NOT NULL COMMENT '创建时间', | |
| 53 | + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', | |
| 54 | + PRIMARY KEY (`Id`), | |
| 55 | + KEY `idx_fl_ltyp_template` (`LabelTypeId`), | |
| 56 | + KEY `idx_fl_ltyp_partner` (`PartnerId`) | |
| 57 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签类型适用Company'; | |
| 58 | + | |
| 59 | +CREATE TABLE IF NOT EXISTS `fl_label_category_partner` ( | |
| 60 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 61 | + `CategoryId` varchar(36) NOT NULL COMMENT 'fl_label_category.Id', | |
| 62 | + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id', | |
| 63 | + `CreationTime` datetime NOT NULL COMMENT '创建时间', | |
| 64 | + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', | |
| 65 | + PRIMARY KEY (`Id`), | |
| 66 | + KEY `idx_fl_lcatp_category` (`CategoryId`), | |
| 67 | + KEY `idx_fl_lcatp_partner` (`PartnerId`) | |
| 68 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签分类适用Company'; | |
| 69 | + | |
| 70 | +CREATE TABLE IF NOT EXISTS `fl_label_multiple_option_partner` ( | |
| 71 | + `Id` varchar(36) NOT NULL COMMENT '主键', | |
| 72 | + `MultipleOptionId` varchar(36) NOT NULL COMMENT 'fl_label_multiple_option.Id', | |
| 73 | + `PartnerId` varchar(36) NOT NULL COMMENT 'fl_partner.Id', | |
| 74 | + `CreationTime` datetime NOT NULL COMMENT '创建时间', | |
| 75 | + `CreatorId` varchar(36) DEFAULT NULL COMMENT '创建者', | |
| 76 | + PRIMARY KEY (`Id`), | |
| 77 | + KEY `idx_fl_lmop_option` (`MultipleOptionId`), | |
| 78 | + KEY `idx_fl_lmop_partner` (`PartnerId`) | |
| 79 | +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='标签多选项适用Company'; | ... | ... |
美国版/Food Labeling Management Code/Yi.Abp.Net8/src/Yi.Abp.Web/appsettings.json
| ... | ... | @@ -20,7 +20,7 @@ |
| 20 | 20 | }, |
| 21 | 21 | //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) |
| 22 | 22 | "App": { |
| 23 | - "SelfUrl": "http://192.168.31.89:19001", | |
| 23 | + "SelfUrl": "http://192.168.31.88:19001", | |
| 24 | 24 | "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:5173;http://localhost:3000" |
| 25 | 25 | }, |
| 26 | 26 | //配置 | ... | ... |
美国版/Food Labeling Management Platform/src/components/labels/LabelCategoriesView.tsx
| ... | ... | @@ -60,6 +60,7 @@ import { |
| 60 | 60 | } from "../ui/pagination"; |
| 61 | 61 | import { |
| 62 | 62 | getLabelCategories, |
| 63 | + getLabelCategory, | |
| 63 | 64 | createLabelCategory, |
| 64 | 65 | updateLabelCategory, |
| 65 | 66 | deleteLabelCategory, |
| ... | ... | @@ -68,10 +69,9 @@ import { getLocations } from "../../services/locationService"; |
| 68 | 69 | import { getGroups } from "../../services/groupService"; |
| 69 | 70 | import { getPartners } from "../../services/partnerService"; |
| 70 | 71 | import { |
| 71 | - buildSpecifiedLocationPayload, | |
| 72 | - effectiveScopePartnerId, | |
| 72 | + buildEntityScopeSaveFromForm, | |
| 73 | 73 | hydrateCategoryScopeFromLocationIds, |
| 74 | - scopePartnerValidationMessage, | |
| 74 | + hydrateEntityScopeFromDto, | |
| 75 | 75 | } from "../../lib/categoryScopeForm"; |
| 76 | 76 | import { CategoryScopeFields } from "../shared/category-scope-fields"; |
| 77 | 77 | import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; |
| ... | ... | @@ -841,7 +841,9 @@ function CreateLabelCategoryDialog({ |
| 841 | 841 | const [displayTextForPhoto, setDisplayTextForPhoto] = useState(""); |
| 842 | 842 | const [buttonBgColor, setButtonBgColor] = useState("#3B82F6"); |
| 843 | 843 | const [selectedPartnerId, setSelectedPartnerId] = useState(""); |
| 844 | + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]); | |
| 844 | 845 | const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]); |
| 846 | + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]); | |
| 845 | 847 | const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]); |
| 846 | 848 | const [form, setForm] = useState<LabelCategoryCreateInput>({ |
| 847 | 849 | categoryName: "", |
| ... | ... | @@ -862,7 +864,9 @@ function CreateLabelCategoryDialog({ |
| 862 | 864 | setDisplayTextForPhoto(""); |
| 863 | 865 | setButtonBgColor("#3B82F6"); |
| 864 | 866 | setSelectedPartnerId(""); |
| 867 | + setSelectedPartnerIds([]); | |
| 865 | 868 | setSelectedRegionNames([]); |
| 869 | + setSelectedRegionIds([]); | |
| 866 | 870 | setSelectedLocationIds([]); |
| 867 | 871 | setForm({ |
| 868 | 872 | categoryName: "", |
| ... | ... | @@ -920,27 +924,20 @@ function CreateLabelCategoryDialog({ |
| 920 | 924 | } |
| 921 | 925 | } |
| 922 | 926 | |
| 923 | - const scopePartnerId = effectiveScopePartnerId( | |
| 927 | + const scopePayload = buildEntityScopeSaveFromForm({ | |
| 924 | 928 | requireCompanySelection, |
| 925 | 929 | selectedPartnerId, |
| 930 | + selectedPartnerIds, | |
| 931 | + selectedRegionNames, | |
| 932 | + selectedRegionIds, | |
| 933 | + selectedLocationIds, | |
| 926 | 934 | fixedPartnerId, |
| 927 | - ); | |
| 928 | - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId); | |
| 929 | - if (partnerErr) { | |
| 930 | - toast.error("Validation failed", { description: partnerErr }); | |
| 931 | - return; | |
| 932 | - } | |
| 933 | - | |
| 934 | - const locPayload = buildSpecifiedLocationPayload( | |
| 935 | 935 | locations, |
| 936 | 936 | partners, |
| 937 | 937 | groups, |
| 938 | - scopePartnerId, | |
| 939 | - selectedRegionNames, | |
| 940 | - selectedLocationIds, | |
| 941 | - ); | |
| 942 | - if (!locPayload.ok) { | |
| 943 | - toast.error("Validation failed", { description: locPayload.message }); | |
| 938 | + }); | |
| 939 | + if (!scopePayload.ok) { | |
| 940 | + toast.error("Validation failed", { description: scopePayload.message }); | |
| 944 | 941 | return; |
| 945 | 942 | } |
| 946 | 943 | |
| ... | ... | @@ -971,8 +968,7 @@ function CreateLabelCategoryDialog({ |
| 971 | 968 | buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null, |
| 972 | 969 | buttonAppearance: tokens, |
| 973 | 970 | buttonStyleJson, |
| 974 | - availabilityType: "SPECIFIED", | |
| 975 | - locationIds: locPayload.locationIds, | |
| 971 | + ...scopePayload.body, | |
| 976 | 972 | }); |
| 977 | 973 | toast.success("Label category created.", { |
| 978 | 974 | description: "The label category has been created successfully.", |
| ... | ... | @@ -1022,6 +1018,11 @@ function CreateLabelCategoryDialog({ |
| 1022 | 1018 | onLocationChange={setSelectedLocationIds} |
| 1023 | 1019 | requireCompanySelection={requireCompanySelection} |
| 1024 | 1020 | fixedPartnerId={fixedPartnerId} |
| 1021 | + templateScopeMode={requireCompanySelection} | |
| 1022 | + selectedPartnerIds={selectedPartnerIds} | |
| 1023 | + onPartnerIdsChange={setSelectedPartnerIds} | |
| 1024 | + selectedRegionIds={selectedRegionIds} | |
| 1025 | + onRegionIdsChange={setSelectedRegionIds} | |
| 1025 | 1026 | /> |
| 1026 | 1027 | |
| 1027 | 1028 | <div className="space-y-2"> |
| ... | ... | @@ -1228,11 +1229,14 @@ function EditLabelCategoryDialog({ |
| 1228 | 1229 | onUpdated: () => void; |
| 1229 | 1230 | }) { |
| 1230 | 1231 | const [submitting, setSubmitting] = useState(false); |
| 1232 | + const [loadingDetail, setLoadingDetail] = useState(false); | |
| 1231 | 1233 | const [apSel, setApSel] = useState({ text: true, color: false, image: false }); |
| 1232 | 1234 | const [displayTextForPhoto, setDisplayTextForPhoto] = useState(""); |
| 1233 | 1235 | const [buttonBgColor, setButtonBgColor] = useState("#3B82F6"); |
| 1234 | 1236 | const [selectedPartnerId, setSelectedPartnerId] = useState(""); |
| 1237 | + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]); | |
| 1235 | 1238 | const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]); |
| 1239 | + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]); | |
| 1236 | 1240 | const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]); |
| 1237 | 1241 | const [form, setForm] = useState<LabelCategoryUpdateInput>({ |
| 1238 | 1242 | categoryName: "", |
| ... | ... | @@ -1249,104 +1253,145 @@ function EditLabelCategoryDialog({ |
| 1249 | 1253 | }, [apSel.text, apSel.color]); |
| 1250 | 1254 | |
| 1251 | 1255 | useEffect(() => { |
| 1252 | - if (!open || !category) return; | |
| 1256 | + if (!open || !category?.id) return; | |
| 1257 | + | |
| 1258 | + const ac = new AbortController(); | |
| 1259 | + setLoadingDetail(true); | |
| 1260 | + | |
| 1261 | + (async () => { | |
| 1262 | + try { | |
| 1263 | + const detail = await getLabelCategory(category.id, ac.signal); | |
| 1264 | + if (ac.signal.aborted) return; | |
| 1265 | + const row = detail.id ? detail : category; | |
| 1266 | + | |
| 1267 | + const hydrateAvailability = () => { | |
| 1268 | + if (requireCompanySelection) { | |
| 1269 | + const scope = hydrateEntityScopeFromDto(row, locations, partners, groups); | |
| 1270 | + setSelectedPartnerIds(scope.partnerIds); | |
| 1271 | + setSelectedRegionIds(scope.regionIds); | |
| 1272 | + setSelectedLocationIds(scope.locationIds); | |
| 1273 | + setSelectedPartnerId(""); | |
| 1274 | + setSelectedRegionNames([]); | |
| 1275 | + return; | |
| 1276 | + } | |
| 1277 | + const rawAt = String(row.availabilityType ?? "ALL").trim().toUpperCase(); | |
| 1278 | + if (rawAt !== "SPECIFIED") { | |
| 1279 | + setSelectedPartnerId(""); | |
| 1280 | + setSelectedRegionNames([]); | |
| 1281 | + setSelectedLocationIds([]); | |
| 1282 | + setSelectedPartnerIds([]); | |
| 1283 | + setSelectedRegionIds([]); | |
| 1284 | + return; | |
| 1285 | + } | |
| 1286 | + const lids = (row.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); | |
| 1287 | + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); | |
| 1288 | + setSelectedPartnerId(scope.partnerId); | |
| 1289 | + setSelectedRegionNames(scope.regionNames); | |
| 1290 | + setSelectedLocationIds(scope.locationIds); | |
| 1291 | + setSelectedPartnerIds([]); | |
| 1292 | + setSelectedRegionIds([]); | |
| 1293 | + }; | |
| 1294 | + | |
| 1295 | + const styleParsed = parseCategoryButtonStyleV1( | |
| 1296 | + row.buttonStyleJson ?? (row as { ButtonStyleJson?: string | null }).ButtonStyleJson, | |
| 1297 | + ); | |
| 1298 | + if (styleParsed) { | |
| 1299 | + const sel = appearanceSelectionFromTokens(styleParsed.appearances); | |
| 1300 | + setApSel(sel); | |
| 1301 | + setDisplayTextForPhoto(String(styleParsed.displayText ?? "")); | |
| 1302 | + setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6"); | |
| 1303 | + setForm({ | |
| 1304 | + categoryName: row.categoryName ?? "", | |
| 1305 | + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null, | |
| 1306 | + state: row.state ?? true, | |
| 1307 | + orderNum: row.orderNum ?? null, | |
| 1308 | + }); | |
| 1309 | + hydrateAvailability(); | |
| 1310 | + return; | |
| 1311 | + } | |
| 1253 | 1312 | |
| 1254 | - const hydrateAvailability = () => { | |
| 1255 | - const rawAt = String(category.availabilityType ?? "ALL").trim().toUpperCase(); | |
| 1256 | - if (rawAt !== "SPECIFIED") { | |
| 1313 | + const rawBa = | |
| 1314 | + row.buttonAppearance ?? | |
| 1315 | + (row as { ButtonAppearance?: string | null }).ButtonAppearance ?? | |
| 1316 | + undefined; | |
| 1317 | + const tokens = parseAppearanceTokens(rawBa); | |
| 1318 | + const valArrZip = parseCategoryPhotoUrlValueArray(row.categoryPhotoUrl); | |
| 1319 | + if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) { | |
| 1320 | + const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, { | |
| 1321 | + categoryName: row.categoryName, | |
| 1322 | + name: undefined, | |
| 1323 | + buttonTextColor: row.buttonTextColor ?? null, | |
| 1324 | + }); | |
| 1325 | + const selZip = appearanceSelectionFromTokens(tokens); | |
| 1326 | + setApSel(selZip); | |
| 1327 | + setDisplayTextForPhoto(String(merged.displayText ?? "")); | |
| 1328 | + setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6"); | |
| 1329 | + setForm({ | |
| 1330 | + categoryName: row.categoryName ?? "", | |
| 1331 | + categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null, | |
| 1332 | + state: row.state ?? true, | |
| 1333 | + orderNum: row.orderNum ?? null, | |
| 1334 | + }); | |
| 1335 | + hydrateAvailability(); | |
| 1336 | + return; | |
| 1337 | + } | |
| 1338 | + | |
| 1339 | + let sel = appearanceSelectionFromTokens(tokens); | |
| 1340 | + if (tokens.length === 0) { | |
| 1341 | + const v = resolveCategoryButtonVisual({ | |
| 1342 | + buttonAppearance: rawBa, | |
| 1343 | + displayText: row.displayText, | |
| 1344 | + buttonBgColor: row.buttonBgColor, | |
| 1345 | + buttonImageUrl: row.buttonImageUrl, | |
| 1346 | + categoryPhotoUrl: row.categoryPhotoUrl, | |
| 1347 | + categoryName: row.categoryName, | |
| 1348 | + }); | |
| 1349 | + if (v.mode === "image") sel = { text: false, color: false, image: true }; | |
| 1350 | + else if (v.mode === "colorText") sel = { text: true, color: true, image: false }; | |
| 1351 | + else if (v.mode === "color") sel = { text: false, color: true, image: false }; | |
| 1352 | + else if (v.mode === "text") sel = { text: true, color: false, image: false }; | |
| 1353 | + else sel = { text: true, color: false, image: false }; | |
| 1354 | + } | |
| 1355 | + setApSel(sel); | |
| 1356 | + const photo = String(row.categoryPhotoUrl ?? "").trim(); | |
| 1357 | + const disp = String(row.displayText ?? "").trim(); | |
| 1358 | + const bgField = normalizeHexColor(row.buttonBgColor); | |
| 1359 | + const hexFromPhoto = normalizeHexColor(photo); | |
| 1360 | + setDisplayTextForPhoto( | |
| 1361 | + String( | |
| 1362 | + disp || | |
| 1363 | + (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") || | |
| 1364 | + (!sel.image && sel.text && !sel.color ? (row.categoryName ?? "").trim() : ""), | |
| 1365 | + ), | |
| 1366 | + ); | |
| 1367 | + setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6"); | |
| 1368 | + setForm({ | |
| 1369 | + categoryName: row.categoryName ?? "", | |
| 1370 | + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null, | |
| 1371 | + state: row.state ?? true, | |
| 1372 | + orderNum: row.orderNum ?? null, | |
| 1373 | + }); | |
| 1374 | + hydrateAvailability(); | |
| 1375 | + } catch { | |
| 1376 | + if (ac.signal.aborted) return; | |
| 1377 | + setForm({ | |
| 1378 | + categoryName: category.categoryName ?? "", | |
| 1379 | + categoryPhotoUrl: category.categoryPhotoUrl ?? null, | |
| 1380 | + state: category.state ?? true, | |
| 1381 | + orderNum: category.orderNum ?? null, | |
| 1382 | + }); | |
| 1257 | 1383 | setSelectedPartnerId(""); |
| 1384 | + setSelectedPartnerIds([]); | |
| 1258 | 1385 | setSelectedRegionNames([]); |
| 1386 | + setSelectedRegionIds([]); | |
| 1259 | 1387 | setSelectedLocationIds([]); |
| 1260 | - return; | |
| 1388 | + } finally { | |
| 1389 | + if (!ac.signal.aborted) setLoadingDetail(false); | |
| 1261 | 1390 | } |
| 1262 | - const lids = (category.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); | |
| 1263 | - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); | |
| 1264 | - setSelectedPartnerId(scope.partnerId); | |
| 1265 | - setSelectedRegionNames(scope.regionNames); | |
| 1266 | - setSelectedLocationIds(scope.locationIds); | |
| 1267 | - }; | |
| 1268 | - | |
| 1269 | - const styleParsed = parseCategoryButtonStyleV1( | |
| 1270 | - category.buttonStyleJson ?? (category as { ButtonStyleJson?: string | null }).ButtonStyleJson, | |
| 1271 | - ); | |
| 1272 | - if (styleParsed) { | |
| 1273 | - const sel = appearanceSelectionFromTokens(styleParsed.appearances); | |
| 1274 | - setApSel(sel); | |
| 1275 | - setDisplayTextForPhoto(String(styleParsed.displayText ?? "")); | |
| 1276 | - setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6"); | |
| 1277 | - setForm({ | |
| 1278 | - categoryName: category.categoryName ?? "", | |
| 1279 | - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null, | |
| 1280 | - state: category.state ?? true, | |
| 1281 | - orderNum: category.orderNum ?? null, | |
| 1282 | - }); | |
| 1283 | - hydrateAvailability(); | |
| 1284 | - return; | |
| 1285 | - } | |
| 1286 | - | |
| 1287 | - const rawBa = | |
| 1288 | - category.buttonAppearance ?? | |
| 1289 | - (category as { ButtonAppearance?: string | null }).ButtonAppearance ?? | |
| 1290 | - undefined; | |
| 1291 | - const tokens = parseAppearanceTokens(rawBa); | |
| 1292 | - const valArrZip = parseCategoryPhotoUrlValueArray(category.categoryPhotoUrl); | |
| 1293 | - if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) { | |
| 1294 | - const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, { | |
| 1295 | - categoryName: category.categoryName, | |
| 1296 | - name: undefined, | |
| 1297 | - buttonTextColor: category.buttonTextColor ?? null, | |
| 1298 | - }); | |
| 1299 | - const selZip = appearanceSelectionFromTokens(tokens); | |
| 1300 | - setApSel(selZip); | |
| 1301 | - setDisplayTextForPhoto(String(merged.displayText ?? "")); | |
| 1302 | - setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6"); | |
| 1303 | - setForm({ | |
| 1304 | - categoryName: category.categoryName ?? "", | |
| 1305 | - categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null, | |
| 1306 | - state: category.state ?? true, | |
| 1307 | - orderNum: category.orderNum ?? null, | |
| 1308 | - }); | |
| 1309 | - hydrateAvailability(); | |
| 1310 | - return; | |
| 1311 | - } | |
| 1391 | + })(); | |
| 1312 | 1392 | |
| 1313 | - let sel = appearanceSelectionFromTokens(tokens); | |
| 1314 | - if (tokens.length === 0) { | |
| 1315 | - const v = resolveCategoryButtonVisual({ | |
| 1316 | - buttonAppearance: rawBa, | |
| 1317 | - displayText: category.displayText, | |
| 1318 | - buttonBgColor: category.buttonBgColor, | |
| 1319 | - buttonImageUrl: category.buttonImageUrl, | |
| 1320 | - categoryPhotoUrl: category.categoryPhotoUrl, | |
| 1321 | - categoryName: category.categoryName, | |
| 1322 | - }); | |
| 1323 | - if (v.mode === "image") sel = { text: false, color: false, image: true }; | |
| 1324 | - else if (v.mode === "colorText") sel = { text: true, color: true, image: false }; | |
| 1325 | - else if (v.mode === "color") sel = { text: false, color: true, image: false }; | |
| 1326 | - else if (v.mode === "text") sel = { text: true, color: false, image: false }; | |
| 1327 | - else sel = { text: true, color: false, image: false }; | |
| 1328 | - } | |
| 1329 | - setApSel(sel); | |
| 1330 | - const photo = String(category.categoryPhotoUrl ?? "").trim(); | |
| 1331 | - const disp = String(category.displayText ?? "").trim(); | |
| 1332 | - const bgField = normalizeHexColor(category.buttonBgColor); | |
| 1333 | - const hexFromPhoto = normalizeHexColor(photo); | |
| 1334 | - setDisplayTextForPhoto( | |
| 1335 | - String( | |
| 1336 | - disp || | |
| 1337 | - (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") || | |
| 1338 | - (!sel.image && sel.text && !sel.color ? (category.categoryName ?? "").trim() : ""), | |
| 1339 | - ), | |
| 1340 | - ); | |
| 1341 | - setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6"); | |
| 1342 | - setForm({ | |
| 1343 | - categoryName: category.categoryName ?? "", | |
| 1344 | - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null, | |
| 1345 | - state: category.state ?? true, | |
| 1346 | - orderNum: category.orderNum ?? null, | |
| 1347 | - }); | |
| 1348 | - hydrateAvailability(); | |
| 1349 | - }, [open, category, locations, partners, groups]); | |
| 1393 | + return () => ac.abort(); | |
| 1394 | + }, [open, category, locations, partners, groups, requireCompanySelection]); | |
| 1350 | 1395 | |
| 1351 | 1396 | const submit = async () => { |
| 1352 | 1397 | if (!category?.id) return; |
| ... | ... | @@ -1391,27 +1436,20 @@ function EditLabelCategoryDialog({ |
| 1391 | 1436 | } |
| 1392 | 1437 | } |
| 1393 | 1438 | |
| 1394 | - const scopePartnerId = effectiveScopePartnerId( | |
| 1439 | + const scopePayload = buildEntityScopeSaveFromForm({ | |
| 1395 | 1440 | requireCompanySelection, |
| 1396 | 1441 | selectedPartnerId, |
| 1442 | + selectedPartnerIds, | |
| 1443 | + selectedRegionNames, | |
| 1444 | + selectedRegionIds, | |
| 1445 | + selectedLocationIds, | |
| 1397 | 1446 | fixedPartnerId, |
| 1398 | - ); | |
| 1399 | - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId); | |
| 1400 | - if (partnerErr) { | |
| 1401 | - toast.error("Validation failed", { description: partnerErr }); | |
| 1402 | - return; | |
| 1403 | - } | |
| 1404 | - | |
| 1405 | - const locPayload = buildSpecifiedLocationPayload( | |
| 1406 | 1447 | locations, |
| 1407 | 1448 | partners, |
| 1408 | 1449 | groups, |
| 1409 | - scopePartnerId, | |
| 1410 | - selectedRegionNames, | |
| 1411 | - selectedLocationIds, | |
| 1412 | - ); | |
| 1413 | - if (!locPayload.ok) { | |
| 1414 | - toast.error("Validation failed", { description: locPayload.message }); | |
| 1450 | + }); | |
| 1451 | + if (!scopePayload.ok) { | |
| 1452 | + toast.error("Validation failed", { description: scopePayload.message }); | |
| 1415 | 1453 | return; |
| 1416 | 1454 | } |
| 1417 | 1455 | |
| ... | ... | @@ -1442,8 +1480,7 @@ function EditLabelCategoryDialog({ |
| 1442 | 1480 | buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null, |
| 1443 | 1481 | buttonAppearance: tokens, |
| 1444 | 1482 | buttonStyleJson, |
| 1445 | - availabilityType: "SPECIFIED", | |
| 1446 | - locationIds: locPayload.locationIds, | |
| 1483 | + ...scopePayload.body, | |
| 1447 | 1484 | }); |
| 1448 | 1485 | toast.success("Label category updated.", { |
| 1449 | 1486 | description: "The label category has been updated successfully.", |
| ... | ... | @@ -1481,19 +1518,28 @@ function EditLabelCategoryDialog({ |
| 1481 | 1518 | /> |
| 1482 | 1519 | </div> |
| 1483 | 1520 | |
| 1484 | - <CategoryScopeFields | |
| 1485 | - partners={partners} | |
| 1486 | - groups={groups} | |
| 1487 | - locations={locations} | |
| 1488 | - selectedPartnerId={selectedPartnerId} | |
| 1489 | - onPartnerChange={setSelectedPartnerId} | |
| 1490 | - selectedRegionNames={selectedRegionNames} | |
| 1491 | - onRegionChange={setSelectedRegionNames} | |
| 1492 | - selectedLocationIds={selectedLocationIds} | |
| 1493 | - onLocationChange={setSelectedLocationIds} | |
| 1494 | - requireCompanySelection={requireCompanySelection} | |
| 1495 | - fixedPartnerId={fixedPartnerId} | |
| 1496 | - /> | |
| 1521 | + {loadingDetail ? ( | |
| 1522 | + <p className="text-sm text-gray-500">Loading company, region and location…</p> | |
| 1523 | + ) : ( | |
| 1524 | + <CategoryScopeFields | |
| 1525 | + partners={partners} | |
| 1526 | + groups={groups} | |
| 1527 | + locations={locations} | |
| 1528 | + selectedPartnerId={selectedPartnerId} | |
| 1529 | + onPartnerChange={setSelectedPartnerId} | |
| 1530 | + selectedRegionNames={selectedRegionNames} | |
| 1531 | + onRegionChange={setSelectedRegionNames} | |
| 1532 | + selectedLocationIds={selectedLocationIds} | |
| 1533 | + onLocationChange={setSelectedLocationIds} | |
| 1534 | + requireCompanySelection={requireCompanySelection} | |
| 1535 | + fixedPartnerId={fixedPartnerId} | |
| 1536 | + templateScopeMode={requireCompanySelection} | |
| 1537 | + selectedPartnerIds={selectedPartnerIds} | |
| 1538 | + onPartnerIdsChange={setSelectedPartnerIds} | |
| 1539 | + selectedRegionIds={selectedRegionIds} | |
| 1540 | + onRegionIdsChange={setSelectedRegionIds} | |
| 1541 | + /> | |
| 1542 | + )} | |
| 1497 | 1543 | |
| 1498 | 1544 | <div className="space-y-2"> |
| 1499 | 1545 | <Label>Button Appearance</Label> |
| ... | ... | @@ -1665,7 +1711,7 @@ function EditLabelCategoryDialog({ |
| 1665 | 1711 | </Button> |
| 1666 | 1712 | <Button |
| 1667 | 1713 | type="button" |
| 1668 | - disabled={submitting} | |
| 1714 | + disabled={submitting || loadingDetail} | |
| 1669 | 1715 | onClick={submit} |
| 1670 | 1716 | className="bg-blue-600 hover:bg-blue-700 text-white" |
| 1671 | 1717 | > | ... | ... |
美国版/Food Labeling Management Platform/src/components/labels/LabelTypesView.tsx
| ... | ... | @@ -51,11 +51,9 @@ import { getLocations } from "../../services/locationService"; |
| 51 | 51 | import { getGroups } from "../../services/groupService"; |
| 52 | 52 | import { getPartners } from "../../services/partnerService"; |
| 53 | 53 | import { |
| 54 | - buildSpecifiedLocationPayload, | |
| 55 | - effectiveScopePartnerId, | |
| 54 | + buildEntityScopeSaveFromForm, | |
| 56 | 55 | hydrateCategoryScopeFromLocationIds, |
| 57 | - regionNamesToGroupIds, | |
| 58 | - scopePartnerValidationMessage, | |
| 56 | + hydrateEntityScopeFromDto, | |
| 59 | 57 | } from "../../lib/categoryScopeForm"; |
| 60 | 58 | import { CategoryScopeFields } from "../shared/category-scope-fields"; |
| 61 | 59 | import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; |
| ... | ... | @@ -822,7 +820,9 @@ function CreateLabelTypeDialog({ |
| 822 | 820 | }) { |
| 823 | 821 | const [submitting, setSubmitting] = useState(false); |
| 824 | 822 | const [selectedPartnerId, setSelectedPartnerId] = useState(""); |
| 823 | + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]); | |
| 825 | 824 | const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]); |
| 825 | + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]); | |
| 826 | 826 | const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]); |
| 827 | 827 | const [form, setForm] = useState<LabelTypeCreateInput>({ |
| 828 | 828 | typeName: "", |
| ... | ... | @@ -832,7 +832,9 @@ function CreateLabelTypeDialog({ |
| 832 | 832 | |
| 833 | 833 | const resetForm = () => { |
| 834 | 834 | setSelectedPartnerId(""); |
| 835 | + setSelectedPartnerIds([]); | |
| 835 | 836 | setSelectedRegionNames([]); |
| 837 | + setSelectedRegionIds([]); | |
| 836 | 838 | setSelectedLocationIds([]); |
| 837 | 839 | setForm({ |
| 838 | 840 | typeName: "", |
| ... | ... | @@ -859,40 +861,29 @@ function CreateLabelTypeDialog({ |
| 859 | 861 | return; |
| 860 | 862 | } |
| 861 | 863 | |
| 862 | - const scopePartnerId = effectiveScopePartnerId( | |
| 864 | + const scopePayload = buildEntityScopeSaveFromForm({ | |
| 863 | 865 | requireCompanySelection, |
| 864 | 866 | selectedPartnerId, |
| 867 | + selectedPartnerIds, | |
| 868 | + selectedRegionNames, | |
| 869 | + selectedRegionIds, | |
| 870 | + selectedLocationIds, | |
| 865 | 871 | fixedPartnerId, |
| 866 | - ); | |
| 867 | - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId); | |
| 868 | - if (partnerErr) { | |
| 869 | - toast.error("Validation failed", { description: partnerErr }); | |
| 870 | - return; | |
| 871 | - } | |
| 872 | - | |
| 873 | - const locPayload = buildSpecifiedLocationPayload( | |
| 874 | 872 | locations, |
| 875 | 873 | partners, |
| 876 | 874 | groups, |
| 877 | - scopePartnerId, | |
| 878 | - selectedRegionNames, | |
| 879 | - selectedLocationIds, | |
| 880 | - ); | |
| 881 | - if (!locPayload.ok) { | |
| 882 | - toast.error("Validation failed", { description: locPayload.message }); | |
| 875 | + }); | |
| 876 | + if (!scopePayload.ok) { | |
| 877 | + toast.error("Validation failed", { description: scopePayload.message }); | |
| 883 | 878 | return; |
| 884 | 879 | } |
| 885 | 880 | |
| 886 | - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId); | |
| 887 | - | |
| 888 | 881 | setSubmitting(true); |
| 889 | 882 | try { |
| 890 | 883 | await createLabelType({ |
| 891 | 884 | ...form, |
| 892 | 885 | typeCode: typeCodeFromName(form.typeName), |
| 893 | - groupIds, | |
| 894 | - regionIds: groupIds, | |
| 895 | - locationIds: locPayload.locationIds, | |
| 886 | + ...scopePayload.body, | |
| 896 | 887 | }); |
| 897 | 888 | toast.success("Label type created.", { |
| 898 | 889 | description: "The label type has been created successfully.", |
| ... | ... | @@ -956,6 +947,11 @@ function CreateLabelTypeDialog({ |
| 956 | 947 | onLocationChange={setSelectedLocationIds} |
| 957 | 948 | requireCompanySelection={requireCompanySelection} |
| 958 | 949 | fixedPartnerId={fixedPartnerId} |
| 950 | + templateScopeMode={requireCompanySelection} | |
| 951 | + selectedPartnerIds={selectedPartnerIds} | |
| 952 | + onPartnerIdsChange={setSelectedPartnerIds} | |
| 953 | + selectedRegionIds={selectedRegionIds} | |
| 954 | + onRegionIdsChange={setSelectedRegionIds} | |
| 959 | 955 | /> |
| 960 | 956 | </div> |
| 961 | 957 | |
| ... | ... | @@ -996,7 +992,9 @@ function EditLabelTypeDialog({ |
| 996 | 992 | const [submitting, setSubmitting] = useState(false); |
| 997 | 993 | const [loadingDetail, setLoadingDetail] = useState(false); |
| 998 | 994 | const [selectedPartnerId, setSelectedPartnerId] = useState(""); |
| 995 | + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]); | |
| 999 | 996 | const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]); |
| 997 | + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]); | |
| 1000 | 998 | const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]); |
| 1001 | 999 | const [form, setForm] = useState<LabelTypeUpdateInput>({ |
| 1002 | 1000 | typeName: "", |
| ... | ... | @@ -1021,12 +1019,24 @@ function EditLabelTypeDialog({ |
| 1021 | 1019 | orderNum: detail.orderNum ?? type.orderNum ?? null, |
| 1022 | 1020 | }); |
| 1023 | 1021 | |
| 1022 | + if (requireCompanySelection) { | |
| 1023 | + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups); | |
| 1024 | + setSelectedPartnerIds(scope.partnerIds); | |
| 1025 | + setSelectedRegionIds(scope.regionIds); | |
| 1026 | + setSelectedLocationIds(scope.locationIds); | |
| 1027 | + setSelectedPartnerId(""); | |
| 1028 | + setSelectedRegionNames([]); | |
| 1029 | + return; | |
| 1030 | + } | |
| 1031 | + | |
| 1024 | 1032 | const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); |
| 1025 | 1033 | if (lids.length > 0) { |
| 1026 | 1034 | const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); |
| 1027 | 1035 | setSelectedPartnerId(scope.partnerId); |
| 1028 | 1036 | setSelectedRegionNames(scope.regionNames); |
| 1029 | 1037 | setSelectedLocationIds(scope.locationIds); |
| 1038 | + setSelectedPartnerIds([]); | |
| 1039 | + setSelectedRegionIds([]); | |
| 1030 | 1040 | return; |
| 1031 | 1041 | } |
| 1032 | 1042 | |
| ... | ... | @@ -1042,6 +1052,8 @@ function EditLabelTypeDialog({ |
| 1042 | 1052 | setSelectedPartnerId(pid); |
| 1043 | 1053 | setSelectedRegionNames(names); |
| 1044 | 1054 | setSelectedLocationIds([]); |
| 1055 | + setSelectedPartnerIds([]); | |
| 1056 | + setSelectedRegionIds([]); | |
| 1045 | 1057 | return; |
| 1046 | 1058 | } |
| 1047 | 1059 | |
| ... | ... | @@ -1057,6 +1069,8 @@ function EditLabelTypeDialog({ |
| 1057 | 1069 | setSelectedRegionNames([]); |
| 1058 | 1070 | setSelectedLocationIds([]); |
| 1059 | 1071 | } |
| 1072 | + setSelectedPartnerIds([]); | |
| 1073 | + setSelectedRegionIds([]); | |
| 1060 | 1074 | } catch { |
| 1061 | 1075 | if (ac.signal.aborted) return; |
| 1062 | 1076 | setForm({ |
| ... | ... | @@ -1065,7 +1079,9 @@ function EditLabelTypeDialog({ |
| 1065 | 1079 | orderNum: type.orderNum ?? null, |
| 1066 | 1080 | }); |
| 1067 | 1081 | setSelectedPartnerId(""); |
| 1082 | + setSelectedPartnerIds([]); | |
| 1068 | 1083 | setSelectedRegionNames([]); |
| 1084 | + setSelectedRegionIds([]); | |
| 1069 | 1085 | setSelectedLocationIds([]); |
| 1070 | 1086 | } finally { |
| 1071 | 1087 | if (!ac.signal.aborted) setLoadingDetail(false); |
| ... | ... | @@ -1073,7 +1089,7 @@ function EditLabelTypeDialog({ |
| 1073 | 1089 | })(); |
| 1074 | 1090 | |
| 1075 | 1091 | return () => ac.abort(); |
| 1076 | - }, [open, type, locations, partners, groups]); | |
| 1092 | + }, [open, type, locations, partners, groups, requireCompanySelection]); | |
| 1077 | 1093 | |
| 1078 | 1094 | const submit = async () => { |
| 1079 | 1095 | if (!type?.id) return; |
| ... | ... | @@ -1088,40 +1104,29 @@ function EditLabelTypeDialog({ |
| 1088 | 1104 | return; |
| 1089 | 1105 | } |
| 1090 | 1106 | |
| 1091 | - const scopePartnerId = effectiveScopePartnerId( | |
| 1107 | + const scopePayload = buildEntityScopeSaveFromForm({ | |
| 1092 | 1108 | requireCompanySelection, |
| 1093 | 1109 | selectedPartnerId, |
| 1110 | + selectedPartnerIds, | |
| 1111 | + selectedRegionNames, | |
| 1112 | + selectedRegionIds, | |
| 1113 | + selectedLocationIds, | |
| 1094 | 1114 | fixedPartnerId, |
| 1095 | - ); | |
| 1096 | - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId); | |
| 1097 | - if (partnerErr) { | |
| 1098 | - toast.error("Validation failed", { description: partnerErr }); | |
| 1099 | - return; | |
| 1100 | - } | |
| 1101 | - | |
| 1102 | - const locPayload = buildSpecifiedLocationPayload( | |
| 1103 | 1115 | locations, |
| 1104 | 1116 | partners, |
| 1105 | 1117 | groups, |
| 1106 | - scopePartnerId, | |
| 1107 | - selectedRegionNames, | |
| 1108 | - selectedLocationIds, | |
| 1109 | - ); | |
| 1110 | - if (!locPayload.ok) { | |
| 1111 | - toast.error("Validation failed", { description: locPayload.message }); | |
| 1118 | + }); | |
| 1119 | + if (!scopePayload.ok) { | |
| 1120 | + toast.error("Validation failed", { description: scopePayload.message }); | |
| 1112 | 1121 | return; |
| 1113 | 1122 | } |
| 1114 | 1123 | |
| 1115 | - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId); | |
| 1116 | - | |
| 1117 | 1124 | setSubmitting(true); |
| 1118 | 1125 | try { |
| 1119 | 1126 | await updateLabelType(type.id, { |
| 1120 | 1127 | ...form, |
| 1121 | 1128 | typeCode: type.typeCode ?? "", |
| 1122 | - groupIds, | |
| 1123 | - regionIds: groupIds, | |
| 1124 | - locationIds: locPayload.locationIds, | |
| 1129 | + ...scopePayload.body, | |
| 1125 | 1130 | }); |
| 1126 | 1131 | toast.success("Label type updated.", { |
| 1127 | 1132 | description: "The label type has been updated successfully.", |
| ... | ... | @@ -1188,6 +1193,11 @@ function EditLabelTypeDialog({ |
| 1188 | 1193 | onLocationChange={setSelectedLocationIds} |
| 1189 | 1194 | requireCompanySelection={requireCompanySelection} |
| 1190 | 1195 | fixedPartnerId={fixedPartnerId} |
| 1196 | + templateScopeMode={requireCompanySelection} | |
| 1197 | + selectedPartnerIds={selectedPartnerIds} | |
| 1198 | + onPartnerIdsChange={setSelectedPartnerIds} | |
| 1199 | + selectedRegionIds={selectedRegionIds} | |
| 1200 | + onRegionIdsChange={setSelectedRegionIds} | |
| 1191 | 1201 | /> |
| 1192 | 1202 | )} |
| 1193 | 1203 | </div> | ... | ... |
美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx
| ... | ... | @@ -51,11 +51,9 @@ import { getLocations } from "../../services/locationService"; |
| 51 | 51 | import { getGroups } from "../../services/groupService"; |
| 52 | 52 | import { getPartners } from "../../services/partnerService"; |
| 53 | 53 | import { |
| 54 | - buildSpecifiedLocationPayload, | |
| 55 | - effectiveScopePartnerId, | |
| 54 | + buildEntityScopeSaveFromForm, | |
| 56 | 55 | hydrateCategoryScopeFromLocationIds, |
| 57 | - regionNamesToGroupIds, | |
| 58 | - scopePartnerValidationMessage, | |
| 56 | + hydrateEntityScopeFromDto, | |
| 59 | 57 | } from "../../lib/categoryScopeForm"; |
| 60 | 58 | import { CategoryScopeFields } from "../shared/category-scope-fields"; |
| 61 | 59 | import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; |
| ... | ... | @@ -806,7 +804,9 @@ function CreateMultipleOptionDialog({ |
| 806 | 804 | }) { |
| 807 | 805 | const [submitting, setSubmitting] = useState(false); |
| 808 | 806 | const [selectedPartnerId, setSelectedPartnerId] = useState(""); |
| 807 | + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]); | |
| 809 | 808 | const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]); |
| 809 | + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]); | |
| 810 | 810 | const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]); |
| 811 | 811 | const [form, setForm] = useState<LabelMultipleOptionCreateInput>({ |
| 812 | 812 | optionName: "", |
| ... | ... | @@ -818,7 +818,9 @@ function CreateMultipleOptionDialog({ |
| 818 | 818 | |
| 819 | 819 | const resetForm = () => { |
| 820 | 820 | setSelectedPartnerId(""); |
| 821 | + setSelectedPartnerIds([]); | |
| 821 | 822 | setSelectedRegionNames([]); |
| 823 | + setSelectedRegionIds([]); | |
| 822 | 824 | setSelectedLocationIds([]); |
| 823 | 825 | setForm({ |
| 824 | 826 | optionName: "", |
| ... | ... | @@ -876,40 +878,29 @@ function CreateMultipleOptionDialog({ |
| 876 | 878 | return; |
| 877 | 879 | } |
| 878 | 880 | |
| 879 | - const scopePartnerId = effectiveScopePartnerId( | |
| 881 | + const scopePayload = buildEntityScopeSaveFromForm({ | |
| 880 | 882 | requireCompanySelection, |
| 881 | 883 | selectedPartnerId, |
| 884 | + selectedPartnerIds, | |
| 885 | + selectedRegionNames, | |
| 886 | + selectedRegionIds, | |
| 887 | + selectedLocationIds, | |
| 882 | 888 | fixedPartnerId, |
| 883 | - ); | |
| 884 | - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId); | |
| 885 | - if (partnerErr) { | |
| 886 | - toast.error("Validation failed", { description: partnerErr }); | |
| 887 | - return; | |
| 888 | - } | |
| 889 | - | |
| 890 | - const locPayload = buildSpecifiedLocationPayload( | |
| 891 | 889 | locations, |
| 892 | 890 | partners, |
| 893 | 891 | groups, |
| 894 | - scopePartnerId, | |
| 895 | - selectedRegionNames, | |
| 896 | - selectedLocationIds, | |
| 897 | - ); | |
| 898 | - if (!locPayload.ok) { | |
| 899 | - toast.error("Validation failed", { description: locPayload.message }); | |
| 892 | + }); | |
| 893 | + if (!scopePayload.ok) { | |
| 894 | + toast.error("Validation failed", { description: scopePayload.message }); | |
| 900 | 895 | return; |
| 901 | 896 | } |
| 902 | 897 | |
| 903 | - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId); | |
| 904 | - | |
| 905 | 898 | setSubmitting(true); |
| 906 | 899 | try { |
| 907 | 900 | await createLabelMultipleOption({ |
| 908 | 901 | ...form, |
| 909 | 902 | optionCode: optionCodeFromName(form.optionName), |
| 910 | - groupIds, | |
| 911 | - regionIds: groupIds, | |
| 912 | - locationIds: locPayload.locationIds, | |
| 903 | + ...scopePayload.body, | |
| 913 | 904 | }); |
| 914 | 905 | toast.success("Multiple option created.", { |
| 915 | 906 | description: "The multiple option has been created successfully.", |
| ... | ... | @@ -1009,6 +1000,11 @@ function CreateMultipleOptionDialog({ |
| 1009 | 1000 | onLocationChange={setSelectedLocationIds} |
| 1010 | 1001 | requireCompanySelection={requireCompanySelection} |
| 1011 | 1002 | fixedPartnerId={fixedPartnerId} |
| 1003 | + templateScopeMode={requireCompanySelection} | |
| 1004 | + selectedPartnerIds={selectedPartnerIds} | |
| 1005 | + onPartnerIdsChange={setSelectedPartnerIds} | |
| 1006 | + selectedRegionIds={selectedRegionIds} | |
| 1007 | + onRegionIdsChange={setSelectedRegionIds} | |
| 1012 | 1008 | /> |
| 1013 | 1009 | </div> |
| 1014 | 1010 | |
| ... | ... | @@ -1049,7 +1045,9 @@ function EditMultipleOptionDialog({ |
| 1049 | 1045 | const [submitting, setSubmitting] = useState(false); |
| 1050 | 1046 | const [loadingDetail, setLoadingDetail] = useState(false); |
| 1051 | 1047 | const [selectedPartnerId, setSelectedPartnerId] = useState(""); |
| 1048 | + const [selectedPartnerIds, setSelectedPartnerIds] = useState<string[]>([]); | |
| 1052 | 1049 | const [selectedRegionNames, setSelectedRegionNames] = useState<string[]>([]); |
| 1050 | + const [selectedRegionIds, setSelectedRegionIds] = useState<string[]>([]); | |
| 1053 | 1051 | const [selectedLocationIds, setSelectedLocationIds] = useState<string[]>([]); |
| 1054 | 1052 | const [form, setForm] = useState<LabelMultipleOptionUpdateInput>({ |
| 1055 | 1053 | optionName: "", |
| ... | ... | @@ -1078,12 +1076,24 @@ function EditMultipleOptionDialog({ |
| 1078 | 1076 | }); |
| 1079 | 1077 | setNewValue(""); |
| 1080 | 1078 | |
| 1079 | + if (requireCompanySelection) { | |
| 1080 | + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups); | |
| 1081 | + setSelectedPartnerIds(scope.partnerIds); | |
| 1082 | + setSelectedRegionIds(scope.regionIds); | |
| 1083 | + setSelectedLocationIds(scope.locationIds); | |
| 1084 | + setSelectedPartnerId(""); | |
| 1085 | + setSelectedRegionNames([]); | |
| 1086 | + return; | |
| 1087 | + } | |
| 1088 | + | |
| 1081 | 1089 | const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); |
| 1082 | 1090 | if (lids.length > 0) { |
| 1083 | 1091 | const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups); |
| 1084 | 1092 | setSelectedPartnerId(scope.partnerId); |
| 1085 | 1093 | setSelectedRegionNames(scope.regionNames); |
| 1086 | 1094 | setSelectedLocationIds(scope.locationIds); |
| 1095 | + setSelectedPartnerIds([]); | |
| 1096 | + setSelectedRegionIds([]); | |
| 1087 | 1097 | return; |
| 1088 | 1098 | } |
| 1089 | 1099 | |
| ... | ... | @@ -1099,6 +1109,8 @@ function EditMultipleOptionDialog({ |
| 1099 | 1109 | setSelectedPartnerId(pid); |
| 1100 | 1110 | setSelectedRegionNames(names); |
| 1101 | 1111 | setSelectedLocationIds([]); |
| 1112 | + setSelectedPartnerIds([]); | |
| 1113 | + setSelectedRegionIds([]); | |
| 1102 | 1114 | return; |
| 1103 | 1115 | } |
| 1104 | 1116 | |
| ... | ... | @@ -1114,6 +1126,8 @@ function EditMultipleOptionDialog({ |
| 1114 | 1126 | setSelectedRegionNames([]); |
| 1115 | 1127 | setSelectedLocationIds([]); |
| 1116 | 1128 | } |
| 1129 | + setSelectedPartnerIds([]); | |
| 1130 | + setSelectedRegionIds([]); | |
| 1117 | 1131 | } catch { |
| 1118 | 1132 | if (ac.signal.aborted) return; |
| 1119 | 1133 | setForm({ |
| ... | ... | @@ -1123,7 +1137,9 @@ function EditMultipleOptionDialog({ |
| 1123 | 1137 | orderNum: option.orderNum ?? null, |
| 1124 | 1138 | }); |
| 1125 | 1139 | setSelectedPartnerId(""); |
| 1140 | + setSelectedPartnerIds([]); | |
| 1126 | 1141 | setSelectedRegionNames([]); |
| 1142 | + setSelectedRegionIds([]); | |
| 1127 | 1143 | setSelectedLocationIds([]); |
| 1128 | 1144 | setNewValue(""); |
| 1129 | 1145 | } finally { |
| ... | ... | @@ -1132,7 +1148,7 @@ function EditMultipleOptionDialog({ |
| 1132 | 1148 | })(); |
| 1133 | 1149 | |
| 1134 | 1150 | return () => ac.abort(); |
| 1135 | - }, [open, option, locations, partners, groups]); | |
| 1151 | + }, [open, option, locations, partners, groups, requireCompanySelection]); | |
| 1136 | 1152 | |
| 1137 | 1153 | const addValue = () => { |
| 1138 | 1154 | const trimmed = newValue.trim(); |
| ... | ... | @@ -1176,40 +1192,29 @@ function EditMultipleOptionDialog({ |
| 1176 | 1192 | return; |
| 1177 | 1193 | } |
| 1178 | 1194 | |
| 1179 | - const scopePartnerId = effectiveScopePartnerId( | |
| 1195 | + const scopePayload = buildEntityScopeSaveFromForm({ | |
| 1180 | 1196 | requireCompanySelection, |
| 1181 | 1197 | selectedPartnerId, |
| 1198 | + selectedPartnerIds, | |
| 1199 | + selectedRegionNames, | |
| 1200 | + selectedRegionIds, | |
| 1201 | + selectedLocationIds, | |
| 1182 | 1202 | fixedPartnerId, |
| 1183 | - ); | |
| 1184 | - const partnerErr = scopePartnerValidationMessage(requireCompanySelection, scopePartnerId); | |
| 1185 | - if (partnerErr) { | |
| 1186 | - toast.error("Validation failed", { description: partnerErr }); | |
| 1187 | - return; | |
| 1188 | - } | |
| 1189 | - | |
| 1190 | - const locPayload = buildSpecifiedLocationPayload( | |
| 1191 | 1203 | locations, |
| 1192 | 1204 | partners, |
| 1193 | 1205 | groups, |
| 1194 | - scopePartnerId, | |
| 1195 | - selectedRegionNames, | |
| 1196 | - selectedLocationIds, | |
| 1197 | - ); | |
| 1198 | - if (!locPayload.ok) { | |
| 1199 | - toast.error("Validation failed", { description: locPayload.message }); | |
| 1206 | + }); | |
| 1207 | + if (!scopePayload.ok) { | |
| 1208 | + toast.error("Validation failed", { description: scopePayload.message }); | |
| 1200 | 1209 | return; |
| 1201 | 1210 | } |
| 1202 | 1211 | |
| 1203 | - const groupIds = regionNamesToGroupIds(selectedRegionNames, groups, scopePartnerId); | |
| 1204 | - | |
| 1205 | 1212 | setSubmitting(true); |
| 1206 | 1213 | try { |
| 1207 | 1214 | await updateLabelMultipleOption(option.id, { |
| 1208 | 1215 | ...form, |
| 1209 | 1216 | optionCode: option.optionCode ?? "", |
| 1210 | - groupIds, | |
| 1211 | - regionIds: groupIds, | |
| 1212 | - locationIds: locPayload.locationIds, | |
| 1217 | + ...scopePayload.body, | |
| 1213 | 1218 | }); |
| 1214 | 1219 | toast.success("Multiple option updated.", { |
| 1215 | 1220 | description: "The multiple option has been updated successfully.", |
| ... | ... | @@ -1312,6 +1317,11 @@ function EditMultipleOptionDialog({ |
| 1312 | 1317 | onLocationChange={setSelectedLocationIds} |
| 1313 | 1318 | requireCompanySelection={requireCompanySelection} |
| 1314 | 1319 | fixedPartnerId={fixedPartnerId} |
| 1320 | + templateScopeMode={requireCompanySelection} | |
| 1321 | + selectedPartnerIds={selectedPartnerIds} | |
| 1322 | + onPartnerIdsChange={setSelectedPartnerIds} | |
| 1323 | + selectedRegionIds={selectedRegionIds} | |
| 1324 | + onRegionIdsChange={setSelectedRegionIds} | |
| 1315 | 1325 | /> |
| 1316 | 1326 | )} |
| 1317 | 1327 | </div> | ... | ... |
美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx
| ... | ... | @@ -390,15 +390,30 @@ export function CategoryScopeFields({ |
| 390 | 390 | {requireCompanySelection ? ( |
| 391 | 391 | <div className="space-y-2"> |
| 392 | 392 | <Label>Company *</Label> |
| 393 | - <SearchableSelect | |
| 394 | - value={selectedPartnerId} | |
| 395 | - onValueChange={onCompanyChange} | |
| 396 | - options={companyOptions} | |
| 397 | - placeholder="Select company" | |
| 398 | - searchPlaceholder="Search company…" | |
| 399 | - emptyText="No companies found." | |
| 400 | - /> | |
| 401 | - <p className="text-xs text-gray-500">Required. Select company before region and location.</p> | |
| 393 | + {templateScopeMode ? ( | |
| 394 | + <SearchableMultiSelect | |
| 395 | + values={selectedPartnerIds} | |
| 396 | + onValuesChange={onCompaniesChange} | |
| 397 | + options={companyOptions} | |
| 398 | + placeholder="All Companies" | |
| 399 | + searchPlaceholder="Search companies…" | |
| 400 | + selectAllRowLabel="Select All" | |
| 401 | + /> | |
| 402 | + ) : ( | |
| 403 | + <SearchableSelect | |
| 404 | + value={selectedPartnerId} | |
| 405 | + onValueChange={onCompanyChange} | |
| 406 | + options={companyOptions} | |
| 407 | + placeholder="Select company" | |
| 408 | + searchPlaceholder="Search company…" | |
| 409 | + emptyText="No companies found." | |
| 410 | + /> | |
| 411 | + )} | |
| 412 | + <p className="text-xs text-gray-500"> | |
| 413 | + {templateScopeMode | |
| 414 | + ? "Required. Leave empty or select all for every company; narrows region and location lists." | |
| 415 | + : "Required. Select company before region and location."} | |
| 416 | + </p> | |
| 402 | 417 | </div> |
| 403 | 418 | ) : null} |
| 404 | 419 | |
| ... | ... | @@ -406,16 +421,24 @@ export function CategoryScopeFields({ |
| 406 | 421 | <div className="space-y-2"> |
| 407 | 422 | <Label>Region *</Label> |
| 408 | 423 | <SearchableMultiSelect |
| 409 | - values={selectedRegionNames} | |
| 410 | - onValuesChange={onRegionMultiChange} | |
| 424 | + values={templateScopeMode ? selectedRegionIds : selectedRegionNames} | |
| 425 | + onValuesChange={templateScopeMode ? onRegionIdsMultiChange : onRegionMultiChange} | |
| 411 | 426 | options={regionOptions} |
| 412 | 427 | placeholder={regionPlaceholder} |
| 413 | 428 | searchPlaceholder="Search regions…" |
| 414 | 429 | selectAllRowLabel="Select All" |
| 415 | - disabled={!companyReady} | |
| 430 | + disabled={!templateScopeMode && !companyReady} | |
| 416 | 431 | /> |
| 417 | 432 | <p className="text-xs text-gray-500"> |
| 418 | - {companyReady ? "Required. Narrows the location list." : requireCompanySelection ? "Select a company first." : "Your account company is loading or not assigned."} | |
| 433 | + {templateScopeMode | |
| 434 | + ? companyReady | |
| 435 | + ? "Required. Leave empty or select all for every region in selected companies." | |
| 436 | + : "Select company first." | |
| 437 | + : companyReady | |
| 438 | + ? "Required. Narrows the location list." | |
| 439 | + : requireCompanySelection | |
| 440 | + ? "Select a company first." | |
| 441 | + : "Your account company is loading or not assigned."} | |
| 419 | 442 | </p> |
| 420 | 443 | </div> |
| 421 | 444 | <div className="space-y-2"> |
| ... | ... | @@ -425,22 +448,34 @@ export function CategoryScopeFields({ |
| 425 | 448 | onValuesChange={onLocationChange} |
| 426 | 449 | options={locationOptionsForPicker} |
| 427 | 450 | placeholder={ |
| 428 | - companyReady | |
| 429 | - ? "Select location(s)…" | |
| 430 | - : requireCompanySelection | |
| 431 | - ? "Select company first" | |
| 432 | - : "Loading company scope…" | |
| 451 | + templateScopeMode | |
| 452 | + ? companyReady | |
| 453 | + ? "All Locations" | |
| 454 | + : requireCompanySelection | |
| 455 | + ? "Select company first" | |
| 456 | + : "Loading company scope…" | |
| 457 | + : companyReady | |
| 458 | + ? "Select location(s)…" | |
| 459 | + : requireCompanySelection | |
| 460 | + ? "Select company first" | |
| 461 | + : "Loading company scope…" | |
| 433 | 462 | } |
| 434 | 463 | searchPlaceholder="Search locations…" |
| 435 | 464 | selectAllRowLabel="ALL" |
| 436 | - disabled={!companyReady} | |
| 465 | + disabled={!templateScopeMode && !companyReady} | |
| 437 | 466 | /> |
| 438 | 467 | <p className="text-xs text-gray-500"> |
| 439 | - {companyReady | |
| 440 | - ? "Required. ALL selects every location in the filtered list (within selected regions)." | |
| 441 | - : requireCompanySelection | |
| 442 | - ? "Select a company first." | |
| 443 | - : "Your account company is loading or not assigned."} | |
| 468 | + {templateScopeMode | |
| 469 | + ? companyReady | |
| 470 | + ? "Required. ALL selects every location in the filtered list (within selected regions)." | |
| 471 | + : requireCompanySelection | |
| 472 | + ? "Select a company first." | |
| 473 | + : "Your account company is loading or not assigned." | |
| 474 | + : companyReady | |
| 475 | + ? "Required. ALL selects every location in the filtered list (within selected regions)." | |
| 476 | + : requireCompanySelection | |
| 477 | + ? "Select a company first." | |
| 478 | + : "Your account company is loading or not assigned."} | |
| 444 | 479 | </p> |
| 445 | 480 | </div> |
| 446 | 481 | </div> | ... | ... |
美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts
| 1 | 1 | import type { GroupListItem } from "../types/group"; |
| 2 | 2 | import type { LocationDto } from "../types/location"; |
| 3 | 3 | import type { PartnerListItem } from "../types/partner"; |
| 4 | +import type { AppliedScopeType } from "../types/labelTemplate"; | |
| 4 | 5 | import { parseAppliedScopeType } from "../types/labelTemplate"; |
| 5 | 6 | |
| 6 | 7 | /** 表单提交用的公司 Id(非管理员取账号绑定公司) */ |
| ... | ... | @@ -424,3 +425,223 @@ export function buildProductScopeSavePayload( |
| 424 | 425 | } |
| 425 | 426 | return { locationIds: locIds }; |
| 426 | 427 | } |
| 428 | + | |
| 429 | +function resolveScopeDimensionForSave( | |
| 430 | + selected: string[], | |
| 431 | + available: string[], | |
| 432 | +): { type: AppliedScopeType; ids: string[] } { | |
| 433 | + const avail = dedupeIds(available); | |
| 434 | + const sel = dedupeIds(selected).filter((id) => !avail.length || avail.includes(id)); | |
| 435 | + if (!avail.length || !sel.length) { | |
| 436 | + return { type: "ALL", ids: [] }; | |
| 437 | + } | |
| 438 | + if (sel.length >= avail.length && avail.every((id) => sel.includes(id))) { | |
| 439 | + return { type: "ALL", ids: [] }; | |
| 440 | + } | |
| 441 | + return { type: "SPECIFIED", ids: sel }; | |
| 442 | +} | |
| 443 | + | |
| 444 | +/** 模板/实体表单:按当前选中 Id 计算可选 Company / Region / Location 全集 */ | |
| 445 | +export function computeTemplateScopeAvailableIds( | |
| 446 | + partners: PartnerListItem[], | |
| 447 | + groups: GroupListItem[], | |
| 448 | + locations: LocationDto[], | |
| 449 | + selectedPartnerIds: string[], | |
| 450 | + selectedRegionIds: string[], | |
| 451 | +): { | |
| 452 | + availablePartnerIds: string[]; | |
| 453 | + availableRegionIds: string[]; | |
| 454 | + availableLocationIds: string[]; | |
| 455 | +} { | |
| 456 | + const availablePartnerIds = partners.map((p) => String(p.id ?? "").trim()).filter(Boolean); | |
| 457 | + const effectivePartnerIds = | |
| 458 | + selectedPartnerIds.length > 0 ? selectedPartnerIds : availablePartnerIds; | |
| 459 | + const availableRegionIds = regionOptionsForPartners(groups, effectivePartnerIds).map( | |
| 460 | + (o) => o.value, | |
| 461 | + ); | |
| 462 | + const availableLocationIds = locationsScopedForTemplateScope( | |
| 463 | + locations, | |
| 464 | + partners, | |
| 465 | + groups, | |
| 466 | + effectivePartnerIds, | |
| 467 | + selectedRegionIds, | |
| 468 | + ).map((l) => l.id); | |
| 469 | + return { availablePartnerIds, availableRegionIds, availableLocationIds }; | |
| 470 | +} | |
| 471 | + | |
| 472 | +export type EntityScopePayloadBody = { | |
| 473 | + appliedPartnerType: AppliedScopeType; | |
| 474 | + partnerIds: string[]; | |
| 475 | + companyIds: string[]; | |
| 476 | + regionIds: string[]; | |
| 477 | + groupIds: string[]; | |
| 478 | + locationIds: string[]; | |
| 479 | + availabilityType: "ALL" | "SPECIFIED"; | |
| 480 | +}; | |
| 481 | + | |
| 482 | +/** | |
| 483 | + * 标签类型 / 分类 / 多选项保存:Company + Region + Location 三维适用范围。 | |
| 484 | + * 空选或全选当前可选项 → 该维度 ALL;部分选中 → SPECIFIED + Id 数组。 | |
| 485 | + */ | |
| 486 | +export function buildEntityPartnerScopePayload(input: { | |
| 487 | + selectedPartnerIds: string[]; | |
| 488 | + selectedRegionIds: string[]; | |
| 489 | + selectedLocationIds: string[]; | |
| 490 | + availablePartnerIds: string[]; | |
| 491 | + availableRegionIds: string[]; | |
| 492 | + availableLocationIds: string[]; | |
| 493 | +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } { | |
| 494 | + const partnerDim = resolveScopeDimensionForSave( | |
| 495 | + input.selectedPartnerIds, | |
| 496 | + input.availablePartnerIds, | |
| 497 | + ); | |
| 498 | + const regionDim = resolveScopeDimensionForSave( | |
| 499 | + input.selectedRegionIds, | |
| 500 | + input.availableRegionIds, | |
| 501 | + ); | |
| 502 | + const locationDim = resolveScopeDimensionForSave( | |
| 503 | + input.selectedLocationIds, | |
| 504 | + input.availableLocationIds, | |
| 505 | + ); | |
| 506 | + | |
| 507 | + const locationSpecified = | |
| 508 | + regionDim.type === "SPECIFIED" || locationDim.type === "SPECIFIED"; | |
| 509 | + const availabilityType: "ALL" | "SPECIFIED" = locationSpecified ? "SPECIFIED" : "ALL"; | |
| 510 | + | |
| 511 | + const anySpecified = | |
| 512 | + partnerDim.type === "SPECIFIED" || locationSpecified; | |
| 513 | + const effectiveLocationIds = | |
| 514 | + locationDim.type === "SPECIFIED" ? locationDim.ids : input.availableLocationIds; | |
| 515 | + | |
| 516 | + if (anySpecified && effectiveLocationIds.length === 0) { | |
| 517 | + return { | |
| 518 | + ok: false, | |
| 519 | + message: "Please adjust company, region, or location so at least one store matches.", | |
| 520 | + }; | |
| 521 | + } | |
| 522 | + | |
| 523 | + const partnerIds = partnerDim.type === "SPECIFIED" ? partnerDim.ids : []; | |
| 524 | + const regionIds = regionDim.type === "SPECIFIED" ? regionDim.ids : []; | |
| 525 | + const locationIds = locationDim.type === "SPECIFIED" ? locationDim.ids : []; | |
| 526 | + | |
| 527 | + return { | |
| 528 | + ok: true, | |
| 529 | + body: { | |
| 530 | + appliedPartnerType: partnerDim.type, | |
| 531 | + partnerIds, | |
| 532 | + companyIds: partnerIds, | |
| 533 | + regionIds, | |
| 534 | + groupIds: regionIds, | |
| 535 | + locationIds, | |
| 536 | + availabilityType, | |
| 537 | + }, | |
| 538 | + }; | |
| 539 | +} | |
| 540 | + | |
| 541 | +/** 创建/编辑弹窗:按管理员多选或单公司模式构建保存 Body */ | |
| 542 | +export function buildEntityScopeSaveFromForm(input: { | |
| 543 | + requireCompanySelection: boolean; | |
| 544 | + selectedPartnerId: string; | |
| 545 | + selectedPartnerIds: string[]; | |
| 546 | + selectedRegionNames: string[]; | |
| 547 | + selectedRegionIds: string[]; | |
| 548 | + selectedLocationIds: string[]; | |
| 549 | + fixedPartnerId: string; | |
| 550 | + locations: LocationDto[]; | |
| 551 | + partners: PartnerListItem[]; | |
| 552 | + groups: GroupListItem[]; | |
| 553 | +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } { | |
| 554 | + if (input.requireCompanySelection) { | |
| 555 | + const { availablePartnerIds, availableRegionIds, availableLocationIds } = | |
| 556 | + computeTemplateScopeAvailableIds( | |
| 557 | + input.partners, | |
| 558 | + input.groups, | |
| 559 | + input.locations, | |
| 560 | + input.selectedPartnerIds, | |
| 561 | + input.selectedRegionIds, | |
| 562 | + ); | |
| 563 | + return buildEntityPartnerScopePayload({ | |
| 564 | + selectedPartnerIds: input.selectedPartnerIds, | |
| 565 | + selectedRegionIds: input.selectedRegionIds, | |
| 566 | + selectedLocationIds: input.selectedLocationIds, | |
| 567 | + availablePartnerIds, | |
| 568 | + availableRegionIds, | |
| 569 | + availableLocationIds, | |
| 570 | + }); | |
| 571 | + } | |
| 572 | + | |
| 573 | + const scopePartnerId = effectiveScopePartnerId( | |
| 574 | + false, | |
| 575 | + input.selectedPartnerId, | |
| 576 | + input.fixedPartnerId, | |
| 577 | + ); | |
| 578 | + const partnerErr = scopePartnerValidationMessage(false, scopePartnerId); | |
| 579 | + if (partnerErr) { | |
| 580 | + return { ok: false, message: partnerErr }; | |
| 581 | + } | |
| 582 | + | |
| 583 | + const locPayload = buildSpecifiedLocationPayload( | |
| 584 | + input.locations, | |
| 585 | + input.partners, | |
| 586 | + input.groups, | |
| 587 | + scopePartnerId, | |
| 588 | + input.selectedRegionNames, | |
| 589 | + input.selectedLocationIds, | |
| 590 | + ); | |
| 591 | + if (!locPayload.ok) { | |
| 592 | + return { ok: false, message: locPayload.message }; | |
| 593 | + } | |
| 594 | + | |
| 595 | + const groupIds = regionNamesToGroupIds( | |
| 596 | + input.selectedRegionNames, | |
| 597 | + input.groups, | |
| 598 | + scopePartnerId, | |
| 599 | + ); | |
| 600 | + const partnerIds = scopePartnerId ? [scopePartnerId] : []; | |
| 601 | + return { | |
| 602 | + ok: true, | |
| 603 | + body: { | |
| 604 | + appliedPartnerType: partnerIds.length ? "SPECIFIED" : "ALL", | |
| 605 | + partnerIds, | |
| 606 | + companyIds: partnerIds, | |
| 607 | + regionIds: groupIds, | |
| 608 | + groupIds, | |
| 609 | + locationIds: locPayload.locationIds, | |
| 610 | + availabilityType: "SPECIFIED", | |
| 611 | + }, | |
| 612 | + }; | |
| 613 | +} | |
| 614 | + | |
| 615 | +/** 编辑弹窗:从详情 DTO 回填三维 Id 数组(管理员多选模式) */ | |
| 616 | +export function hydrateEntityScopeFromDto( | |
| 617 | + dto: { | |
| 618 | + appliedPartnerType?: string | null; | |
| 619 | + appliedRegionType?: string | null; | |
| 620 | + appliedLocation?: string | null; | |
| 621 | + appliedLocationType?: string | null; | |
| 622 | + availabilityType?: string | null; | |
| 623 | + partnerIds?: string[] | null; | |
| 624 | + companyIds?: string[] | null; | |
| 625 | + regionIds?: string[] | null; | |
| 626 | + groupIds?: string[] | null; | |
| 627 | + locationIds?: string[] | null; | |
| 628 | + appliedLocationIds?: string[] | null; | |
| 629 | + }, | |
| 630 | + locations: LocationDto[], | |
| 631 | + partners: PartnerListItem[], | |
| 632 | + groups: GroupListItem[], | |
| 633 | +): { partnerIds: string[]; regionIds: string[]; locationIds: string[] } { | |
| 634 | + return hydrateLabelTemplateScopeFromDto( | |
| 635 | + { | |
| 636 | + ...dto, | |
| 637 | + appliedLocation: | |
| 638 | + dto.appliedLocation ?? | |
| 639 | + (String(dto.availabilityType ?? "").trim().toUpperCase() === "SPECIFIED" | |
| 640 | + ? "SPECIFIED" | |
| 641 | + : dto.appliedLocation), | |
| 642 | + }, | |
| 643 | + locations, | |
| 644 | + partners, | |
| 645 | + groups, | |
| 646 | + ); | |
| 647 | +} | ... | ... |
美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts
| ... | ... | @@ -22,6 +22,13 @@ const PATH = "/label-category"; |
| 22 | 22 | |
| 23 | 23 | function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto { |
| 24 | 24 | const r = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>; |
| 25 | + const parseIds = (v: unknown): string[] | undefined => { | |
| 26 | + if (!Array.isArray(v)) return undefined; | |
| 27 | + return [...new Set(v.map((x) => String(x).trim()).filter(Boolean))]; | |
| 28 | + }; | |
| 29 | + const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds); | |
| 30 | + const locationIds = parseIds(r.locationIds ?? r.LocationIds); | |
| 31 | + const partnerIds = parseIds(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds); | |
| 25 | 32 | const noRaw = r.noOfLabels ?? r.NoOfLabels; |
| 26 | 33 | const noOfLabels = |
| 27 | 34 | typeof noRaw === "number" && Number.isFinite(noRaw) |
| ... | ... | @@ -36,7 +43,60 @@ function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto { |
| 36 | 43 | : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) |
| 37 | 44 | ? Number(orderRaw) |
| 38 | 45 | : null; |
| 39 | - return { ...(r as object), id: String(r.id ?? r.Id ?? ""), noOfLabels, orderNum } as LabelCategoryDto; | |
| 46 | + const companyRaw = r.company ?? r.Company; | |
| 47 | + return { | |
| 48 | + ...(r as object), | |
| 49 | + id: String(r.id ?? r.Id ?? ""), | |
| 50 | + appliedPartnerType: String(r.appliedPartnerType ?? r.AppliedPartnerType ?? "").trim() || undefined, | |
| 51 | + partnerIds, | |
| 52 | + companyIds: partnerIds, | |
| 53 | + availabilityType: String(r.availabilityType ?? r.AvailabilityType ?? "").trim() || undefined, | |
| 54 | + groupIds, | |
| 55 | + regionIds: groupIds, | |
| 56 | + locationIds, | |
| 57 | + company: typeof companyRaw === "string" ? companyRaw : null, | |
| 58 | + noOfLabels, | |
| 59 | + orderNum, | |
| 60 | + } as LabelCategoryDto; | |
| 61 | +} | |
| 62 | + | |
| 63 | +function scopeBodyFromInput(input: LabelCategoryCreateInput): Record<string, unknown> { | |
| 64 | + const groupIds = [ | |
| 65 | + ...new Set( | |
| 66 | + [...(input.groupIds ?? []), ...(input.regionIds ?? [])] | |
| 67 | + .map((x) => String(x).trim()) | |
| 68 | + .filter(Boolean), | |
| 69 | + ), | |
| 70 | + ]; | |
| 71 | + const locationIds = [ | |
| 72 | + ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), | |
| 73 | + ]; | |
| 74 | + const partnerIds = [ | |
| 75 | + ...new Set( | |
| 76 | + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])] | |
| 77 | + .map((x) => String(x).trim()) | |
| 78 | + .filter(Boolean), | |
| 79 | + ), | |
| 80 | + ]; | |
| 81 | + const body: Record<string, unknown> = {}; | |
| 82 | + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase(); | |
| 83 | + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") { | |
| 84 | + body.appliedPartnerType = appliedPartnerType; | |
| 85 | + } | |
| 86 | + if (partnerIds.length) { | |
| 87 | + body.partnerIds = partnerIds; | |
| 88 | + body.companyIds = partnerIds; | |
| 89 | + } | |
| 90 | + const availabilityType = String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL"; | |
| 91 | + body.availabilityType = availabilityType; | |
| 92 | + if (groupIds.length) { | |
| 93 | + body.groupIds = groupIds; | |
| 94 | + body.regionIds = groupIds; | |
| 95 | + } | |
| 96 | + if (locationIds.length) { | |
| 97 | + body.locationIds = locationIds; | |
| 98 | + } | |
| 99 | + return body; | |
| 40 | 100 | } |
| 41 | 101 | |
| 42 | 102 | export async function getLabelCategories(input: LabelCategoryGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelCategoryDto>> { |
| ... | ... | @@ -60,11 +120,12 @@ export async function getLabelCategories(input: LabelCategoryGetListInput, signa |
| 60 | 120 | } |
| 61 | 121 | |
| 62 | 122 | export async function getLabelCategory(id: string, signal?: AbortSignal): Promise<LabelCategoryDto> { |
| 63 | - return api.requestJson<LabelCategoryDto>({ | |
| 123 | + const raw = await api.requestJson<unknown>({ | |
| 64 | 124 | path: `${PATH}/${encodeURIComponent(id)}`, |
| 65 | 125 | method: "GET", |
| 66 | 126 | signal, |
| 67 | 127 | }); |
| 128 | + return normalizeLabelCategoryDto(raw); | |
| 68 | 129 | } |
| 69 | 130 | |
| 70 | 131 | export async function createLabelCategory(input: LabelCategoryCreateInput): Promise<LabelCategoryDto> { |
| ... | ... | @@ -82,17 +143,13 @@ export async function createLabelCategory(input: LabelCategoryCreateInput): Prom |
| 82 | 143 | buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null, |
| 83 | 144 | state: input.state ?? true, |
| 84 | 145 | orderNum: input.orderNum, |
| 85 | - availabilityType: String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL", | |
| 86 | - locationIds: | |
| 87 | - Array.isArray(input.locationIds) && input.locationIds.length | |
| 88 | - ? [...new Set(input.locationIds.map((x) => String(x).trim()).filter(Boolean))] | |
| 89 | - : null, | |
| 146 | + ...scopeBodyFromInput(input), | |
| 90 | 147 | }, |
| 91 | 148 | }); |
| 92 | 149 | } |
| 93 | 150 | |
| 94 | 151 | export async function updateLabelCategory(id: string, input: LabelCategoryUpdateInput): Promise<LabelCategoryDto> { |
| 95 | - return api.requestJson<LabelCategoryDto>({ | |
| 152 | + const raw = await api.requestJson<unknown>({ | |
| 96 | 153 | path: `${PATH}/${encodeURIComponent(id)}`, |
| 97 | 154 | method: "PUT", |
| 98 | 155 | body: { |
| ... | ... | @@ -106,13 +163,10 @@ export async function updateLabelCategory(id: string, input: LabelCategoryUpdate |
| 106 | 163 | buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null, |
| 107 | 164 | state: input.state ?? true, |
| 108 | 165 | orderNum: input.orderNum, |
| 109 | - availabilityType: String(input.availabilityType ?? "ALL").trim().toUpperCase() || "ALL", | |
| 110 | - locationIds: | |
| 111 | - Array.isArray(input.locationIds) && input.locationIds.length | |
| 112 | - ? [...new Set(input.locationIds.map((x) => String(x).trim()).filter(Boolean))] | |
| 113 | - : null, | |
| 166 | + ...scopeBodyFromInput(input), | |
| 114 | 167 | }, |
| 115 | 168 | }); |
| 169 | + return normalizeLabelCategoryDto(raw); | |
| 116 | 170 | } |
| 117 | 171 | |
| 118 | 172 | export async function deleteLabelCategory(id: string): Promise<void> { | ... | ... |
美国版/Food Labeling Management Platform/src/services/labelMultipleOptionService.ts
| ... | ... | @@ -63,13 +63,35 @@ function normalizeLabelMultipleOptionDto(item: LabelMultipleOptionDto): LabelMul |
| 63 | 63 | const locationIds = parseIds( |
| 64 | 64 | (raw as Record<string, unknown>).locationIds ?? (raw as Record<string, unknown>).LocationIds, |
| 65 | 65 | ); |
| 66 | + const partnerIds = parseIds( | |
| 67 | + (raw as Record<string, unknown>).partnerIds ?? | |
| 68 | + (raw as Record<string, unknown>).PartnerIds ?? | |
| 69 | + (raw as Record<string, unknown>).companyIds ?? | |
| 70 | + (raw as Record<string, unknown>).CompanyIds, | |
| 71 | + ); | |
| 72 | + const companyRaw = (raw as Record<string, unknown>).company ?? (raw as Record<string, unknown>).Company; | |
| 66 | 73 | return { |
| 67 | 74 | ...item, |
| 68 | 75 | optionValuesJson: parseOptionValuesJsonField(raw.optionValuesJson), |
| 69 | 76 | lastEdited, |
| 77 | + appliedPartnerType: | |
| 78 | + String( | |
| 79 | + (raw as Record<string, unknown>).appliedPartnerType ?? | |
| 80 | + (raw as Record<string, unknown>).AppliedPartnerType ?? | |
| 81 | + "", | |
| 82 | + ).trim() || undefined, | |
| 83 | + partnerIds, | |
| 84 | + companyIds: partnerIds, | |
| 85 | + availabilityType: | |
| 86 | + String( | |
| 87 | + (raw as Record<string, unknown>).availabilityType ?? | |
| 88 | + (raw as Record<string, unknown>).AvailabilityType ?? | |
| 89 | + "", | |
| 90 | + ).trim() || undefined, | |
| 70 | 91 | groupIds, |
| 71 | 92 | regionIds: groupIds, |
| 72 | 93 | locationIds, |
| 94 | + company: typeof companyRaw === "string" ? companyRaw : null, | |
| 73 | 95 | }; |
| 74 | 96 | } |
| 75 | 97 | |
| ... | ... | @@ -96,11 +118,34 @@ function scopeBodyFromInput(input: LabelMultipleOptionCreateInput): Record<strin |
| 96 | 118 | const locationIds = [ |
| 97 | 119 | ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), |
| 98 | 120 | ]; |
| 99 | - return { | |
| 100 | - groupIds: groupIds.length ? groupIds : undefined, | |
| 101 | - regionIds: groupIds.length ? groupIds : undefined, | |
| 102 | - locationIds: locationIds.length ? locationIds : undefined, | |
| 103 | - }; | |
| 121 | + const partnerIds = [ | |
| 122 | + ...new Set( | |
| 123 | + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])] | |
| 124 | + .map((x) => String(x).trim()) | |
| 125 | + .filter(Boolean), | |
| 126 | + ), | |
| 127 | + ]; | |
| 128 | + const body: Record<string, unknown> = {}; | |
| 129 | + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase(); | |
| 130 | + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") { | |
| 131 | + body.appliedPartnerType = appliedPartnerType; | |
| 132 | + } | |
| 133 | + if (partnerIds.length) { | |
| 134 | + body.partnerIds = partnerIds; | |
| 135 | + body.companyIds = partnerIds; | |
| 136 | + } | |
| 137 | + const availabilityType = String(input.availabilityType ?? "").trim().toUpperCase(); | |
| 138 | + if (availabilityType === "ALL" || availabilityType === "SPECIFIED") { | |
| 139 | + body.availabilityType = availabilityType; | |
| 140 | + } | |
| 141 | + if (groupIds.length) { | |
| 142 | + body.groupIds = groupIds; | |
| 143 | + body.regionIds = groupIds; | |
| 144 | + } | |
| 145 | + if (locationIds.length) { | |
| 146 | + body.locationIds = locationIds; | |
| 147 | + } | |
| 148 | + return body; | |
| 104 | 149 | } |
| 105 | 150 | |
| 106 | 151 | export async function getLabelMultipleOptions(input: LabelMultipleOptionGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelMultipleOptionDto>> { | ... | ... |
美国版/Food Labeling Management Platform/src/services/labelTypeService.ts
| ... | ... | @@ -27,6 +27,10 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto { |
| 27 | 27 | }; |
| 28 | 28 | const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds); |
| 29 | 29 | const locationIds = parseIds(r.locationIds ?? r.LocationIds); |
| 30 | + const partnerIds = parseIds(r.partnerIds ?? r.PartnerIds ?? r.companyIds ?? r.CompanyIds); | |
| 31 | + const appliedPartnerType = String(r.appliedPartnerType ?? r.AppliedPartnerType ?? "").trim() || undefined; | |
| 32 | + const availabilityType = String(r.availabilityType ?? r.AvailabilityType ?? "").trim() || undefined; | |
| 33 | + const companyRaw = r.company ?? r.Company; | |
| 30 | 34 | const noRaw = r.noOfLabels ?? r.NoOfLabels; |
| 31 | 35 | const noOfLabels = |
| 32 | 36 | typeof noRaw === "number" && Number.isFinite(noRaw) |
| ... | ... | @@ -46,9 +50,14 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto { |
| 46 | 50 | return { |
| 47 | 51 | ...(r as object), |
| 48 | 52 | id: String(r.id ?? r.Id ?? ""), |
| 53 | + appliedPartnerType, | |
| 54 | + partnerIds, | |
| 55 | + companyIds: partnerIds, | |
| 56 | + availabilityType, | |
| 49 | 57 | groupIds, |
| 50 | 58 | regionIds: groupIds, |
| 51 | 59 | locationIds, |
| 60 | + company: typeof companyRaw === "string" ? companyRaw : null, | |
| 52 | 61 | noOfLabels, |
| 53 | 62 | lastEdited, |
| 54 | 63 | region: typeof regionRaw === "string" ? regionRaw : null, |
| ... | ... | @@ -67,11 +76,34 @@ function scopeBodyFromInput(input: LabelTypeCreateInput): Record<string, unknown |
| 67 | 76 | const locationIds = [ |
| 68 | 77 | ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), |
| 69 | 78 | ]; |
| 70 | - return { | |
| 71 | - groupIds: groupIds.length ? groupIds : undefined, | |
| 72 | - regionIds: groupIds.length ? groupIds : undefined, | |
| 73 | - locationIds: locationIds.length ? locationIds : undefined, | |
| 74 | - }; | |
| 79 | + const partnerIds = [ | |
| 80 | + ...new Set( | |
| 81 | + [...(input.partnerIds ?? []), ...(input.companyIds ?? [])] | |
| 82 | + .map((x) => String(x).trim()) | |
| 83 | + .filter(Boolean), | |
| 84 | + ), | |
| 85 | + ]; | |
| 86 | + const body: Record<string, unknown> = {}; | |
| 87 | + const appliedPartnerType = String(input.appliedPartnerType ?? "").trim().toUpperCase(); | |
| 88 | + if (appliedPartnerType === "ALL" || appliedPartnerType === "SPECIFIED") { | |
| 89 | + body.appliedPartnerType = appliedPartnerType; | |
| 90 | + } | |
| 91 | + if (partnerIds.length) { | |
| 92 | + body.partnerIds = partnerIds; | |
| 93 | + body.companyIds = partnerIds; | |
| 94 | + } | |
| 95 | + const availabilityType = String(input.availabilityType ?? "").trim().toUpperCase(); | |
| 96 | + if (availabilityType === "ALL" || availabilityType === "SPECIFIED") { | |
| 97 | + body.availabilityType = availabilityType; | |
| 98 | + } | |
| 99 | + if (groupIds.length) { | |
| 100 | + body.groupIds = groupIds; | |
| 101 | + body.regionIds = groupIds; | |
| 102 | + } | |
| 103 | + if (locationIds.length) { | |
| 104 | + body.locationIds = locationIds; | |
| 105 | + } | |
| 106 | + return body; | |
| 75 | 107 | } |
| 76 | 108 | |
| 77 | 109 | export async function getLabelTypes(input: LabelTypeGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelTypeDto>> { | ... | ... |
美国版/Food Labeling Management Platform/src/types/label.ts
| ... | ... | @@ -66,7 +66,7 @@ export type LabelCreateInput = { |
| 66 | 66 | /** 适用门店(至少 1 个;Select All 时为当前 Region 下全部门店 Id) */ |
| 67 | 67 | locationIds: string[]; |
| 68 | 68 | labelCategoryId: string; |
| 69 | - labelTypeId: string; | |
| 69 | + labelTypeId?: string | null; | |
| 70 | 70 | productIds: string[]; // 至少 1 个 |
| 71 | 71 | labelInfoJson?: Record<string, unknown> | null; |
| 72 | 72 | state?: boolean; |
| ... | ... | @@ -81,7 +81,7 @@ export type LabelUpdateInput = { |
| 81 | 81 | /** 适用门店(至少 1 个) */ |
| 82 | 82 | locationIds: string[]; |
| 83 | 83 | labelCategoryId: string; |
| 84 | - labelTypeId: string; | |
| 84 | + labelTypeId?: string | null; | |
| 85 | 85 | productIds: string[]; // 至少 1 个 |
| 86 | 86 | labelInfoJson?: Record<string, unknown> | null; |
| 87 | 87 | state?: boolean; | ... | ... |
美国版/Food Labeling Management Platform/src/types/labelCategory.ts
| ... | ... | @@ -18,10 +18,19 @@ export type LabelCategoryDto = { |
| 18 | 18 | /** 该分类下标签数量(列表列 No. of Label) */ |
| 19 | 19 | noOfLabels?: number | null; |
| 20 | 20 | creationTime?: string | null; |
| 21 | + /** 适用 Company:ALL / SPECIFIED */ | |
| 22 | + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; | |
| 23 | + partnerIds?: string[] | null; | |
| 24 | + companyIds?: string[] | null; | |
| 21 | 25 | /** ALL / SPECIFIED */ |
| 22 | 26 | availabilityType?: "ALL" | "SPECIFIED" | string | null; |
| 27 | + /** SPECIFIED 时绑定的 Region Id */ | |
| 28 | + regionIds?: string[] | null; | |
| 29 | + groupIds?: string[] | null; | |
| 23 | 30 | /** SPECIFIED 时绑定的门店 Id */ |
| 24 | 31 | locationIds?: string[] | null; |
| 32 | + /** 列表:Company 展示 */ | |
| 33 | + company?: string | null; | |
| 25 | 34 | /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */ |
| 26 | 35 | lastEdited?: string | null; |
| 27 | 36 | }; |
| ... | ... | @@ -56,7 +65,13 @@ export type LabelCategoryCreateInput = { |
| 56 | 65 | buttonStyleJson?: string | null; |
| 57 | 66 | state?: boolean; |
| 58 | 67 | orderNum?: number | null; |
| 68 | + /** 适用 Company:ALL / SPECIFIED */ | |
| 69 | + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; | |
| 70 | + partnerIds?: string[] | null; | |
| 71 | + companyIds?: string[] | null; | |
| 59 | 72 | availabilityType?: "ALL" | "SPECIFIED" | string | null; |
| 73 | + regionIds?: string[] | null; | |
| 74 | + groupIds?: string[] | null; | |
| 60 | 75 | locationIds?: string[] | null; |
| 61 | 76 | }; |
| 62 | 77 | ... | ... |
美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts
| ... | ... | @@ -7,6 +7,12 @@ export type LabelMultipleOptionDto = { |
| 7 | 7 | state?: boolean | null; |
| 8 | 8 | orderNum?: number | null; |
| 9 | 9 | creationTime?: string | null; |
| 10 | + /** 适用 Company:ALL / SPECIFIED */ | |
| 11 | + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; | |
| 12 | + partnerIds?: string[] | null; | |
| 13 | + companyIds?: string[] | null; | |
| 14 | + /** 门店可用范围:ALL / SPECIFIED */ | |
| 15 | + availabilityType?: "ALL" | "SPECIFIED" | string | null; | |
| 10 | 16 | /** 列表:最近编辑时间 */ |
| 11 | 17 | lastEdited?: string | null; |
| 12 | 18 | /** 适用范围:Region Id(`fl_group.Id`) */ |
| ... | ... | @@ -14,6 +20,8 @@ export type LabelMultipleOptionDto = { |
| 14 | 20 | regionIds?: string[] | null; |
| 15 | 21 | /** 适用范围:门店 Id */ |
| 16 | 22 | locationIds?: string[] | null; |
| 23 | + /** 列表:Company 展示 */ | |
| 24 | + company?: string | null; | |
| 17 | 25 | }; |
| 18 | 26 | |
| 19 | 27 | export type PagedResultDto<T> = { |
| ... | ... | @@ -39,6 +47,12 @@ export type LabelMultipleOptionCreateInput = { |
| 39 | 47 | optionValuesJson: string[]; |
| 40 | 48 | state?: boolean; |
| 41 | 49 | orderNum?: number | null; |
| 50 | + /** 适用 Company:ALL / SPECIFIED */ | |
| 51 | + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; | |
| 52 | + partnerIds?: string[] | null; | |
| 53 | + companyIds?: string[] | null; | |
| 54 | + /** 门店可用范围:ALL / SPECIFIED */ | |
| 55 | + availabilityType?: "ALL" | "SPECIFIED" | string | null; | |
| 42 | 56 | groupIds?: string[] | null; |
| 43 | 57 | regionIds?: string[] | null; |
| 44 | 58 | locationIds?: string[] | null; | ... | ... |
美国版/Food Labeling Management Platform/src/types/labelType.ts
| ... | ... | @@ -5,11 +5,19 @@ export type LabelTypeDto = { |
| 5 | 5 | state?: boolean | null; |
| 6 | 6 | orderNum?: number | null; |
| 7 | 7 | creationTime?: string | null; |
| 8 | + /** 适用 Company:ALL / SPECIFIED */ | |
| 9 | + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; | |
| 10 | + partnerIds?: string[] | null; | |
| 11 | + companyIds?: string[] | null; | |
| 12 | + /** 门店可用范围:ALL / SPECIFIED */ | |
| 13 | + availabilityType?: "ALL" | "SPECIFIED" | string | null; | |
| 8 | 14 | /** 详情:Region Id 列表(`fl_group.Id`) */ |
| 9 | 15 | groupIds?: string[] | null; |
| 10 | 16 | regionIds?: string[] | null; |
| 11 | 17 | /** 详情:门店 Id 列表 */ |
| 12 | 18 | locationIds?: string[] | null; |
| 19 | + /** 列表:Company 展示 */ | |
| 20 | + company?: string | null; | |
| 13 | 21 | /** 列表:该类型下标签数量 */ |
| 14 | 22 | noOfLabels?: number | null; |
| 15 | 23 | /** 列表:最近编辑时间 */ |
| ... | ... | @@ -44,6 +52,12 @@ export type LabelTypeCreateInput = { |
| 44 | 52 | typeName: string; |
| 45 | 53 | state?: boolean; |
| 46 | 54 | orderNum?: number | null; |
| 55 | + /** 适用 Company:ALL / SPECIFIED */ | |
| 56 | + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; | |
| 57 | + partnerIds?: string[] | null; | |
| 58 | + companyIds?: string[] | null; | |
| 59 | + /** 门店可用范围:ALL / SPECIFIED */ | |
| 60 | + availabilityType?: "ALL" | "SPECIFIED" | string | null; | |
| 47 | 61 | /** Region 多选(`fl_group.Id`) */ |
| 48 | 62 | groupIds?: string[] | null; |
| 49 | 63 | regionIds?: string[] | null; | ... | ... |