515 lines
18 KiB
React
515 lines
18 KiB
React
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import { Table, Tabs, Tag, Button, Input, DatePicker, Space, message, Modal, Descriptions } from 'antd';
|
||
|
|
import { SearchOutlined, DownloadOutlined, ReloadOutlined, TeamOutlined, PlayCircleOutlined, CalendarOutlined } from '@ant-design/icons';
|
||
|
|
import { useDispatch } from 'react-redux';
|
||
|
|
import { getAdminPanel } from '../../features/AdminPanel/AdminPanel';
|
||
|
|
import '../Styles/WebinarSubmissionsViewer.scss';
|
||
|
|
|
||
|
|
const { TabPane } = Tabs;
|
||
|
|
const { RangePicker } = DatePicker;
|
||
|
|
|
||
|
|
const WebinarSubmissionsViewer = () => {
|
||
|
|
const dispatch = useDispatch();
|
||
|
|
const [registrations, setRegistrations] = useState([]);
|
||
|
|
const [recordingAccess, setRecordingAccess] = useState([]);
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [searchText, setSearchText] = useState('');
|
||
|
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||
|
|
const [selectedRecord, setSelectedRecord] = useState(null);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
loadData();
|
||
|
|
}, [dispatch]);
|
||
|
|
|
||
|
|
const loadData = async () => {
|
||
|
|
setLoading(true);
|
||
|
|
try {
|
||
|
|
const response = await dispatch(getAdminPanel()).unwrap();
|
||
|
|
|
||
|
|
// Filter registrations
|
||
|
|
const registrationData = response.filter(item =>
|
||
|
|
item.SectionName && item.SectionName.startsWith('WebinarRegistrations_')
|
||
|
|
);
|
||
|
|
|
||
|
|
// Filter recording access
|
||
|
|
const recordingData = response.filter(item =>
|
||
|
|
item.SectionName && item.SectionName.startsWith('WebinarRecordingAccess_')
|
||
|
|
);
|
||
|
|
|
||
|
|
// Transform registrations
|
||
|
|
const transformedRegistrations = registrationData.map((item, index) => {
|
||
|
|
let content = {};
|
||
|
|
try {
|
||
|
|
const contentStr = item.Content || item.SectionContent || '{}';
|
||
|
|
content = JSON.parse(contentStr);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Error parsing content:', e);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
key: item.SectionId || index,
|
||
|
|
id: item.SectionId,
|
||
|
|
sectionName: item.SectionName,
|
||
|
|
sectionHdr: item.SectionHdr,
|
||
|
|
sectionDesc: item.SectionDesc,
|
||
|
|
createdAt: item.CreatedDate || new Date().toISOString(),
|
||
|
|
...content,
|
||
|
|
rawData: item
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
// Transform recording access
|
||
|
|
const transformedRecording = recordingData.map((item, index) => {
|
||
|
|
let content = {};
|
||
|
|
try {
|
||
|
|
const contentString = item.Content || item.SectionContent || '{}';
|
||
|
|
content = JSON.parse(contentString);
|
||
|
|
} catch (e) {
|
||
|
|
console.error('Error parsing content:', e);
|
||
|
|
}
|
||
|
|
|
||
|
|
return {
|
||
|
|
key: item.SectionId || index,
|
||
|
|
id: item.SectionId,
|
||
|
|
...content,
|
||
|
|
createdAt: item.CreatedDate,
|
||
|
|
rawData: item
|
||
|
|
};
|
||
|
|
});
|
||
|
|
|
||
|
|
setRegistrations(transformedRegistrations);
|
||
|
|
setRecordingAccess(transformedRecording);
|
||
|
|
setLoading(false);
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error loading data:', error);
|
||
|
|
message.error('Failed to load webinar submissions');
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleRefresh = () => {
|
||
|
|
loadData();
|
||
|
|
message.success('Data refreshed successfully');
|
||
|
|
};
|
||
|
|
|
||
|
|
// Registration columns
|
||
|
|
const registrationColumns = [
|
||
|
|
{
|
||
|
|
title: 'ID',
|
||
|
|
dataIndex: 'id',
|
||
|
|
key: 'id',
|
||
|
|
width: 60,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Submitted By',
|
||
|
|
dataIndex: 'sectionDesc',
|
||
|
|
key: 'sectionDesc',
|
||
|
|
width: 200,
|
||
|
|
render: (text) => {
|
||
|
|
// Extract name from "Registration from [Name]"
|
||
|
|
const match = text?.match(/Registration from (.+)/);
|
||
|
|
return match ? match[1] : text || '-';
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Type',
|
||
|
|
dataIndex: 'sectionHdr',
|
||
|
|
key: 'sectionHdr',
|
||
|
|
width: 150,
|
||
|
|
render: (text) => <Tag color="blue">{text}</Tag>,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Submitted At',
|
||
|
|
dataIndex: 'createdAt',
|
||
|
|
key: 'createdAt',
|
||
|
|
render: (text) => text ? new Date(text).toLocaleString('en-IN', {
|
||
|
|
year: 'numeric',
|
||
|
|
month: 'short',
|
||
|
|
day: 'numeric',
|
||
|
|
hour: '2-digit',
|
||
|
|
minute: '2-digit'
|
||
|
|
}) : '-',
|
||
|
|
sorter: (a, b) => new Date(a.createdAt) - new Date(b.createdAt),
|
||
|
|
width: 180,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'WhatsApp',
|
||
|
|
key: 'whatsapp',
|
||
|
|
width: 120,
|
||
|
|
render: (_, record) => {
|
||
|
|
try {
|
||
|
|
let contentStr = record.rawData?.Content || record.rawData?.SectionContent || '{}';
|
||
|
|
|
||
|
|
// Fallback to HomePageDetails
|
||
|
|
if (contentStr === '{}' && record.rawData?.HomePageDetails?.length > 0) {
|
||
|
|
const detail = record.rawData.HomePageDetails.find(d => d.DetailHdr === 'FormData');
|
||
|
|
if (detail) contentStr = detail.DetailDesc;
|
||
|
|
}
|
||
|
|
|
||
|
|
const content = JSON.parse(contentStr);
|
||
|
|
return content.whatsapp || <span style={{ color: '#ccc' }}>-</span>;
|
||
|
|
} catch (e) {
|
||
|
|
return <span style={{ color: 'red' }}>Error</span>;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Location',
|
||
|
|
key: 'location',
|
||
|
|
width: 150,
|
||
|
|
render: (_, record) => {
|
||
|
|
try {
|
||
|
|
let contentStr = record.rawData?.Content || record.rawData?.SectionContent || '{}';
|
||
|
|
|
||
|
|
// Fallback to HomePageDetails
|
||
|
|
if (contentStr === '{}' && record.rawData?.HomePageDetails?.length > 0) {
|
||
|
|
const detail = record.rawData.HomePageDetails.find(d => d.DetailHdr === 'FormData');
|
||
|
|
if (detail) contentStr = detail.DetailDesc;
|
||
|
|
}
|
||
|
|
|
||
|
|
const content = JSON.parse(contentStr);
|
||
|
|
const loc = [content.city, content.state].filter(Boolean).join(', ');
|
||
|
|
return loc || <span style={{ color: '#ccc' }}>-</span>;
|
||
|
|
} catch (e) {
|
||
|
|
return '-';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Form Data',
|
||
|
|
key: 'formData',
|
||
|
|
render: (_, record) => {
|
||
|
|
// Try to parse and show key fields
|
||
|
|
let content = {};
|
||
|
|
try {
|
||
|
|
let contentStr = record.rawData?.Content || record.rawData?.SectionContent || '{}';
|
||
|
|
|
||
|
|
// Fallback to HomePageDetails
|
||
|
|
if (contentStr === '{}' && record.rawData?.HomePageDetails?.length > 0) {
|
||
|
|
const detail = record.rawData.HomePageDetails.find(d => d.DetailHdr === 'FormData');
|
||
|
|
if (detail) contentStr = detail.DetailDesc;
|
||
|
|
}
|
||
|
|
|
||
|
|
content = JSON.parse(contentStr);
|
||
|
|
} catch (e) {
|
||
|
|
// Ignore parse errors
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ fontSize: 12 }}>
|
||
|
|
{content.email && <div><strong>Email:</strong> {content.email}</div>}
|
||
|
|
{content.storeName && <div><strong>Store:</strong> {content.storeName}</div>}
|
||
|
|
{content.outlets && <div><strong>Outlets:</strong> {content.outlets}</div>}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
},
|
||
|
|
width: 200,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Actions',
|
||
|
|
key: 'actions',
|
||
|
|
render: (_, record) => (
|
||
|
|
<Space>
|
||
|
|
<Button
|
||
|
|
size="small"
|
||
|
|
type="primary"
|
||
|
|
onClick={() => {
|
||
|
|
console.log('Viewing record:', record); // Debug log
|
||
|
|
setSelectedRecord(record);
|
||
|
|
setIsModalVisible(true);
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
View Full Details
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
),
|
||
|
|
width: 150,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// Recording access columns
|
||
|
|
const recordingAccessColumns = [
|
||
|
|
{
|
||
|
|
title: 'Full Name',
|
||
|
|
dataIndex: 'fullName',
|
||
|
|
key: 'fullName',
|
||
|
|
filteredValue: searchText ? [searchText] : null,
|
||
|
|
onFilter: (value, record) => {
|
||
|
|
return (
|
||
|
|
record.fullName?.toLowerCase().includes(value.toLowerCase()) ||
|
||
|
|
record.email?.toLowerCase().includes(value.toLowerCase())
|
||
|
|
);
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Email',
|
||
|
|
dataIndex: 'email',
|
||
|
|
key: 'email',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Store Name',
|
||
|
|
dataIndex: 'storeName',
|
||
|
|
key: 'storeName',
|
||
|
|
render: (text) => text || '-',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'UTM Source',
|
||
|
|
dataIndex: 'utm_source',
|
||
|
|
key: 'utm_source',
|
||
|
|
render: (text) => text || '-',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'UTM Campaign',
|
||
|
|
dataIndex: 'utm_campaign',
|
||
|
|
key: 'utm_campaign',
|
||
|
|
render: (text) => text || '-',
|
||
|
|
},
|
||
|
|
{
|
||
|
|
title: 'Submitted At',
|
||
|
|
dataIndex: 'submittedAt',
|
||
|
|
key: 'submittedAt',
|
||
|
|
render: (text) => new Date(text).toLocaleString('en-IN'),
|
||
|
|
sorter: (a, b) => new Date(a.submittedAt) - new Date(b.submittedAt),
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
// Export to CSV
|
||
|
|
const exportToCSV = (data, filename) => {
|
||
|
|
if (!data || data.length === 0) {
|
||
|
|
message.warning('No data to export');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const headers = Object.keys(data[0]).filter(key => !['key', 'rawData'].includes(key));
|
||
|
|
const csvContent = [
|
||
|
|
headers.join(','),
|
||
|
|
...data.map(row =>
|
||
|
|
headers.map(header => {
|
||
|
|
const value = row[header];
|
||
|
|
if (Array.isArray(value)) return `"${value.join('; ')}"`;
|
||
|
|
if (typeof value === 'object') return `"${JSON.stringify(value)}"`;
|
||
|
|
return `"${value || ''}"`;
|
||
|
|
}).join(',')
|
||
|
|
)
|
||
|
|
].join('\n');
|
||
|
|
|
||
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||
|
|
const link = document.createElement('a');
|
||
|
|
const url = URL.createObjectURL(blob);
|
||
|
|
link.setAttribute('href', url);
|
||
|
|
link.setAttribute('download', `${filename}_${new Date().toISOString().split('T')[0]}.csv`);
|
||
|
|
link.style.visibility = 'hidden';
|
||
|
|
document.body.appendChild(link);
|
||
|
|
link.click();
|
||
|
|
document.body.removeChild(link);
|
||
|
|
message.success('Exported successfully!');
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="webinar-submissions-viewer">
|
||
|
|
<div className="viewer-header">
|
||
|
|
<div className="header-left">
|
||
|
|
<h2>Webinar Submissions</h2>
|
||
|
|
<p>View and manage all webinar registrations and recording access requests</p>
|
||
|
|
</div>
|
||
|
|
<div className="header-actions">
|
||
|
|
<Button
|
||
|
|
icon={<ReloadOutlined />}
|
||
|
|
onClick={handleRefresh}
|
||
|
|
loading={loading}
|
||
|
|
>
|
||
|
|
Refresh
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Stats Cards */}
|
||
|
|
<div className="stats-section">
|
||
|
|
<div className="stat-card">
|
||
|
|
<div className="stat-icon primary">
|
||
|
|
<TeamOutlined />
|
||
|
|
</div>
|
||
|
|
<div className="stat-value">{registrations.length}</div>
|
||
|
|
<div className="stat-label">Total Registrations</div>
|
||
|
|
</div>
|
||
|
|
<div className="stat-card">
|
||
|
|
<div className="stat-icon success">
|
||
|
|
<PlayCircleOutlined />
|
||
|
|
</div>
|
||
|
|
<div className="stat-value">{recordingAccess.length}</div>
|
||
|
|
<div className="stat-label">Recording Requests</div>
|
||
|
|
</div>
|
||
|
|
<div className="stat-card">
|
||
|
|
<div className="stat-icon info">
|
||
|
|
<CalendarOutlined />
|
||
|
|
</div>
|
||
|
|
<div className="stat-value">
|
||
|
|
{registrations.length > 0
|
||
|
|
? new Date(registrations[registrations.length - 1]?.createdAt).toLocaleDateString('en-IN', { month: 'short', day: 'numeric' })
|
||
|
|
: '-'
|
||
|
|
}
|
||
|
|
</div>
|
||
|
|
<div className="stat-label">Latest Submission</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<Tabs defaultActiveKey="1">
|
||
|
|
<TabPane
|
||
|
|
tab={`Registrations (${registrations.length})`}
|
||
|
|
key="1"
|
||
|
|
>
|
||
|
|
<Space style={{ marginBottom: 16 }}>
|
||
|
|
<Input
|
||
|
|
placeholder="Search by name, email, or store"
|
||
|
|
prefix={<SearchOutlined />}
|
||
|
|
value={searchText}
|
||
|
|
onChange={(e) => setSearchText(e.target.value)}
|
||
|
|
style={{ width: 300 }}
|
||
|
|
/>
|
||
|
|
<Button
|
||
|
|
icon={<DownloadOutlined />}
|
||
|
|
onClick={() => exportToCSV(registrations, 'webinar_registrations')}
|
||
|
|
>
|
||
|
|
Export CSV
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
|
||
|
|
<Table
|
||
|
|
columns={registrationColumns}
|
||
|
|
dataSource={registrations}
|
||
|
|
loading={loading}
|
||
|
|
scroll={{ x: 1500 }}
|
||
|
|
pagination={{
|
||
|
|
pageSize: 10,
|
||
|
|
showSizeChanger: true,
|
||
|
|
showTotal: (total) => `Total ${total} registrations`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</TabPane>
|
||
|
|
|
||
|
|
<TabPane
|
||
|
|
tab={`Recording Access (${recordingAccess.length})`}
|
||
|
|
key="2"
|
||
|
|
>
|
||
|
|
<Space style={{ marginBottom: 16 }}>
|
||
|
|
<Input
|
||
|
|
placeholder="Search by name or email"
|
||
|
|
prefix={<SearchOutlined />}
|
||
|
|
value={searchText}
|
||
|
|
onChange={(e) => setSearchText(e.target.value)}
|
||
|
|
style={{ width: 300 }}
|
||
|
|
/>
|
||
|
|
<Button
|
||
|
|
icon={<DownloadOutlined />}
|
||
|
|
onClick={() => exportToCSV(recordingAccess, 'recording_access_requests')}
|
||
|
|
>
|
||
|
|
Export CSV
|
||
|
|
</Button>
|
||
|
|
</Space>
|
||
|
|
|
||
|
|
<Table
|
||
|
|
columns={recordingAccessColumns}
|
||
|
|
dataSource={recordingAccess}
|
||
|
|
loading={loading}
|
||
|
|
scroll={{ x: 1200 }}
|
||
|
|
pagination={{
|
||
|
|
pageSize: 10,
|
||
|
|
showSizeChanger: true,
|
||
|
|
showTotal: (total) => `Total ${total} requests`,
|
||
|
|
}}
|
||
|
|
/>
|
||
|
|
</TabPane>
|
||
|
|
</Tabs>
|
||
|
|
|
||
|
|
{/* Details Modal */}
|
||
|
|
<Modal
|
||
|
|
title="Registration Details"
|
||
|
|
open={isModalVisible}
|
||
|
|
onCancel={() => setIsModalVisible(false)}
|
||
|
|
footer={[
|
||
|
|
<Button key="close" onClick={() => setIsModalVisible(false)}>
|
||
|
|
Close
|
||
|
|
</Button>
|
||
|
|
]}
|
||
|
|
width={800}
|
||
|
|
>
|
||
|
|
{selectedRecord && (
|
||
|
|
<Descriptions bordered column={1} size="small">
|
||
|
|
<Descriptions.Item label="ID">{selectedRecord.id}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Section Name">{selectedRecord.sectionName}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Type">{selectedRecord.sectionHdr}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Description">{selectedRecord.sectionDesc}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Submitted At">
|
||
|
|
{selectedRecord.createdAt ? new Date(selectedRecord.createdAt).toLocaleString('en-IN') : '-'}
|
||
|
|
</Descriptions.Item>
|
||
|
|
|
||
|
|
{(() => {
|
||
|
|
try {
|
||
|
|
let contentStr = selectedRecord.rawData?.Content || selectedRecord.rawData?.SectionContent || '{}';
|
||
|
|
|
||
|
|
// Fallback to HomePageDetails
|
||
|
|
if (contentStr === '{}' && selectedRecord.rawData?.HomePageDetails?.length > 0) {
|
||
|
|
const detail = selectedRecord.rawData.HomePageDetails.find(d => d.DetailHdr === 'FormData');
|
||
|
|
if (detail) contentStr = detail.DetailDesc;
|
||
|
|
}
|
||
|
|
|
||
|
|
const content = JSON.parse(contentStr);
|
||
|
|
|
||
|
|
// If Content has data, show it
|
||
|
|
if (Object.keys(content).length > 0) {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<Descriptions.Item label="Full Name">{content.fullName || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Email">{content.email || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Store Name">{content.storeName || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="City">{content.city || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="State">{content.state || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="WhatsApp">{content.whatsapp || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Outlets">{content.outlets || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Current POS">{content.currentPOS || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Problems">
|
||
|
|
{content.problems && Array.isArray(content.problems)
|
||
|
|
? content.problems.join(', ')
|
||
|
|
: '-'}
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Language">{content.language || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="UTM Source">{content.utm_source || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="UTM Medium">{content.utm_medium || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="UTM Campaign">{content.utm_campaign || '-'}</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Page URL">{content.pageUrl || '-'}</Descriptions.Item>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<Descriptions.Item label="Form Data">
|
||
|
|
<span style={{ color: '#999' }}>No form data available in Content field</span>
|
||
|
|
</Descriptions.Item>
|
||
|
|
<Descriptions.Item label="Raw Data Debug">
|
||
|
|
<pre style={{ fontSize: '10px', maxHeight: '200px', overflow: 'auto' }}>
|
||
|
|
{JSON.stringify(selectedRecord.rawData, null, 2)}
|
||
|
|
</pre>
|
||
|
|
</Descriptions.Item>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
return (
|
||
|
|
<Descriptions.Item label="Form Data">
|
||
|
|
<span style={{ color: 'red' }}>Error parsing form data</span>
|
||
|
|
<pre style={{ fontSize: '10px', maxHeight: '200px', overflow: 'auto' }}>
|
||
|
|
{JSON.stringify(selectedRecord.rawData, null, 2)}
|
||
|
|
</pre>
|
||
|
|
</Descriptions.Item>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
})()}
|
||
|
|
</Descriptions>
|
||
|
|
)}
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default WebinarSubmissionsViewer;
|