fix(templates): real scene count on template pages (was always 0)
The card + detail read template.sceneCount, but the API never sent one — so the frontend mapper hardcoded sceneCount:0 for every DB-backed template. - content-svc: ContainerSummaryResponse + ContainerDetailResponse now carry SceneCount. The list computes it with one grouped query (scenes per aspect project, max across aspects); the detail loads scenes and counts them. - frontend: V2ContainerSummary.scene_count → AdminProject.sceneCount → the catalog card/detail (adminProjectToCatalogTemplate no longer hardcodes 0). Verified on the live local API: fr-instagram-promo → 5, single-scene templates → 1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -51,8 +51,18 @@ public class TemplateService(ContentDbContext db)
|
|||||||
var total = await q.LongCountAsync();
|
var total = await q.LongCountAsync();
|
||||||
var items = await q.Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync();
|
var items = await q.Skip((req.Page - 1) * req.PageSize).Take(req.PageSize).ToListAsync();
|
||||||
|
|
||||||
|
// Scene count per template = scenes in a single aspect project (max across
|
||||||
|
// projects). One grouped query, no scene entities loaded.
|
||||||
|
var ids = items.Select(i => i.Id).ToList();
|
||||||
|
var sceneCounts = (await db.Projects
|
||||||
|
.Where(p => p.DeletedAt == null && ids.Contains(p.ContainerId))
|
||||||
|
.Select(p => new { p.ContainerId, Cnt = p.Scenes.Count(s => s.DeletedAt == null) })
|
||||||
|
.ToListAsync())
|
||||||
|
.GroupBy(x => x.ContainerId)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Max(x => x.Cnt));
|
||||||
|
|
||||||
return new PagedResponse<ContainerSummaryResponse>(
|
return new PagedResponse<ContainerSummaryResponse>(
|
||||||
items.Select(MapContainerSummary),
|
items.Select(c => MapContainerSummary(c, sceneCounts.GetValueOrDefault(c.Id, 0))),
|
||||||
new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize))
|
new PaginationMeta(req.Page, req.PageSize, total, (int)Math.Ceiling((double)total / req.PageSize))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -62,7 +72,7 @@ public class TemplateService(ContentDbContext db)
|
|||||||
var container = await db.ProjectContainers
|
var container = await db.ProjectContainers
|
||||||
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
|
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
|
||||||
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
|
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
|
||||||
.Include(x => x.Projects.Where(p => p.DeletedAt == null))
|
.Include(x => x.Projects.Where(p => p.DeletedAt == null)).ThenInclude(p => p.Scenes.Where(s => s.DeletedAt == null))
|
||||||
.FirstOrDefaultAsync(x => x.Slug == slug)
|
.FirstOrDefaultAsync(x => x.Slug == slug)
|
||||||
?? throw new KeyNotFoundException($"Container '{slug}' not found");
|
?? throw new KeyNotFoundException($"Container '{slug}' not found");
|
||||||
|
|
||||||
@@ -74,7 +84,7 @@ public class TemplateService(ContentDbContext db)
|
|||||||
var container = await db.ProjectContainers
|
var container = await db.ProjectContainers
|
||||||
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
|
.Include(x => x.ContainerCategories).ThenInclude(x => x.Category)
|
||||||
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
|
.Include(x => x.ContainerTags).ThenInclude(x => x.Tag)
|
||||||
.Include(x => x.Projects.Where(p => p.DeletedAt == null))
|
.Include(x => x.Projects.Where(p => p.DeletedAt == null)).ThenInclude(p => p.Scenes.Where(s => s.DeletedAt == null))
|
||||||
.FirstOrDefaultAsync(x => x.Id == id)
|
.FirstOrDefaultAsync(x => x.Id == id)
|
||||||
?? throw new KeyNotFoundException($"Container {id} not found");
|
?? throw new KeyNotFoundException($"Container {id} not found");
|
||||||
|
|
||||||
@@ -450,12 +460,13 @@ public class TemplateService(ContentDbContext db)
|
|||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ContainerSummaryResponse MapContainerSummary(ProjectContainer c) => new(
|
private static ContainerSummaryResponse MapContainerSummary(ProjectContainer c, int sceneCount = 0) => new(
|
||||||
c.Id, c.Slug, c.Name, c.Description, c.Image, c.Demo, c.MiniDemo,
|
c.Id, c.Slug, c.Name, c.Description, c.Image, c.Demo, c.MiniDemo,
|
||||||
c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(),
|
c.IsPublished, c.IsPremium, c.IsMockup, c.PrimaryMode.ToString(),
|
||||||
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
|
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
|
||||||
c.ContainerCategories.Select(cc => cc.Category.Slug).ToList(),
|
c.ContainerCategories.Select(cc => cc.Category.Slug).ToList(),
|
||||||
c.ContainerTags.Select(ct => ct.Tag.Name).ToList()
|
c.ContainerTags.Select(ct => ct.Tag.Name).ToList(),
|
||||||
|
sceneCount
|
||||||
);
|
);
|
||||||
|
|
||||||
private static ContainerDetailResponse MapContainerDetail(ProjectContainer c) => new(
|
private static ContainerDetailResponse MapContainerDetail(ProjectContainer c) => new(
|
||||||
@@ -465,7 +476,8 @@ public class TemplateService(ContentDbContext db)
|
|||||||
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
|
c.RateAvg, c.RateCount, c.ViewCount, c.UseCount, c.Sort, c.SortDate,
|
||||||
c.Projects.Select(MapProject).ToList(),
|
c.Projects.Select(MapProject).ToList(),
|
||||||
c.ContainerCategories.Select(cc => MapCategoryFlat(cc.Category)).ToList(),
|
c.ContainerCategories.Select(cc => MapCategoryFlat(cc.Category)).ToList(),
|
||||||
c.ContainerTags.Select(ct => new TagResponse(ct.Tag.Id, ct.Tag.Name, ct.Tag.LatinName, ct.Tag.Slug, ct.Tag.AppliesToMode, ct.Tag.IsActive)).ToList()
|
c.ContainerTags.Select(ct => new TagResponse(ct.Tag.Id, ct.Tag.Name, ct.Tag.LatinName, ct.Tag.Slug, ct.Tag.AppliesToMode, ct.Tag.IsActive)).ToList(),
|
||||||
|
c.Projects.Count == 0 ? 0 : c.Projects.Max(p => p.Scenes.Count)
|
||||||
);
|
);
|
||||||
|
|
||||||
private static CategoryResponse MapCategoryFlat(Category c) => new(
|
private static CategoryResponse MapCategoryFlat(Category c) => new(
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ public record ContainerSummaryResponse(
|
|||||||
int Sort,
|
int Sort,
|
||||||
DateTime SortDate,
|
DateTime SortDate,
|
||||||
List<string> CategorySlugs,
|
List<string> CategorySlugs,
|
||||||
List<string> Tags
|
List<string> Tags,
|
||||||
|
int SceneCount
|
||||||
);
|
);
|
||||||
|
|
||||||
public record ContainerDetailResponse(
|
public record ContainerDetailResponse(
|
||||||
@@ -109,7 +110,8 @@ public record ContainerDetailResponse(
|
|||||||
DateTime SortDate,
|
DateTime SortDate,
|
||||||
List<ProjectResponse> Projects,
|
List<ProjectResponse> Projects,
|
||||||
List<CategoryResponse> Categories,
|
List<CategoryResponse> Categories,
|
||||||
List<TagResponse> Tags
|
List<TagResponse> Tags,
|
||||||
|
int SceneCount
|
||||||
);
|
);
|
||||||
|
|
||||||
public record ProjectResponse(
|
public record ProjectResponse(
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export interface AdminProject {
|
|||||||
metaJson?: string;
|
metaJson?: string;
|
||||||
sortOrder: number;
|
sortOrder: number;
|
||||||
mediaCount: number;
|
mediaCount: number;
|
||||||
|
sceneCount: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
@@ -85,6 +86,7 @@ interface V2ContainerSummary {
|
|||||||
sort_date: string;
|
sort_date: string;
|
||||||
category_slugs: string[];
|
category_slugs: string[];
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
scene_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface V2PagedContainers {
|
interface V2PagedContainers {
|
||||||
@@ -130,6 +132,7 @@ function containerToAdminProject(c: V2ContainerSummary): AdminProject {
|
|||||||
metaJson: undefined,
|
metaJson: undefined,
|
||||||
sortOrder: c.sort,
|
sortOrder: c.sort,
|
||||||
mediaCount: 0,
|
mediaCount: 0,
|
||||||
|
sceneCount: c.scene_count ?? 0,
|
||||||
createdAt: c.sort_date ?? "",
|
createdAt: c.sort_date ?? "",
|
||||||
updatedAt: c.sort_date ?? "",
|
updatedAt: c.sort_date ?? "",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -511,6 +511,7 @@ export interface AdminProjectLike {
|
|||||||
categoryName?: string;
|
categoryName?: string;
|
||||||
coverImageUrl?: string;
|
coverImageUrl?: string;
|
||||||
previewVideoUrl?: string;
|
previewVideoUrl?: string;
|
||||||
|
sceneCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function adminProjectToCatalogTemplate(
|
export function adminProjectToCatalogTemplate(
|
||||||
@@ -523,7 +524,7 @@ export function adminProjectToCatalogTemplate(
|
|||||||
aspectRatio: "widescreen",
|
aspectRatio: "widescreen",
|
||||||
durationType: "flexible",
|
durationType: "flexible",
|
||||||
premium: false,
|
premium: false,
|
||||||
sceneCount: 0,
|
sceneCount: p.sceneCount ?? 0,
|
||||||
supports4k: false,
|
supports4k: false,
|
||||||
colorChange: true,
|
colorChange: true,
|
||||||
scriptToVideo: false,
|
scriptToVideo: false,
|
||||||
|
|||||||
Reference in New Issue
Block a user