// ─────────────────────────────────────────────────────────── // Huio Control Panel — User Feedback View (cp-feedback-view.jsx) // ─────────────────────────────────────────────────────────── if (!window.CP_STYLES) throw new Error('[cp-feedback-view] window.CP_STYLES not loaded'); function renderFeedback({ lang, t, appStyles: rawAppStyles, feedbacks = [], feedbackSearchQuery, setFeedbackSearchQuery, feedbackCategoryFilter, setFeedbackCategoryFilter, feedbackRatingFilter, setFeedbackRatingFilter, feedbackStatusFilter, setFeedbackStatusFilter, feedbackCurrentPage, setFeedbackCurrentPage, feedbacksPerPage, setSelectedFeedback, }) { const appStyles = rawAppStyles || window.CP_STYLES || {}; // Safe data array & pre-normalized filter criteria (O(1) loop invariant hoisting) const rawList = Array.isArray(feedbacks) ? feedbacks : []; const query = (typeof feedbackSearchQuery === 'string') ? feedbackSearchQuery.trim().toLowerCase() : ''; const catFilter = feedbackCategoryFilter || 'all'; const ratingFilter = feedbackRatingFilter || 'all'; const statusFilter = feedbackStatusFilter || 'all'; const perPage = Number(feedbacksPerPage) > 0 ? Number(feedbacksPerPage) : 20; // Single-pass computation of quick metrics (O(N) with 0 intermediate array allocations) let ratingsCount = 0; let ratingsSum = 0; let bugsCount = 0; let featuresCount = 0; let billingCount = 0; let pendingCount = 0; for (let i = 0; i < rawList.length; i++) { const fb = rawList[i]; if (!fb) continue; if (typeof fb.rating === 'number' && !isNaN(fb.rating)) { ratingsCount++; ratingsSum += fb.rating; } if (fb.category === 'bug') bugsCount++; else if (fb.category === 'feature') featuresCount++; else if (fb.category === 'billing') billingCount++; if (fb.status === 'new') pendingCount++; } const avgRating = ratingsCount > 0 ? (ratingsSum / ratingsCount).toFixed(1) : 'N/A'; // Filters with defensive null-safety on string properties const filteredFeedbacks = rawList.filter(fb => { if (!fb) return false; if (query) { const msg = (fb.message || '').toLowerCase(); const name = (fb.user_name || '').toLowerCase(); const phone = String(fb.user_phone || ''); if (!msg.includes(query) && !name.includes(query) && !phone.includes(query)) { return false; } } if (catFilter !== 'all' && fb.category !== catFilter) return false; if (ratingFilter !== 'all' && fb.rating !== Number(ratingFilter)) return false; if (statusFilter !== 'all' && fb.status !== statusFilter) return false; return true; }); // Pagination with boundary clamping const totalItems = filteredFeedbacks.length; const totalPages = Math.ceil(totalItems / perPage) || 1; const safeCurrentPage = Math.max(1, Math.min(Number(feedbackCurrentPage) || 1, totalPages)); const startIndex = (safeCurrentPage - 1) * perPage; const endIndex = startIndex + perPage; const paginatedFeedbacks = filteredFeedbacks.slice(startIndex, endIndex); // Categories translation helper const getCategoryLabel = (cat) => { switch (cat) { case 'bug': return t('feedback.category.bug') || 'Bug Report 🐛'; case 'feature': return t('feedback.category.feature') || 'Feature Request 💡'; case 'billing': return t('feedback.category.billing') || 'Billing 💳'; default: return t('feedback.category.general') || 'General Review 💬'; } }; const getStatusLabel = (st) => { switch (st) { case 'read': return t('feedback.status.read') || 'Read'; case 'in_progress': return t('feedback.status.in_progress') || 'In Progress'; case 'resolved': return t('feedback.status.resolved') || 'Resolved'; default: return t('feedback.status.new') || 'New'; } }; const getStatusBadgeStyle = (st) => { switch (st) { case 'read': return { background: 'rgba(74, 144, 226, 0.12)', color: '#4a90e2' }; case 'in_progress': return { background: 'rgba(245, 166, 35, 0.12)', color: '#f5a623' }; case 'resolved': return { background: 'rgba(79, 107, 61, 0.12)', color: 'var(--hu-forest)' }; default: return { background: 'rgba(217, 83, 79, 0.12)', color: 'var(--hu-rust)' }; } }; return (
{t('feedback.sub') || 'Review user feedback and ratings.'}
| {t('feedback.col.date')} | {t('feedback.col.user')} | {t('feedback.col.category')} | {t('feedback.col.rating')} | {t('feedback.col.message')} | {t('feedback.col.status')} | {lang === 'vi' ? 'Hành động' : 'Actions'} |
|---|---|---|---|---|---|---|
| {lang === 'vi' ? 'Không tìm thấy góp ý nào' : 'No feedbacks found'} | ||||||
|
{formattedTime}
{formattedDate}
|
{fb.user_name || 'Anonymous'}
{fb.user_phone ? fb.user_phone : 'N/A'}
{fb.user_id && (
ID: {String(fb.user_id).substring(0, 8)}...
)}
|
{getCategoryLabel(fb.category)} | {'★'.repeat(safeRating)} {'★'.repeat(5 - safeRating)} | {fb.message || '—'} | {getStatusLabel(fb.status)} | |