// ─────────────────────────────────────────────────────────── // 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.title') || 'User Feedback'}

{t('feedback.sub') || 'Review user feedback and ratings.'}

{/* Metrics cards */}
0 ? `Based on ${ratingsCount} ratings` : 'No ratings yet'} isPositive={Number(avgRating) >= 4.0} iconKind="star" />
{/* Filter controls */}
{/* Category Filter */}
{t('feedback.col.category')}:
{/* Rating Filter */}
{t('feedback.col.rating')}:
{/* Status Filter */}
{t('feedback.col.status')}:
{/* Search query */}
{ setFeedbackSearchQuery(e.target.value); setFeedbackCurrentPage(1); }} placeholder={t('common.search')} className="hu-input" style={{ height: '100%', fontSize: '13px' }} />
{/* Feedbacks List */}
{paginatedFeedbacks.length === 0 ? ( ) : ( paginatedFeedbacks.map((fb, index) => { const dateObj = new Date(fb.created_at); const isInvalidDate = isNaN(dateObj.getTime()); const formattedTime = isInvalidDate ? '—' : dateObj.toTimeString().split(' ')[0]; const formattedDate = isInvalidDate ? '—' : dateObj.toLocaleDateString(lang === 'vi' ? 'vi-VN' : 'en-US', { month: 'short', day: 'numeric', year: 'numeric' }); const safeRating = Math.max(0, Math.min(5, Math.floor(Number(fb.rating) || 0))); const rowKey = fb.id || `fb_${fb.created_at || index}_${fb.user_id || index}_${index}`; return ( ); }) )}
{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)}
{/* Pagination Controls with sliding window protection */} {totalPages > 1 && (
{lang === 'vi' ? `Hiển thị ${startIndex + 1}-${Math.min(endIndex, totalItems)} trong tổng số ${totalItems} kết quả` : `Showing ${startIndex + 1}-${Math.min(endIndex, totalItems)} of ${totalItems} results`}
{totalPages <= 7 ? ( Array.from({ length: totalPages }, (_, i) => i + 1).map(p => ( )) ) : ( <> {safeCurrentPage > 3 && …} {Array.from({ length: totalPages }, (_, i) => i + 1) .filter(p => p !== 1 && p !== totalPages && Math.abs(p - safeCurrentPage) <= 1) .map(p => ( ))} {safeCurrentPage < totalPages - 2 && …} )}
)}
); } window.CpFeedbackView = { renderFeedback, };