import React, { useState, useEffect, useRef } from 'react'; // Load necessary CDN libraries if they aren't already present const loadScript = (src) => new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = src; s.async = true; s.onload = resolve; s.onerror = () => reject(new Error(`Failed to load ${src}`)); document.head.appendChild(s); }); // A hook to handle loading external scripts const useExternalScripts = (scripts) => { const [loaded, setLoaded] = useState(false); useEffect(() => { const loaders = scripts.filter(src => { if (src.includes('react@18') && window.React) return false; if (src.includes('react-dom@18') && window.ReactDOM) return false; if (src.includes('echarts') && window.echarts) return false; if (src.includes('papaparse') && window.Papa) return false; if (src.includes('xlsx') && window.XLSX) return false; if (src.includes('html2canvas') && window.html2canvas) return false; if (src.includes('jspdf') && window.jspdf) return false; return true; }).map(loadScript); if (loaders.length > 0) { Promise.all(loaders).then(() => setLoaded(true)).catch(err => console.error('CDN load error:', err)); } else { setLoaded(true); } }, [scripts]); return loaded; }; // Main App Component const App = () => { // State for file uploads and control const [zohoFile, setZohoFile] = useState(null); const [imamRosterFile, setImamRosterFile] = useState(null); const [selectedMonth, setSelectedMonth] = useState(''); const [availableMonths, setAvailableMonths] = useState([]); const [includeCSAT, setIncludeCSAT] = useState(false); const [good, setGood] = useState(0); const [okay, setOkay] = useState(0); const [bad, setBad] = useState(0); const [reportData, setReportData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const scripts = [ 'https://unpkg.com/react@18/umd/react.development.js', 'https://unpkg.com/react-dom@18/umd/react-dom.development.js', 'https://cdn.jsdelivr.net/npm/echarts@5.4.1/dist/echarts.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js', 'https://cdn.jsdelivr.net/npm/html2canvas@1.4.1/dist/html2canvas.min.js', 'https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js', ]; const scriptsLoaded = useExternalScripts(scripts); // Color definitions for the theme const brandPrimary = '#BD1F5B'; const brandSecondary = '#597585'; const darkGray = '#111827'; const lighterGray = '#374151'; const offWhite = '#f9fafb'; const textColor = '#e5e7eb'; // Helper functions (unchanged from original code) const pad2 = (n) => n.toString().padStart(2, '0'); const formatDuration = (sec) => { if (typeof sec !== 'number' || isNaN(sec)) return 'N/A'; const h = Math.floor(sec / 3600); const m = Math.floor((sec % 3600) / 60); return `${pad2(h)} hours ${pad2(m)} mins`; }; const cleanName = (name) => { if (typeof name !== 'string') return ''; let cleaned = name.split(/\s+/).filter(w => w.length > 1 || !/^[a-zA-Z]\.$/.test(w)).join(' '); cleaned = cleaned.replace(/[^a-zA-Z\s]/g, ''); return cleaned.trim().toLowerCase(); }; const getWeekOfMonth = (date) => { const first = new Date(date.getFullYear(), date.getMonth(), 1); const adj = date.getDate() + first.getDay(); return Math.floor(adj / 7) + ((adj % 7) > 0 ? 1 : 0); }; const mapCountryToTickets = (tickets, roster) => tickets.map(t => { const cleanedContactName = cleanName(t['Contact Name']); const matched = roster.find(i => cleanName(i['Imam Name']) === cleanedContactName); return { ...t, 'Cleaned Contact Name': cleanedContactName, 'Mapped Country': matched && matched['Country'] ? matched['Country'] : 'Unknown', 'Original Imam Name Roster': matched && matched['Imam Name'] ? matched['Imam Name'] : 'Unknown', 'Mapped Imam Number': matched && matched['Imam Number'] ? matched['Imam Number'] : 'N/A' }; }); const calcCSAT = (g, o, b) => { const t = g + o + b; if (!t) return 'No data available'; const score = (((g * 5) + (o * 3) + (b * 1)) / t).toFixed(2); return `${score} (Good: ${g}, Okay: ${o}, Bad: ${b})`; }; const analyzeData = (dfTicketsMapped, targetMonthStart, csat) => { let dfTickets = dfTicketsMapped.map(ticket => { const createdTime = ticket['Created Time'] ? new Date(ticket['Created Time']) : null; const ticketClosedTime = ticket['Ticket Closed Time'] ? new Date(ticket['Ticket Closed Time']) : null; const timeToRespond = ticket['Time to Respond'] ? new Date(ticket['Time to Respond']) : null; let timeDifferenceSeconds = null; if (createdTime && timeToRespond && !isNaN(createdTime) && !isNaN(timeToRespond)) { timeDifferenceSeconds = (timeToRespond.getTime() - createdTime.getTime()) / 1000; } return { ...ticket, 'Created Time': createdTime, 'Ticket Closed Time': ticketClosedTime, 'Time to Respond': timeToRespond, 'Time Difference Seconds': timeDifferenceSeconds, 'Month': createdTime ? createdTime.getMonth() : null, 'Month_Name': createdTime ? createdTime.toLocaleString('default', { month: 'long' }) : null, 'Year': createdTime ? createdTime.getFullYear() : null, 'Week_of_Year': createdTime ? Math.ceil((createdTime - new Date(createdTime.getFullYear(), 0, 1)) / (1000 * 60 * 60 * 24 * 7)) : null, 'Week_of_Month': createdTime ? getWeekOfMonth(createdTime) : null }; }).filter(t => t['Created Time'] && !isNaN(t['Created Time'])); const yearlyData = { total_queries: dfTickets.length, query_type_breakdown: Object.entries(dfTickets.reduce((a, t) => { const c = t['Classification'] || 'Unclassified'; a[c] = (a[c] || 0) + 1; return a; }, {})), busiest_week: {}, popular_query_hours: Object.entries(dfTickets.reduce((a, t) => { const h = t['Created Time'] ? t['Created Time'].getHours() : 'Unknown'; a[h] = (a[h] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]), within_24_hours: 0, over_24_hours: 0, weekend_tickets: 0, avg_response_time: 0, tickets_resolved: dfTickets.filter(t => t['Ticket Closed Time']).length, customer_satisfaction: csat, monthly_trend: Object.entries(dfTickets.reduce((a, t) => { if (t['Created Time']) { const d = t['Created Time']; const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`; a[key] = (a[key] || 0) + 1; } return a; }, {})).sort((a, b) => a[0].localeCompare(b[0])), reopening_rate: (dfTickets.filter(t => String(t['Reopened']).toLowerCase() === 'true').length / (dfTickets.length || 1)) * 100, first_contact_resolution_rate: (dfTickets.filter(t => String(t['First Contact Resolution']).toLowerCase() === 'true').length / (dfTickets.length || 1)) * 100, country_query_counts: Object.entries(dfTickets.reduce((a, t) => { const c = t['Mapped Country'] || 'Unknown'; a[c] = (a[c] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]), top_5_imams_overall: [] }; const byWeekYear = dfTickets.reduce((a, t) => { if (t['Week_of_Year'] != null) a[t['Week_of_Year']] = (a[t['Week_of_Year']] || 0) + 1; return a; }, {}); if (Object.keys(byWeekYear).length) { const maxW = Object.keys(byWeekYear).reduce((x, y) => byWeekYear[x] > byWeekYear[y] ? x : y); yearlyData.busiest_week = { [`Week ${maxW}`]: byWeekYear[maxW] }; } else yearlyData.busiest_week = { 'No data available': 0 }; let sumRTY = 0, cntRTY = 0; dfTickets.forEach(t => { if (t['Created Time'] && t['Time to Respond'] && t['Time Difference Seconds'] != null) { sumRTY += t['Time Difference Seconds']; cntRTY++; const d = t['Created Time']; if (d.getDay() === 6 || d.getDay() === 0 || (d.getDay() === 5 && d.getHours() >= 13) || (d.getDay() === 1 && d.getHours() < 8)) yearlyData.weekend_tickets++; if (t['Time Difference Seconds'] <= 24 * 3600) yearlyData.within_24_hours++; else yearlyData.over_24_hours++; } }); yearlyData.avg_response_time = cntRTY ? (sumRTY / cntRTY) : 0; const imamAggY = dfTickets.reduce((a, t) => { const key = `${t['Original Imam Name Roster']||'Unknown Imam'}|${t['Mapped Country']||'Unknown Country'}|${t['Mapped Imam Number']||'N/A'}`; a[key] = (a[key] || 0) + 1; return a; }, {}); yearlyData.top_5_imams_overall = Object.entries(imamAggY).map(([k, count]) => { const [name, country, number] = k.split('|'); return { 'Original Imam Name Roster': name, 'Mapped Country': country, 'Mapped Imam Number': number, 'Query Count': count }; }).sort((a, b) => b['Query Count'] - a['Query Count']).slice(0, 5); let monthSet = dfTickets; if (targetMonthStart) { const end = new Date(targetMonthStart.getFullYear(), targetMonthStart.getMonth() + 1, 1); monthSet = dfTickets.filter(t => t['Created Time'] >= targetMonthStart && t['Created Time'] < end); } const monthlyData = { total_queries: monthSet.length, query_type_breakdown: Object.entries(monthSet.reduce((a, t) => { const c = t['Classification'] || 'Unclassified'; a[c] = (a[c] || 0) + 1; return a; }, {})), busiest_week: {}, popular_query_hours: Object.entries(monthSet.reduce((a, t) => { const h = t['Created Time'] ? t['Created Time'].getHours() : 'Unknown'; a[h] = (a[h] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]), within_24_hours: 0, over_24_hours: 0, weekend_tickets: 0, avg_response_time: 0, tickets_resolved: monthSet.filter(t => t['Ticket Closed Time']).length, customer_satisfaction: csat, monthly_trend: Object.entries(monthSet.reduce((a, t) => { if (t['Created Time']) { const d = t['Created Time']; const key = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}`; a[key] = (a[key] || 0) + 1; } return a; }, {})).sort((a, b) => a[0].localeCompare(b[0])), reopening_rate: (monthSet.filter(t => String(t['Reopened']).toLowerCase() === 'true').length / (monthSet.length || 1)) * 100, first_contact_resolution_rate: (monthSet.filter(t => String(t['First Contact Resolution']).toLowerCase() === 'true').length / (monthSet.length || 1)) * 100, country_query_counts: Object.entries(monthSet.reduce((a, t) => { const c = t['Mapped Country'] || 'Unknown'; a[c] = (a[c] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]), top_5_imams_overall: [] }; const byWeekMonth = monthSet.reduce((a, t) => { if (t['Week_of_Month'] != null) a[t['Week_of_Month']] = (a[t['Week_of_Month']] || 0) + 1; return a; }, {}); if (Object.keys(byWeekMonth).length) { const maxW = Object.keys(byWeekMonth).reduce((x, y) => byWeekMonth[x] > byWeekMonth[y] ? x : y); monthlyData.busiest_week = { [`Week ${maxW}`]: byWeekMonth[maxW] }; } else monthlyData.busiest_week = { 'No data available': 0 }; let sumRTM = 0, cntRTM = 0; monthSet.forEach(t => { if (t['Created Time'] && t['Time to Respond'] && t['Time Difference Seconds'] != null) { sumRTM += t['Time Difference Seconds']; cntRTM++; const d = t['Created Time']; if (d.getDay() === 6 || d.getDay() === 0 || (d.getDay() === 5 && d.getHours() >= 13) || (d.getDay() === 1 && d.getHours() < 8)) monthlyData.weekend_tickets++; if (t['Time Difference Seconds'] <= 24 * 3600) monthlyData.within_24_hours++; else monthlyData.over_24_hours++; } }); monthlyData.avg_response_time = cntRTM ? (sumRTM / cntRTM) : 0; const imamAggM = monthSet.reduce((a, t) => { const key = `${t['Original Imam Name Roster']||'Unknown Imam'}|${t['Mapped Country']||'Unknown Country'}|${t['Mapped Imam Number']||'N/A'}`; a[key] = (a[key] || 0) + 1; return a; }, {}); monthlyData.top_5_imams_overall = Object.entries(imamAggM).map(([k, count]) => { const [name, country, number] = k.split('|'); return { 'Original Imam Name Roster': name, 'Mapped Country': country, 'Mapped Imam Number': number, 'Query Count': count }; }).sort((a, b) => b['Query Count'] - a['Query Count']).slice(0, 5); return { overall_data_for_month: monthlyData, overall_data_for_year: yearlyData }; }; // ECharts component for bar charts const EChartsComponent = ({ id, data, title, color }) => { const ref = useRef(null); useEffect(() => { if (!ref.current || !window.echarts) return; const chart = window.echarts.init(ref.current, 'dark'); const labels = data.map(d => d.name); const values = data.map(d => d.value); chart.setOption({ backgroundColor: 'transparent', tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' }, textStyle: { fontSize: 14, color: offWhite }, formatter: (params) => { const p = params[0]; return `
{value}
| {header} | ))}||||
|---|---|---|---|---|
| {index + 1} | {item['Original Imam Name Roster']} | {item['Mapped Country']} | {item['Mapped Imam Number']} | {item['Query Count']} |
Analyze Zoho Desk data and Imam Roster performance with detailed reports.
{/* File Upload Section */}Total Queries: {reportData.overall_data_for_month.total_queries}
Total Queries: {reportData.overall_data_for_year.total_queries}