prepare(" SELECT * FROM projects WHERE id = ? AND client_id = ? "); $stmt->execute([$project_id, $_SESSION['client_id']]); $project = $stmt->fetch(); if (!$project) { header('Location: projects-list.php'); exit; } // ===== PROJECT COST CALCULATIONS ===== // Load pricing from company_settings $pricing = [ 'base_sample' => 0, 'base_incentive' => 0, 'incr_sample' => 0, 'incr_incentive' => 0, 'usd_factor' => 0, 'eur_factor' => 0, ]; $client_currency = 'INR'; $currency_symbol = '₹'; try { // Get client currency $stmt = $pdo->prepare("SELECT currency FROM clients WHERE id = ?"); $stmt->execute([$_SESSION['client_id']]); $cRow = $stmt->fetch(); if ($cRow && !empty($cRow['currency'])) { $client_currency = $cRow['currency']; if ($client_currency === 'USD') $currency_symbol = '$'; elseif ($client_currency === 'EUR') $currency_symbol = '€'; } // Load pricing config $stmt = $pdo->query("SELECT setting_key, setting_value FROM company_settings WHERE setting_key LIKE 'pricing_%'"); while ($row = $stmt->fetch()) { switch ($row['setting_key']) { case 'pricing_base_sample_cost': $pricing['base_sample'] = (float)$row['setting_value']; break; case 'pricing_base_incentive': $pricing['base_incentive'] = (float)$row['setting_value']; break; case 'pricing_loi_increment_sample': $pricing['incr_sample'] = (float)$row['setting_value']; break; case 'pricing_loi_increment_incentive': $pricing['incr_incentive'] = (float)$row['setting_value']; break; case 'pricing_usd_factor': $pricing['usd_factor'] = (float)$row['setting_value']; break; case 'pricing_eur_factor': $pricing['eur_factor'] = (float)$row['setting_value']; break; } } } catch (Exception $e) { error_log("Pricing load error: " . $e->getMessage()); } // Pricing formula: extraMin = max(0, LOI - 5) // sample_cost_per = base_sample + (extraMin * incr_sample) // incentive_per = base_incentive + (extraMin * incr_incentive) function calcCosts($loi_minutes, $sample_count, $pricing, $client_currency) { $extraMin = max(0, $loi_minutes - 5); $sampleCostINR = $pricing['base_sample'] + ($extraMin * $pricing['incr_sample']); $incentiveINR = $pricing['base_incentive'] + ($extraMin * $pricing['incr_incentive']); // Currency conversion $sampleCost = $sampleCostINR; $incentive = $incentiveINR; if ($client_currency === 'USD') { $factor = 1 + $pricing['usd_factor'] / 100; $sampleCost = $sampleCostINR * $factor; $incentive = $incentiveINR * $factor; } elseif ($client_currency === 'EUR') { $factor = 1 + $pricing['eur_factor'] / 100; $sampleCost = $sampleCostINR * $factor; $incentive = $incentiveINR * $factor; } $totalPerSample = $sampleCost + $incentive; return [ 'sample_per' => $sampleCost, 'incentive_per' => $incentive, 'total_per' => $totalPerSample, 'sample_total' => $sampleCost * $sample_count, 'incentive_total' => $incentive * $sample_count, 'total' => $totalPerSample * $sample_count, 'loi' => $loi_minutes, 'sample_count' => $sample_count, ]; } // 1. ESTIMATED COST (project's eloi + sample_size) $estCost = calcCosts($project['eloi'], $project['sample_size'], $pricing, $client_currency); // 2. ACTUAL COST (avg LOI of valid completes + valid complete count) // Valid = status='complete' AND quality_flag='valid' (excludes speedster, ip_duplicate, client_flagged) $actualLoi = 0; $actualSample = 0; $actualCost = null; $loiFallback = false; try { $stmt = $pdo->prepare(" SELECT COUNT(*) as valid_completes, AVG(actual_loi_seconds) as avg_loi_seconds, SUM(CASE WHEN actual_loi_seconds IS NOT NULL THEN 1 ELSE 0 END) as with_loi_count FROM survey_urls WHERE project_id = ? AND status = 'complete' AND quality_flag = 'valid' "); $stmt->execute([$project['project_id']]); $actStats = $stmt->fetch(PDO::FETCH_ASSOC); if ($actStats && $actStats['valid_completes'] > 0) { $actualSample = (int)$actStats['valid_completes']; if ($actStats['with_loi_count'] > 0 && $actStats['avg_loi_seconds'] > 0) { // Use actual average LOI from tracked completes $actualLoi = round($actStats['avg_loi_seconds'] / 60, 1); } else { // No LOI data (pre-tracking completes) — fall back to project ELOI $actualLoi = (float)$project['eloi']; $loiFallback = true; } $actualCost = calcCosts($actualLoi, $actualSample, $pricing, $client_currency); } // Also get rejected count separately for display $stmt2 = $pdo->prepare(" SELECT COUNT(*) as cnt FROM survey_urls WHERE project_id = ? AND status = 'complete' AND quality_flag != 'valid' "); $stmt2->execute([$project['project_id']]); $rejRow = $stmt2->fetch(); $rejectedCount = $rejRow ? (int)$rejRow['cnt'] : 0; } catch (Exception $e) { error_log("Actual cost calc error: " . $e->getMessage()); } $isClosed = ($project['status'] === 'Closed'); $hasPricing = ($pricing['base_sample'] > 0 || $pricing['base_incentive'] > 0); // Fetch selections for this project try { $stmt = $pdo->prepare(" SELECT ps.*, COUNT(DISTINCT sm.user_id) as actual_selected, c.company_name as created_by_name, SUM(CASE WHEN sm.sample_status = 'pending' THEN 1 ELSE 0 END) as cnt_pending, SUM(CASE WHEN sm.sample_status = 'complete' THEN 1 ELSE 0 END) as cnt_complete, SUM(CASE WHEN sm.sample_status = 'screen_out' THEN 1 ELSE 0 END) as cnt_screen_out, SUM(CASE WHEN sm.sample_status = 'quota_full' THEN 1 ELSE 0 END) as cnt_quota_full, SUM(CASE WHEN sm.sample_status = 'over_quota' THEN 1 ELSE 0 END) as cnt_over_quota FROM project_selections ps LEFT JOIN selection_members sm ON ps.id = sm.selection_id LEFT JOIN clients c ON ps.created_by = c.id WHERE ps.project_id = ? AND ps.client_id = ? GROUP BY ps.id ORDER BY ps.created_at DESC "); $stmt->execute([$project_id, $_SESSION['client_id']]); $selections = $stmt->fetchAll(); // Get count of URLs sent per selection (via sent_to_user_id matching selection_members) $selectionUrlCounts = []; $selectionLoiStats = []; $projectLoiStats = ['avg_loi' => 0, 'min_loi' => 0, 'max_loi' => 0, 'total_completes' => 0, 'speedsters' => 0, 'ip_dups' => 0]; if (!empty($selections)) { $selIds = array_column($selections, 'id'); $placeholders = implode(',', array_fill(0, count($selIds), '?')); $stmt = $pdo->prepare(" SELECT sm.selection_id, COUNT(DISTINCT su.id) as urls_sent, SUM(CASE WHEN su.status = 'complete' THEN 1 ELSE 0 END) as urls_complete, SUM(CASE WHEN su.status = 'partial' THEN 1 ELSE 0 END) as urls_partial, SUM(CASE WHEN su.status IN ('earlyscreenout','latescreenout') THEN 1 ELSE 0 END) as urls_screenout, SUM(CASE WHEN su.status = 'timeout' THEN 1 ELSE 0 END) as urls_timeout, SUM(CASE WHEN su.status = 'quotafull' THEN 1 ELSE 0 END) as urls_quotafull FROM selection_members sm INNER JOIN survey_urls su ON su.sent_to_user_id = sm.user_id AND su.project_id = ? WHERE sm.selection_id IN ($placeholders) GROUP BY sm.selection_id "); $stmt->execute(array_merge([$project['project_id']], $selIds)); foreach ($stmt->fetchAll() as $row) { $selectionUrlCounts[$row['selection_id']] = $row; } // LOI stats per selection $stmt = $pdo->prepare(" SELECT sm.selection_id, AVG(CASE WHEN su.status = 'complete' AND su.actual_loi_seconds IS NOT NULL THEN su.actual_loi_seconds END) as avg_loi, MIN(CASE WHEN su.status = 'complete' AND su.actual_loi_seconds IS NOT NULL THEN su.actual_loi_seconds END) as min_loi, MAX(CASE WHEN su.status = 'complete' AND su.actual_loi_seconds IS NOT NULL THEN su.actual_loi_seconds END) as max_loi, SUM(CASE WHEN su.quality_flag = 'speedster' THEN 1 ELSE 0 END) as speedsters, SUM(CASE WHEN su.quality_flag = 'ip_duplicate' THEN 1 ELSE 0 END) as ip_dups FROM selection_members sm INNER JOIN survey_urls su ON su.sent_to_user_id = sm.user_id AND su.project_id = ? WHERE sm.selection_id IN ($placeholders) GROUP BY sm.selection_id "); $stmt->execute(array_merge([$project['project_id']], $selIds)); foreach ($stmt->fetchAll() as $row) { $selectionLoiStats[$row['selection_id']] = $row; } // Project-wide LOI stats $stmt = $pdo->prepare(" SELECT AVG(CASE WHEN status = 'complete' AND actual_loi_seconds IS NOT NULL THEN actual_loi_seconds END) as avg_loi, MIN(CASE WHEN status = 'complete' AND actual_loi_seconds IS NOT NULL THEN actual_loi_seconds END) as min_loi, MAX(CASE WHEN status = 'complete' AND actual_loi_seconds IS NOT NULL THEN actual_loi_seconds END) as max_loi, SUM(CASE WHEN status = 'complete' THEN 1 ELSE 0 END) as total_completes, SUM(CASE WHEN quality_flag = 'speedster' THEN 1 ELSE 0 END) as speedsters, SUM(CASE WHEN quality_flag = 'ip_duplicate' THEN 1 ELSE 0 END) as ip_dups FROM survey_urls WHERE project_id = ? "); $stmt->execute([$project['project_id']]); $pStats = $stmt->fetch(PDO::FETCH_ASSOC); if ($pStats) $projectLoiStats = $pStats; } } catch (Exception $e) { error_log("Selections fetch error: " . $e->getMessage()); $selections = []; $selectionUrlCounts = []; } $page_title = 'View Project'; $is_live = $project['status'] === 'Live'; // Status colors $status_colors = [ 'Created' => '#059669', 'Targeted' => '#ffc107', 'Live' => '#28a745', 'On hold' => '#dc2626', 'Closed' => '#666' ]; $status_color = $status_colors[$project['status']] ?? '#666'; // Define redirect statuses $redirect_statuses = [ 'complete' => ['name' => 'Complete', 'color' => '#28a745'], 'partial' => ['name' => 'Partial Complete', 'color' => '#059669'], 'earlyscreenout' => ['name' => 'Early Screen Out', 'color' => '#dc2626'], 'latescreenout' => ['name' => 'Late Screen Out', 'color' => '#ffc107'], 'timeout' => ['name' => 'Timed Out', 'color' => '#666'], 'quotafull' => ['name' => 'Quota Full', 'color' => '#ff6b6b'] ]; $base_url = 'https://relevantreflex.com/r/'; include 'client-portal-header.php'; ?>
| Selection ID | Name | Status | Required | Selected | Sample Status | URLs Sent | Avg LOI | Quality Flags | Incidence | Created | Actions |
|---|---|---|---|---|---|---|---|---|---|---|---|
|
0): ?>
Pending:
0): ?>
Complete:
0): ?>
Screen Out:
0): ?>
Quota Full:
0): ?>
Over Quota:
No members
|
0): ?>
Sent:
0): ?>
0): ?>
SO:
0): ?>
TO:
None
|
m - |
0 || $ipDups > 0): ?>
0): ?>
⚡ Speedster 1 ? 's' : ''; ?>
0): ?>
🔗 IP Dup 1 ? 's' : ''; ?>
Clean
|
% |
| Batch # | Type | Total | Status Breakdown | Created | Actions |
|---|---|---|---|---|---|
| Batch # |
['label' => 'Available', 'class' => 'pill-available'],
'sent' => ['label' => 'Sent', 'class' => 'pill-sent'],
'clicked' => ['label' => 'Clicked', 'class' => 'pill-clicked'],
'complete' => ['label' => 'Complete', 'class' => 'pill-complete'],
'partial' => ['label' => 'Partial', 'class' => 'pill-partial'],
'earlyscreenout' => ['label' => 'Early SO', 'class' => 'pill-earlyscreenout'],
'latescreenout' => ['label' => 'Late SO', 'class' => 'pill-latescreenout'],
'timeout' => ['label' => 'Timeout', 'class' => 'pill-timeout'],
'quotafull' => ['label' => 'Quota Full', 'class' => 'pill-quotafull'],
];
?>
$status_info): ?>
0): ?>
:
No activity yet
|
🔍 Note: URLs that remain without a result for over 2 hours are automatically marked as Timeout.