1276 lines
42 KiB
PHP
1276 lines
42 KiB
PHP
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
|
|
|
<div class="container">
|
|
<h2 class="h3 mb-4">Campaign View</h2>
|
|
|
|
<!-- Add Tree Navigation Buttons -->
|
|
<div class="btn-group mb-4" role="group" aria-label="Navigation Level">
|
|
<button type="button" class="btn btn-outline-primary mr-2" id="campaignLevelBtn" onclick="updateTable('campaign', null)" disabled>
|
|
Campaigns
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary mr-2" id="adsetLevelBtn" onclick="updateTable('adset', null)" disabled>
|
|
Ad Sets
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary mr-2" id="adLevelBtn" onclick="updateTable('ad', null)" disabled>
|
|
Ads
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filter Dropdowns -->
|
|
<div class="row mb-3">
|
|
<div class="col-12 col-md-3 mb-2 d-none">
|
|
<label class="small mb-1">Campaign</label>
|
|
<select class="form-control form-control-sm" id="campaignFilter">
|
|
<option value="">All Campaigns</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3 mb-2 d-none">
|
|
<label class="small mb-1">Ad Set</label>
|
|
<select class="form-control form-control-sm" id="adsetFilter">
|
|
<option value="">All Ad Sets</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3 mb-2 d-none">
|
|
<label class="small mb-1">Ad</label>
|
|
<select class="form-control form-control-sm" id="adFilter">
|
|
<option value="">All Ads</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-12 col-md-3 mb-2">
|
|
<label class="small mb-1">Date Range</label>
|
|
<div class="input-group">
|
|
<input type="date" class="form-control form-control-sm" id="startDateFilter">
|
|
<input type="date" class="form-control form-control-sm" id="endDateFilter">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Filters -->
|
|
<div class="active-filters mb-4">
|
|
<div class="d-flex flex-wrap" id="activeFilters">
|
|
<!-- Active filters will be inserted here via JavaScript -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Results Count -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<div class="small text-muted">
|
|
Showing <span id="entriesCount">0</span> entries
|
|
</div>
|
|
<div class="column-visibility-controls">
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" id="toggleColumnControls">
|
|
<i class="fas fa-eye"></i> Show/Hide Columns
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Data Table -->
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover table-striped" id="campaignDataTable">
|
|
<thead class="thead-light">
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Campaign Name</th>
|
|
<th>Ad Set Name</th>
|
|
<th>Ad Name</th>
|
|
<?php
|
|
if (!empty($data['campaign_data'])) {
|
|
$firstRow = reset($data['campaign_data']);
|
|
foreach ($firstRow as $key => $value) {
|
|
if (!in_array($key, ['date', 'campaign_name', 'ad_set_name', 'ad_name'])) {
|
|
echo '<th>' . ucwords(str_replace('_', ' ', $key)) . '</th>';
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- Data rows will be populated via JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- New slide-in panel for column visibility -->
|
|
<div id="slideInPanel" class="slide-in-panel">
|
|
<div class="panel-header">
|
|
<h5>Column Visibility</h5>
|
|
<button id="closePanel" class="close">×</button>
|
|
</div>
|
|
<div class="panel-body">
|
|
<!-- <div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="dateColumn" checked>
|
|
<label class="form-check-label" for="dateColumn">Date</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="campaignColumn" checked>
|
|
<label class="form-check-label" for="campaignColumn">Campaign Name</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="adsetColumn" checked>
|
|
<label class="form-check-label" for="adsetColumn">Ad Set Name</label>
|
|
</div>
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="adColumn" checked>
|
|
<label class="form-check-label" for="adColumn">Ad Name</label>
|
|
</div> -->
|
|
<?php
|
|
if (!empty($data['campaign_data'])) {
|
|
$firstRow = reset($data['campaign_data']);
|
|
foreach ($firstRow as $key => $value) {
|
|
if (!in_array($key, ['date', 'campaign_name', 'ad_set_name', 'ad_name'])) {
|
|
$columnId = str_replace('_', '', $key) . 'Column';
|
|
echo '<div class="form-check">';
|
|
echo '<input class="form-check-input" type="checkbox" id="' . $columnId . '" checked>';
|
|
echo '<label class="form-check-label" for="' . $columnId . '">' . ucwords(str_replace('_', ' ', $key)) . '</label>';
|
|
echo '</div>';
|
|
}
|
|
}
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const campaignData = <?php echo json_encode($data['campaign_data']); ?>;
|
|
const activeFilters = {};
|
|
|
|
// First, add this configuration object for aggregation formulas
|
|
const aggregationRules = {
|
|
'cpm': {
|
|
formula: (metrics) => metrics.amount_spent / (metrics.impressions / 1000),
|
|
dependencies: ['amount_spent', 'impressions']
|
|
},
|
|
'unique_outbound_click_ctr': {
|
|
formula: (metrics) => (metrics.unique_outbound_clicks / metrics.reach) * 100,
|
|
dependencies: ['unique_outbound_clicks', 'reach']
|
|
},
|
|
'cost_per_unique_outbound_click': {
|
|
formula: (metrics) => metrics.amount_spent / metrics.unique_outbound_clicks,
|
|
dependencies: ['amount_spent', 'unique_outbound_clicks']
|
|
},
|
|
'cpl': {
|
|
formula: (metrics) => metrics.amount_spent / metrics.new_leads,
|
|
dependencies: ['amount_spent', 'new_leads']
|
|
},
|
|
'optin_%': {
|
|
formula: (metrics) => (metrics.new_leads / metrics.unique_outbound_clicks) * 100,
|
|
dependencies: ['new_leads', 'unique_outbound_clicks']
|
|
},
|
|
'cpa': {
|
|
formula: (metrics) => metrics.amount_spent / metrics.appointments_booked,
|
|
dependencies: ['amount_spent', 'appointments_booked']
|
|
},
|
|
'appointments_booked_%': {
|
|
formula: (metrics) => (metrics.appointments_booked / metrics.new_leads) * 100,
|
|
dependencies: ['appointments_booked', 'new_leads']
|
|
},
|
|
'cps': {
|
|
formula: (metrics) => metrics.amount_spent / metrics.showed_appointments,
|
|
dependencies: ['amount_spent', 'showed_appointments']
|
|
},
|
|
'cpq': {
|
|
formula: (metrics) => metrics.amount_spent / metrics.qualified_appointments,
|
|
dependencies: ['amount_spent', 'qualified_appointments']
|
|
},
|
|
'cac': {
|
|
formula: (metrics) => metrics.amount_spent / metrics.sales,
|
|
dependencies: ['amount_spent', 'sales']
|
|
},
|
|
'cc_roi': {
|
|
formula: (metrics) => metrics.cash_collected / metrics.amount_spent,
|
|
dependencies: ['cash_collected', 'amount_spent']
|
|
},
|
|
'cv_roi': {
|
|
formula: (metrics) => metrics.contract_value / metrics.amount_spent,
|
|
dependencies: ['contract_value', 'amount_spent']
|
|
},
|
|
};
|
|
|
|
// Initialize filters with unique values
|
|
function initializeFilters() {
|
|
const filters = {
|
|
campaign_name: new Set(),
|
|
ad_set_name: new Set(),
|
|
ad_name: new Set(),
|
|
date: new Set()
|
|
};
|
|
|
|
campaignData.forEach(row => {
|
|
filters.campaign_name.add(row.campaign_name);
|
|
filters.ad_set_name.add(row.ad_set_name);
|
|
filters.ad_name.add(row.ad_name);
|
|
filters.date.add(row.date);
|
|
});
|
|
|
|
// Populate dropdowns
|
|
populateDropdown('campaignFilter', Array.from(filters.campaign_name));
|
|
populateDropdown('adsetFilter', Array.from(filters.ad_set_name));
|
|
populateDropdown('adFilter', Array.from(filters.ad_name));
|
|
populateDropdown('dateFilter', Array.from(filters.date));
|
|
|
|
// Set default date range to last 6 months
|
|
const today = new Date();
|
|
const sixMonthsAgo = new Date();
|
|
sixMonthsAgo.setMonth(today.getMonth() - 6);
|
|
|
|
// Format dates to YYYY-MM-DD
|
|
const formatDate = (date) => date.toISOString().split('T')[0];
|
|
|
|
console.log(formatDate(sixMonthsAgo));
|
|
document.getElementById('startDateFilter').value = formatDate(sixMonthsAgo);
|
|
document.getElementById('endDateFilter').value = formatDate(today);
|
|
|
|
// Trigger change events for date filters
|
|
document.getElementById('startDateFilter').dispatchEvent(new Event('change'));
|
|
document.getElementById('endDateFilter').dispatchEvent(new Event('change'));
|
|
}
|
|
|
|
function populateDropdown(id, values) {
|
|
const select = document.getElementById(id);
|
|
if (!select) return; // Check if the element exists
|
|
|
|
const currentValue = select.value;
|
|
|
|
// Clear existing options except first
|
|
select.innerHTML = '<option value="">All ' +
|
|
(id === 'campaignFilter' ? 'Campaigns' :
|
|
id === 'adsetFilter' ? 'Ad Sets' :
|
|
id === 'dateFilter' ? 'Dates' : 'Ads') + '</option>';
|
|
|
|
// Add new options
|
|
values.sort().forEach(value => {
|
|
const option = document.createElement('option');
|
|
option.value = value;
|
|
option.textContent = value;
|
|
select.appendChild(option);
|
|
});
|
|
|
|
// Restore selected value if it still exists
|
|
if (currentValue && values.includes(currentValue)) {
|
|
select.value = currentValue;
|
|
}
|
|
}
|
|
|
|
function aggregateDataByDate(filteredData) {
|
|
const aggregated = {};
|
|
|
|
filteredData.forEach(row => {
|
|
if (!aggregated[row.date]) {
|
|
aggregated[row.date] = {
|
|
date: row.date,
|
|
campaign_name: row.campaign_name,
|
|
ad_set_name: row.ad_set_name,
|
|
ad_name: row.ad_name,
|
|
_metrics: {} // Hidden property to store raw numbers
|
|
};
|
|
|
|
// Initialize other columns
|
|
Object.entries(row).forEach(([key, value]) => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
// Remove currency symbol and commas for numerical processing
|
|
let cleanValue = typeof value === 'string' ?
|
|
value.replace(/[$,]/g, '') :
|
|
value;
|
|
|
|
// Store the original format ($ or %) for later
|
|
aggregated[row.date][`${key}_format`] =
|
|
String(value).includes('$') ? '$' :
|
|
String(value).includes('%') ? '%' :
|
|
'';
|
|
|
|
// Store raw number in _metrics
|
|
aggregated[row.date]._metrics[key] = parseFloat(cleanValue) || 0;
|
|
}
|
|
});
|
|
} else {
|
|
// Sum up the numerical values
|
|
Object.entries(row).forEach(([key, value]) => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
let cleanValue = typeof value === 'string' ?
|
|
value.replace(/[$,]/g, '') :
|
|
value;
|
|
aggregated[row.date]._metrics[key] += parseFloat(cleanValue) || 0;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Calculate aggregates and format values
|
|
Object.values(aggregated).forEach(row => {
|
|
// First calculate special formulas
|
|
Object.entries(aggregationRules).forEach(([key, rule]) => {
|
|
// Check if all dependencies are available and non-zero
|
|
const canCalculate = rule.dependencies.every(dep =>
|
|
row._metrics[dep] !== undefined &&
|
|
row._metrics[dep] !== 0
|
|
);
|
|
|
|
if (canCalculate) {
|
|
row._metrics[key] = rule.formula(row._metrics);
|
|
} else {
|
|
row._metrics[key] = 0;
|
|
}
|
|
});
|
|
|
|
// Format all metrics for display
|
|
Object.entries(row._metrics).forEach(([key, value]) => {
|
|
console.log("metric key:", key)
|
|
// Skip if the key is internal
|
|
if (key.startsWith('_')) return;
|
|
|
|
// Determine format and precision
|
|
let formattedValue;
|
|
if (row[`${key}_format`] === '$') {
|
|
formattedValue = '$' + value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
} else if (row[`${key}_format`] === '%' || key.includes('ctr') || key.includes('percentage') || key.includes('%')) {
|
|
formattedValue = value.toFixed(2) + '%';
|
|
} else if (key === 'cpm' || key.includes('cost_per') || key === 'cpl' || key === 'cps' || key === 'cpq' || key === 'cac' || key ==='cpa' ) {
|
|
formattedValue = '$' + value.toFixed(2);
|
|
} else {
|
|
formattedValue = value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|
|
|
|
row[key] = formattedValue;
|
|
delete row[`${key}_format`]; // Clean up format helper property
|
|
});
|
|
|
|
// Clean up internal _metrics object
|
|
delete row._metrics;
|
|
});
|
|
|
|
return Object.values(aggregated);
|
|
}
|
|
|
|
function updateTableOld() {
|
|
const tbody = document.querySelector('#campaignDataTable tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
// Filter data from original dataset
|
|
const filteredData = campaignData.filter(row => {
|
|
const rowDate = new Date(row.date);
|
|
const startDate = activeFilters.start_date ? new Date(activeFilters.start_date) : null;
|
|
const endDate = activeFilters.end_date ? new Date(activeFilters.end_date) : null;
|
|
|
|
return (!activeFilters.campaign_name || row.campaign_name === activeFilters.campaign_name) &&
|
|
(!activeFilters.ad_set_name || row.ad_set_name === activeFilters.ad_set_name) &&
|
|
(!activeFilters.ad_name || row.ad_name === activeFilters.ad_name) &&
|
|
(!activeFilters.start_date || (startDate && rowDate >= startDate)) &&
|
|
(!activeFilters.end_date || (endDate && rowDate <= endDate));
|
|
});
|
|
|
|
// Group data by date
|
|
const groupedByDate = {};
|
|
filteredData.forEach(row => {
|
|
if (!groupedByDate[row.date]) {
|
|
groupedByDate[row.date] = [];
|
|
}
|
|
groupedByDate[row.date].push(row);
|
|
});
|
|
|
|
// Update total entries count
|
|
document.getElementById('entriesCount').textContent = filteredData.length;
|
|
|
|
let previousDate = null;
|
|
|
|
// Display data grouped by date with aggregates
|
|
Object.entries(groupedByDate).forEach(([date, rows]) => {
|
|
// First, show all individual rows
|
|
rows.forEach(row => {
|
|
const tr = document.createElement('tr');
|
|
const isSameDate = previousDate === row.date;
|
|
|
|
tr.innerHTML = `
|
|
<td class="small">${isSameDate ? '' : row.date}</td>
|
|
<td class="small clickable-cell" data-filter-type="campaign_name">${row.campaign_name}</td>
|
|
<td class="small clickable-cell" data-filter-type="ad_set_name">${row.ad_set_name}</td>
|
|
<td class="small clickable-cell" data-filter-type="ad_name">${row.ad_name}</td>
|
|
`;
|
|
|
|
// Add remaining columns
|
|
Object.entries(row).forEach(([key, value]) => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
tr.innerHTML += `<td class="small">${value}</td>`;
|
|
}
|
|
});
|
|
|
|
tbody.appendChild(tr);
|
|
previousDate = row.date;
|
|
});
|
|
|
|
// Then show aggregate row if there are multiple entries for this date
|
|
if (rows.length > 1) {
|
|
const aggregateRow = document.createElement('tr');
|
|
aggregateRow.className = 'aggregate-row';
|
|
|
|
// Initialize metrics object for aggregation
|
|
const metrics = {
|
|
_metrics: {}
|
|
};
|
|
|
|
// let titles = {};
|
|
|
|
// First pass: sum up all the raw values
|
|
rows.forEach(row => {
|
|
|
|
|
|
Object.entries(row).forEach(([key, value]) => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
if (!metrics._metrics[key]) {
|
|
metrics._metrics[key] = 0;
|
|
}
|
|
// Clean the value
|
|
const cleanValue = typeof value === 'string' ?
|
|
parseFloat(value.replace(/[$,%]/g, '')) :
|
|
parseFloat(value);
|
|
|
|
metrics._metrics[key] += cleanValue || 0;
|
|
}
|
|
|
|
|
|
|
|
});
|
|
});
|
|
|
|
// Second pass: apply special formulas
|
|
Object.entries(aggregationRules).forEach(([key, rule]) => {
|
|
const canCalculate = rule.dependencies.every(dep =>
|
|
metrics._metrics[dep] !== undefined &&
|
|
metrics._metrics[dep] !== 0
|
|
);
|
|
|
|
if (canCalculate) {
|
|
metrics._metrics[key] = rule.formula(metrics._metrics);
|
|
} else {
|
|
metrics._metrics[key] = 0;
|
|
}
|
|
});
|
|
|
|
// Create the aggregate row HTML
|
|
aggregateRow.innerHTML = `
|
|
<td class="small"></td>
|
|
<td class="small" data-filter-type="campaign_name"></td>
|
|
<td class="small" data-filter-type="ad_set_name"></td>
|
|
<td class="small" data-filter-type="ad_name"></td>
|
|
`;
|
|
|
|
// Add formatted values
|
|
Object.entries(metrics._metrics).forEach(([key, value]) => {
|
|
let formattedValue;
|
|
if (key.includes('amount_spent') || key === 'cpm' || key.includes('cost_per')
|
|
|| key === 'cpl' || key === 'cpl' || key === 'cps' || key === 'cpq'
|
|
|| key === 'cac' || key ==='cpa') {
|
|
formattedValue = '$' + value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
} else if (key.includes('ctr') || key.includes('%')) {
|
|
formattedValue = value.toFixed(2) + '%';
|
|
} else {
|
|
formattedValue = value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|
|
|
|
aggregateRow.innerHTML += `<td class="small font-weight-bold">${formattedValue}</td>`;
|
|
});
|
|
|
|
tbody.appendChild(aggregateRow);
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateActiveFilters() {
|
|
const container = document.getElementById('activeFilters');
|
|
container.innerHTML = '';
|
|
|
|
Object.entries(activeFilters).forEach(([key, value]) => {
|
|
if (value) {
|
|
const badge = document.createElement('div');
|
|
badge.className = 'badge badge-primary mr-2 mb-2';
|
|
badge.innerHTML = `
|
|
${value}
|
|
<button type="button" class="close ml-2"
|
|
onclick="removeFilter('${key}')">×</button>
|
|
`;
|
|
container.appendChild(badge);
|
|
}
|
|
});
|
|
}
|
|
|
|
function removeFilter(filterKey) {
|
|
activeFilters[filterKey] = null;
|
|
document.getElementById(filterKey === 'campaign_name' ? 'campaignFilter' :
|
|
filterKey === 'ad_set_name' ? 'adsetFilter' :
|
|
filterKey === 'date' ? 'dateFilter' : 'adFilter').value = '';
|
|
|
|
// When removing a filter, restore visibility of that column
|
|
switch(filterKey) {
|
|
case 'campaign_name':
|
|
columnVisibility.campaign_name = true;
|
|
document.getElementById('campaignColumn').checked = true;
|
|
break;
|
|
case 'ad_set_name':
|
|
columnVisibility.ad_set_name = true;
|
|
document.getElementById('adsetColumn').checked = true;
|
|
break;
|
|
case 'ad_name':
|
|
columnVisibility.ad_name = true;
|
|
document.getElementById('adColumn').checked = true;
|
|
break;
|
|
}
|
|
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility();
|
|
}
|
|
|
|
// Event Listeners
|
|
document.getElementById('campaignFilter').addEventListener('change', function(e) {
|
|
activeFilters.campaign_name = e.target.value || null;
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility();
|
|
});
|
|
|
|
document.getElementById('adsetFilter').addEventListener('change', function(e) {
|
|
activeFilters.ad_set_name = e.target.value || null;
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility();
|
|
});
|
|
|
|
document.getElementById('adFilter').addEventListener('change', function(e) {
|
|
activeFilters.ad_name = e.target.value || null;
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility();
|
|
});
|
|
|
|
document.getElementById('startDateFilter').addEventListener('change', function(e) {
|
|
activeFilters.start_date = e.target.value || null;
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility();
|
|
});
|
|
|
|
document.getElementById('endDateFilter').addEventListener('change', function(e) {
|
|
activeFilters.end_date = e.target.value || null;
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility();
|
|
});
|
|
|
|
// Add this new function after the existing functions
|
|
function handleCellClick(e) {
|
|
const cell = e.target;
|
|
if (!cell.classList.contains('clickable-cell-old')) return;
|
|
|
|
const filterType = cell.dataset.filterType;
|
|
const filterValue = cell.innerText;
|
|
|
|
// Update the corresponding dropdown
|
|
const dropdownId = filterType === 'campaign_name' ? 'campaignFilter' :
|
|
filterType === 'ad_set_name' ? 'adsetFilter' : 'adFilter';
|
|
|
|
document.getElementById(dropdownId).value = filterValue;
|
|
|
|
// Update active filters
|
|
activeFilters[filterType] = filterValue;
|
|
|
|
// Hide/show columns based on the clicked cell
|
|
columnVisibility.date = true; // Always keep date visible
|
|
switch (filterType) {
|
|
case 'campaign_name':
|
|
columnVisibility.campaign_name = false;
|
|
columnVisibility.ad_set_name = true; // Show Ad Set Name
|
|
columnVisibility.ad_name = false; // Hide Ad Name
|
|
break;
|
|
case 'ad_set_name':
|
|
columnVisibility.campaign_name = false; // Hide Campaign Name
|
|
columnVisibility.ad_set_name = false; // Show Ad Set Name
|
|
columnVisibility.ad_name = true; // Show Ad Name
|
|
break;
|
|
case 'ad_name':
|
|
columnVisibility.campaign_name = false; // Hide Campaign Name
|
|
columnVisibility.ad_set_name = false; // Hide Ad Set Name
|
|
columnVisibility.ad_name = false; // Hide Ad Name
|
|
break;
|
|
}
|
|
|
|
// Update checkboxes
|
|
document.getElementById('dateColumn').checked = true;
|
|
document.getElementById('campaignColumn').checked = columnVisibility.campaign_name;
|
|
document.getElementById('adsetColumn').checked = columnVisibility.ad_set_name;
|
|
document.getElementById('adColumn').checked = columnVisibility.ad_name;
|
|
|
|
updateActiveFilters();
|
|
updateTable();
|
|
updateColumnVisibility()
|
|
}
|
|
|
|
// Add this to your event listeners section
|
|
document.getElementById('campaignDataTable').addEventListener('click', handleCellClick);
|
|
|
|
// Call initializeFilters when the DOM is fully loaded
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initializeFilters();
|
|
updateTable('campaign'); // Update table on load
|
|
updateColumnVisibility(); // Update column visibility on load
|
|
});
|
|
|
|
// Column visibility state
|
|
const columnVisibility = {
|
|
date: false,
|
|
campaign_name: true,
|
|
ad_set_name: false, // Hide Ad Set Name by default
|
|
ad_name: false // Hide Ad Name by default
|
|
};
|
|
|
|
// Initialize additional column visibility
|
|
if (campaignData.length > 0) {
|
|
Object.keys(campaignData[0]).forEach(key => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
columnVisibility[key] = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Function to update column visibility
|
|
function updateColumnVisibility() {
|
|
const table = document.getElementById('campaignDataTable');
|
|
const headers = table.querySelectorAll('th');
|
|
|
|
// Create a mapping of column names to their indices
|
|
const columnIndices = {};
|
|
headers.forEach((header, index) => {
|
|
const columnName = header.textContent.toLowerCase().replace(/ /g, '_');
|
|
columnIndices[columnName] = index + 1;
|
|
header.style.display = columnVisibility[columnName] ? '' : 'none';
|
|
});
|
|
|
|
// Update visibility for all columns in main table
|
|
Object.entries(columnVisibility).forEach(([key, isVisible]) => {
|
|
const columnIndex = columnIndices[key];
|
|
if (columnIndex) {
|
|
table.querySelectorAll(`td:nth-child(${columnIndex}), th:nth-child(${columnIndex})`).forEach(cell => {
|
|
cell.style.display = isVisible ? '' : 'none';
|
|
});
|
|
}
|
|
});
|
|
|
|
// Update visibility for detail rows
|
|
document.querySelectorAll('.detail-row').forEach(detailRow => {
|
|
const detailTable = detailRow.querySelector('table');
|
|
if (detailTable) {
|
|
detailTable.querySelectorAll('tr').forEach(tr => {
|
|
Array.from(tr.children).forEach((cell, index) => {
|
|
const columnName = headers[index].textContent.toLowerCase().replace(/ /g, '_');
|
|
cell.style.display = columnVisibility[columnName] ? '' : 'none';
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
// Recalculate fixed column positions
|
|
updateFixedColumnPositions();
|
|
}
|
|
|
|
function updateFixedColumnPositions() {
|
|
const table = document.getElementById('campaignDataTable');
|
|
let currentLeft = 0;
|
|
|
|
// Only apply fixed positioning to the first 4 columns
|
|
for (let i = 1; i <= 4; i++) {
|
|
const cells = table.querySelectorAll(`td:nth-child(${i}), th:nth-child(${i})`);
|
|
if (cells.length && cells[0].style.display !== 'none') {
|
|
cells.forEach(cell => {
|
|
cell.style.left = `${currentLeft}px`;
|
|
});
|
|
currentLeft += cells[0].offsetWidth;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add event listeners for all column toggles
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Add listeners for the default columns
|
|
const defaultColumns = ['date', 'campaign', 'adset', 'ad'];
|
|
defaultColumns.forEach(col => {
|
|
const checkbox = document.getElementById(`${col}Column`);
|
|
if (checkbox) {
|
|
checkbox.addEventListener('change', function(e) {
|
|
const key = col === 'date' ? 'date' :
|
|
col === 'campaign' ? 'campaign_name' :
|
|
col === 'adset' ? 'ad_set_name' : 'ad_name';
|
|
columnVisibility[key] = e.target.checked;
|
|
updateColumnVisibility();
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add listeners for additional columns
|
|
if (campaignData.length > 0) {
|
|
Object.keys(campaignData[0]).forEach(key => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
const columnId = `${key.replace(/_/g, '')}Column`;
|
|
const checkbox = document.getElementById(columnId);
|
|
if (checkbox) {
|
|
checkbox.addEventListener('change', function(e) {
|
|
columnVisibility[key] = e.target.checked;
|
|
updateColumnVisibility();
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
// Add to your window load or document ready event
|
|
window.addEventListener('load', function() {
|
|
updateColumnVisibility();
|
|
});
|
|
|
|
document.getElementById('toggleColumnControls').addEventListener('click', function() {
|
|
const panel = document.getElementById('slideInPanel');
|
|
panel.classList.toggle('open');
|
|
});
|
|
|
|
document.getElementById('closePanel').addEventListener('click', function() {
|
|
const panel = document.getElementById('slideInPanel');
|
|
panel.classList.remove('open');
|
|
});
|
|
|
|
// Add these utility functions at the top of your script
|
|
function getUniqueValues(data, key) {
|
|
return [...new Set(data.map(item => item[key]))];
|
|
}
|
|
|
|
function filterByDateRange(data) {
|
|
return data.filter(row => {
|
|
const rowDate = new Date(row.date);
|
|
const startDate = activeFilters.start_date ? new Date(activeFilters.start_date) : null;
|
|
const endDate = activeFilters.end_date ? new Date(activeFilters.end_date) : null;
|
|
|
|
return (!startDate || rowDate >= startDate) &&
|
|
(!endDate || rowDate <= endDate);
|
|
});
|
|
}
|
|
|
|
function aggregateData(data, groupByKey) {
|
|
const aggregated = {};
|
|
|
|
data.forEach(row => {
|
|
const key = row[groupByKey];
|
|
if (!aggregated[key]) {
|
|
aggregated[key] = {
|
|
[groupByKey]: key,
|
|
_metrics: {}
|
|
};
|
|
}
|
|
|
|
// Sum up all numeric values
|
|
Object.entries(row).forEach(([field, value]) => {
|
|
if (!['date', 'campaign_name', 'ad_set_name', 'ad_name'].includes(field)) {
|
|
if (!aggregated[key]._metrics[field]) {
|
|
aggregated[key]._metrics[field] = 0;
|
|
}
|
|
const cleanValue = typeof value === 'string' ?
|
|
parseFloat(value.replace(/[$,%]/g, '')) :
|
|
parseFloat(value);
|
|
|
|
aggregated[key]._metrics[field] += cleanValue || 0;
|
|
}
|
|
});
|
|
});
|
|
|
|
// Calculate derived metrics using aggregationRules
|
|
Object.values(aggregated).forEach(item => {
|
|
Object.entries(aggregationRules).forEach(([key, rule]) => {
|
|
const canCalculate = rule.dependencies.every(dep =>
|
|
item._metrics[dep] !== undefined &&
|
|
item._metrics[dep] !== 0
|
|
);
|
|
|
|
if (canCalculate) {
|
|
item._metrics[key] = rule.formula(item._metrics);
|
|
} else {
|
|
item._metrics[key] = 0;
|
|
}
|
|
});
|
|
|
|
// Format values for display
|
|
Object.entries(item._metrics).forEach(([key, value]) => {
|
|
if (key.includes('amount_spent') || key === 'cpm' || key.includes('cost_per')
|
|
|| key === 'cpl' || key === 'cps' || key === 'cpq'
|
|
|| key === 'cac' || key ==='cpa') {
|
|
item[key] = '$' + value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
} else if (key.includes('ctr') || key.includes('%')) {
|
|
item[key] = value.toFixed(2) + '%';
|
|
} else {
|
|
item[key] = value.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
}
|
|
});
|
|
|
|
delete item._metrics;
|
|
});
|
|
|
|
return Object.values(aggregated);
|
|
}
|
|
|
|
function updateTable(level = 'campaign', selectedValue = null) {
|
|
const tbody = document.querySelector('#campaignDataTable tbody');
|
|
tbody.innerHTML = '';
|
|
|
|
// Make a copy and filter by date range
|
|
let filteredData = filterByDateRange([...campaignData]);
|
|
|
|
// Filter by selected values
|
|
if (selectedValue) {
|
|
switch(level) {
|
|
case 'campaign':
|
|
filteredData = filteredData.filter(row => row.campaign_name === selectedValue);
|
|
break;
|
|
case 'adset':
|
|
filteredData = filteredData.filter(row => row.campaign_name === selectedValue);
|
|
break;
|
|
case 'ad':
|
|
filteredData = filteredData.filter(row => row.ad_set_name === selectedValue);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Determine grouping key based on level
|
|
const groupByKey = level === 'campaign' ? 'campaign_name' :
|
|
level === 'adset' ? 'ad_set_name' : 'ad_name';
|
|
|
|
// Aggregate data
|
|
const aggregatedData = aggregateData(filteredData, groupByKey);
|
|
|
|
// Update entries count
|
|
document.getElementById('entriesCount').textContent = aggregatedData.length;
|
|
|
|
switch (level) {
|
|
case 'campaign':
|
|
columnVisibility.campaign_name = true;
|
|
columnVisibility.ad_set_name = false; // Show Ad Set Name
|
|
columnVisibility.ad_name = false; // Hide Ad Name
|
|
break;
|
|
case 'adset':
|
|
columnVisibility.campaign_name = false; // Hide Campaign Name
|
|
columnVisibility.ad_set_name = true; // Show Ad Set Name
|
|
columnVisibility.ad_name = false; // Show Ad Name
|
|
break;
|
|
case 'ad':
|
|
columnVisibility.campaign_name = false; // Hide Campaign Name
|
|
columnVisibility.ad_set_name = false; // Hide Ad Set Name
|
|
columnVisibility.ad_name = true; // Hide Ad Name
|
|
break;
|
|
}
|
|
|
|
|
|
// Display data
|
|
aggregatedData.forEach(row => {
|
|
const tr = document.createElement('tr');
|
|
tr.setAttribute('data-' + level, row[groupByKey]);
|
|
|
|
// Create expand/collapse button if not at ad level
|
|
const expandButton = level !== 'ad' ?
|
|
`<button class="btn btn-link btn-sm p-0" onclick="drillDown('${level}', '${row[groupByKey]}')">
|
|
<i class="fas fa-chevron-right"></i>
|
|
</button>` : '';
|
|
|
|
|
|
tr.innerHTML = `
|
|
<td class="small"></td>
|
|
<td onclick="drillDown('campaign', '${row[groupByKey]}')" class="small clickable-cell">
|
|
${level === 'campaign' ? row[groupByKey] : ''}
|
|
</td>
|
|
<td onclick="drillDown('adset', '${row[groupByKey]}')" class="small clickable-cell">
|
|
${level === 'adset' ? row[groupByKey] : ''}
|
|
</td>
|
|
<td class="small">
|
|
${level === 'ad' ? row[groupByKey] : ''}
|
|
</td>
|
|
`;
|
|
|
|
// Add metric columns
|
|
Object.entries(row).forEach(([key, value]) => {
|
|
if (!['campaign_name', 'ad_set_name', 'ad_name'].includes(key)) {
|
|
tr.innerHTML += `<td class="small">${value}</td>`;
|
|
}
|
|
});
|
|
|
|
tbody.appendChild(tr);
|
|
updateColumnVisibility();
|
|
});
|
|
}
|
|
|
|
function drillDown(currentLevel, selectedValue) {
|
|
console.log(currentLevel, selectedValue)
|
|
// Remove any existing expanded rows
|
|
document.querySelectorAll('.details-row').forEach(row => row.remove());
|
|
|
|
const nextLevel = currentLevel === 'campaign' ? 'adset' : 'ad';
|
|
|
|
updateTable(nextLevel, selectedValue);
|
|
updateNavigationButtons(currentLevel, selectedValue);
|
|
}
|
|
|
|
function updateNavigationButtons(level, selectedValue) {
|
|
const campaignLevelBtn = document.getElementById('campaignLevelBtn');
|
|
const adsetLevelBtn = document.getElementById('adsetLevelBtn');
|
|
const adLevelBtn = document.getElementById('adLevelBtn');
|
|
|
|
if (level === 'campaign') {
|
|
campaignLevelBtn.classList.add('active');
|
|
campaignLevelBtn.onClick = () => updateTable('campaign', null);
|
|
campaignLevelBtn.disabled = false;
|
|
|
|
adsetLevelBtn.onClick = () => drillDown('campaign', selectedValue);
|
|
adsetLevelBtn.classList.remove('active');
|
|
adsetLevelBtn.disabled = false;
|
|
|
|
adLevelBtn.disabled = true;
|
|
} else if (level === 'adset') {
|
|
// campaignLevelBtn.classList.remove('active');
|
|
|
|
adsetLevelBtn.classList.add('active');
|
|
adsetLevelBtn.disabled = false;
|
|
|
|
adLevelBtn.onClick = () => drillDown('adset', selectedValue);
|
|
adLevelBtn.classList.remove('active');
|
|
adLevelBtn.disabled = false;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
// Initialize the view
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Set default date range to last 6 months
|
|
const today = new Date();
|
|
const sixMonthsAgo = new Date();
|
|
sixMonthsAgo.setMonth(today.getMonth() - 6);
|
|
|
|
const formatDate = (date) => date.toISOString().split('T')[0];
|
|
|
|
document.getElementById('startDateFilter').value = formatDate(sixMonthsAgo);
|
|
document.getElementById('endDateFilter').value = formatDate(today);
|
|
|
|
activeFilters.start_date = formatDate(sixMonthsAgo);
|
|
activeFilters.end_date = formatDate(today);
|
|
|
|
updateTable('campaign');
|
|
});
|
|
</script>
|
|
|
|
<style>
|
|
.badge {
|
|
padding: 6px 10px;
|
|
font-size: 12px;
|
|
background-color: #007bff;
|
|
color: white;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.badge .close {
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
opacity: 0.8;
|
|
text-shadow: none;
|
|
color: white;
|
|
padding-left: 6px;
|
|
}
|
|
|
|
.badge .close:hover {
|
|
opacity: 1;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.table th {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.table td {
|
|
font-size: 13px;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.form-control-sm {
|
|
font-size: 13px;
|
|
}
|
|
|
|
.active-filters {
|
|
min-height: 30px;
|
|
}
|
|
|
|
/* Make table responsive with horizontal scroll */
|
|
.table-responsive {
|
|
max-height: calc(100vh - 300px);
|
|
overflow-y: auto;
|
|
overflow-x: auto;
|
|
position: relative;
|
|
}
|
|
|
|
.table {
|
|
position: relative;
|
|
}
|
|
|
|
/* Fixed columns */
|
|
.table th:nth-child(-n+4),
|
|
.table td:nth-child(-n+4) {
|
|
position: sticky;
|
|
background-color: #fff;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* Set horizontal positions for each fixed column */
|
|
.table th:nth-child(1),
|
|
.table td:nth-child(1) {
|
|
left: 0;
|
|
z-index: 2;
|
|
}
|
|
|
|
.table th:nth-child(2),
|
|
.table td:nth-child(2) {
|
|
left: 100px; /* Adjust based on your first column width */
|
|
}
|
|
|
|
.table th:nth-child(3),
|
|
.table td:nth-child(3) {
|
|
left: 250px; /* Adjust based on your first + second column width */
|
|
}
|
|
|
|
.table th:nth-child(4),
|
|
.table td:nth-child(4) {
|
|
left: 400px; /* Adjust based on your first + second + third column width */
|
|
}
|
|
|
|
/* Add shadow to indicate scroll */
|
|
.table td:nth-child(4)::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: -5px;
|
|
height: 100%;
|
|
width: 5px;
|
|
background: linear-gradient(to right, rgba(0,0,0,0.1), rgba(0,0,0,0));
|
|
}
|
|
|
|
/* Ensure headers stay on top */
|
|
.table thead th {
|
|
position: sticky;
|
|
top: 0;
|
|
background-color: #f8f9fa;
|
|
z-index: 3;
|
|
}
|
|
|
|
/* Fixed header corners need higher z-index */
|
|
.table thead th:nth-child(-n+4) {
|
|
z-index: 4;
|
|
}
|
|
|
|
/* Add minimum widths to prevent content squishing */
|
|
.table td:nth-child(1) {
|
|
min-width: 100px; /* Date column */
|
|
}
|
|
.table td:nth-child(2) {
|
|
min-width: 150px; /* Campaign name column */
|
|
}
|
|
.table td:nth-child(3) {
|
|
min-width: 150px; /* Ad set name column */
|
|
}
|
|
.table td:nth-child(4) {
|
|
min-width: 150px; /* Ad name column */
|
|
}
|
|
|
|
.clickable-cell {
|
|
cursor: pointer;
|
|
position: relative;
|
|
}
|
|
|
|
.clickable-cell:hover {
|
|
background-color: rgba(0, 123, 255, 0.1);
|
|
text-decoration: underline;
|
|
color: #1877f2; /* Facebook blue */
|
|
}
|
|
|
|
.column-visibility-controls {
|
|
position: relative;
|
|
}
|
|
|
|
.column-visibility-controls .form-check {
|
|
white-space: nowrap;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.column-visibility-controls .form-check-label {
|
|
font-size: 0.875rem;
|
|
margin-left: 0.5rem;
|
|
}
|
|
|
|
/* Update the column controls dropdown styles */
|
|
.collapse.position-absolute {
|
|
max-height: 300px; /* Fixed height */
|
|
overflow-y: auto; /* Enable vertical scrolling */
|
|
width: 200px; /* Fixed width */
|
|
border: 1px solid rgba(0,0,0,.125);
|
|
border-radius: 4px;
|
|
}
|
|
|
|
/* Add some padding between checkboxes for better spacing */
|
|
.column-visibility-controls .form-check {
|
|
padding: 6px 12px;
|
|
}
|
|
|
|
/* Add hover effect for better UX */
|
|
.column-visibility-controls .form-check:hover {
|
|
background-color: rgba(0,0,0,.03);
|
|
}
|
|
|
|
/* Style the scrollbar for better appearance */
|
|
.collapse.position-absolute::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.collapse.position-absolute::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
}
|
|
|
|
.collapse.position-absolute::-webkit-scrollbar-thumb {
|
|
background: #888;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.collapse.position-absolute::-webkit-scrollbar-thumb:hover {
|
|
background: #555;
|
|
}
|
|
|
|
.slide-in-panel {
|
|
position: fixed;
|
|
top: 0;
|
|
right: -300px; /* Start off-screen */
|
|
width: 300px; /* Width of the panel */
|
|
height: 100%;
|
|
background-color: white;
|
|
box-shadow: -2px 0 5px rgba(0,0,0,0.5);
|
|
transition: right 0.3s ease; /* Smooth transition */
|
|
z-index: 1001;
|
|
overflow-y: auto; /* Allow scrolling */
|
|
}
|
|
|
|
.slide-in-panel.open {
|
|
right: 0; /* Slide in */
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 10px;
|
|
background-color: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
}
|
|
|
|
.panel-body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.toggle-details {
|
|
color: #007bff;
|
|
cursor: pointer;
|
|
padding: 0;
|
|
margin-right: 0.5rem;
|
|
border: none;
|
|
background: none;
|
|
display: inline-block;
|
|
}
|
|
|
|
.toggle-details:hover {
|
|
color: #0056b3;
|
|
}
|
|
|
|
.toggle-details:focus {
|
|
outline: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.toggle-details i {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.detail-row {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
.detail-row td {
|
|
padding: 0 !important; /* Remove padding from container td */
|
|
}
|
|
|
|
.detail-row .table {
|
|
margin: 0;
|
|
background: transparent;
|
|
width: 100%;
|
|
}
|
|
|
|
/* Match nested table styling with parent table */
|
|
.detail-row .table td,
|
|
.detail-row .table th {
|
|
padding: 0.3rem; /* Match parent table padding */
|
|
vertical-align: middle;
|
|
border-top: 1px solid #dee2e6;
|
|
font-size: 13px; /* Match parent table font size */
|
|
}
|
|
|
|
.detail-row .table tr:first-child td {
|
|
border-top: none; /* Remove top border for first row */
|
|
}
|
|
|
|
/* Indent the nested table */
|
|
.detail-row .ml-4 {
|
|
margin-left: 1.5rem !important;
|
|
padding-right: 1.5rem;
|
|
width: calc(100% - 3rem); /* Account for left and right margin */
|
|
}
|
|
|
|
/* Ensure proper alignment with parent table */
|
|
.detail-row .table td:first-child {
|
|
padding-left: 1rem;
|
|
}
|
|
|
|
/* Match hover effects */
|
|
.detail-row .table tr:hover {
|
|
background-color: rgba(0, 0, 0, 0.075);
|
|
}
|
|
|
|
/* Ensure proper text alignment */
|
|
.detail-row .table td {
|
|
text-align: left;
|
|
}
|
|
|
|
/* Match fixed columns if any */
|
|
.detail-row .table td:nth-child(-n+4) {
|
|
background-color: #f8f9fa;
|
|
}
|
|
|
|
/* Make sure Font Awesome is included */
|
|
td.small.d-flex {
|
|
display: flex !important;
|
|
align-items: center;
|
|
}
|
|
|
|
.aggregate-row {
|
|
background-color: #f8f9fa;
|
|
border-top: 2px solid #dee2e6;
|
|
border-bottom: 2px solid #dee2e6;
|
|
}
|
|
|
|
.aggregate-row td {
|
|
color: #495057;
|
|
}
|
|
|
|
.aggregate-row:hover {
|
|
background-color: #f8f9fa !important;
|
|
}
|
|
</style>
|