// ───────────────────────────────────────────────────────────
// Huio Control Panel — Docs Views: Roadmap & Changelog (cp-docs-views.jsx)
// ───────────────────────────────────────────────────────────
if (!window.CP_STYLES) throw new Error('[cp-docs-views] window.CP_STYLES not loaded (check cp-styles.jsx)');
const parseRoadmap = (md) => {
if (!md) return [];
const lines = md.split('\n');
const categories = [];
let currentCat = null;
lines.forEach(line => {
if (line.startsWith('### ')) {
const title = line.replace('### ', '').trim();
currentCat = {
title: title,
tasks: []
};
categories.push(currentCat);
} else if (line.startsWith('## ') && !line.includes('🧭') && !line.includes('📈') && !line.includes('Какая') && !line.includes('Правила') && !line.includes('Настройка') && !line.includes('At A Glance') && !line.includes('Quick Navigation') && !line.includes('Стратегия')) {
const title = line.replace('## ', '').trim();
currentCat = {
title: title,
tasks: [],
isMainSection: true
};
categories.push(currentCat);
} else if (currentCat) {
const taskMatch = line.match(/^\s*-\s*\[(x| )\]\s*(.+)$/i);
if (taskMatch) {
currentCat.tasks.push({
text: taskMatch[2].trim(),
completed: taskMatch[1].toLowerCase() === 'x'
});
}
}
});
return categories.filter(c => c.tasks.length > 0);
};
const parseChangelog = (md) => {
if (!md) return [];
const lines = md.split('\n');
const releases = [];
let currentRelease = null;
let currentSection = null;
lines.forEach(line => {
const releaseMatch = line.match(/^##\s+\[([^\]]+)\]\s*—\s*(.+)$/);
if (releaseMatch) {
currentRelease = {
version: releaseMatch[1],
date: releaseMatch[2].trim(),
sections: []
};
releases.push(currentRelease);
currentSection = null;
} else if (currentRelease && line.startsWith('### ')) {
const secTitle = line.replace('### ', '').trim();
currentSection = {
title: secTitle,
items: []
};
currentRelease.sections.push(currentSection);
} else if (currentRelease && line.trim().startsWith('- ')) {
const itemText = line.trim().substring(2);
if (currentSection) {
currentSection.items.push(itemText);
} else {
if (currentRelease.sections.length === 0) {
currentSection = {
title: 'General',
items: []
};
currentRelease.sections.push(currentSection);
}
currentRelease.sections[0].items.push(itemText);
}
}
});
return releases;
};
const formatTaskText = (text) => {
const parts = [];
let lastIndex = 0;
const regex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
let match;
while ((match = regex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.substring(lastIndex, match.index));
}
const linkPath = match[1];
const displayName = match[2] || linkPath.split('/').pop();
parts.push(
{displayName}
);
lastIndex = regex.lastIndex;
}
if (lastIndex < text.length) {
parts.push(text.substring(lastIndex));
}
return parts.length > 0 ? parts : text;
};
const renderRoadmap = ({
lang,
appStyles = window.CP_STYLES,
isRoadmapLoading,
roadmapContent,
roadmapSearch,
setRoadmapSearch,
roadmapFilter,
setRoadmapFilter,
}) => {
if (isRoadmapLoading) {
return (
);
}
const categories = parseRoadmap(roadmapContent);
let total = 0;
let completed = 0;
categories.forEach(c => {
c.tasks.forEach(t => {
total++;
if (t.completed) completed++;
});
});
const percent = total > 0 ? Math.round((completed / total) * 100) : 95;
const filteredCategories = categories.map(cat => {
const filteredTasks = cat.tasks.filter(task => {
if (roadmapSearch.trim() && !task.text.toLowerCase().includes(roadmapSearch.toLowerCase())) {
return false;
}
if (roadmapFilter === 'completed' && !task.completed) return false;
if (roadmapFilter === 'pending' && task.completed) return false;
return true;
});
return { ...cat, tasks: filteredTasks };
}).filter(cat => cat.tasks.length > 0);
const activeTheme = document.documentElement.getAttribute('data-theme') || 'candy';
const btnActiveStyle = (theme) => ({
background: 'var(--hu-bg-inverse)',
color: 'var(--hu-fg-inverse)',
border: 'none',
borderRadius: 'var(--hu-r-pill)',
fontSize: '10px',
fontWeight: '700',
padding: '4px 10px',
cursor: 'pointer'
});
const btnInactiveStyle = {
background: 'transparent',
color: 'var(--hu-fg-3)',
border: 'none',
borderRadius: 'var(--hu-r-pill)',
fontSize: '10px',
fontWeight: '600',
padding: '4px 10px',
cursor: 'pointer'
};
return (
{lang === 'vi' ? 'Lộ trình phát triển' : 'Product Roadmap'}
{lang === 'vi'
? 'Theo dõi tiến trình phát triển và hoàn thiện các chức năng hệ thống.'
: 'Track the implementation progress of system features and release phases.'}
{lang === 'vi' ? 'Tiến độ tổng thể' : 'Overall Progress'}
{lang === 'vi' ? 'Hoàn thành' : 'Completion Rate'}: {percent}%
{completed} / {total} {lang === 'vi' ? 'nhiệm vụ' : 'tasks'}
{filteredCategories.map((cat, idx) => {
const catTotal = cat.tasks.length;
const catCompleted = cat.tasks.filter(t => t.completed).length;
const catPercent = catTotal > 0 ? Math.round((catCompleted / catTotal) * 100) : 0;
return (
{cat.title}
{catPercent}%
{cat.tasks.map((task, tIdx) => (
{task.completed && '✓'}
{formatTaskText(task.text)}
))}
);
})}
{filteredCategories.length === 0 && (
{lang === 'vi' ? 'Không tìm thấy tính năng nào khớp.' : 'No matching features found.'}
)}
);
};
const renderChangelog = ({
lang,
appStyles = window.CP_STYLES,
isChangelogLoading,
changelogContent,
changelogSearch,
setChangelogSearch,
changelogLimit,
setChangelogLimit,
}) => {
if (isChangelogLoading) {
return (
);
}
const releases = parseChangelog(changelogContent);
const filteredReleases = releases.map(rel => {
const filteredSections = rel.sections.map(sec => {
const filteredItems = sec.items.filter(item => {
if (changelogSearch.trim()) {
return item.toLowerCase().includes(changelogSearch.toLowerCase());
}
return true;
});
return { ...sec, items: filteredItems };
}).filter(sec => sec.items.length > 0);
return { ...rel, sections: filteredSections };
}).filter(rel => rel.sections.length > 0);
const isSearching = Boolean(changelogSearch.trim());
const visibleReleases = isSearching ? filteredReleases : filteredReleases.slice(0, changelogLimit);
const hasMoreReleases = !isSearching && filteredReleases.length > changelogLimit;
const parseChangelogItem = (itemText) => {
const match = itemText.match(/^\*\*([^*:]+)\*\*\s*:\s*(.+)$/);
if (match) {
return {
tag: match[1].trim(),
content: match[2].trim()
};
}
return { tag: null, content: itemText };
};
const getTagColorClass = (tag) => {
if (!tag) return '';
const t = tag.toLowerCase();
if (t.includes('bug') || t.includes('fix') || t.includes('ошиб')) return 'hu-chip-coral';
if (t.includes('perf') || t.includes('оптими')) return 'hu-chip-green';
if (t.includes('legal') || t.includes('юрид')) return 'hu-chip-ochre';
if (t.includes('design') || t.includes('дизайн') || t.includes('ui')) return 'hu-chip-gold';
if (t.includes('push') || t.includes('увед') || t.includes('notif')) return 'hu-chip-teal';
return '';
};
return (
{lang === 'vi' ? 'Lịch sử thay đổi' : 'System Changelog'}
{lang === 'vi'
? 'Nhật ký các bản cập nhật, sửa lỗi và cải tiến hệ thống.'
: 'Chronological timeline of product releases, system bug fixes, and feature enhancements.'}
setChangelogSearch(e.target.value)}
style={{
padding: '8px 16px',
borderRadius: 'var(--hu-r-pill)',
border: '1.5px solid var(--hu-border)',
background: 'var(--hu-bg-card)',
color: 'var(--hu-fg)',
outline: 'none',
fontSize: '13px',
width: '100%',
maxWidth: '300px',
fontFamily: 'inherit'
}}
/>
{visibleReleases.length === 0 && (
{lang === 'vi' ? 'Không tìm thấy nhật ký phiên bản nào phù hợp.' : 'No matching release logs found.'}
)}
{visibleReleases.map((rel, idx) => (
v{rel.version}
📅 {rel.date}
{rel.sections.map((sec, sIdx) => (
{sec.title && sec.title !== 'General' && (
{sec.title}
)}
{sec.items.map((itemText, iIdx) => {
const parsed = parseChangelogItem(itemText);
return (
✦
{parsed.tag && (
{parsed.tag}
)}
{formatTaskText(parsed.content)}
);
})}
))}
))}
{hasMoreReleases && (
setChangelogLimit(prev => prev + 10)}
>
{lang === 'vi' ? `Tải thêm phiên bản (+10) — Còn lại ${filteredReleases.length - changelogLimit}` : `Load more releases (+10) — ${filteredReleases.length - changelogLimit} remaining`}
)}
{filteredReleases.length === 0 && (
{lang === 'vi' ? 'Không tìm thấy bản cập nhật nào.' : 'No update logs found.'}
)}
);
};
window.CpDocsViews = {
parseRoadmap,
parseChangelog,
formatTaskText,
renderRoadmap,
renderChangelog
};