Files
php_assessment_1/campaignView copy.php
T
emmymayo 77037e7e84 init
2025-02-04 23:06:08 +01:00

1040 lines
34 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>
<!-- Filter Dropdowns -->
<div class="row mb-3">
<div class="col-12 col-md-3 mb-2">
<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">
<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">
<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">&times;</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,
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 updateTable() {
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}')">&times;</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')) 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(); // Ensure the table is updated after initializing filters
updateColumnVisibility(); // Update column visibility on load
});
// Column visibility state
const columnVisibility = {
date: true,
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');
});
</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>