Commit ff0f4ef58b86e7eccc993fc7abac62b9bc5e5570

Authored by 杨鑫
2 parents c9bc327f 1b68006d

合并

Showing 51 changed files with 2361 additions and 562 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,7 @@ public class LabelCreateInputVo @@ -45,7 +45,7 @@ public class LabelCreateInputVo
45 45
46 public string LabelCategoryId { get; set; } = string.Empty; 46 public string LabelCategoryId { get; set; } = string.Empty;
47 47
48 - /// <summary>标签类型 Id(可选)</summary> 48 + /// <summary>标签类型 Id(<c>fl_label_type.Id</c>);可选,未传时标签不绑定类型</summary>
49 public string? LabelTypeId { get; set; } 49 public string? LabelTypeId { get; set; }
50 50
51 public List<string> ProductIds { get; set; } = new(); 51 public List<string> ProductIds { get; set; } = new();
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelGetListInputVo.cs
@@ -31,6 +31,7 @@ public class LabelGetListInputVo : PagedAndSortedResultRequestDto @@ -31,6 +31,7 @@ public class LabelGetListInputVo : PagedAndSortedResultRequestDto
31 31
32 public string? LabelCategoryId { get; set; } 32 public string? LabelCategoryId { get; set; }
33 33
  34 + /// <summary>按标签类型 Id 筛选(可选)</summary>
34 public string? LabelTypeId { get; set; } 35 public string? LabelTypeId { get; set; }
35 36
36 /// <summary> 37 /// <summary>
美国版/Food Labeling Management Code/Yi.Abp.Net8/module/food-labeling-us/FoodLabeling.Application.Contracts/Dtos/Label/LabelUpdateInputVo.cs
@@ -31,7 +31,7 @@ public class LabelUpdateInputVo @@ -31,7 +31,7 @@ public class LabelUpdateInputVo
31 31
32 public string LabelCategoryId { get; set; } = string.Empty; 32 public string LabelCategoryId { get; set; } = string.Empty;
33 33
34 - /// <summary>标签类型 Id(可选)</summary> 34 + /// <summary>标签类型 Id(<c>fl_label_type.Id</c>);可选,未传时清空类型绑定</summary>
35 public string? LabelTypeId { get; set; } 35 public string? LabelTypeId { get; set; }
36 36
37 public List<string> ProductIds { get; set; } = new(); 37 public List<string> ProductIds { get; set; } = new();
美国版/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 namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory; 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelCategory;
2 4
3 -public class LabelCategoryCreateInputVo 5 +public class LabelCategoryCreateInputVo : ILabelEntityPartnerScopeInput
4 { 6 {
5 public string CategoryCode { get; set; } = string.Empty; 7 public string CategoryCode { get; set; } = string.Empty;
6 8
@@ -24,6 +26,17 @@ public class LabelCategoryCreateInputVo @@ -24,6 +26,17 @@ public class LabelCategoryCreateInputVo
24 public string ButtonAppearance { get; set; } = "TEXT"; 26 public string ButtonAppearance { get; set; } = "TEXT";
25 27
26 /// <summary> 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 /// 门店可用范围:ALL / SPECIFIED 40 /// 门店可用范围:ALL / SPECIFIED
28 /// </summary> 41 /// </summary>
29 public string AvailabilityType { get; set; } = "ALL"; 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,6 +8,9 @@ public class LabelCategoryGetListInputVo : PagedAndSortedResultRequestDto
8 8
9 public bool? State { get; set; } 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 /// <summary> 14 /// <summary>
12 /// Region 筛选(<c>fl_group.Id</c>);含 <c>availabilityType=ALL</c> 的分类 15 /// Region 筛选(<c>fl_group.Id</c>);含 <c>availabilityType=ALL</c> 的分类
13 /// </summary> 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,6 +18,16 @@ public class LabelCategoryGetListOutputDto
18 18
19 public string AvailabilityType { get; set; } = "ALL"; 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 /// <summary>适用 Region 展示(ALL 时为 All Regions)</summary> 31 /// <summary>适用 Region 展示(ALL 时为 All Regions)</summary>
22 public string Region { get; set; } = string.Empty; 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,6 +18,16 @@ public class LabelCategoryGetOutputDto
18 18
19 public string AvailabilityType { get; set; } = "ALL"; 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 /// <summary>适用 Region Id 列表(多选;<c>ALL</c> 时为空)</summary> 31 /// <summary>适用 Region Id 列表(多选;<c>ALL</c> 时为空)</summary>
22 public List<string> RegionIds { get; set; } = new(); 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 namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption; 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelMultipleOption;
2 4
3 -public class LabelMultipleOptionCreateInputVo 5 +public class LabelMultipleOptionCreateInputVo : ILabelEntityPartnerScopeInput
4 { 6 {
5 /// <summary>多选项编码(可选;未传或空字符串时存空,列表/详情出参为「无」)</summary> 7 /// <summary>多选项编码(可选;未传或空字符串时存空,列表/详情出参为「无」)</summary>
6 public string? OptionCode { get; set; } 8 public string? OptionCode { get; set; }
@@ -12,6 +14,17 @@ public class LabelMultipleOptionCreateInputVo @@ -12,6 +14,17 @@ public class LabelMultipleOptionCreateInputVo
12 public bool State { get; set; } = true; 14 public bool State { get; set; } = true;
13 15
14 /// <summary> 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 /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED 28 /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED
16 /// </summary> 29 /// </summary>
17 public string AvailabilityType { get; set; } = "ALL"; 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,6 +8,9 @@ public class LabelMultipleOptionGetListInputVo : PagedAndSortedResultRequestDto
8 8
9 public bool? State { get; set; } 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 /// <summary> 14 /// <summary>
12 /// Region 筛选(<c>fl_group.Id</c>) 15 /// Region 筛选(<c>fl_group.Id</c>)
13 /// </summary> 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,6 +14,14 @@ public class LabelMultipleOptionGetListOutputDto
14 14
15 public string AvailabilityType { get; set; } = "ALL"; 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 public int OrderNum { get; set; } 25 public int OrderNum { get; set; }
18 26
19 public DateTime LastEdited { get; set; } 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,6 +16,14 @@ public class LabelMultipleOptionGetOutputDto
16 16
17 public string AvailabilityType { get; set; } = "ALL"; 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 public List<string> RegionIds { get; set; } = new(); 27 public List<string> RegionIds { get; set; } = new();
20 28
21 public List<string> GroupIds { get; set; } = new(); 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 namespace FoodLabeling.Application.Contracts.Dtos.LabelType; 3 namespace FoodLabeling.Application.Contracts.Dtos.LabelType;
2 4
3 -public class LabelTypeCreateInputVo 5 +public class LabelTypeCreateInputVo : ILabelEntityPartnerScopeInput
4 { 6 {
5 public string TypeCode { get; set; } = string.Empty; 7 public string TypeCode { get; set; } = string.Empty;
6 8
@@ -9,6 +11,17 @@ public class LabelTypeCreateInputVo @@ -9,6 +11,17 @@ public class LabelTypeCreateInputVo
9 public bool State { get; set; } = true; 11 public bool State { get; set; } = true;
10 12
11 /// <summary> 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 /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED 25 /// 门店可用范围:ALL / SPECIFIED;传了 <see cref="RegionIds"/> 或 <see cref="LocationIds"/> 时自动为 SPECIFIED
13 /// </summary> 26 /// </summary>
14 public string AvailabilityType { get; set; } = "ALL"; 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,6 +8,9 @@ public class LabelTypeGetListInputVo : PagedAndSortedResultRequestDto
8 8
9 public bool? State { get; set; } 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 /// <summary> 14 /// <summary>
12 /// Region 筛选(<c>fl_group.Id</c>);仅返回在该 Region 下存在标签实例的类型 15 /// Region 筛选(<c>fl_group.Id</c>);仅返回在该 Region 下存在标签实例的类型
13 /// </summary> 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,6 +12,16 @@ public class LabelTypeGetListOutputDto
12 12
13 public string AvailabilityType { get; set; } = "ALL"; 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 public int OrderNum { get; set; } 25 public int OrderNum { get; set; }
16 26
17 /// <summary>列表列 No. of Labels:该类型下未删除标签数(统计 <c>fl_label</c>,非库表物理列)</summary> 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,6 +14,16 @@ public class LabelTypeGetOutputDto
14 14
15 public string AvailabilityType { get; set; } = "ALL"; 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 public List<string> RegionIds { get; set; } = new(); 27 public List<string> RegionIds { get; set; } = new();
18 28
19 public List<string> GroupIds { get; set; } = new(); 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,11 +18,16 @@ public class ProductCategoryGetListInputVo : PagedAndSortedResultRequestDto
18 public bool? State { get; set; } 18 public bool? State { get; set; }
19 19
20 /// <summary> 20 /// <summary>
21 - /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类,以及 <c>availabilityType=ALL</c> 的分类 21 + /// 按 Region 筛选(<c>fl_group.Id</c>):返回适用于该 Region 下任一门门店的分类
22 /// </summary> 22 /// </summary>
23 public string? GroupId { get; set; } 23 public string? GroupId { get; set; }
24 24
25 /// <summary> 25 /// <summary>
  26 + /// 按 Company 筛选(<c>fl_partner.Id</c>)
  27 + /// </summary>
  28 + public string? PartnerId { get; set; }
  29 +
  30 + /// <summary>
26 /// 按门店筛选(<c>location.Id</c>,Guid 字符串);优先于 <see cref="GroupId"/> 31 /// 按门店筛选(<c>location.Id</c>,Guid 字符串);优先于 <see cref="GroupId"/>
27 /// </summary> 32 /// </summary>
28 public string? LocationId { get; set; } 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,7 +11,7 @@ namespace FoodLabeling.Application.Contracts.IServices;
11 public interface ILabelAppService : IApplicationService 11 public interface ILabelAppService : IApplicationService
12 { 12 {
13 /// <summary> 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 /// </summary> 15 /// </summary>
16 Task<PagedResultWithPageDto<LabelGetListOutputDto>> GetListAsync(LabelGetListInputVo input); 16 Task<PagedResultWithPageDto<LabelGetListOutputDto>> GetListAsync(LabelGetListInputVo input);
17 17
美国版/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,12 +11,12 @@ namespace FoodLabeling.Application.Contracts.IServices;
11 public interface IUsAppAuthAppService : IApplicationService 11 public interface IUsAppAuthAppService : IApplicationService
12 { 12 {
13 /// <summary> 13 /// <summary>
14 - /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回 userlocation 绑定的门店 14 + /// App 登录:使用邮箱 + 密码校验并签发 Token,同时返回可选门店(Company Admin 为绑定 Company 下全部门店)
15 /// </summary> 15 /// </summary>
16 Task<UsAppLoginOutputDto> LoginAsync(UsAppLoginInputVo input); 16 Task<UsAppLoginOutputDto> LoginAsync(UsAppLoginInputVo input);
17 17
18 /// <summary> 18 /// <summary>
19 - /// 获取当前登录账号已绑定的门店(用于切换门店等场景) 19 + /// 获取当前登录账号可选门店(Company Admin 返回绑定 Company 下全部门店,用于切换门店等场景)
20 /// </summary> 20 /// </summary>
21 Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync(); 21 Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync();
22 22
@@ -62,7 +62,7 @@ public interface IUsAppAuthAppService : IApplicationService @@ -62,7 +62,7 @@ public interface IUsAppAuthAppService : IApplicationService
62 Task ChangePasswordAsync(UsAppChangePasswordInputVo input); 62 Task ChangePasswordAsync(UsAppChangePasswordInputVo input);
63 63
64 /// <summary> 64 /// <summary>
65 - /// 按门店 Id 查询 Location 详情(须为当前账号 userlocation 绑定门店) 65 + /// 按门店 Id 查询 Location 详情(须为当前账号可选门店;Company Admin 含绑定 Company 下任意门店)
66 /// </summary> 66 /// </summary>
67 /// <param name="locationId">门店 Guid 字符串</param> 67 /// <param name="locationId">门店 Guid 字符串</param>
68 /// <returns>店名、地址、电话、经营时间(operatingHours)、店长信息</returns> 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,6 +54,61 @@ public static class LocationScopeBindingHelper
54 return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList(); 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 public static async Task<List<string>> ResolveDisplayRegionIdsForCurrentUserAsync( 112 public static async Task<List<string>> ResolveDisplayRegionIdsForCurrentUserAsync(
58 ISqlSugarClient db, 113 ISqlSugarClient db,
59 ICurrentUser currentUser) 114 ICurrentUser currentUser)
@@ -63,12 +118,7 @@ public static class LocationScopeBindingHelper @@ -63,12 +118,7 @@ public static class LocationScopeBindingHelper
63 return new List<string>(); 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 /// <summary> 124 /// <summary>
@@ -93,9 +143,7 @@ public static class LocationScopeBindingHelper @@ -93,9 +143,7 @@ public static class LocationScopeBindingHelper
93 var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); 143 var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds);
94 var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); 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 regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); 148 regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds);
101 } 149 }
@@ -120,7 +168,7 @@ public static class LocationScopeBindingHelper @@ -120,7 +168,7 @@ public static class LocationScopeBindingHelper
120 var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); 168 var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds);
121 var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); 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 regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); 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,9 +409,29 @@ public static class ReportsDateTimeDisplayHelper
409 409
410 410
411 return false; 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,7 +8,7 @@ namespace FoodLabeling.Application.Helpers;
8 8
9 /// <summary> 9 /// <summary>
10 /// Reports 门店数据范围:管理员可查全部(可按 Company/Region/Location 入参收窄); 10 /// Reports 门店数据范围:管理员可查全部(可按 Company/Region/Location 入参收窄);
11 -/// 非管理员仅统计 <c>userlocation</c> 绑定门店的打印任务 11 +/// Company Admin 为绑定 Company 下全部门店;其它非管理员为 <c>userlocation</c> 绑定门店
12 /// </summary> 12 /// </summary>
13 public static class ReportsLocationScopeHelper 13 public static class ReportsLocationScopeHelper
14 { 14 {
@@ -16,7 +16,7 @@ public static class ReportsLocationScopeHelper @@ -16,7 +16,7 @@ public static class ReportsLocationScopeHelper
16 /// 解析报表统计适用的门店 Id 列表。 16 /// 解析报表统计适用的门店 Id 列表。
17 /// <list type="bullet"> 17 /// <list type="bullet">
18 /// <item>返回 <c>null</c>:不限制门店(管理员且未传 Company/Region/Location 筛选);</item> 18 /// <item>返回 <c>null</c>:不限制门店(管理员且未传 Company/Region/Location 筛选);</item>
19 - /// <item>返回空列表:无绑定门店或筛选后无交集;</item> 19 + /// <item>返回空列表:无可访问门店或筛选后无交集;</item>
20 /// <item>返回非空列表:仅统计这些 <c>fl_label_print_task.LocationId</c>。</item> 20 /// <item>返回非空列表:仅统计这些 <c>fl_label_print_task.LocationId</c>。</item>
21 /// </list> 21 /// </list>
22 /// </summary> 22 /// </summary>
@@ -39,22 +39,16 @@ public static class ReportsLocationScopeHelper @@ -39,22 +39,16 @@ public static class ReportsLocationScopeHelper
39 return new List<string>(); 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 return new List<string>(); 46 return new List<string>();
53 } 47 }
54 48
55 if (filterIds is null) 49 if (filterIds is null)
56 { 50 {
57 - return boundIds; 51 + return accessibleIds;
58 } 52 }
59 53
60 if (filterIds.Count == 0) 54 if (filterIds.Count == 0)
@@ -62,8 +56,8 @@ public static class ReportsLocationScopeHelper @@ -62,8 +56,8 @@ public static class ReportsLocationScopeHelper
62 return new List<string>(); 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 /// <summary> 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,10 +53,10 @@ public static class ReportsPrintLogExpiryHelper
53 return ReportsDateTimeDisplayHelper.FormatExpiryDateTime(resolved); 53 return ReportsDateTimeDisplayHelper.FormatExpiryDateTime(resolved);
54 } 54 }
55 55
56 - var raw = ExtractExpiryText(printInputJson); 56 + var raw = ExtractExpiryText(printInputJson, reference);
57 if (string.Equals(raw, "无", StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(renderTemplateJson)) 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 return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, reference); 62 return ReportsDateTimeDisplayHelper.FormatExpiryDisplay(raw, reference);
@@ -185,7 +185,7 @@ public static class ReportsPrintLogExpiryHelper @@ -185,7 +185,7 @@ public static class ReportsPrintLogExpiryHelper
185 return false; 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 private static string? ResolveElementDisplayText(JsonElement el, DateTime reference) 191 private static string? ResolveElementDisplayText(JsonElement el, DateTime reference)
@@ -453,35 +453,6 @@ public static class ReportsPrintLogExpiryHelper @@ -453,35 +453,6 @@ public static class ReportsPrintLogExpiryHelper
453 dt.Hour != 0 || dt.Minute != 0; 453 dt.Hour != 0 || dt.Minute != 0;
454 454
455 /// <summary> 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 /// </summary> 457 /// </summary>
487 public static string ExtractExpiryText(string? printInputJson, DateTime anchor) 458 public static string ExtractExpiryText(string? printInputJson, DateTime anchor)
@@ -830,7 +801,7 @@ public static class ReportsPrintLogExpiryHelper @@ -830,7 +801,7 @@ public static class ReportsPrintLogExpiryHelper
830 } 801 }
831 802
832 var t = raw.Trim(); 803 var t = raw.Trim();
833 - if (!t.StartsWith('{', StringComparison.Ordinal)) 804 + if (!t.StartsWith("{"))
834 { 805 {
835 if (Regex.IsMatch(t, @"^\d+$")) 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 using SqlSugar; 1 using SqlSugar;
2 using Volo.Abp.Users; 2 using Volo.Abp.Users;
3 -using Yi.Framework.Rbac.Domain.Entities;  
4 using Yi.Framework.Rbac.Domain.Shared.Consts; 3 using Yi.Framework.Rbac.Domain.Shared.Consts;
5 4
6 namespace FoodLabeling.Application.Helpers; 5 namespace FoodLabeling.Application.Helpers;
@@ -55,12 +54,17 @@ public static class TeamMemberRoleHelper @@ -55,12 +54,17 @@ public static class TeamMemberRoleHelper
55 return false; 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 /// <summary> 70 /// <summary>
@@ -73,8 +77,16 @@ public static class TeamMemberRoleHelper @@ -73,8 +77,16 @@ public static class TeamMemberRoleHelper
73 return false; 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 if (role is null) 90 if (role is null)
79 { 91 {
80 return false; 92 return false;
@@ -84,6 +96,19 @@ public static class TeamMemberRoleHelper @@ -84,6 +96,19 @@ public static class TeamMemberRoleHelper
84 } 96 }
85 97
86 /// <summary> 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 /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。 112 /// 列表/详情出参:Partner Admin → Company Admin(与 Web UI 一致)。
88 /// </summary> 113 /// </summary>
89 public static string? FormatDisplayRoleName(string? roleName) 114 public static string? FormatDisplayRoleName(string? roleName)
@@ -148,4 +173,11 @@ public static class TeamMemberRoleHelper @@ -148,4 +173,11 @@ public static class TeamMemberRoleHelper
148 173
149 private static string NormalizeRoleKey(string value) => 174 private static string NormalizeRoleKey(string value) =>
150 new string(value.Trim().ToLowerInvariant().Where(c => char.IsLetterOrDigit(c)).ToArray()); 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,7 +13,7 @@ namespace FoodLabeling.Application.Helpers;
13 public static class UsAppPrintLogScopeHelper 13 public static class UsAppPrintLogScopeHelper
14 { 14 {
15 /// <summary> 15 /// <summary>
16 - /// 管理员(<see cref="ReportsRoleHelper.IsAdminRole"/>)或角色码/名含 <c>partner</c>(忽略大小写,如 Partner Admin) 16 + /// 管理员、Company Admin,或角色码/名含 <c>partner</c>(如 Partner Admin):可查看门店下全部打印记录
17 /// </summary> 17 /// </summary>
18 public static async Task<bool> CanViewAllPrintsAtLocationAsync( 18 public static async Task<bool> CanViewAllPrintsAtLocationAsync(
19 ICurrentUser currentUser, 19 ICurrentUser currentUser,
@@ -29,6 +29,11 @@ public static class UsAppPrintLogScopeHelper @@ -29,6 +29,11 @@ public static class UsAppPrintLogScopeHelper
29 return false; 29 return false;
30 } 30 }
31 31
  32 + if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser))
  33 + {
  34 + return true;
  35 + }
  36 +
32 var roleRows = await db.Ado.SqlQueryAsync<UserRoleRow>( 37 var roleRows = await db.Ado.SqlQueryAsync<UserRoleRow>(
33 @"SELECT r.RoleCode, r.RoleName 38 @"SELECT r.RoleCode, r.RoleName
34 FROM UserRole ur 39 FROM UserRole ur
@@ -41,30 +46,18 @@ public static class UsAppPrintLogScopeHelper @@ -41,30 +46,18 @@ public static class UsAppPrintLogScopeHelper
41 } 46 }
42 47
43 /// <summary> 48 /// <summary>
44 - /// 校验当前用户是否绑定指定门店 49 + /// 校验当前用户是否可访问指定门店(含 Company Admin 公司下全部门店)
45 /// </summary> 50 /// </summary>
46 public static async Task EnsureUserBoundToLocationAsync( 51 public static async Task EnsureUserBoundToLocationAsync(
47 ICurrentUser currentUser, 52 ICurrentUser currentUser,
48 ISqlSugarClient db, 53 ISqlSugarClient db,
49 string locationId) 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 /// <summary> 59 /// <summary>
67 - /// 校验是否可访问指定门店:平台管理员(<see cref="ReportsRoleHelper.IsAdminRole"/>)不校验 userlocation;其它账号须绑定。 60 + /// 校验是否可访问指定门店:平台管理员不校验;Company Admin 可访问绑定 Company 下任意门店;其它账号须 userlocation 绑定。
68 /// </summary> 61 /// </summary>
69 public static async Task EnsureUserCanAccessLocationAsync( 62 public static async Task EnsureUserCanAccessLocationAsync(
70 ICurrentUser currentUser, 63 ICurrentUser currentUser,
@@ -76,7 +69,23 @@ public static class UsAppPrintLogScopeHelper @@ -76,7 +69,23 @@ public static class UsAppPrintLogScopeHelper
76 return; 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 /// <summary> 91 /// <summary>
@@ -107,10 +116,9 @@ public static class UsAppPrintLogScopeHelper @@ -107,10 +116,9 @@ public static class UsAppPrintLogScopeHelper
107 var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); 116 var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
108 var guids = userIdStrings 117 var guids = userIdStrings
109 .Where(x => !string.IsNullOrWhiteSpace(x)) 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 .Distinct() 122 .Distinct()
115 .ToList(); 123 .ToList();
116 if (guids.Count == 0) 124 if (guids.Count == 0)
@@ -118,19 +126,27 @@ public static class UsAppPrintLogScopeHelper @@ -118,19 +126,27 @@ public static class UsAppPrintLogScopeHelper
118 return map; 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 foreach (var u in users) 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 return map; 152 return map;
@@ -143,7 +159,35 @@ public static class UsAppPrintLogScopeHelper @@ -143,7 +159,35 @@ public static class UsAppPrintLogScopeHelper
143 return "无"; 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 private static bool ContainsPartnerKeyword(string? value) => 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,12 +151,15 @@ public class LabelAppService : ApplicationService, ILabelAppService
151 }; 151 };
152 } 152 }
153 153
154 - // 查询标签基础信息(分类/类型/模板) 154 + // 查询标签基础信息(分类/类型/模板);类型 LeftJoin,允许未绑定 labelTypeId 的标签出现在列表
155 var labelRows = await _dbContext.SqlSugarClient 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 .Where((l, c, t, tpl) => pageLabelIds.Contains(l.Id)) 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 .Select((l, c, t, tpl) => new 163 .Select((l, c, t, tpl) => new
161 { 164 {
162 l.Id, 165 l.Id,
@@ -248,7 +251,7 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -248,7 +251,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
248 Products = products, 251 Products = products,
249 TemplateName = x.TemplateName ?? string.Empty, 252 TemplateName = x.TemplateName ?? string.Empty,
250 TemplateCode = x.TemplateCode ?? string.Empty, 253 TemplateCode = x.TemplateCode ?? string.Empty,
251 - LabelTypeName = x.LabelTypeName ?? string.Empty, 254 + LabelTypeName = string.IsNullOrWhiteSpace(x.LabelTypeName) ? "无" : x.LabelTypeName,
252 State = x.State, 255 State = x.State,
253 LastEdited = x.LastEdited, 256 LastEdited = x.LastEdited,
254 HasError = false 257 HasError = false
@@ -289,8 +292,12 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -289,8 +292,12 @@ public class LabelAppService : ApplicationService, ILabelAppService
289 .FirstAsync(x => x.Id == label.TemplateId); 292 .FirstAsync(x => x.Id == label.TemplateId);
290 var category = await db.Queryable<FlLabelCategoryDbEntity>() 293 var category = await db.Queryable<FlLabelCategoryDbEntity>()
291 .FirstAsync(x => x.Id == label.LabelCategoryId); 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 var productIds = await db.Queryable<FlLabelProductDbEntity>() 302 var productIds = await db.Queryable<FlLabelProductDbEntity>()
296 .Where(x => x.LabelId == label.Id) 303 .Where(x => x.LabelId == label.Id)
@@ -368,10 +375,8 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -368,10 +375,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
368 { 375 {
369 throw new UserFriendlyException("标签分类Id不能为空"); 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 var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync( 381 var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync(
377 _dbContext.SqlSugarClient, 382 _dbContext.SqlSugarClient,
@@ -413,7 +418,7 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -413,7 +418,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
413 TemplateId = template.Id, 418 TemplateId = template.Id,
414 LocationId = scope.PrimaryLocationId, 419 LocationId = scope.PrimaryLocationId,
415 LabelCategoryId = input.LabelCategoryId?.Trim(), 420 LabelCategoryId = input.LabelCategoryId?.Trim(),
416 - LabelTypeId = input.LabelTypeId?.Trim(), 421 + LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId),
417 State = input.State, 422 State = input.State,
418 LabelType = null, 423 LabelType = null,
419 LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson) 424 LabelInfoJson = input.LabelInfoJson == null ? null : JsonSerializer.Serialize(input.LabelInfoJson)
@@ -484,10 +489,8 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -484,10 +489,8 @@ public class LabelAppService : ApplicationService, ILabelAppService
484 { 489 {
485 throw new UserFriendlyException("标签分类Id不能为空"); 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 var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync( 495 var scope = await LabelRegionScopeHelper.ResolveScopeForSaveAsync(
493 _dbContext.SqlSugarClient, 496 _dbContext.SqlSugarClient,
@@ -510,7 +513,7 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -510,7 +513,7 @@ public class LabelAppService : ApplicationService, ILabelAppService
510 label.TemplateId = template.Id; 513 label.TemplateId = template.Id;
511 label.LocationId = scope.PrimaryLocationId; 514 label.LocationId = scope.PrimaryLocationId;
512 label.LabelCategoryId = input.LabelCategoryId?.Trim(); 515 label.LabelCategoryId = input.LabelCategoryId?.Trim();
513 - label.LabelTypeId = input.LabelTypeId?.Trim(); 516 + label.LabelTypeId = NormalizeOptionalLabelTypeId(input.LabelTypeId);
514 label.State = input.State; 517 label.State = input.State;
515 label.LastModifierId = CurrentUser?.Id?.ToString(); 518 label.LastModifierId = CurrentUser?.Id?.ToString();
516 label.LastModificationTime = now; 519 label.LastModificationTime = now;
@@ -873,5 +876,27 @@ public class LabelAppService : ApplicationService, ILabelAppService @@ -873,5 +876,27 @@ public class LabelAppService : ApplicationService, ILabelAppService
873 var v = (raw ?? string.Empty).Trim().ToLowerInvariant(); 876 var v = (raw ?? string.Empty).Trim().ToLowerInvariant();
874 return v is "line" or "dotted" ? v : "none"; 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,7 +35,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
35 (x.DisplayText != null && x.DisplayText.Contains(keyword!))) 35 (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
36 .WhereIF(input.State != null, x => x.State == input.State); 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 // Sorting 仅允许白名单字段,避免 Unknown column/注入风险 40 // Sorting 仅允许白名单字段,避免 Unknown column/注入风险
41 if (!string.IsNullOrWhiteSpace(input.Sorting)) 41 if (!string.IsNullOrWhiteSpace(input.Sorting))
@@ -72,10 +72,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -72,10 +72,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
72 72
73 var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids); 73 var labelStatsMap = await BuildCategoryLabelStatsMapAsync(ids);
74 var scopeMap = await BuildCategoryScopeMapAsync(entities); 74 var scopeMap = await BuildCategoryScopeMapAsync(entities);
  75 + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  76 + _dbContext.SqlSugarClient,
  77 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Category,
  78 + ids);
75 79
76 var items = entities.Select(x => 80 var items = entities.Select(x =>
77 { 81 {
78 scopeMap.TryGetValue(x.Id, out var scope); 82 scopeMap.TryGetValue(x.Id, out var scope);
  83 + partnerScopeMap.TryGetValue(x.Id, out var partnerScope);
79 labelStatsMap.TryGetValue(x.Id, out var labelStats); 84 labelStatsMap.TryGetValue(x.Id, out var labelStats);
80 var creationTime = ResolveCategoryCreationTime(x); 85 var creationTime = ResolveCategoryCreationTime(x);
81 return new LabelCategoryGetListOutputDto 86 return new LabelCategoryGetListOutputDto
@@ -88,6 +93,10 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -88,6 +93,10 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
88 State = x.State, 93 State = x.State,
89 ButtonAppearance = x.ButtonAppearance, 94 ButtonAppearance = x.ButtonAppearance,
90 AvailabilityType = x.AvailabilityType, 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 OrderNum = x.OrderNum, 100 OrderNum = x.OrderNum,
92 NoOfLabels = labelStats?.Count ?? 0, 101 NoOfLabels = labelStats?.Count ?? 0,
93 CreationTime = creationTime, 102 CreationTime = creationTime,
@@ -112,6 +121,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -112,6 +121,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
112 } 121 }
113 122
114 var dto = MapToGetOutput(entity); 123 var dto = MapToGetOutput(entity);
  124 + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id);
115 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) 125 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
116 { 126 {
117 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>() 127 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryLocationDbEntity>()
@@ -139,7 +149,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -139,7 +149,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
139 149
140 var displayText = input.DisplayText?.Trim(); 150 var displayText = input.DisplayText?.Trim();
141 var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() 154 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
145 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name)); 155 .AnyAsync(x => !x.IsDeleted && (x.CategoryCode == code || x.CategoryName == name));
@@ -170,6 +180,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -170,6 +180,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
170 }; 180 };
171 181
172 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 182 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  183 + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now);
173 await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); 184 await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
174 return await GetAsync(entity.Id); 185 return await GetAsync(entity.Id);
175 } 186 }
@@ -192,7 +203,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -192,7 +203,7 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
192 203
193 var displayText = input.DisplayText?.Trim(); 204 var displayText = input.DisplayText?.Trim();
194 var appearance = CategoryAppearanceStorageHelper.NormalizeButtonAppearanceForStorage(input.ButtonAppearance); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>() 208 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelCategoryDbEntity>()
198 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name)); 209 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.CategoryCode == code || x.CategoryName == name));
@@ -213,6 +224,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -213,6 +224,8 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
213 entity.LastModifierId = CurrentUser?.Id?.ToString(); 224 entity.LastModifierId = CurrentUser?.Id?.ToString();
214 225
215 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 226 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  227 + await SaveCategoryPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId,
  228 + entity.LastModificationTime ?? DateTime.Now);
216 await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, 229 await SaveCategoryLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
217 entity.LastModificationTime ?? DateTime.Now); 230 entity.LastModificationTime ?? DateTime.Now);
218 return await GetAsync(id); 231 return await GetAsync(id);
@@ -234,6 +247,14 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -234,6 +247,14 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
234 throw new UserFriendlyException("该标签分类已被标签引用,无法删除"); 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 entity.IsDeleted = true; 258 entity.IsDeleted = true;
238 entity.LastModificationTime = DateTime.Now; 259 entity.LastModificationTime = DateTime.Now;
239 entity.LastModifierId = CurrentUser?.Id?.ToString(); 260 entity.LastModifierId = CurrentUser?.Id?.ToString();
@@ -256,9 +277,15 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -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 LabelCategoryCreateInputVo input) 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 var regionIds = NormalizeRegionIds(input); 289 var regionIds = NormalizeRegionIds(input);
263 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); 290 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
264 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 291 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
@@ -278,20 +305,37 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -278,20 +305,37 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
278 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); 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 private static List<string> NormalizeRegionIds(LabelCategoryCreateInputVo input) 341 private static List<string> NormalizeRegionIds(LabelCategoryCreateInputVo input)
@@ -312,17 +356,24 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -312,17 +356,24 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
312 356
313 private async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryScopeFilterAsync( 357 private async Task<ISugarQueryable<FlLabelCategoryDbEntity>> ApplyCategoryScopeFilterAsync(
314 ISugarQueryable<FlLabelCategoryDbEntity> query, 358 ISugarQueryable<FlLabelCategoryDbEntity> query,
  359 + string? partnerId,
315 string? groupId, 360 string? groupId,
316 string? locationId) 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 return query; 373 return query;
323 } 374 }
324 375
325 - if (scopeLocIds.Count == 0) 376 + if (scopedLocationIds.Count == 0)
326 { 377 {
327 return query.Where(c => c.AvailabilityType == "ALL"); 378 return query.Where(c => c.AvailabilityType == "ALL");
328 } 379 }
@@ -330,10 +381,43 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ @@ -330,10 +381,43 @@ public class LabelCategoryAppService : ApplicationService, ILabelCategoryAppServ
330 return query.Where(c => 381 return query.Where(c =>
331 c.AvailabilityType == "ALL" || 382 c.AvailabilityType == "ALL" ||
332 SqlFunc.Subqueryable<FlLabelCategoryLocationDbEntity>() 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 .Any()); 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 private const string AllRegionsDisplay = "All Regions"; 421 private const string AllRegionsDisplay = "All Regions";
338 private const string AllLocationsDisplay = "All Locations"; 422 private const string AllLocationsDisplay = "All Locations";
339 private const string EmptyDisplay = "无"; 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,14 +27,22 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
27 { 27 {
28 RefAsync<int> total = 0; 28 RefAsync<int> total = 0;
29 var keyword = input.Keyword?.Trim(); 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 var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>() 39 var query = _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionDbEntity>()
34 .Where(x => !x.IsDeleted) 40 .Where(x => !x.IsDeleted)
35 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!)) 41 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.OptionCode.Contains(keyword!) || x.OptionName.Contains(keyword!))
36 .WhereIF(input.State != null, x => x.State == input.State); 42 .WhereIF(input.State != null, x => x.State == input.State);
37 43
  44 + query = await LabelEntityPartnerScopeHelper.ApplyMultipleOptionPartnerListFilterAsync(
  45 + _dbContext.SqlSugarClient, query, scopedPartnerIds);
38 query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds); 46 query = ApplyMultipleOptionScopeFilter(query, scopedLocationIds);
39 47
40 if (!string.IsNullOrWhiteSpace(input.Sorting)) 48 if (!string.IsNullOrWhiteSpace(input.Sorting))
@@ -48,10 +56,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -48,10 +56,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
48 56
49 var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total); 57 var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
50 var scopeMap = await BuildMultipleOptionScopeMapAsync(entities); 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 var items = entities.Select(x => 64 var items = entities.Select(x =>
53 { 65 {
54 scopeMap.TryGetValue(x.Id, out var scope); 66 scopeMap.TryGetValue(x.Id, out var scope);
  67 + partnerScopeMap.TryGetValue(x.Id, out var partnerScope);
55 return new LabelMultipleOptionGetListOutputDto 68 return new LabelMultipleOptionGetListOutputDto
56 { 69 {
57 Id = x.Id, 70 Id = x.Id,
@@ -60,6 +73,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -60,6 +73,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
60 OptionValuesJson = x.OptionValuesJson, 73 OptionValuesJson = x.OptionValuesJson,
61 State = x.State, 74 State = x.State,
62 AvailabilityType = x.AvailabilityType, 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 OrderNum = x.OrderNum, 80 OrderNum = x.OrderNum,
64 LastEdited = x.LastModificationTime ?? x.CreationTime, 81 LastEdited = x.LastModificationTime ?? x.CreationTime,
65 Region = scope?.Region ?? EmptyDisplay, 82 Region = scope?.Region ?? EmptyDisplay,
@@ -82,6 +99,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -82,6 +99,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
82 } 99 }
83 100
84 var dto = MapToGetOutput(entity); 101 var dto = MapToGetOutput(entity);
  102 + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id);
85 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) 103 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
86 { 104 {
87 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionLocationDbEntity>() 105 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelMultipleOptionLocationDbEntity>()
@@ -107,7 +125,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -107,7 +125,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
107 throw new UserFriendlyException("多选项名称不能为空"); 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 if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: null)) 130 if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: null))
113 { 131 {
@@ -134,6 +152,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -134,6 +152,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
134 }; 152 };
135 153
136 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 154 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  155 + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, currentUserId, now);
137 await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); 156 await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
138 return await GetAsync(entity.Id); 157 return await GetAsync(entity.Id);
139 } 158 }
@@ -154,7 +173,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -154,7 +173,7 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
154 throw new UserFriendlyException("多选项名称不能为空"); 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 if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: id)) 178 if (await IsMultipleOptionDuplicatedAsync(code, name, excludeId: id))
160 { 179 {
@@ -171,6 +190,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -171,6 +190,8 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
171 entity.LastModifierId = CurrentUser?.Id?.ToString(); 190 entity.LastModifierId = CurrentUser?.Id?.ToString();
172 191
173 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 192 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  193 + await SaveMultipleOptionPartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId,
  194 + entity.LastModificationTime ?? DateTime.Now);
174 await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, 195 await SaveMultipleOptionLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
175 entity.LastModificationTime ?? DateTime.Now); 196 entity.LastModificationTime ?? DateTime.Now);
176 return await GetAsync(id); 197 return await GetAsync(id);
@@ -185,6 +206,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -185,6 +206,10 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
185 return; 206 return;
186 } 207 }
187 208
  209 + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync(
  210 + _dbContext.SqlSugarClient,
  211 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.MultipleOption,
  212 + id);
188 await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>() 213 await _dbContext.SqlSugarClient.Deleteable<FlLabelMultipleOptionLocationDbEntity>()
189 .Where(x => x.MultipleOptionId == id) 214 .Where(x => x.MultipleOptionId == id)
190 .ExecuteCommandAsync(); 215 .ExecuteCommandAsync();
@@ -199,9 +224,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -199,9 +224,15 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
199 private const string AllRegionsDisplay = "All Regions"; 224 private const string AllRegionsDisplay = "All Regions";
200 private const string AllLocationsDisplay = "All Locations"; 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 LabelMultipleOptionCreateInputVo input) 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 var regionIds = NormalizeRegionIds(input); 236 var regionIds = NormalizeRegionIds(input);
206 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); 237 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
207 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 238 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
@@ -221,20 +252,70 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO @@ -221,20 +252,70 @@ public class LabelMultipleOptionAppService : ApplicationService, ILabelMultipleO
221 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); 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 private static List<string> NormalizeRegionIds(LabelMultipleOptionCreateInputVo input) 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,14 +27,22 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
27 { 27 {
28 RefAsync<int> total = 0; 28 RefAsync<int> total = 0;
29 var keyword = input.Keyword?.Trim(); 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 var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() 39 var query = _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
34 .Where(x => !x.IsDeleted) 40 .Where(x => !x.IsDeleted)
35 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!)) 41 .WhereIF(!string.IsNullOrWhiteSpace(keyword), x => x.TypeCode.Contains(keyword!) || x.TypeName.Contains(keyword!))
36 .WhereIF(input.State != null, x => x.State == input.State); 42 .WhereIF(input.State != null, x => x.State == input.State);
37 43
  44 + query = await LabelEntityPartnerScopeHelper.ApplyTypePartnerListFilterAsync(
  45 + _dbContext.SqlSugarClient, query, scopedPartnerIds);
38 query = ApplyLabelTypeScopeFilter(query, scopedLocationIds); 46 query = ApplyLabelTypeScopeFilter(query, scopedLocationIds);
39 47
40 if (!string.IsNullOrWhiteSpace(input.Sorting)) 48 if (!string.IsNullOrWhiteSpace(input.Sorting))
@@ -51,10 +59,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -51,10 +59,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
51 59
52 var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds); 60 var countMap = await BuildTypeLabelStatsMapAsync(ids, scopedLocationIds);
53 var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities); 61 var scopeMap = await BuildTypeConfiguredScopeMapAsync(entities);
  62 + var partnerScopeMap = await LabelEntityPartnerScopeHelper.BuildPartnerScopeDisplayMapAsync(
  63 + _dbContext.SqlSugarClient,
  64 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type,
  65 + ids);
54 66
55 var items = entities.Select(x => 67 var items = entities.Select(x =>
56 { 68 {
57 scopeMap.TryGetValue(x.Id, out var scope); 69 scopeMap.TryGetValue(x.Id, out var scope);
  70 + partnerScopeMap.TryGetValue(x.Id, out var partnerScope);
58 countMap.TryGetValue(x.Id, out var stats); 71 countMap.TryGetValue(x.Id, out var stats);
59 var locationDisplay = scope?.Location ?? EmptyDisplay; 72 var locationDisplay = scope?.Location ?? EmptyDisplay;
60 return new LabelTypeGetListOutputDto 73 return new LabelTypeGetListOutputDto
@@ -64,6 +77,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -64,6 +77,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
64 TypeName = x.TypeName, 77 TypeName = x.TypeName,
65 State = x.State, 78 State = x.State,
66 AvailabilityType = x.AvailabilityType, 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 OrderNum = x.OrderNum, 84 OrderNum = x.OrderNum,
68 NoOfLabels = stats?.Count ?? 0, 85 NoOfLabels = stats?.Count ?? 0,
69 LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime, 86 LastEdited = stats?.MaxEdited ?? x.LastModificationTime ?? x.CreationTime,
@@ -88,6 +105,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -88,6 +105,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
88 } 105 }
89 106
90 var dto = MapToGetOutput(entity); 107 var dto = MapToGetOutput(entity);
  108 + await ApplyPartnerScopeToGetOutputAsync(dto, entity.Id);
91 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) 109 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
92 { 110 {
93 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeLocationDbEntity>() 111 var locationIds = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeLocationDbEntity>()
@@ -113,7 +131,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -113,7 +131,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
113 throw new UserFriendlyException("类型编码和名称不能为空"); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() 136 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
119 .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name)); 137 .AnyAsync(x => !x.IsDeleted && (x.TypeCode == code || x.TypeName == name));
@@ -141,6 +159,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -141,6 +159,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
141 }; 159 };
142 160
143 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync(); 161 await _dbContext.SqlSugarClient.Insertable(entity).ExecuteCommandAsync();
  162 + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, currentUserId, now);
144 await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now); 163 await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, currentUserId, now);
145 return await GetAsync(entity.Id); 164 return await GetAsync(entity.Id);
146 } 165 }
@@ -161,7 +180,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -161,7 +180,7 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
161 throw new UserFriendlyException("类型编码和名称不能为空"); 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 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>() 185 var duplicated = await _dbContext.SqlSugarClient.Queryable<FlLabelTypeDbEntity>()
167 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name)); 186 .AnyAsync(x => !x.IsDeleted && x.Id != id && (x.TypeCode == code || x.TypeName == name));
@@ -179,6 +198,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -179,6 +198,8 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
179 entity.LastModifierId = CurrentUser?.Id?.ToString(); 198 entity.LastModifierId = CurrentUser?.Id?.ToString();
180 199
181 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync(); 200 await _dbContext.SqlSugarClient.Updateable(entity).ExecuteCommandAsync();
  201 + await SaveTypePartnerScopeAsync(entity.Id, partnerScope, entity.LastModifierId,
  202 + entity.LastModificationTime ?? DateTime.Now);
182 await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId, 203 await SaveTypeLocationsAsync(entity.Id, availabilityType, mergedLocationIds, entity.LastModifierId,
183 entity.LastModificationTime ?? DateTime.Now); 204 entity.LastModificationTime ?? DateTime.Now);
184 return await GetAsync(id); 205 return await GetAsync(id);
@@ -200,6 +221,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -200,6 +221,10 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
200 throw new UserFriendlyException("该标签类型已被标签引用,无法删除"); 221 throw new UserFriendlyException("该标签类型已被标签引用,无法删除");
201 } 222 }
202 223
  224 + await LabelEntityPartnerScopeHelper.DeletePartnerScopeRowsAsync(
  225 + _dbContext.SqlSugarClient,
  226 + LabelEntityPartnerScopeHelper.LabelEntityPartnerKind.Type,
  227 + id);
203 await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>() 228 await _dbContext.SqlSugarClient.Deleteable<FlLabelTypeLocationDbEntity>()
204 .Where(x => x.LabelTypeId == id) 229 .Where(x => x.LabelTypeId == id)
205 .ExecuteCommandAsync(); 230 .ExecuteCommandAsync();
@@ -214,9 +239,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -214,9 +239,15 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
214 private const string AllRegionsDisplay = "All Regions"; 239 private const string AllRegionsDisplay = "All Regions";
215 private const string AllLocationsDisplay = "All Locations"; 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 LabelTypeCreateInputVo input) 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 var regionIds = NormalizeRegionIds(input); 251 var regionIds = NormalizeRegionIds(input);
221 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds); 252 var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(input.LocationIds);
222 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant(); 253 var availabilityType = (input.AvailabilityType ?? "ALL").Trim().ToUpperInvariant();
@@ -236,20 +267,70 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService @@ -236,20 +267,70 @@ public class LabelTypeAppService : ApplicationService, ILabelTypeAppService
236 throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); 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 private static List<string> NormalizeRegionIds(LabelTypeCreateInputVo input) 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,7 +41,7 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
41 (x.DisplayText != null && x.DisplayText.Contains(keyword!))) 41 (x.DisplayText != null && x.DisplayText.Contains(keyword!)))
42 .WhereIF(input.State != null, x => x.State == input.State); 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 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column 46 // Sorting 仅允许白名单字段,避免不同数据库列命名导致 Unknown column
47 // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险) 47 // 同时避免将 input.Sorting 原样拼接到 SQL(存在注入风险)
@@ -116,6 +116,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -116,6 +116,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
116 throw new UserFriendlyException("类别不存在"); 116 throw new UserFriendlyException("类别不存在");
117 } 117 }
118 118
  119 + await EnsureCategoryVisibleToCurrentUserAsync(entity);
  120 +
119 var dto = MapToGetOutput(entity); 121 var dto = MapToGetOutput(entity);
120 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase)) 122 if (string.Equals(entity.AvailabilityType, "SPECIFIED", StringComparison.OrdinalIgnoreCase))
121 { 123 {
@@ -189,6 +191,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -189,6 +191,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
189 throw new UserFriendlyException("类别不存在"); 191 throw new UserFriendlyException("类别不存在");
190 } 192 }
191 193
  194 + await EnsureCategoryVisibleToCurrentUserAsync(entity);
  195 +
192 var code = NormalizeCategoryCode(input.CategoryCode); 196 var code = NormalizeCategoryCode(input.CategoryCode);
193 var name = input.CategoryName?.Trim(); 197 var name = input.CategoryName?.Trim();
194 if (string.IsNullOrWhiteSpace(name)) 198 if (string.IsNullOrWhiteSpace(name))
@@ -231,6 +235,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -231,6 +235,8 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
231 return; 235 return;
232 } 236 }
233 237
  238 + await EnsureCategoryVisibleToCurrentUserAsync(entity);
  239 +
234 // 若被产品引用则不允许删除 240 // 若被产品引用则不允许删除
235 var usedByProduct = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>() 241 var usedByProduct = await _dbContext.SqlSugarClient.Queryable<FlProductDbEntity>()
236 .AnyAsync(x => !x.IsDeleted && x.CategoryId == id); 242 .AnyAsync(x => !x.IsDeleted && x.CategoryId == id);
@@ -321,28 +327,59 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp @@ -321,28 +327,59 @@ public class ProductCategoryAppService : ApplicationService, IProductCategoryApp
321 327
322 private async Task<ISugarQueryable<FlProductCategoryDbEntity>> ApplyCategoryScopeFilterAsync( 328 private async Task<ISugarQueryable<FlProductCategoryDbEntity>> ApplyCategoryScopeFilterAsync(
323 ISugarQueryable<FlProductCategoryDbEntity> query, 329 ISugarQueryable<FlProductCategoryDbEntity> query,
  330 + string? partnerId,
324 string? groupId, 331 string? groupId,
325 string? locationId) 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 return query; 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 return query.Where(c => 347 return query.Where(c =>
340 - c.AvailabilityType == "ALL" ||  
341 SqlFunc.Subqueryable<FlProductCategoryLocationDbEntity>() 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 .Any()); 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 private async Task SaveCategoryLocationsAsync( 383 private async Task SaveCategoryLocationsAsync(
347 string categoryId, 384 string categoryId,
348 string availabilityType, 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,20 +51,25 @@ public class ReportsAppService : ApplicationService, IReportsAppService
51 throw new UserFriendlyException("用户未登录"); 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 if (locationIds is not null && locationIds.Count == 0) 60 if (locationIds is not null && locationIds.Count == 0)
56 { 61 {
57 return EmptyPrintLogPage(input); 62 return EmptyPrintLogPage(input);
58 } 63 }
59 64
60 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); 65 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
61 - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); 66 + var viewAllPrints = await ResolveViewAllPrintsAsync();
62 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 67 var currentUserIdStr = CurrentUser.Id.Value.ToString();
63 var keyword = input.Keyword?.Trim(); 68 var keyword = input.Keyword?.Trim();
64 69
65 RefAsync<int> total = 0; 70 RefAsync<int> total = 0;
66 71
67 - var query = BuildReportTaskCore(locationIds, isAdmin, currentUserIdStr, keyword) 72 + var query = BuildReportTaskCore(locationIds, viewAllPrints, currentUserIdStr, keyword)
68 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) 73 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
69 .Where((t, l, p, lc, pc, loc, tpl) => 74 .Where((t, l, p, lc, pc, loc, tpl) =>
70 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && 75 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
@@ -166,18 +171,23 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -166,18 +171,23 @@ public class ReportsAppService : ApplicationService, IReportsAppService
166 throw new UserFriendlyException("用户未登录"); 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 if (locationIds is not null && locationIds.Count == 0) 180 if (locationIds is not null && locationIds.Count == 0)
171 { 181 {
172 return BuildEmptyPdf("print-log-empty.pdf"); 182 return BuildEmptyPdf("print-log-empty.pdf");
173 } 183 }
174 184
175 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); 185 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
176 - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); 186 + var viewAllPrints = await ResolveViewAllPrintsAsync();
177 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 187 var currentUserIdStr = CurrentUser.Id.Value.ToString();
178 var keyword = input.Keyword?.Trim(); 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 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) 191 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
182 .Where((t, l, p, lc, pc, loc, tpl) => 192 .Where((t, l, p, lc, pc, loc, tpl) =>
183 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && 193 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
@@ -305,7 +315,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -305,7 +315,12 @@ public class ReportsAppService : ApplicationService, IReportsAppService
305 throw new UserFriendlyException("用户未登录"); 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 if (locationIds is not null && locationIds.Count == 0) 324 if (locationIds is not null && locationIds.Count == 0)
310 { 325 {
311 var emptyMs = ReportsPrintLogExcelHelper.BuildWorkbook(Array.Empty<ReportsPrintLogListItemDto>()); 326 var emptyMs = ReportsPrintLogExcelHelper.BuildWorkbook(Array.Empty<ReportsPrintLogListItemDto>());
@@ -315,11 +330,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -315,11 +330,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService
315 } 330 }
316 331
317 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); 332 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
318 - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); 333 + var viewAllPrints = await ResolveViewAllPrintsAsync();
319 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 334 var currentUserIdStr = CurrentUser.Id.Value.ToString();
320 var keyword = input.Keyword?.Trim(); 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 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) 338 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
324 .Where((t, l, p, lc, pc, loc, tpl) => 339 .Where((t, l, p, lc, pc, loc, tpl) =>
325 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart && 340 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= rangeStart &&
@@ -414,11 +429,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -414,11 +429,11 @@ public class ReportsAppService : ApplicationService, IReportsAppService
414 } 429 }
415 430
416 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate); 431 var (rangeStart, rangeEndExcl) = ResolveDateRange(input.StartDate, input.EndDate);
417 - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); 432 + var viewAllPrints = await ResolveViewAllPrintsAsync();
418 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 433 var currentUserIdStr = CurrentUser.Id.Value.ToString();
419 var templateKeyword = input.Keyword?.Trim(); 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 restrictToCreator: false) 437 restrictToCreator: false)
423 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id) 438 .LeftJoin<FlLabelTemplateDbEntity>((t, l, p, lc, pc, loc, tpl) => t.TemplateId == tpl.Id)
424 .Where((t, l, p, lc, pc, loc, tpl) => 439 .Where((t, l, p, lc, pc, loc, tpl) =>
@@ -499,17 +514,17 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -499,17 +514,17 @@ public class ReportsAppService : ApplicationService, IReportsAppService
499 514
500 var prevEndExcl = curStart; 515 var prevEndExcl = curStart;
501 var prevStart = curStart - span; 516 var prevStart = curStart - span;
502 - var isAdmin = ReportsRoleHelper.IsAdminRole(CurrentUser); 517 + var viewAllPrints = await ResolveViewAllPrintsAsync();
503 var currentUserIdStr = CurrentUser.Id.Value.ToString(); 518 var currentUserIdStr = CurrentUser.Id.Value.ToString();
504 var keyword = input.Keyword?.Trim(); 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 .Where((t, l, p, lc, pc, loc) => 522 .Where((t, l, p, lc, pc, loc) =>
508 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && 523 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
509 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl) 524 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < curEndExcl)
510 .CountAsync(); 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 .Where((t, l, p, lc, pc, loc) => 528 .Where((t, l, p, lc, pc, loc) =>
514 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart && 529 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= prevStart &&
515 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl) 530 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < prevEndExcl)
@@ -520,7 +535,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -520,7 +535,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
520 var avgDaily = Math.Round((decimal)totalCur / dayCount, 2); 535 var avgDaily = Math.Round((decimal)totalCur / dayCount, 2);
521 var avgDailyPrev = Math.Round((decimal)totalPrev / prevDayCount, 2); 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 .Where((t, l, p, lc, pc, loc) => 539 .Where((t, l, p, lc, pc, loc) =>
525 l.LabelCategoryId != null && 540 l.LabelCategoryId != null &&
526 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && 541 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
@@ -531,7 +546,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -531,7 +546,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
531 546
532 var topCat = categoryRows.OrderByDescending(x => x.Cnt).FirstOrDefault(); 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 .Where((t, l, p, lc, pc, loc) => 550 .Where((t, l, p, lc, pc, loc) =>
536 !string.IsNullOrEmpty(p.Id) && 551 !string.IsNullOrEmpty(p.Id) &&
537 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart && 552 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= curStart &&
@@ -552,7 +567,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -552,7 +567,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
552 567
553 var trendEndExcl = trendEndDay.AddDays(1); 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 .Where((t, l, p, lc, pc, loc) => 571 .Where((t, l, p, lc, pc, loc) =>
557 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay && 572 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) >= trendStartDay &&
558 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl) 573 SqlFunc.IsNull(t.PrintedAt, t.CreationTime) < trendEndExcl)
@@ -704,7 +719,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -704,7 +719,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
704 FlProductCategoryDbEntity, LocationAggregateRoot> 719 FlProductCategoryDbEntity, LocationAggregateRoot>
705 BuildReportTaskCore( 720 BuildReportTaskCore(
706 List<string>? locationIds, 721 List<string>? locationIds,
707 - bool isAdmin, 722 + bool viewAllPrints,
708 string currentUserIdStr, 723 string currentUserIdStr,
709 string? keyword, 724 string? keyword,
710 bool restrictToCreator = true) 725 bool restrictToCreator = true)
@@ -717,7 +732,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -717,7 +732,7 @@ public class ReportsAppService : ApplicationService, IReportsAppService
717 .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) => 732 .LeftJoin<LocationAggregateRoot>((t, l, p, lc, pc, loc) =>
718 t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId) 733 t.LocationId != null && SqlFunc.ToString(loc.Id) == t.LocationId)
719 .Where((t, l, p, lc, pc, loc) => !loc.IsDeleted) 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 .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!)) 736 .WhereIF(locationIds is not null, (t, l, p, lc, pc, loc) => locationIds!.Contains(t.LocationId!))
722 .WhereIF(!string.IsNullOrWhiteSpace(keyword), 737 .WhereIF(!string.IsNullOrWhiteSpace(keyword),
723 (t, l, p, lc, pc, loc) => 738 (t, l, p, lc, pc, loc) =>
@@ -739,56 +754,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService @@ -739,56 +754,8 @@ public class ReportsAppService : ApplicationService, IReportsAppService
739 private static decimal CalcChangeRate(int current, int previous) => 754 private static decimal CalcChangeRate(int current, int previous) =>
740 CalcChangeRate((decimal)current, (decimal)previous); 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 private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate, 760 private static (DateTime rangeStart, DateTime rangeEndExcl) ResolveDateRange(DateTime? startDate,
794 DateTime? endDate) 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,7 +85,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
85 /// </summary> 85 /// </summary>
86 /// <remarks> 86 /// <remarks>
87 /// 行为与系统 <c>AccountService.PostLoginAsync</c> 一致(含验证码、登录日志事件)。 87 /// 行为与系统 <c>AccountService.PostLoginAsync</c> 一致(含验证码、登录日志事件)。
88 - /// 门店数据来自 <c>userlocation</c> 与 <c>location</c> 表 88 + /// 门店来自 <c>userlocation</c>;Company Admin 展开为绑定 Company 下全部门店
89 /// </remarks> 89 /// </remarks>
90 /// <param name="input">邮箱、密码;若系统开启验证码则需传 Uuid、Code</param> 90 /// <param name="input">邮箱、密码;若系统开启验证码则需传 Uuid、Code</param>
91 /// <returns>Token、RefreshToken 与绑定门店</returns> 91 /// <returns>Token、RefreshToken 与绑定门店</returns>
@@ -150,7 +150,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -150,7 +150,7 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
150 _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input); 150 _forgotPasswordByEmailService.ResetPasswordByEmailAsync(input);
151 151
152 /// <summary> 152 /// <summary>
153 - /// 获取当前登录用户已绑定的门店(切换门店时可重新拉取) 153 + /// 获取当前登录用户可选门店(切换门店时可重新拉取);Company Admin 返回绑定 Company 下全部门店。
154 /// </summary> 154 /// </summary>
155 [Authorize] 155 [Authorize]
156 public virtual async Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync() 156 public virtual async Task<List<UsAppBoundLocationDto>> GetMyLocationsAsync()
@@ -549,21 +549,25 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService @@ -549,21 +549,25 @@ public class UsAppAuthAppService : ApplicationService, IUsAppAuthAppService
549 549
550 private async Task<List<UsAppBoundLocationDto>> LoadBoundLocationsAsync(Guid userId) 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 return new List<UsAppBoundLocationDto>(); 566 return new List<UsAppBoundLocationDto>();
561 } 567 }
562 568
563 - var wanted = links.Distinct().ToList();  
564 var locations = (await _dbContext.SqlSugarClient.Queryable<LocationAggregateRoot>() 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 .ToListAsync()) 571 .ToListAsync())
568 .OrderBy(x => x.OrderNum) 572 .OrderBy(x => x.OrderNum)
569 .ThenBy(x => x.LocationName) 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,9 +945,18 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
945 }) 945 })
946 .ToPageListAsync(input.SkipCount, input.MaxResultCount, total); 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 var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync( 957 var operatorMap = await UsAppPrintLogScopeHelper.LoadOperatorNameMapAsync(
949 _dbContext.SqlSugarClient, 958 _dbContext.SqlSugarClient,
950 - pageRows.Select(x => x.CreatedBy)); 959 + createdByByTaskId.Values.Concat(pageRows.Select(x => x.CreatedBy)));
951 960
952 var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync( 961 var dailyLabelIdMap = await ReportsPrintLogDailyLabelIdHelper.ResolveDailyLabelIdsAsync(
953 _dbContext.SqlSugarClient, 962 _dbContext.SqlSugarClient,
@@ -968,7 +977,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ @@ -968,7 +977,9 @@ public class UsAppLabelingAppService : ApplicationService, IUsAppLabelingAppServ
968 LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit), 977 LabelSizeText = FormatLabelSizeWithUnit(x.TemplateWidth, x.TemplateHeight, x.TemplateUnit),
969 PrintInputJson = x.PrintInputJson, 978 PrintInputJson = x.PrintInputJson,
970 PrintedAt = x.PrintedAt ?? x.CreationTime, 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 LocationName = locationName 983 LocationName = locationName
973 }).ToList(); 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,7 +20,7 @@
20 }, 20 },
21 //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049) 21 //应用启动:SelfUrl 供 Program.cs UseUrls 绑定;用 0.0.0.0 避免写成固定局域网 IP 在本机无该网卡时启动失败(WinError 10049)
22 "App": { 22 "App": {
23 - "SelfUrl": "http://192.168.31.89:19001", 23 + "SelfUrl": "http://192.168.31.88:19001",
24 "CorsOrigins": "http://localhost:19001;http://localhost:18000;http://localhost:5666;http://localhost:5173;http://localhost:3000" 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,6 +60,7 @@ import {
60 } from "../ui/pagination"; 60 } from "../ui/pagination";
61 import { 61 import {
62 getLabelCategories, 62 getLabelCategories,
  63 + getLabelCategory,
63 createLabelCategory, 64 createLabelCategory,
64 updateLabelCategory, 65 updateLabelCategory,
65 deleteLabelCategory, 66 deleteLabelCategory,
@@ -68,9 +69,9 @@ import { getLocations } from &quot;../../services/locationService&quot;; @@ -68,9 +69,9 @@ import { getLocations } from &quot;../../services/locationService&quot;;
68 import { getGroups } from "../../services/groupService"; 69 import { getGroups } from "../../services/groupService";
69 import { getPartners } from "../../services/partnerService"; 70 import { getPartners } from "../../services/partnerService";
70 import { 71 import {
71 - buildLabelingScopeSaveBody, 72 + buildEntityScopeSaveFromForm,
72 hydrateCategoryScopeFromLocationIds, 73 hydrateCategoryScopeFromLocationIds,
73 - hydrateLabelingScopeFromLocationIds, 74 + hydrateEntityScopeFromDto,
74 } from "../../lib/categoryScopeForm"; 75 } from "../../lib/categoryScopeForm";
75 import { CategoryScopeFields } from "../shared/category-scope-fields"; 76 import { CategoryScopeFields } from "../shared/category-scope-fields";
76 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; 77 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth";
@@ -923,17 +924,17 @@ function CreateLabelCategoryDialog({ @@ -923,17 +924,17 @@ function CreateLabelCategoryDialog({
923 } 924 }
924 } 925 }
925 926
926 - const scopePayload = buildLabelingScopeSaveBody({ 927 + const scopePayload = buildEntityScopeSaveFromForm({
927 requireCompanySelection, 928 requireCompanySelection,
928 selectedPartnerId, 929 selectedPartnerId,
929 - fixedPartnerId,  
930 selectedPartnerIds, 930 selectedPartnerIds,
931 selectedRegionNames, 931 selectedRegionNames,
932 selectedRegionIds, 932 selectedRegionIds,
933 selectedLocationIds, 933 selectedLocationIds,
  934 + fixedPartnerId,
  935 + locations,
934 partners, 936 partners,
935 groups, 937 groups,
936 - locations,  
937 }); 938 });
938 if (!scopePayload.ok) { 939 if (!scopePayload.ok) {
939 toast.error("Validation failed", { description: scopePayload.message }); 940 toast.error("Validation failed", { description: scopePayload.message });
@@ -967,13 +968,7 @@ function CreateLabelCategoryDialog({ @@ -967,13 +968,7 @@ function CreateLabelCategoryDialog({
967 buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null, 968 buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null,
968 buttonAppearance: tokens, 969 buttonAppearance: tokens,
969 buttonStyleJson, 970 buttonStyleJson,
970 - availabilityType: scopePayload.body.availabilityType,  
971 - appliedPartnerType: scopePayload.body.appliedPartnerType,  
972 - partnerIds: scopePayload.body.partnerIds,  
973 - companyIds: scopePayload.body.companyIds,  
974 - regionIds: scopePayload.body.regionIds,  
975 - groupIds: scopePayload.body.groupIds,  
976 - locationIds: scopePayload.body.locationIds, 971 + ...scopePayload.body,
977 }); 972 });
978 toast.success("Label category created.", { 973 toast.success("Label category created.", {
979 description: "The label category has been created successfully.", 974 description: "The label category has been created successfully.",
@@ -1028,6 +1023,10 @@ function CreateLabelCategoryDialog({ @@ -1028,6 +1023,10 @@ function CreateLabelCategoryDialog({
1028 requireCompanySelection={requireCompanySelection} 1023 requireCompanySelection={requireCompanySelection}
1029 fixedPartnerId={fixedPartnerId} 1024 fixedPartnerId={fixedPartnerId}
1030 templateScopeMode={requireCompanySelection} 1025 templateScopeMode={requireCompanySelection}
  1026 + selectedPartnerIds={selectedPartnerIds}
  1027 + onPartnerIdsChange={setSelectedPartnerIds}
  1028 + selectedRegionIds={selectedRegionIds}
  1029 + onRegionIdsChange={setSelectedRegionIds}
1031 /> 1030 />
1032 1031
1033 <div className="space-y-2"> 1032 <div className="space-y-2">
@@ -1234,6 +1233,7 @@ function EditLabelCategoryDialog({ @@ -1234,6 +1233,7 @@ function EditLabelCategoryDialog({
1234 onUpdated: () => void; 1233 onUpdated: () => void;
1235 }) { 1234 }) {
1236 const [submitting, setSubmitting] = useState(false); 1235 const [submitting, setSubmitting] = useState(false);
  1236 + const [loadingDetail, setLoadingDetail] = useState(false);
1237 const [apSel, setApSel] = useState({ text: true, color: false, image: false }); 1237 const [apSel, setApSel] = useState({ text: true, color: false, image: false });
1238 const [displayTextForPhoto, setDisplayTextForPhoto] = useState(""); 1238 const [displayTextForPhoto, setDisplayTextForPhoto] = useState("");
1239 const [buttonBgColor, setButtonBgColor] = useState("#3B82F6"); 1239 const [buttonBgColor, setButtonBgColor] = useState("#3B82F6");
@@ -1257,117 +1257,145 @@ function EditLabelCategoryDialog({ @@ -1257,117 +1257,145 @@ function EditLabelCategoryDialog({
1257 }, [apSel.text, apSel.color]); 1257 }, [apSel.text, apSel.color]);
1258 1258
1259 useEffect(() => { 1259 useEffect(() => {
1260 - if (!open || !category) return; 1260 + if (!open || !category?.id) return;
1261 1261
1262 - const hydrateAvailability = () => {  
1263 - const rawAt = String(category.availabilityType ?? "ALL").trim().toUpperCase();  
1264 - if (rawAt !== "SPECIFIED") { 1262 + const ac = new AbortController();
  1263 + setLoadingDetail(true);
  1264 +
  1265 + (async () => {
  1266 + try {
  1267 + const detail = await getLabelCategory(category.id, ac.signal);
  1268 + if (ac.signal.aborted) return;
  1269 + const row = detail.id ? detail : category;
  1270 +
  1271 + const hydrateAvailability = () => {
  1272 + if (requireCompanySelection) {
  1273 + const scope = hydrateEntityScopeFromDto(row, locations, partners, groups);
  1274 + setSelectedPartnerIds(scope.partnerIds);
  1275 + setSelectedRegionIds(scope.regionIds);
  1276 + setSelectedLocationIds(scope.locationIds);
  1277 + setSelectedPartnerId("");
  1278 + setSelectedRegionNames([]);
  1279 + return;
  1280 + }
  1281 + const rawAt = String(row.availabilityType ?? "ALL").trim().toUpperCase();
  1282 + if (rawAt !== "SPECIFIED") {
  1283 + setSelectedPartnerId("");
  1284 + setSelectedRegionNames([]);
  1285 + setSelectedLocationIds([]);
  1286 + setSelectedPartnerIds([]);
  1287 + setSelectedRegionIds([]);
  1288 + return;
  1289 + }
  1290 + const lids = (row.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
  1291 + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
  1292 + setSelectedPartnerId(scope.partnerId);
  1293 + setSelectedRegionNames(scope.regionNames);
  1294 + setSelectedLocationIds(scope.locationIds);
  1295 + setSelectedPartnerIds([]);
  1296 + setSelectedRegionIds([]);
  1297 + };
  1298 +
  1299 + const styleParsed = parseCategoryButtonStyleV1(
  1300 + row.buttonStyleJson ?? (row as { ButtonStyleJson?: string | null }).ButtonStyleJson,
  1301 + );
  1302 + if (styleParsed) {
  1303 + const sel = appearanceSelectionFromTokens(styleParsed.appearances);
  1304 + setApSel(sel);
  1305 + setDisplayTextForPhoto(String(styleParsed.displayText ?? ""));
  1306 + setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6");
  1307 + setForm({
  1308 + categoryName: row.categoryName ?? "",
  1309 + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null,
  1310 + state: row.state ?? true,
  1311 + orderNum: row.orderNum ?? null,
  1312 + });
  1313 + hydrateAvailability();
  1314 + return;
  1315 + }
  1316 +
  1317 + const rawBa =
  1318 + row.buttonAppearance ??
  1319 + (row as { ButtonAppearance?: string | null }).ButtonAppearance ??
  1320 + undefined;
  1321 + const tokens = parseAppearanceTokens(rawBa);
  1322 + const valArrZip = parseCategoryPhotoUrlValueArray(row.categoryPhotoUrl);
  1323 + if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) {
  1324 + const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, {
  1325 + categoryName: row.categoryName,
  1326 + name: undefined,
  1327 + buttonTextColor: row.buttonTextColor ?? null,
  1328 + });
  1329 + const selZip = appearanceSelectionFromTokens(tokens);
  1330 + setApSel(selZip);
  1331 + setDisplayTextForPhoto(String(merged.displayText ?? ""));
  1332 + setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6");
  1333 + setForm({
  1334 + categoryName: row.categoryName ?? "",
  1335 + categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null,
  1336 + state: row.state ?? true,
  1337 + orderNum: row.orderNum ?? null,
  1338 + });
  1339 + hydrateAvailability();
  1340 + return;
  1341 + }
  1342 +
  1343 + let sel = appearanceSelectionFromTokens(tokens);
  1344 + if (tokens.length === 0) {
  1345 + const v = resolveCategoryButtonVisual({
  1346 + buttonAppearance: rawBa,
  1347 + displayText: row.displayText,
  1348 + buttonBgColor: row.buttonBgColor,
  1349 + buttonImageUrl: row.buttonImageUrl,
  1350 + categoryPhotoUrl: row.categoryPhotoUrl,
  1351 + categoryName: row.categoryName,
  1352 + });
  1353 + if (v.mode === "image") sel = { text: false, color: false, image: true };
  1354 + else if (v.mode === "colorText") sel = { text: true, color: true, image: false };
  1355 + else if (v.mode === "color") sel = { text: false, color: true, image: false };
  1356 + else if (v.mode === "text") sel = { text: true, color: false, image: false };
  1357 + else sel = { text: true, color: false, image: false };
  1358 + }
  1359 + setApSel(sel);
  1360 + const photo = String(row.categoryPhotoUrl ?? "").trim();
  1361 + const disp = String(row.displayText ?? "").trim();
  1362 + const bgField = normalizeHexColor(row.buttonBgColor);
  1363 + const hexFromPhoto = normalizeHexColor(photo);
  1364 + setDisplayTextForPhoto(
  1365 + String(
  1366 + disp ||
  1367 + (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") ||
  1368 + (!sel.image && sel.text && !sel.color ? (row.categoryName ?? "").trim() : ""),
  1369 + ),
  1370 + );
  1371 + setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6");
  1372 + setForm({
  1373 + categoryName: row.categoryName ?? "",
  1374 + categoryPhotoUrl: sel.image ? row.categoryPhotoUrl ?? null : null,
  1375 + state: row.state ?? true,
  1376 + orderNum: row.orderNum ?? null,
  1377 + });
  1378 + hydrateAvailability();
  1379 + } catch {
  1380 + if (ac.signal.aborted) return;
  1381 + setForm({
  1382 + categoryName: category.categoryName ?? "",
  1383 + categoryPhotoUrl: category.categoryPhotoUrl ?? null,
  1384 + state: category.state ?? true,
  1385 + orderNum: category.orderNum ?? null,
  1386 + });
1265 setSelectedPartnerId(""); 1387 setSelectedPartnerId("");
1266 setSelectedPartnerIds([]); 1388 setSelectedPartnerIds([]);
1267 setSelectedRegionNames([]); 1389 setSelectedRegionNames([]);
1268 setSelectedRegionIds([]); 1390 setSelectedRegionIds([]);
1269 setSelectedLocationIds([]); 1391 setSelectedLocationIds([]);
1270 - return;  
1271 - }  
1272 - const lids = (category.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);  
1273 - if (requireCompanySelection) {  
1274 - const scope = hydrateLabelingScopeFromLocationIds(lids, locations, partners, groups);  
1275 - setSelectedPartnerIds(scope.partnerIds);  
1276 - setSelectedRegionIds(scope.regionIds);  
1277 - setSelectedLocationIds(scope.locationIds);  
1278 - setSelectedPartnerId("");  
1279 - setSelectedRegionNames([]);  
1280 - return; 1392 + } finally {
  1393 + if (!ac.signal.aborted) setLoadingDetail(false);
1281 } 1394 }
1282 - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);  
1283 - setSelectedPartnerId(scope.partnerId);  
1284 - setSelectedRegionNames(scope.regionNames);  
1285 - setSelectedLocationIds(scope.locationIds);  
1286 - setSelectedPartnerIds([]);  
1287 - setSelectedRegionIds([]);  
1288 - };  
1289 -  
1290 - const styleParsed = parseCategoryButtonStyleV1(  
1291 - category.buttonStyleJson ?? (category as { ButtonStyleJson?: string | null }).ButtonStyleJson,  
1292 - );  
1293 - if (styleParsed) {  
1294 - const sel = appearanceSelectionFromTokens(styleParsed.appearances);  
1295 - setApSel(sel);  
1296 - setDisplayTextForPhoto(String(styleParsed.displayText ?? ""));  
1297 - setButtonBgColor(styleParsed.buttonBgColor || "#3B82F6");  
1298 - setForm({  
1299 - categoryName: category.categoryName ?? "",  
1300 - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null,  
1301 - state: category.state ?? true,  
1302 - orderNum: category.orderNum ?? null,  
1303 - });  
1304 - hydrateAvailability();  
1305 - return;  
1306 - }  
1307 -  
1308 - const rawBa =  
1309 - category.buttonAppearance ??  
1310 - (category as { ButtonAppearance?: string | null }).ButtonAppearance ??  
1311 - undefined;  
1312 - const tokens = parseAppearanceTokens(rawBa);  
1313 - const valArrZip = parseCategoryPhotoUrlValueArray(category.categoryPhotoUrl);  
1314 - if (valArrZip && tokens.length > 0 && tokens.length === valArrZip.length) {  
1315 - const merged = visualInputFromAppearanceAndValueArray(tokens, valArrZip, {  
1316 - categoryName: category.categoryName,  
1317 - name: undefined,  
1318 - buttonTextColor: category.buttonTextColor ?? null,  
1319 - });  
1320 - const selZip = appearanceSelectionFromTokens(tokens);  
1321 - setApSel(selZip);  
1322 - setDisplayTextForPhoto(String(merged.displayText ?? ""));  
1323 - setButtonBgColor(merged.buttonBgColor?.trim() ? merged.buttonBgColor : "#3B82F6");  
1324 - setForm({  
1325 - categoryName: category.categoryName ?? "",  
1326 - categoryPhotoUrl: selZip.image ? (merged.buttonImageUrl ?? "").trim() || null : null,  
1327 - state: category.state ?? true,  
1328 - orderNum: category.orderNum ?? null,  
1329 - });  
1330 - hydrateAvailability();  
1331 - return;  
1332 - } 1395 + })();
1333 1396
1334 - let sel = appearanceSelectionFromTokens(tokens);  
1335 - if (tokens.length === 0) {  
1336 - const v = resolveCategoryButtonVisual({  
1337 - buttonAppearance: rawBa,  
1338 - displayText: category.displayText,  
1339 - buttonBgColor: category.buttonBgColor,  
1340 - buttonImageUrl: category.buttonImageUrl,  
1341 - categoryPhotoUrl: category.categoryPhotoUrl,  
1342 - categoryName: category.categoryName,  
1343 - });  
1344 - if (v.mode === "image") sel = { text: false, color: false, image: true };  
1345 - else if (v.mode === "colorText") sel = { text: true, color: true, image: false };  
1346 - else if (v.mode === "color") sel = { text: false, color: true, image: false };  
1347 - else if (v.mode === "text") sel = { text: true, color: false, image: false };  
1348 - else sel = { text: true, color: false, image: false };  
1349 - }  
1350 - setApSel(sel);  
1351 - const photo = String(category.categoryPhotoUrl ?? "").trim();  
1352 - const disp = String(category.displayText ?? "").trim();  
1353 - const bgField = normalizeHexColor(category.buttonBgColor);  
1354 - const hexFromPhoto = normalizeHexColor(photo);  
1355 - setDisplayTextForPhoto(  
1356 - String(  
1357 - disp ||  
1358 - (!sel.image && sel.text && photo && !hexFromPhoto && !looksLikeImageUrl(photo) ? photo : "") ||  
1359 - (!sel.image && sel.text && !sel.color ? (category.categoryName ?? "").trim() : ""),  
1360 - ),  
1361 - );  
1362 - setButtonBgColor(bgField || (sel.color && hexFromPhoto ? hexFromPhoto : "") || "#3B82F6");  
1363 - setForm({  
1364 - categoryName: category.categoryName ?? "",  
1365 - categoryPhotoUrl: sel.image ? category.categoryPhotoUrl ?? null : null,  
1366 - state: category.state ?? true,  
1367 - orderNum: category.orderNum ?? null,  
1368 - });  
1369 - hydrateAvailability();  
1370 - }, [open, category, locations, partners, groups]); 1397 + return () => ac.abort();
  1398 + }, [open, category, locations, partners, groups, requireCompanySelection]);
1371 1399
1372 const submit = async () => { 1400 const submit = async () => {
1373 if (!category?.id) return; 1401 if (!category?.id) return;
@@ -1412,17 +1440,17 @@ function EditLabelCategoryDialog({ @@ -1412,17 +1440,17 @@ function EditLabelCategoryDialog({
1412 } 1440 }
1413 } 1441 }
1414 1442
1415 - const scopePayload = buildLabelingScopeSaveBody({ 1443 + const scopePayload = buildEntityScopeSaveFromForm({
1416 requireCompanySelection, 1444 requireCompanySelection,
1417 selectedPartnerId, 1445 selectedPartnerId,
1418 - fixedPartnerId,  
1419 selectedPartnerIds, 1446 selectedPartnerIds,
1420 selectedRegionNames, 1447 selectedRegionNames,
1421 selectedRegionIds, 1448 selectedRegionIds,
1422 selectedLocationIds, 1449 selectedLocationIds,
  1450 + fixedPartnerId,
  1451 + locations,
1423 partners, 1452 partners,
1424 groups, 1453 groups,
1425 - locations,  
1426 }); 1454 });
1427 if (!scopePayload.ok) { 1455 if (!scopePayload.ok) {
1428 toast.error("Validation failed", { description: scopePayload.message }); 1456 toast.error("Validation failed", { description: scopePayload.message });
@@ -1456,13 +1484,7 @@ function EditLabelCategoryDialog({ @@ -1456,13 +1484,7 @@ function EditLabelCategoryDialog({
1456 buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null, 1484 buttonImageUrl: apSel.image ? String(form.categoryPhotoUrl ?? "").trim() || null : null,
1457 buttonAppearance: tokens, 1485 buttonAppearance: tokens,
1458 buttonStyleJson, 1486 buttonStyleJson,
1459 - availabilityType: scopePayload.body.availabilityType,  
1460 - appliedPartnerType: scopePayload.body.appliedPartnerType,  
1461 - partnerIds: scopePayload.body.partnerIds,  
1462 - companyIds: scopePayload.body.companyIds,  
1463 - regionIds: scopePayload.body.regionIds,  
1464 - groupIds: scopePayload.body.groupIds,  
1465 - locationIds: scopePayload.body.locationIds, 1487 + ...scopePayload.body,
1466 }); 1488 });
1467 toast.success("Label category updated.", { 1489 toast.success("Label category updated.", {
1468 description: "The label category has been updated successfully.", 1490 description: "The label category has been updated successfully.",
@@ -1500,24 +1522,28 @@ function EditLabelCategoryDialog({ @@ -1500,24 +1522,28 @@ function EditLabelCategoryDialog({
1500 /> 1522 />
1501 </div> 1523 </div>
1502 1524
1503 - <CategoryScopeFields  
1504 - partners={partners}  
1505 - groups={groups}  
1506 - locations={locations}  
1507 - selectedPartnerId={selectedPartnerId}  
1508 - onPartnerChange={setSelectedPartnerId}  
1509 - selectedPartnerIds={selectedPartnerIds}  
1510 - onPartnerIdsChange={setSelectedPartnerIds}  
1511 - selectedRegionNames={selectedRegionNames}  
1512 - onRegionChange={setSelectedRegionNames}  
1513 - selectedRegionIds={selectedRegionIds}  
1514 - onRegionIdsChange={setSelectedRegionIds}  
1515 - selectedLocationIds={selectedLocationIds}  
1516 - onLocationChange={setSelectedLocationIds}  
1517 - requireCompanySelection={requireCompanySelection}  
1518 - fixedPartnerId={fixedPartnerId}  
1519 - templateScopeMode={requireCompanySelection}  
1520 - /> 1525 + {loadingDetail ? (
  1526 + <p className="text-sm text-gray-500">Loading company, region and location…</p>
  1527 + ) : (
  1528 + <CategoryScopeFields
  1529 + partners={partners}
  1530 + groups={groups}
  1531 + locations={locations}
  1532 + selectedPartnerId={selectedPartnerId}
  1533 + onPartnerChange={setSelectedPartnerId}
  1534 + selectedRegionNames={selectedRegionNames}
  1535 + onRegionChange={setSelectedRegionNames}
  1536 + selectedLocationIds={selectedLocationIds}
  1537 + onLocationChange={setSelectedLocationIds}
  1538 + requireCompanySelection={requireCompanySelection}
  1539 + fixedPartnerId={fixedPartnerId}
  1540 + templateScopeMode={requireCompanySelection}
  1541 + selectedPartnerIds={selectedPartnerIds}
  1542 + onPartnerIdsChange={setSelectedPartnerIds}
  1543 + selectedRegionIds={selectedRegionIds}
  1544 + onRegionIdsChange={setSelectedRegionIds}
  1545 + />
  1546 + )}
1521 1547
1522 <div className="space-y-2"> 1548 <div className="space-y-2">
1523 <Label>Button Appearance</Label> 1549 <Label>Button Appearance</Label>
@@ -1689,7 +1715,7 @@ function EditLabelCategoryDialog({ @@ -1689,7 +1715,7 @@ function EditLabelCategoryDialog({
1689 </Button> 1715 </Button>
1690 <Button 1716 <Button
1691 type="button" 1717 type="button"
1692 - disabled={submitting} 1718 + disabled={submitting || loadingDetail}
1693 onClick={submit} 1719 onClick={submit}
1694 className="bg-blue-600 hover:bg-blue-700 text-white" 1720 className="bg-blue-600 hover:bg-blue-700 text-white"
1695 > 1721 >
美国版/Food Labeling Management Platform/src/components/labels/LabelTypesView.tsx
@@ -51,9 +51,9 @@ import { getLocations } from &quot;../../services/locationService&quot;; @@ -51,9 +51,9 @@ import { getLocations } from &quot;../../services/locationService&quot;;
51 import { getGroups } from "../../services/groupService"; 51 import { getGroups } from "../../services/groupService";
52 import { getPartners } from "../../services/partnerService"; 52 import { getPartners } from "../../services/partnerService";
53 import { 53 import {
54 - buildLabelingScopeSaveBody, 54 + buildEntityScopeSaveFromForm,
55 hydrateCategoryScopeFromLocationIds, 55 hydrateCategoryScopeFromLocationIds,
56 - hydrateLabelingScopeFromLocationIds, 56 + hydrateEntityScopeFromDto,
57 } from "../../lib/categoryScopeForm"; 57 } from "../../lib/categoryScopeForm";
58 import { CategoryScopeFields } from "../shared/category-scope-fields"; 58 import { CategoryScopeFields } from "../shared/category-scope-fields";
59 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; 59 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth";
@@ -832,7 +832,9 @@ function CreateLabelTypeDialog({ @@ -832,7 +832,9 @@ function CreateLabelTypeDialog({
832 832
833 const resetForm = () => { 833 const resetForm = () => {
834 setSelectedPartnerId(""); 834 setSelectedPartnerId("");
  835 + setSelectedPartnerIds([]);
835 setSelectedRegionNames([]); 836 setSelectedRegionNames([]);
  837 + setSelectedRegionIds([]);
836 setSelectedLocationIds([]); 838 setSelectedLocationIds([]);
837 setForm({ 839 setForm({
838 typeName: "", 840 typeName: "",
@@ -859,17 +861,17 @@ function CreateLabelTypeDialog({ @@ -859,17 +861,17 @@ function CreateLabelTypeDialog({
859 return; 861 return;
860 } 862 }
861 863
862 - const scopePayload = buildLabelingScopeSaveBody({ 864 + const scopePayload = buildEntityScopeSaveFromForm({
863 requireCompanySelection, 865 requireCompanySelection,
864 selectedPartnerId, 866 selectedPartnerId,
865 - fixedPartnerId,  
866 selectedPartnerIds, 867 selectedPartnerIds,
867 selectedRegionNames, 868 selectedRegionNames,
868 selectedRegionIds, 869 selectedRegionIds,
869 selectedLocationIds, 870 selectedLocationIds,
  871 + fixedPartnerId,
  872 + locations,
870 partners, 873 partners,
871 groups, 874 groups,
872 - locations,  
873 }); 875 });
874 if (!scopePayload.ok) { 876 if (!scopePayload.ok) {
875 toast.error("Validation failed", { description: scopePayload.message }); 877 toast.error("Validation failed", { description: scopePayload.message });
@@ -881,13 +883,7 @@ function CreateLabelTypeDialog({ @@ -881,13 +883,7 @@ function CreateLabelTypeDialog({
881 await createLabelType({ 883 await createLabelType({
882 ...form, 884 ...form,
883 typeCode: typeCodeFromName(form.typeName), 885 typeCode: typeCodeFromName(form.typeName),
884 - availabilityType: scopePayload.body.availabilityType,  
885 - appliedPartnerType: scopePayload.body.appliedPartnerType,  
886 - partnerIds: scopePayload.body.partnerIds,  
887 - companyIds: scopePayload.body.companyIds,  
888 - groupIds: scopePayload.body.groupIds,  
889 - regionIds: scopePayload.body.regionIds,  
890 - locationIds: scopePayload.body.locationIds, 886 + ...scopePayload.body,
891 }); 887 });
892 toast.success("Label type created.", { 888 toast.success("Label type created.", {
893 description: "The label type has been created successfully.", 889 description: "The label type has been created successfully.",
@@ -956,6 +952,10 @@ function CreateLabelTypeDialog({ @@ -956,6 +952,10 @@ function CreateLabelTypeDialog({
956 requireCompanySelection={requireCompanySelection} 952 requireCompanySelection={requireCompanySelection}
957 fixedPartnerId={fixedPartnerId} 953 fixedPartnerId={fixedPartnerId}
958 templateScopeMode={requireCompanySelection} 954 templateScopeMode={requireCompanySelection}
  955 + selectedPartnerIds={selectedPartnerIds}
  956 + onPartnerIdsChange={setSelectedPartnerIds}
  957 + selectedRegionIds={selectedRegionIds}
  958 + onRegionIdsChange={setSelectedRegionIds}
959 /> 959 />
960 </div> 960 </div>
961 961
@@ -1023,23 +1023,24 @@ function EditLabelTypeDialog({ @@ -1023,23 +1023,24 @@ function EditLabelTypeDialog({
1023 orderNum: detail.orderNum ?? type.orderNum ?? null, 1023 orderNum: detail.orderNum ?? type.orderNum ?? null,
1024 }); 1024 });
1025 1025
  1026 + if (requireCompanySelection) {
  1027 + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups);
  1028 + setSelectedPartnerIds(scope.partnerIds);
  1029 + setSelectedRegionIds(scope.regionIds);
  1030 + setSelectedLocationIds(scope.locationIds);
  1031 + setSelectedPartnerId("");
  1032 + setSelectedRegionNames([]);
  1033 + return;
  1034 + }
  1035 +
1026 const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); 1036 const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
1027 if (lids.length > 0) { 1037 if (lids.length > 0) {
1028 - if (requireCompanySelection) {  
1029 - const scope = hydrateLabelingScopeFromLocationIds(lids, locations, partners, groups);  
1030 - setSelectedPartnerIds(scope.partnerIds);  
1031 - setSelectedRegionIds(scope.regionIds);  
1032 - setSelectedLocationIds(scope.locationIds);  
1033 - setSelectedPartnerId("");  
1034 - setSelectedRegionNames([]);  
1035 - } else {  
1036 - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);  
1037 - setSelectedPartnerId(scope.partnerId);  
1038 - setSelectedRegionNames(scope.regionNames);  
1039 - setSelectedLocationIds(scope.locationIds);  
1040 - setSelectedPartnerIds([]);  
1041 - setSelectedRegionIds([]);  
1042 - } 1038 + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
  1039 + setSelectedPartnerId(scope.partnerId);
  1040 + setSelectedRegionNames(scope.regionNames);
  1041 + setSelectedLocationIds(scope.locationIds);
  1042 + setSelectedPartnerIds([]);
  1043 + setSelectedRegionIds([]);
1043 return; 1044 return;
1044 } 1045 }
1045 1046
@@ -1048,26 +1049,15 @@ function EditLabelTypeDialog({ @@ -1048,26 +1049,15 @@ function EditLabelTypeDialog({
1048 .filter(Boolean); 1049 .filter(Boolean);
1049 if (gids.length > 0) { 1050 if (gids.length > 0) {
1050 const matchedGroups = groups.filter((g) => gids.includes(g.id)); 1051 const matchedGroups = groups.filter((g) => gids.includes(g.id));
1051 - if (requireCompanySelection) {  
1052 - const partnerIds = [  
1053 - ...new Set(matchedGroups.map((g) => String(g.partnerId ?? "").trim()).filter(Boolean)),  
1054 - ];  
1055 - setSelectedPartnerIds(partnerIds);  
1056 - setSelectedRegionIds(gids);  
1057 - setSelectedLocationIds([]);  
1058 - setSelectedPartnerId("");  
1059 - setSelectedRegionNames([]);  
1060 - } else {  
1061 - const pid = matchedGroups[0]?.partnerId ?? "";  
1062 - const names = [  
1063 - ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)),  
1064 - ];  
1065 - setSelectedPartnerId(pid);  
1066 - setSelectedRegionNames(names);  
1067 - setSelectedLocationIds([]);  
1068 - setSelectedPartnerIds([]);  
1069 - setSelectedRegionIds([]);  
1070 - } 1052 + const pid = matchedGroups[0]?.partnerId ?? "";
  1053 + const names = [
  1054 + ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)),
  1055 + ];
  1056 + setSelectedPartnerId(pid);
  1057 + setSelectedRegionNames(names);
  1058 + setSelectedLocationIds([]);
  1059 + setSelectedPartnerIds([]);
  1060 + setSelectedRegionIds([]);
1071 return; 1061 return;
1072 } 1062 }
1073 1063
@@ -1096,6 +1086,8 @@ function EditLabelTypeDialog({ @@ -1096,6 +1086,8 @@ function EditLabelTypeDialog({
1096 setSelectedRegionIds([]); 1086 setSelectedRegionIds([]);
1097 setSelectedLocationIds([]); 1087 setSelectedLocationIds([]);
1098 } 1088 }
  1089 + setSelectedPartnerIds([]);
  1090 + setSelectedRegionIds([]);
1099 } catch { 1091 } catch {
1100 if (ac.signal.aborted) return; 1092 if (ac.signal.aborted) return;
1101 setForm({ 1093 setForm({
@@ -1104,7 +1096,9 @@ function EditLabelTypeDialog({ @@ -1104,7 +1096,9 @@ function EditLabelTypeDialog({
1104 orderNum: type.orderNum ?? null, 1096 orderNum: type.orderNum ?? null,
1105 }); 1097 });
1106 setSelectedPartnerId(""); 1098 setSelectedPartnerId("");
  1099 + setSelectedPartnerIds([]);
1107 setSelectedRegionNames([]); 1100 setSelectedRegionNames([]);
  1101 + setSelectedRegionIds([]);
1108 setSelectedLocationIds([]); 1102 setSelectedLocationIds([]);
1109 } finally { 1103 } finally {
1110 if (!ac.signal.aborted) setLoadingDetail(false); 1104 if (!ac.signal.aborted) setLoadingDetail(false);
@@ -1127,17 +1121,17 @@ function EditLabelTypeDialog({ @@ -1127,17 +1121,17 @@ function EditLabelTypeDialog({
1127 return; 1121 return;
1128 } 1122 }
1129 1123
1130 - const scopePayload = buildLabelingScopeSaveBody({ 1124 + const scopePayload = buildEntityScopeSaveFromForm({
1131 requireCompanySelection, 1125 requireCompanySelection,
1132 selectedPartnerId, 1126 selectedPartnerId,
1133 - fixedPartnerId,  
1134 selectedPartnerIds, 1127 selectedPartnerIds,
1135 selectedRegionNames, 1128 selectedRegionNames,
1136 selectedRegionIds, 1129 selectedRegionIds,
1137 selectedLocationIds, 1130 selectedLocationIds,
  1131 + fixedPartnerId,
  1132 + locations,
1138 partners, 1133 partners,
1139 groups, 1134 groups,
1140 - locations,  
1141 }); 1135 });
1142 if (!scopePayload.ok) { 1136 if (!scopePayload.ok) {
1143 toast.error("Validation failed", { description: scopePayload.message }); 1137 toast.error("Validation failed", { description: scopePayload.message });
@@ -1149,13 +1143,7 @@ function EditLabelTypeDialog({ @@ -1149,13 +1143,7 @@ function EditLabelTypeDialog({
1149 await updateLabelType(type.id, { 1143 await updateLabelType(type.id, {
1150 ...form, 1144 ...form,
1151 typeCode: type.typeCode ?? "", 1145 typeCode: type.typeCode ?? "",
1152 - availabilityType: scopePayload.body.availabilityType,  
1153 - appliedPartnerType: scopePayload.body.appliedPartnerType,  
1154 - partnerIds: scopePayload.body.partnerIds,  
1155 - companyIds: scopePayload.body.companyIds,  
1156 - groupIds: scopePayload.body.groupIds,  
1157 - regionIds: scopePayload.body.regionIds,  
1158 - locationIds: scopePayload.body.locationIds, 1146 + ...scopePayload.body,
1159 }); 1147 });
1160 toast.success("Label type updated.", { 1148 toast.success("Label type updated.", {
1161 description: "The label type has been updated successfully.", 1149 description: "The label type has been updated successfully.",
@@ -1227,6 +1215,10 @@ function EditLabelTypeDialog({ @@ -1227,6 +1215,10 @@ function EditLabelTypeDialog({
1227 requireCompanySelection={requireCompanySelection} 1215 requireCompanySelection={requireCompanySelection}
1228 fixedPartnerId={fixedPartnerId} 1216 fixedPartnerId={fixedPartnerId}
1229 templateScopeMode={requireCompanySelection} 1217 templateScopeMode={requireCompanySelection}
  1218 + selectedPartnerIds={selectedPartnerIds}
  1219 + onPartnerIdsChange={setSelectedPartnerIds}
  1220 + selectedRegionIds={selectedRegionIds}
  1221 + onRegionIdsChange={setSelectedRegionIds}
1230 /> 1222 />
1231 )} 1223 )}
1232 </div> 1224 </div>
美国版/Food Labeling Management Platform/src/components/labels/MultipleOptionsView.tsx
@@ -51,9 +51,9 @@ import { getLocations } from &quot;../../services/locationService&quot;; @@ -51,9 +51,9 @@ import { getLocations } from &quot;../../services/locationService&quot;;
51 import { getGroups } from "../../services/groupService"; 51 import { getGroups } from "../../services/groupService";
52 import { getPartners } from "../../services/partnerService"; 52 import { getPartners } from "../../services/partnerService";
53 import { 53 import {
54 - buildLabelingScopeSaveBody, 54 + buildEntityScopeSaveFromForm,
55 hydrateCategoryScopeFromLocationIds, 55 hydrateCategoryScopeFromLocationIds,
56 - hydrateLabelingScopeFromLocationIds, 56 + hydrateEntityScopeFromDto,
57 } from "../../lib/categoryScopeForm"; 57 } from "../../lib/categoryScopeForm";
58 import { CategoryScopeFields } from "../shared/category-scope-fields"; 58 import { CategoryScopeFields } from "../shared/category-scope-fields";
59 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth"; 59 import { useCategoryScopeAuth } from "../../hooks/useCategoryScopeAuth";
@@ -818,7 +818,9 @@ function CreateMultipleOptionDialog({ @@ -818,7 +818,9 @@ function CreateMultipleOptionDialog({
818 818
819 const resetForm = () => { 819 const resetForm = () => {
820 setSelectedPartnerId(""); 820 setSelectedPartnerId("");
  821 + setSelectedPartnerIds([]);
821 setSelectedRegionNames([]); 822 setSelectedRegionNames([]);
  823 + setSelectedRegionIds([]);
822 setSelectedLocationIds([]); 824 setSelectedLocationIds([]);
823 setForm({ 825 setForm({
824 optionName: "", 826 optionName: "",
@@ -876,17 +878,17 @@ function CreateMultipleOptionDialog({ @@ -876,17 +878,17 @@ function CreateMultipleOptionDialog({
876 return; 878 return;
877 } 879 }
878 880
879 - const scopePayload = buildLabelingScopeSaveBody({ 881 + const scopePayload = buildEntityScopeSaveFromForm({
880 requireCompanySelection, 882 requireCompanySelection,
881 selectedPartnerId, 883 selectedPartnerId,
882 - fixedPartnerId,  
883 selectedPartnerIds, 884 selectedPartnerIds,
884 selectedRegionNames, 885 selectedRegionNames,
885 selectedRegionIds, 886 selectedRegionIds,
886 selectedLocationIds, 887 selectedLocationIds,
  888 + fixedPartnerId,
  889 + locations,
887 partners, 890 partners,
888 groups, 891 groups,
889 - locations,  
890 }); 892 });
891 if (!scopePayload.ok) { 893 if (!scopePayload.ok) {
892 toast.error("Validation failed", { description: scopePayload.message }); 894 toast.error("Validation failed", { description: scopePayload.message });
@@ -898,13 +900,7 @@ function CreateMultipleOptionDialog({ @@ -898,13 +900,7 @@ function CreateMultipleOptionDialog({
898 await createLabelMultipleOption({ 900 await createLabelMultipleOption({
899 ...form, 901 ...form,
900 optionCode: optionCodeFromName(form.optionName), 902 optionCode: optionCodeFromName(form.optionName),
901 - availabilityType: scopePayload.body.availabilityType,  
902 - appliedPartnerType: scopePayload.body.appliedPartnerType,  
903 - partnerIds: scopePayload.body.partnerIds,  
904 - companyIds: scopePayload.body.companyIds,  
905 - groupIds: scopePayload.body.groupIds,  
906 - regionIds: scopePayload.body.regionIds,  
907 - locationIds: scopePayload.body.locationIds, 903 + ...scopePayload.body,
908 }); 904 });
909 toast.success("Multiple option created.", { 905 toast.success("Multiple option created.", {
910 description: "The multiple option has been created successfully.", 906 description: "The multiple option has been created successfully.",
@@ -1009,6 +1005,10 @@ function CreateMultipleOptionDialog({ @@ -1009,6 +1005,10 @@ function CreateMultipleOptionDialog({
1009 requireCompanySelection={requireCompanySelection} 1005 requireCompanySelection={requireCompanySelection}
1010 fixedPartnerId={fixedPartnerId} 1006 fixedPartnerId={fixedPartnerId}
1011 templateScopeMode={requireCompanySelection} 1007 templateScopeMode={requireCompanySelection}
  1008 + selectedPartnerIds={selectedPartnerIds}
  1009 + onPartnerIdsChange={setSelectedPartnerIds}
  1010 + selectedRegionIds={selectedRegionIds}
  1011 + onRegionIdsChange={setSelectedRegionIds}
1012 /> 1012 />
1013 </div> 1013 </div>
1014 1014
@@ -1080,23 +1080,24 @@ function EditMultipleOptionDialog({ @@ -1080,23 +1080,24 @@ function EditMultipleOptionDialog({
1080 }); 1080 });
1081 setNewValue(""); 1081 setNewValue("");
1082 1082
  1083 + if (requireCompanySelection) {
  1084 + const scope = hydrateEntityScopeFromDto(detail, locations, partners, groups);
  1085 + setSelectedPartnerIds(scope.partnerIds);
  1086 + setSelectedRegionIds(scope.regionIds);
  1087 + setSelectedLocationIds(scope.locationIds);
  1088 + setSelectedPartnerId("");
  1089 + setSelectedRegionNames([]);
  1090 + return;
  1091 + }
  1092 +
1083 const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean); 1093 const lids = (detail.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean);
1084 if (lids.length > 0) { 1094 if (lids.length > 0) {
1085 - if (requireCompanySelection) {  
1086 - const scope = hydrateLabelingScopeFromLocationIds(lids, locations, partners, groups);  
1087 - setSelectedPartnerIds(scope.partnerIds);  
1088 - setSelectedRegionIds(scope.regionIds);  
1089 - setSelectedLocationIds(scope.locationIds);  
1090 - setSelectedPartnerId("");  
1091 - setSelectedRegionNames([]);  
1092 - } else {  
1093 - const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);  
1094 - setSelectedPartnerId(scope.partnerId);  
1095 - setSelectedRegionNames(scope.regionNames);  
1096 - setSelectedLocationIds(scope.locationIds);  
1097 - setSelectedPartnerIds([]);  
1098 - setSelectedRegionIds([]);  
1099 - } 1095 + const scope = hydrateCategoryScopeFromLocationIds(lids, locations, partners, groups);
  1096 + setSelectedPartnerId(scope.partnerId);
  1097 + setSelectedRegionNames(scope.regionNames);
  1098 + setSelectedLocationIds(scope.locationIds);
  1099 + setSelectedPartnerIds([]);
  1100 + setSelectedRegionIds([]);
1100 return; 1101 return;
1101 } 1102 }
1102 1103
@@ -1105,26 +1106,15 @@ function EditMultipleOptionDialog({ @@ -1105,26 +1106,15 @@ function EditMultipleOptionDialog({
1105 .filter(Boolean); 1106 .filter(Boolean);
1106 if (gids.length > 0) { 1107 if (gids.length > 0) {
1107 const matchedGroups = groups.filter((g) => gids.includes(g.id)); 1108 const matchedGroups = groups.filter((g) => gids.includes(g.id));
1108 - if (requireCompanySelection) {  
1109 - const partnerIds = [  
1110 - ...new Set(matchedGroups.map((g) => String(g.partnerId ?? "").trim()).filter(Boolean)),  
1111 - ];  
1112 - setSelectedPartnerIds(partnerIds);  
1113 - setSelectedRegionIds(gids);  
1114 - setSelectedLocationIds([]);  
1115 - setSelectedPartnerId("");  
1116 - setSelectedRegionNames([]);  
1117 - } else {  
1118 - const pid = matchedGroups[0]?.partnerId ?? "";  
1119 - const names = [  
1120 - ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)),  
1121 - ];  
1122 - setSelectedPartnerId(pid);  
1123 - setSelectedRegionNames(names);  
1124 - setSelectedLocationIds([]);  
1125 - setSelectedPartnerIds([]);  
1126 - setSelectedRegionIds([]);  
1127 - } 1109 + const pid = matchedGroups[0]?.partnerId ?? "";
  1110 + const names = [
  1111 + ...new Set(matchedGroups.map((g) => (g.groupName ?? "").trim()).filter(Boolean)),
  1112 + ];
  1113 + setSelectedPartnerId(pid);
  1114 + setSelectedRegionNames(names);
  1115 + setSelectedLocationIds([]);
  1116 + setSelectedPartnerIds([]);
  1117 + setSelectedRegionIds([]);
1128 return; 1118 return;
1129 } 1119 }
1130 1120
@@ -1153,6 +1143,8 @@ function EditMultipleOptionDialog({ @@ -1153,6 +1143,8 @@ function EditMultipleOptionDialog({
1153 setSelectedRegionIds([]); 1143 setSelectedRegionIds([]);
1154 setSelectedLocationIds([]); 1144 setSelectedLocationIds([]);
1155 } 1145 }
  1146 + setSelectedPartnerIds([]);
  1147 + setSelectedRegionIds([]);
1156 } catch { 1148 } catch {
1157 if (ac.signal.aborted) return; 1149 if (ac.signal.aborted) return;
1158 setForm({ 1150 setForm({
@@ -1162,7 +1154,9 @@ function EditMultipleOptionDialog({ @@ -1162,7 +1154,9 @@ function EditMultipleOptionDialog({
1162 orderNum: option.orderNum ?? null, 1154 orderNum: option.orderNum ?? null,
1163 }); 1155 });
1164 setSelectedPartnerId(""); 1156 setSelectedPartnerId("");
  1157 + setSelectedPartnerIds([]);
1165 setSelectedRegionNames([]); 1158 setSelectedRegionNames([]);
  1159 + setSelectedRegionIds([]);
1166 setSelectedLocationIds([]); 1160 setSelectedLocationIds([]);
1167 setNewValue(""); 1161 setNewValue("");
1168 } finally { 1162 } finally {
@@ -1215,17 +1209,17 @@ function EditMultipleOptionDialog({ @@ -1215,17 +1209,17 @@ function EditMultipleOptionDialog({
1215 return; 1209 return;
1216 } 1210 }
1217 1211
1218 - const scopePayload = buildLabelingScopeSaveBody({ 1212 + const scopePayload = buildEntityScopeSaveFromForm({
1219 requireCompanySelection, 1213 requireCompanySelection,
1220 selectedPartnerId, 1214 selectedPartnerId,
1221 - fixedPartnerId,  
1222 selectedPartnerIds, 1215 selectedPartnerIds,
1223 selectedRegionNames, 1216 selectedRegionNames,
1224 selectedRegionIds, 1217 selectedRegionIds,
1225 selectedLocationIds, 1218 selectedLocationIds,
  1219 + fixedPartnerId,
  1220 + locations,
1226 partners, 1221 partners,
1227 groups, 1222 groups,
1228 - locations,  
1229 }); 1223 });
1230 if (!scopePayload.ok) { 1224 if (!scopePayload.ok) {
1231 toast.error("Validation failed", { description: scopePayload.message }); 1225 toast.error("Validation failed", { description: scopePayload.message });
@@ -1237,13 +1231,7 @@ function EditMultipleOptionDialog({ @@ -1237,13 +1231,7 @@ function EditMultipleOptionDialog({
1237 await updateLabelMultipleOption(option.id, { 1231 await updateLabelMultipleOption(option.id, {
1238 ...form, 1232 ...form,
1239 optionCode: option.optionCode ?? "", 1233 optionCode: option.optionCode ?? "",
1240 - availabilityType: scopePayload.body.availabilityType,  
1241 - appliedPartnerType: scopePayload.body.appliedPartnerType,  
1242 - partnerIds: scopePayload.body.partnerIds,  
1243 - companyIds: scopePayload.body.companyIds,  
1244 - groupIds: scopePayload.body.groupIds,  
1245 - regionIds: scopePayload.body.regionIds,  
1246 - locationIds: scopePayload.body.locationIds, 1234 + ...scopePayload.body,
1247 }); 1235 });
1248 toast.success("Multiple option updated.", { 1236 toast.success("Multiple option updated.", {
1249 description: "The multiple option has been updated successfully.", 1237 description: "The multiple option has been updated successfully.",
@@ -1351,6 +1339,10 @@ function EditMultipleOptionDialog({ @@ -1351,6 +1339,10 @@ function EditMultipleOptionDialog({
1351 requireCompanySelection={requireCompanySelection} 1339 requireCompanySelection={requireCompanySelection}
1352 fixedPartnerId={fixedPartnerId} 1340 fixedPartnerId={fixedPartnerId}
1353 templateScopeMode={requireCompanySelection} 1341 templateScopeMode={requireCompanySelection}
  1342 + selectedPartnerIds={selectedPartnerIds}
  1343 + onPartnerIdsChange={setSelectedPartnerIds}
  1344 + selectedRegionIds={selectedRegionIds}
  1345 + onRegionIdsChange={setSelectedRegionIds}
1354 /> 1346 />
1355 )} 1347 )}
1356 </div> 1348 </div>
美国版/Food Labeling Management Platform/src/components/shared/category-scope-fields.tsx
@@ -411,7 +411,7 @@ export function CategoryScopeFields({ @@ -411,7 +411,7 @@ export function CategoryScopeFields({
411 )} 411 )}
412 <p className="text-xs text-gray-500"> 412 <p className="text-xs text-gray-500">
413 {templateScopeMode 413 {templateScopeMode
414 - ? "Required. Select one or more companies, or Select All." 414 + ? "Required. Leave empty or select all for every company; narrows region and location lists."
415 : "Required. Select company before region and location."} 415 : "Required. Select company before region and location."}
416 </p> 416 </p>
417 </div> 417 </div>
@@ -427,10 +427,18 @@ export function CategoryScopeFields({ @@ -427,10 +427,18 @@ export function CategoryScopeFields({
427 placeholder={regionPlaceholder} 427 placeholder={regionPlaceholder}
428 searchPlaceholder="Search regions…" 428 searchPlaceholder="Search regions…"
429 selectAllRowLabel="Select All" 429 selectAllRowLabel="Select All"
430 - disabled={!companyReady} 430 + disabled={!templateScopeMode && !companyReady}
431 /> 431 />
432 <p className="text-xs text-gray-500"> 432 <p className="text-xs text-gray-500">
433 - {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."}
434 </p> 442 </p>
435 </div> 443 </div>
436 <div className="space-y-2"> 444 <div className="space-y-2">
@@ -440,22 +448,34 @@ export function CategoryScopeFields({ @@ -440,22 +448,34 @@ export function CategoryScopeFields({
440 onValuesChange={onLocationChange} 448 onValuesChange={onLocationChange}
441 options={locationOptionsForPicker} 449 options={locationOptionsForPicker}
442 placeholder={ 450 placeholder={
443 - companyReady  
444 - ? "Select location(s)…"  
445 - : requireCompanySelection  
446 - ? "Select company first"  
447 - : "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…"
448 } 462 }
449 searchPlaceholder="Search locations…" 463 searchPlaceholder="Search locations…"
450 selectAllRowLabel="ALL" 464 selectAllRowLabel="ALL"
451 - disabled={!companyReady} 465 + disabled={!templateScopeMode && !companyReady}
452 /> 466 />
453 <p className="text-xs text-gray-500"> 467 <p className="text-xs text-gray-500">
454 - {companyReady  
455 - ? "Required. ALL selects every location in the filtered list (within selected regions)."  
456 - : requireCompanySelection  
457 - ? "Select a company first."  
458 - : "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."}
459 </p> 479 </p>
460 </div> 480 </div>
461 </div> 481 </div>
美国版/Food Labeling Management Platform/src/lib/categoryScopeForm.ts
1 import type { GroupListItem } from "../types/group"; 1 import type { GroupListItem } from "../types/group";
2 import type { LocationDto } from "../types/location"; 2 import type { LocationDto } from "../types/location";
3 import type { PartnerListItem } from "../types/partner"; 3 import type { PartnerListItem } from "../types/partner";
4 -import {  
5 - buildLabelTemplateScopePayload,  
6 - type AppliedScopeType,  
7 - parseAppliedScopeType,  
8 -} from "../types/labelTemplate"; 4 +import type { AppliedScopeType } from "../types/labelTemplate";
  5 +import { parseAppliedScopeType } from "../types/labelTemplate";
9 6
10 /** 表单提交用的公司 Id(非管理员取账号绑定公司) */ 7 /** 表单提交用的公司 Id(非管理员取账号绑定公司) */
11 export function effectiveScopePartnerId( 8 export function effectiveScopePartnerId(
@@ -616,3 +613,223 @@ export function buildProductScopeSavePayload( @@ -616,3 +613,223 @@ export function buildProductScopeSavePayload(
616 } 613 }
617 return { locationIds: locIds }; 614 return { locationIds: locIds };
618 } 615 }
  616 +
  617 +function resolveScopeDimensionForSave(
  618 + selected: string[],
  619 + available: string[],
  620 +): { type: AppliedScopeType; ids: string[] } {
  621 + const avail = dedupeIds(available);
  622 + const sel = dedupeIds(selected).filter((id) => !avail.length || avail.includes(id));
  623 + if (!avail.length || !sel.length) {
  624 + return { type: "ALL", ids: [] };
  625 + }
  626 + if (sel.length >= avail.length && avail.every((id) => sel.includes(id))) {
  627 + return { type: "ALL", ids: [] };
  628 + }
  629 + return { type: "SPECIFIED", ids: sel };
  630 +}
  631 +
  632 +/** 模板/实体表单:按当前选中 Id 计算可选 Company / Region / Location 全集 */
  633 +export function computeTemplateScopeAvailableIds(
  634 + partners: PartnerListItem[],
  635 + groups: GroupListItem[],
  636 + locations: LocationDto[],
  637 + selectedPartnerIds: string[],
  638 + selectedRegionIds: string[],
  639 +): {
  640 + availablePartnerIds: string[];
  641 + availableRegionIds: string[];
  642 + availableLocationIds: string[];
  643 +} {
  644 + const availablePartnerIds = partners.map((p) => String(p.id ?? "").trim()).filter(Boolean);
  645 + const effectivePartnerIds =
  646 + selectedPartnerIds.length > 0 ? selectedPartnerIds : availablePartnerIds;
  647 + const availableRegionIds = regionOptionsForPartners(groups, effectivePartnerIds).map(
  648 + (o) => o.value,
  649 + );
  650 + const availableLocationIds = locationsScopedForTemplateScope(
  651 + locations,
  652 + partners,
  653 + groups,
  654 + effectivePartnerIds,
  655 + selectedRegionIds,
  656 + ).map((l) => l.id);
  657 + return { availablePartnerIds, availableRegionIds, availableLocationIds };
  658 +}
  659 +
  660 +export type EntityScopePayloadBody = {
  661 + appliedPartnerType: AppliedScopeType;
  662 + partnerIds: string[];
  663 + companyIds: string[];
  664 + regionIds: string[];
  665 + groupIds: string[];
  666 + locationIds: string[];
  667 + availabilityType: "ALL" | "SPECIFIED";
  668 +};
  669 +
  670 +/**
  671 + * 标签类型 / 分类 / 多选项保存:Company + Region + Location 三维适用范围。
  672 + * 空选或全选当前可选项 → 该维度 ALL;部分选中 → SPECIFIED + Id 数组。
  673 + */
  674 +export function buildEntityPartnerScopePayload(input: {
  675 + selectedPartnerIds: string[];
  676 + selectedRegionIds: string[];
  677 + selectedLocationIds: string[];
  678 + availablePartnerIds: string[];
  679 + availableRegionIds: string[];
  680 + availableLocationIds: string[];
  681 +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } {
  682 + const partnerDim = resolveScopeDimensionForSave(
  683 + input.selectedPartnerIds,
  684 + input.availablePartnerIds,
  685 + );
  686 + const regionDim = resolveScopeDimensionForSave(
  687 + input.selectedRegionIds,
  688 + input.availableRegionIds,
  689 + );
  690 + const locationDim = resolveScopeDimensionForSave(
  691 + input.selectedLocationIds,
  692 + input.availableLocationIds,
  693 + );
  694 +
  695 + const locationSpecified =
  696 + regionDim.type === "SPECIFIED" || locationDim.type === "SPECIFIED";
  697 + const availabilityType: "ALL" | "SPECIFIED" = locationSpecified ? "SPECIFIED" : "ALL";
  698 +
  699 + const anySpecified =
  700 + partnerDim.type === "SPECIFIED" || locationSpecified;
  701 + const effectiveLocationIds =
  702 + locationDim.type === "SPECIFIED" ? locationDim.ids : input.availableLocationIds;
  703 +
  704 + if (anySpecified && effectiveLocationIds.length === 0) {
  705 + return {
  706 + ok: false,
  707 + message: "Please adjust company, region, or location so at least one store matches.",
  708 + };
  709 + }
  710 +
  711 + const partnerIds = partnerDim.type === "SPECIFIED" ? partnerDim.ids : [];
  712 + const regionIds = regionDim.type === "SPECIFIED" ? regionDim.ids : [];
  713 + const locationIds = locationDim.type === "SPECIFIED" ? locationDim.ids : [];
  714 +
  715 + return {
  716 + ok: true,
  717 + body: {
  718 + appliedPartnerType: partnerDim.type,
  719 + partnerIds,
  720 + companyIds: partnerIds,
  721 + regionIds,
  722 + groupIds: regionIds,
  723 + locationIds,
  724 + availabilityType,
  725 + },
  726 + };
  727 +}
  728 +
  729 +/** 创建/编辑弹窗:按管理员多选或单公司模式构建保存 Body */
  730 +export function buildEntityScopeSaveFromForm(input: {
  731 + requireCompanySelection: boolean;
  732 + selectedPartnerId: string;
  733 + selectedPartnerIds: string[];
  734 + selectedRegionNames: string[];
  735 + selectedRegionIds: string[];
  736 + selectedLocationIds: string[];
  737 + fixedPartnerId: string;
  738 + locations: LocationDto[];
  739 + partners: PartnerListItem[];
  740 + groups: GroupListItem[];
  741 +}): { ok: true; body: EntityScopePayloadBody } | { ok: false; message: string } {
  742 + if (input.requireCompanySelection) {
  743 + const { availablePartnerIds, availableRegionIds, availableLocationIds } =
  744 + computeTemplateScopeAvailableIds(
  745 + input.partners,
  746 + input.groups,
  747 + input.locations,
  748 + input.selectedPartnerIds,
  749 + input.selectedRegionIds,
  750 + );
  751 + return buildEntityPartnerScopePayload({
  752 + selectedPartnerIds: input.selectedPartnerIds,
  753 + selectedRegionIds: input.selectedRegionIds,
  754 + selectedLocationIds: input.selectedLocationIds,
  755 + availablePartnerIds,
  756 + availableRegionIds,
  757 + availableLocationIds,
  758 + });
  759 + }
  760 +
  761 + const scopePartnerId = effectiveScopePartnerId(
  762 + false,
  763 + input.selectedPartnerId,
  764 + input.fixedPartnerId,
  765 + );
  766 + const partnerErr = scopePartnerValidationMessage(false, scopePartnerId);
  767 + if (partnerErr) {
  768 + return { ok: false, message: partnerErr };
  769 + }
  770 +
  771 + const locPayload = buildSpecifiedLocationPayload(
  772 + input.locations,
  773 + input.partners,
  774 + input.groups,
  775 + scopePartnerId,
  776 + input.selectedRegionNames,
  777 + input.selectedLocationIds,
  778 + );
  779 + if (!locPayload.ok) {
  780 + return { ok: false, message: locPayload.message };
  781 + }
  782 +
  783 + const groupIds = regionNamesToGroupIds(
  784 + input.selectedRegionNames,
  785 + input.groups,
  786 + scopePartnerId,
  787 + );
  788 + const partnerIds = scopePartnerId ? [scopePartnerId] : [];
  789 + return {
  790 + ok: true,
  791 + body: {
  792 + appliedPartnerType: partnerIds.length ? "SPECIFIED" : "ALL",
  793 + partnerIds,
  794 + companyIds: partnerIds,
  795 + regionIds: groupIds,
  796 + groupIds,
  797 + locationIds: locPayload.locationIds,
  798 + availabilityType: "SPECIFIED",
  799 + },
  800 + };
  801 +}
  802 +
  803 +/** 编辑弹窗:从详情 DTO 回填三维 Id 数组(管理员多选模式) */
  804 +export function hydrateEntityScopeFromDto(
  805 + dto: {
  806 + appliedPartnerType?: string | null;
  807 + appliedRegionType?: string | null;
  808 + appliedLocation?: string | null;
  809 + appliedLocationType?: string | null;
  810 + availabilityType?: string | null;
  811 + partnerIds?: string[] | null;
  812 + companyIds?: string[] | null;
  813 + regionIds?: string[] | null;
  814 + groupIds?: string[] | null;
  815 + locationIds?: string[] | null;
  816 + appliedLocationIds?: string[] | null;
  817 + },
  818 + locations: LocationDto[],
  819 + partners: PartnerListItem[],
  820 + groups: GroupListItem[],
  821 +): { partnerIds: string[]; regionIds: string[]; locationIds: string[] } {
  822 + return hydrateLabelTemplateScopeFromDto(
  823 + {
  824 + ...dto,
  825 + appliedLocation:
  826 + dto.appliedLocation ??
  827 + (String(dto.availabilityType ?? "").trim().toUpperCase() === "SPECIFIED"
  828 + ? "SPECIFIED"
  829 + : dto.appliedLocation),
  830 + },
  831 + locations,
  832 + partners,
  833 + groups,
  834 + );
  835 +}
美国版/Food Labeling Management Platform/src/services/labelCategoryService.ts
@@ -22,6 +22,13 @@ const PATH = &quot;/label-category&quot;; @@ -22,6 +22,13 @@ const PATH = &quot;/label-category&quot;;
22 22
23 function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto { 23 function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto {
24 const r = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>; 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 const noRaw = r.noOfLabels ?? r.NoOfLabels; 32 const noRaw = r.noOfLabels ?? r.NoOfLabels;
26 const noOfLabels = 33 const noOfLabels =
27 typeof noRaw === "number" && Number.isFinite(noRaw) 34 typeof noRaw === "number" && Number.isFinite(noRaw)
@@ -36,7 +43,60 @@ function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto { @@ -36,7 +43,60 @@ function normalizeLabelCategoryDto(raw: unknown): LabelCategoryDto {
36 : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw)) 43 : typeof orderRaw === "string" && orderRaw.trim() !== "" && Number.isFinite(Number(orderRaw))
37 ? Number(orderRaw) 44 ? Number(orderRaw)
38 : null; 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 export async function getLabelCategories(input: LabelCategoryGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelCategoryDto>> { 102 export async function getLabelCategories(input: LabelCategoryGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelCategoryDto>> {
@@ -60,11 +120,12 @@ export async function getLabelCategories(input: LabelCategoryGetListInput, signa @@ -60,11 +120,12 @@ export async function getLabelCategories(input: LabelCategoryGetListInput, signa
60 } 120 }
61 121
62 export async function getLabelCategory(id: string, signal?: AbortSignal): Promise<LabelCategoryDto> { 122 export async function getLabelCategory(id: string, signal?: AbortSignal): Promise<LabelCategoryDto> {
63 - return api.requestJson<LabelCategoryDto>({ 123 + const raw = await api.requestJson<unknown>({
64 path: `${PATH}/${encodeURIComponent(id)}`, 124 path: `${PATH}/${encodeURIComponent(id)}`,
65 method: "GET", 125 method: "GET",
66 signal, 126 signal,
67 }); 127 });
  128 + return normalizeLabelCategoryDto(raw);
68 } 129 }
69 130
70 export async function createLabelCategory(input: LabelCategoryCreateInput): Promise<LabelCategoryDto> { 131 export async function createLabelCategory(input: LabelCategoryCreateInput): Promise<LabelCategoryDto> {
@@ -82,17 +143,13 @@ export async function createLabelCategory(input: LabelCategoryCreateInput): Prom @@ -82,17 +143,13 @@ export async function createLabelCategory(input: LabelCategoryCreateInput): Prom
82 buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null, 143 buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null,
83 state: input.state ?? true, 144 state: input.state ?? true,
84 orderNum: input.orderNum, 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 export async function updateLabelCategory(id: string, input: LabelCategoryUpdateInput): Promise<LabelCategoryDto> { 151 export async function updateLabelCategory(id: string, input: LabelCategoryUpdateInput): Promise<LabelCategoryDto> {
95 - return api.requestJson<LabelCategoryDto>({ 152 + const raw = await api.requestJson<unknown>({
96 path: `${PATH}/${encodeURIComponent(id)}`, 153 path: `${PATH}/${encodeURIComponent(id)}`,
97 method: "PUT", 154 method: "PUT",
98 body: { 155 body: {
@@ -106,13 +163,10 @@ export async function updateLabelCategory(id: string, input: LabelCategoryUpdate @@ -106,13 +163,10 @@ export async function updateLabelCategory(id: string, input: LabelCategoryUpdate
106 buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null, 163 buttonStyleJson: (input.buttonStyleJson ?? "").trim() || null,
107 state: input.state ?? true, 164 state: input.state ?? true,
108 orderNum: input.orderNum, 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 export async function deleteLabelCategory(id: string): Promise<void> { 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,13 +63,35 @@ function normalizeLabelMultipleOptionDto(item: LabelMultipleOptionDto): LabelMul
63 const locationIds = parseIds( 63 const locationIds = parseIds(
64 (raw as Record<string, unknown>).locationIds ?? (raw as Record<string, unknown>).LocationIds, 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 return { 73 return {
67 ...item, 74 ...item,
68 optionValuesJson: parseOptionValuesJsonField(raw.optionValuesJson), 75 optionValuesJson: parseOptionValuesJsonField(raw.optionValuesJson),
69 lastEdited, 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 groupIds, 91 groupIds,
71 regionIds: groupIds, 92 regionIds: groupIds,
72 locationIds, 93 locationIds,
  94 + company: typeof companyRaw === "string" ? companyRaw : null,
73 }; 95 };
74 } 96 }
75 97
@@ -96,11 +118,34 @@ function scopeBodyFromInput(input: LabelMultipleOptionCreateInput): Record&lt;strin @@ -96,11 +118,34 @@ function scopeBodyFromInput(input: LabelMultipleOptionCreateInput): Record&lt;strin
96 const locationIds = [ 118 const locationIds = [
97 ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), 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 export async function getLabelMultipleOptions(input: LabelMultipleOptionGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelMultipleOptionDto>> { 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,6 +27,10 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto {
27 }; 27 };
28 const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds); 28 const groupIds = parseIds(r.groupIds ?? r.GroupIds ?? r.regionIds ?? r.RegionIds);
29 const locationIds = parseIds(r.locationIds ?? r.LocationIds); 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 const noRaw = r.noOfLabels ?? r.NoOfLabels; 34 const noRaw = r.noOfLabels ?? r.NoOfLabels;
31 const noOfLabels = 35 const noOfLabels =
32 typeof noRaw === "number" && Number.isFinite(noRaw) 36 typeof noRaw === "number" && Number.isFinite(noRaw)
@@ -46,9 +50,14 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto { @@ -46,9 +50,14 @@ function normalizeLabelTypeDto(raw: unknown): LabelTypeDto {
46 return { 50 return {
47 ...(r as object), 51 ...(r as object),
48 id: String(r.id ?? r.Id ?? ""), 52 id: String(r.id ?? r.Id ?? ""),
  53 + appliedPartnerType,
  54 + partnerIds,
  55 + companyIds: partnerIds,
  56 + availabilityType,
49 groupIds, 57 groupIds,
50 regionIds: groupIds, 58 regionIds: groupIds,
51 locationIds, 59 locationIds,
  60 + company: typeof companyRaw === "string" ? companyRaw : null,
52 noOfLabels, 61 noOfLabels,
53 lastEdited, 62 lastEdited,
54 region: typeof regionRaw === "string" ? regionRaw : null, 63 region: typeof regionRaw === "string" ? regionRaw : null,
@@ -67,11 +76,34 @@ function scopeBodyFromInput(input: LabelTypeCreateInput): Record&lt;string, unknown @@ -67,11 +76,34 @@ function scopeBodyFromInput(input: LabelTypeCreateInput): Record&lt;string, unknown
67 const locationIds = [ 76 const locationIds = [
68 ...new Set((input.locationIds ?? []).map((x) => String(x).trim()).filter(Boolean)), 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 export async function getLabelTypes(input: LabelTypeGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelTypeDto>> { 109 export async function getLabelTypes(input: LabelTypeGetListInput, signal?: AbortSignal): Promise<PagedResultDto<LabelTypeDto>> {
美国版/Food Labeling Management Platform/src/types/label.ts
@@ -81,7 +81,7 @@ export type LabelUpdateInput = { @@ -81,7 +81,7 @@ export type LabelUpdateInput = {
81 /** 适用门店(至少 1 个) */ 81 /** 适用门店(至少 1 个) */
82 locationIds: string[]; 82 locationIds: string[];
83 labelCategoryId: string; 83 labelCategoryId: string;
84 - labelTypeId: string; 84 + labelTypeId?: string | null;
85 productIds: string[]; // 至少 1 个 85 productIds: string[]; // 至少 1 个
86 labelInfoJson?: Record<string, unknown> | null; 86 labelInfoJson?: Record<string, unknown> | null;
87 state?: boolean; 87 state?: boolean;
美国版/Food Labeling Management Platform/src/types/labelCategory.ts
@@ -18,10 +18,19 @@ export type LabelCategoryDto = { @@ -18,10 +18,19 @@ export type LabelCategoryDto = {
18 /** 该分类下标签数量(列表列 No. of Label) */ 18 /** 该分类下标签数量(列表列 No. of Label) */
19 noOfLabels?: number | null; 19 noOfLabels?: number | null;
20 creationTime?: string | null; 20 creationTime?: string | null;
  21 + /** 适用 Company:ALL / SPECIFIED */
  22 + appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
  23 + partnerIds?: string[] | null;
  24 + companyIds?: string[] | null;
21 /** ALL / SPECIFIED */ 25 /** ALL / SPECIFIED */
22 availabilityType?: "ALL" | "SPECIFIED" | string | null; 26 availabilityType?: "ALL" | "SPECIFIED" | string | null;
  27 + /** SPECIFIED 时绑定的 Region Id */
  28 + regionIds?: string[] | null;
  29 + groupIds?: string[] | null;
23 /** SPECIFIED 时绑定的门店 Id */ 30 /** SPECIFIED 时绑定的门店 Id */
24 locationIds?: string[] | null; 31 locationIds?: string[] | null;
  32 + /** 列表:Company 展示 */
  33 + company?: string | null;
25 /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */ 34 /** 最近编辑时间(列表接口映射 LastModificationTime ?? CreationTime) */
26 lastEdited?: string | null; 35 lastEdited?: string | null;
27 }; 36 };
@@ -56,10 +65,11 @@ export type LabelCategoryCreateInput = { @@ -56,10 +65,11 @@ export type LabelCategoryCreateInput = {
56 buttonStyleJson?: string | null; 65 buttonStyleJson?: string | null;
57 state?: boolean; 66 state?: boolean;
58 orderNum?: number | null; 67 orderNum?: number | null;
59 - availabilityType?: "ALL" | "SPECIFIED" | string | null; 68 + /** 适用 Company:ALL / SPECIFIED */
60 appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; 69 appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
61 partnerIds?: string[] | null; 70 partnerIds?: string[] | null;
62 companyIds?: string[] | null; 71 companyIds?: string[] | null;
  72 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
63 regionIds?: string[] | null; 73 regionIds?: string[] | null;
64 groupIds?: string[] | null; 74 groupIds?: string[] | null;
65 locationIds?: string[] | null; 75 locationIds?: string[] | null;
美国版/Food Labeling Management Platform/src/types/labelMultipleOption.ts
@@ -7,6 +7,12 @@ export type LabelMultipleOptionDto = { @@ -7,6 +7,12 @@ export type LabelMultipleOptionDto = {
7 state?: boolean | null; 7 state?: boolean | null;
8 orderNum?: number | null; 8 orderNum?: number | null;
9 creationTime?: string | null; 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 lastEdited?: string | null; 17 lastEdited?: string | null;
12 /** 适用范围:Region Id(`fl_group.Id`) */ 18 /** 适用范围:Region Id(`fl_group.Id`) */
@@ -14,6 +20,8 @@ export type LabelMultipleOptionDto = { @@ -14,6 +20,8 @@ export type LabelMultipleOptionDto = {
14 regionIds?: string[] | null; 20 regionIds?: string[] | null;
15 /** 适用范围:门店 Id */ 21 /** 适用范围:门店 Id */
16 locationIds?: string[] | null; 22 locationIds?: string[] | null;
  23 + /** 列表:Company 展示 */
  24 + company?: string | null;
17 }; 25 };
18 26
19 export type PagedResultDto<T> = { 27 export type PagedResultDto<T> = {
@@ -39,10 +47,12 @@ export type LabelMultipleOptionCreateInput = { @@ -39,10 +47,12 @@ export type LabelMultipleOptionCreateInput = {
39 optionValuesJson: string[]; 47 optionValuesJson: string[];
40 state?: boolean; 48 state?: boolean;
41 orderNum?: number | null; 49 orderNum?: number | null;
42 - availabilityType?: "ALL" | "SPECIFIED" | string | null; 50 + /** 适用 Company:ALL / SPECIFIED */
43 appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; 51 appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
44 partnerIds?: string[] | null; 52 partnerIds?: string[] | null;
45 companyIds?: string[] | null; 53 companyIds?: string[] | null;
  54 + /** 门店可用范围:ALL / SPECIFIED */
  55 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
46 groupIds?: string[] | null; 56 groupIds?: string[] | null;
47 regionIds?: string[] | null; 57 regionIds?: string[] | null;
48 locationIds?: string[] | null; 58 locationIds?: string[] | null;
美国版/Food Labeling Management Platform/src/types/labelType.ts
@@ -5,11 +5,19 @@ export type LabelTypeDto = { @@ -5,11 +5,19 @@ export type LabelTypeDto = {
5 state?: boolean | null; 5 state?: boolean | null;
6 orderNum?: number | null; 6 orderNum?: number | null;
7 creationTime?: string | null; 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 /** 详情:Region Id 列表(`fl_group.Id`) */ 14 /** 详情:Region Id 列表(`fl_group.Id`) */
9 groupIds?: string[] | null; 15 groupIds?: string[] | null;
10 regionIds?: string[] | null; 16 regionIds?: string[] | null;
11 /** 详情:门店 Id 列表 */ 17 /** 详情:门店 Id 列表 */
12 locationIds?: string[] | null; 18 locationIds?: string[] | null;
  19 + /** 列表:Company 展示 */
  20 + company?: string | null;
13 /** 列表:该类型下标签数量 */ 21 /** 列表:该类型下标签数量 */
14 noOfLabels?: number | null; 22 noOfLabels?: number | null;
15 /** 列表:最近编辑时间 */ 23 /** 列表:最近编辑时间 */
@@ -44,10 +52,12 @@ export type LabelTypeCreateInput = { @@ -44,10 +52,12 @@ export type LabelTypeCreateInput = {
44 typeName: string; 52 typeName: string;
45 state?: boolean; 53 state?: boolean;
46 orderNum?: number | null; 54 orderNum?: number | null;
47 - availabilityType?: "ALL" | "SPECIFIED" | string | null; 55 + /** 适用 Company:ALL / SPECIFIED */
48 appliedPartnerType?: "ALL" | "SPECIFIED" | string | null; 56 appliedPartnerType?: "ALL" | "SPECIFIED" | string | null;
49 partnerIds?: string[] | null; 57 partnerIds?: string[] | null;
50 companyIds?: string[] | null; 58 companyIds?: string[] | null;
  59 + /** 门店可用范围:ALL / SPECIFIED */
  60 + availabilityType?: "ALL" | "SPECIFIED" | string | null;
51 /** Region 多选(`fl_group.Id`) */ 61 /** Region 多选(`fl_group.Id`) */
52 groupIds?: string[] | null; 62 groupIds?: string[] | null;
53 regionIds?: string[] | null; 63 regionIds?: string[] | null;