Spaces:
Sleeping
Sleeping
| /** | |
| * Project Hierarchy Utilities | |
| * | |
| * Functions for extracting language from ATLAS-ID, detecting content category, | |
| * calculating hierarchy depth, and generating job IDs. | |
| */ | |
| import type { Project, ContentCategory, HierarchyNode, Episode } from "../types/project"; | |
| /** | |
| * Extracts target language code from ATLAS-ID | |
| * Format: "1234567-DE" → "DE" | |
| */ | |
| export function extractLanguageFromAtlasId(atlasId: string | undefined): string | null { | |
| if (!atlasId) return null; | |
| const match = atlasId.match(/-([A-Z]{2})$/i); | |
| return match ? match[1].toUpperCase() : null; | |
| } | |
| /** | |
| * Extracts the base ID from ATLAS-ID (without language suffix) | |
| * Format: "1234567-DE" → "1234567" | |
| */ | |
| export function extractBaseAtlasId(atlasId: string | undefined): string | null { | |
| if (!atlasId) return null; | |
| const match = atlasId.match(/^(\d+)/); | |
| return match ? match[1] : null; | |
| } | |
| /** | |
| * Generates episode job ID from project ATLAS-ID and episode sequence | |
| * Format: "1234567-DE" + episode 1 → "1234567_1" | |
| */ | |
| export function generateEpisodeJobId(atlasId: string | undefined, episodeNumber: number): string | null { | |
| const baseId = extractBaseAtlasId(atlasId); | |
| if (!baseId) return null; | |
| return `${baseId}_${episodeNumber}`; | |
| } | |
| /** | |
| * Parses episode job ID to extract base ID and sequence number | |
| * Format: "1234567_1" → { baseId: "1234567", sequence: 1 } | |
| */ | |
| export function parseEpisodeJobId(jobId: string | undefined): { baseId: string; sequence: number } | null { | |
| if (!jobId) return null; | |
| const match = jobId.match(/^(\d+)_(\d+)$/); | |
| if (!match) return null; | |
| return { | |
| baseId: match[1], | |
| sequence: parseInt(match[2], 10) | |
| }; | |
| } | |
| /** | |
| * A flexible project type that accepts both the strict Project type | |
| * and the inferred type from mockProjects | |
| */ | |
| type ProjectLike = { | |
| content?: string; | |
| type?: string; | |
| season?: string | null; | |
| batch?: string | null; | |
| episodesList?: unknown[]; | |
| parentGroupId?: string | null; | |
| projectGroupTitle?: string | null; | |
| atlasId?: string; | |
| bcId?: string; | |
| language?: string; | |
| }; | |
| /** | |
| * Determines content category from project fields | |
| * Uses 'content' field primarily (Feature, Episode, Trailer, Promo, etc.) | |
| */ | |
| export function getContentCategory(project: ProjectLike): ContentCategory { | |
| const content = project.content?.toLowerCase() || ""; | |
| const type = project.type?.toLowerCase() || ""; | |
| if (content === "feature" || type.includes("film") || type.includes("movie")) { | |
| return "movie"; | |
| } | |
| if (content === "episode" || type.includes("series")) { | |
| return "series"; | |
| } | |
| if (content === "trailer") { | |
| return "trailer"; | |
| } | |
| if (content === "promo" || content === "advertisement") { | |
| return "promo"; | |
| } | |
| return "other"; | |
| } | |
| /** | |
| * Returns display icon identifier for content category | |
| */ | |
| export function getContentCategoryIcon(category: ContentCategory): string { | |
| switch (category) { | |
| case "series": return "tv"; | |
| case "movie": return "film"; | |
| case "trailer": return "clapperboard"; | |
| case "promo": return "megaphone"; | |
| default: return "file"; | |
| } | |
| } | |
| /** | |
| * Returns human-readable label for content category | |
| */ | |
| export function getContentCategoryLabel(category: ContentCategory): string { | |
| switch (category) { | |
| case "series": return "Series"; | |
| case "movie": return "Movie"; | |
| case "trailer": return "Trailer"; | |
| case "promo": return "Promo"; | |
| default: return "Other"; | |
| } | |
| } | |
| /** | |
| * Calculates hierarchy depth for a project (levels below title) | |
| * Returns number of levels below title: Season(0-1) + Batch(0-1) + Episodes(0-1) | |
| * | |
| * Note: This counts levels in the PROJECT's internal hierarchy, not including | |
| * external grouping. Group level is handled separately at the ProjectGroup level. | |
| * | |
| * Examples: | |
| * - Movie (flat): depth = 0 (no seasons, batches, or episodes list) | |
| * - Series S01: depth = 1 (season only) | |
| * - Series S01/B01: depth = 2 (season + batch) | |
| * - Series S01/B01/EP1-10: depth = 3 (season + batch + episodes) | |
| */ | |
| export function getHierarchyDepth(project: Project): number { | |
| let depth = 0; | |
| if (project.season) { | |
| depth += 1; | |
| } | |
| if (project.batch) { | |
| depth += 1; | |
| } | |
| if (project.episodesList && project.episodesList.length > 0) { | |
| depth += 1; | |
| } | |
| return depth; | |
| } | |
| /** | |
| * Determines which hierarchy levels are present for a project | |
| * | |
| * Note: `belongsToGroup` indicates whether this project is part of a ProjectGroup, | |
| * not a structural hierarchy level within the project itself. | |
| */ | |
| export function getHierarchyLevels(project: ProjectLike): { | |
| belongsToGroup: boolean; | |
| hasSeason: boolean; | |
| hasBatch: boolean; | |
| hasEpisodes: boolean; | |
| } { | |
| return { | |
| belongsToGroup: !!(project.parentGroupId || project.projectGroupTitle), | |
| hasSeason: !!project.season, | |
| hasBatch: !!project.batch, | |
| hasEpisodes: !!(project.episodesList && project.episodesList.length > 0) | |
| }; | |
| } | |
| /** | |
| * Converts a flat project to a hierarchy tree node with children | |
| */ | |
| export function projectToHierarchyNode(project: Project, parentId?: string): HierarchyNode { | |
| const atlasId = project.atlasId || project.bcId; | |
| const language = project.language || extractLanguageFromAtlasId(atlasId) || undefined; | |
| const contentCategory = getContentCategory(project); | |
| const children: HierarchyNode[] = []; | |
| let currentDepth = 1; | |
| // Add season level if present | |
| if (project.season) { | |
| const seasonNode: HierarchyNode = { | |
| id: `${project.id}-season-${project.season}`, | |
| type: "season", | |
| label: project.season, | |
| language, | |
| depth: currentDepth + 1, | |
| parentId: project.id, | |
| projectId: project.id, | |
| children: [] | |
| }; | |
| // Add batch level if present | |
| if (project.batch) { | |
| const batchNode: HierarchyNode = { | |
| id: `${project.id}-batch-${project.batch}`, | |
| type: "batch", | |
| label: project.batch, | |
| language, | |
| depth: currentDepth + 2, | |
| parentId: seasonNode.id, | |
| projectId: project.id, | |
| children: [] | |
| }; | |
| // Add episodes under batch | |
| if (project.episodesList && project.episodesList.length > 0) { | |
| batchNode.children = project.episodesList.map((ep, idx) => | |
| episodeToHierarchyNode(ep, project, batchNode.id, currentDepth + 3) | |
| ); | |
| } | |
| seasonNode.children = [batchNode]; | |
| } else if (project.episodesList && project.episodesList.length > 0) { | |
| // Episodes directly under season (no batch) | |
| seasonNode.children = project.episodesList.map((ep, idx) => | |
| episodeToHierarchyNode(ep, project, seasonNode.id, currentDepth + 2) | |
| ); | |
| } | |
| children.push(seasonNode); | |
| } else if (project.batch) { | |
| // Batch without season | |
| const batchNode: HierarchyNode = { | |
| id: `${project.id}-batch-${project.batch}`, | |
| type: "batch", | |
| label: project.batch, | |
| language, | |
| depth: currentDepth + 1, | |
| parentId: project.id, | |
| projectId: project.id, | |
| children: [] | |
| }; | |
| if (project.episodesList && project.episodesList.length > 0) { | |
| batchNode.children = project.episodesList.map((ep, idx) => | |
| episodeToHierarchyNode(ep, project, batchNode.id, currentDepth + 2) | |
| ); | |
| } | |
| children.push(batchNode); | |
| } else if (project.episodesList && project.episodesList.length > 0) { | |
| // Episodes directly under title (flat structure) | |
| children.push(...project.episodesList.map((ep, idx) => | |
| episodeToHierarchyNode(ep, project, project.id, currentDepth + 1) | |
| )); | |
| } | |
| return { | |
| id: project.id, | |
| type: "title", | |
| label: project.title, | |
| atlasId, | |
| language, | |
| contentCategory, | |
| status: project.status, | |
| depth: currentDepth, | |
| parentId, | |
| projectId: project.id, | |
| children: children.length > 0 ? children : undefined | |
| }; | |
| } | |
| /** | |
| * Converts an episode to a hierarchy node | |
| */ | |
| function episodeToHierarchyNode( | |
| episode: Episode, | |
| project: Project, | |
| parentId: string, | |
| depth: number | |
| ): HierarchyNode { | |
| const atlasId = project.atlasId || project.bcId; | |
| const jobId = episode.jobId || generateEpisodeJobId(atlasId, episode.number) || undefined; | |
| const language = project.language || extractLanguageFromAtlasId(atlasId) || undefined; | |
| return { | |
| id: `${project.id}-ep-${episode.number}`, | |
| type: "episode", | |
| label: `${episode.prefix || "EP"}${episode.number} - ${episode.title}`, | |
| jobId, | |
| language, | |
| status: episode.milestones?.delivery?.status, | |
| depth, | |
| parentId, | |
| projectId: project.id, | |
| episodeNumber: episode.number | |
| }; | |
| } | |
| /** | |
| * Groups projects by their ATLAS-ID to identify shared orders | |
| */ | |
| export function groupProjectsByAtlasId(projects: Project[]): Map<string, Project[]> { | |
| const groups = new Map<string, Project[]>(); | |
| for (const project of projects) { | |
| const baseId = extractBaseAtlasId(project.atlasId || project.bcId); | |
| if (!baseId) continue; | |
| const existing = groups.get(baseId) || []; | |
| existing.push(project); | |
| groups.set(baseId, existing); | |
| } | |
| return groups; | |
| } | |
| /** | |
| * Groups projects by their target language | |
| */ | |
| export function groupProjectsByLanguage(projects: Project[]): Map<string, Project[]> { | |
| const groups = new Map<string, Project[]>(); | |
| for (const project of projects) { | |
| const atlasId = project.atlasId || project.bcId; | |
| const language = project.language || extractLanguageFromAtlasId(atlasId) || "Unknown"; | |
| const existing = groups.get(language) || []; | |
| existing.push(project); | |
| groups.set(language, existing); | |
| } | |
| return groups; | |
| } | |
| /** | |
| * Gets unique languages from a list of projects | |
| */ | |
| export function getUniqueLanguages(projects: Project[]): string[] { | |
| const languages = new Set<string>(); | |
| for (const project of projects) { | |
| const atlasId = project.atlasId || project.bcId; | |
| const language = project.language || extractLanguageFromAtlasId(atlasId); | |
| if (language) { | |
| languages.add(language); | |
| } | |
| } | |
| return Array.from(languages).sort(); | |
| } | |
| /** | |
| * Filters projects by content category | |
| * Uses generics to preserve the input array type | |
| */ | |
| export function filterByContentCategory<T extends ProjectLike>(projects: T[], categories: ContentCategory | ContentCategory[]): T[] { | |
| const categoryArray = Array.isArray(categories) ? categories : [categories]; | |
| if (categoryArray.length === 0) return projects; | |
| return projects.filter(project => { | |
| const category = getContentCategory(project); | |
| return categoryArray.includes(category); | |
| }); | |
| } | |
| /** | |
| * Filters projects by language | |
| * Uses generics to preserve the input array type | |
| */ | |
| export function filterByLanguage<T extends ProjectLike>( | |
| projects: T[], | |
| languages: string | string[] | |
| ): T[] { | |
| const langArray = Array.isArray(languages) ? languages : [languages]; | |
| if (langArray.length === 0) return projects; | |
| return projects.filter(project => { | |
| const atlasId = project.atlasId || project.bcId || ""; | |
| const projectLanguage = project.language || extractLanguageFromAtlasId(atlasId); | |
| return projectLanguage && langArray.includes(projectLanguage); | |
| }); | |
| } | |
| /** | |
| * Filters projects by hierarchy depth (number of levels: 0-3) | |
| * Uses generics to preserve the input array type | |
| */ | |
| export function filterByHierarchyDepth<T extends ProjectLike>( | |
| projects: T[], | |
| depth: number | { | |
| hasSeasons?: boolean; | |
| hasBatches?: boolean; | |
| hasEpisodes?: boolean; | |
| isFlat?: boolean; | |
| belongsToGroup?: boolean; | |
| } | |
| ): T[] { | |
| // Simple depth filter | |
| if (typeof depth === "number") { | |
| return projects.filter(project => { | |
| let projectDepth = 0; | |
| if (project.season) projectDepth += 1; | |
| if (project.batch) projectDepth += 1; | |
| if (project.episodesList && project.episodesList.length > 0) projectDepth += 1; | |
| return projectDepth === depth; | |
| }); | |
| } | |
| // Options-based filter | |
| const options = depth; | |
| return projects.filter(project => { | |
| const levels = getHierarchyLevels(project); | |
| if (options.hasSeasons !== undefined && levels.hasSeason !== options.hasSeasons) { | |
| return false; | |
| } | |
| if (options.hasBatches !== undefined && levels.hasBatch !== options.hasBatches) { | |
| return false; | |
| } | |
| if (options.hasEpisodes !== undefined && levels.hasEpisodes !== options.hasEpisodes) { | |
| return false; | |
| } | |
| if (options.belongsToGroup !== undefined && levels.belongsToGroup !== options.belongsToGroup) { | |
| return false; | |
| } | |
| if (options.isFlat !== undefined) { | |
| const isFlat = !levels.hasSeason && !levels.hasBatch && !levels.hasEpisodes; | |
| if (isFlat !== options.isFlat) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| }); | |
| } | |