427 lines
20 KiB
PHP
427 lines
20 KiB
PHP
<?php
|
|
/**
|
|
* MSP Quote Wizard - Admin Dashboard
|
|
* Simple PHP admin interface for viewing and managing quotes
|
|
*/
|
|
|
|
session_start();
|
|
|
|
// Configuration
|
|
define('ADMIN_PASSWORD', 'QuoteAdmin2026!'); // Change in production
|
|
define('DB_HOST', '172.16.3.30');
|
|
define('DB_NAME', 'claudetools');
|
|
define('DB_USER', 'claudetools');
|
|
define('DB_PASS', 'CT_e8fcd5a3952030a79ed6debae6c954ed');
|
|
|
|
// Handle logout
|
|
if (isset($_GET['logout'])) {
|
|
session_destroy();
|
|
header('Location: index.php');
|
|
exit;
|
|
}
|
|
|
|
// Handle login
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['password'])) {
|
|
if ($_POST['password'] === ADMIN_PASSWORD) {
|
|
$_SESSION['admin_authenticated'] = true;
|
|
header('Location: index.php');
|
|
exit;
|
|
} else {
|
|
$login_error = 'Invalid password';
|
|
}
|
|
}
|
|
|
|
// Check authentication
|
|
if (!isset($_SESSION['admin_authenticated']) || !$_SESSION['admin_authenticated']) {
|
|
showLoginPage($login_error ?? null);
|
|
exit;
|
|
}
|
|
|
|
// Database connection
|
|
try {
|
|
$pdo = new PDO(
|
|
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
|
|
DB_USER,
|
|
DB_PASS,
|
|
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
|
|
);
|
|
} catch (PDOException $e) {
|
|
die("Database connection failed: " . $e->getMessage());
|
|
}
|
|
|
|
// Handle status update
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['update_status'])) {
|
|
$quote_id = $_POST['quote_id'];
|
|
$new_status = $_POST['new_status'];
|
|
$stmt = $pdo->prepare("UPDATE quotes SET status = ?, updated_at = NOW() WHERE id = ?");
|
|
$stmt->execute([$new_status, $quote_id]);
|
|
header('Location: index.php?updated=1');
|
|
exit;
|
|
}
|
|
|
|
// Get statistics
|
|
$stats = getStats($pdo);
|
|
|
|
// Get quotes with filtering
|
|
$status_filter = $_GET['status'] ?? '';
|
|
$search = $_GET['search'] ?? '';
|
|
$quotes = getQuotes($pdo, $status_filter, $search);
|
|
|
|
// Get single quote details if requested
|
|
$quote_detail = null;
|
|
if (isset($_GET['id'])) {
|
|
$quote_detail = getQuoteDetail($pdo, $_GET['id']);
|
|
}
|
|
|
|
// Helper functions
|
|
function getStats($pdo) {
|
|
$stats = [];
|
|
|
|
// Total quotes
|
|
$stmt = $pdo->query("SELECT COUNT(*) FROM quotes");
|
|
$stats['total'] = $stmt->fetchColumn();
|
|
|
|
// By status
|
|
$stmt = $pdo->query("SELECT status, COUNT(*) as count FROM quotes GROUP BY status");
|
|
$stats['by_status'] = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
|
|
|
|
// Total monthly value (submitted quotes only)
|
|
$stmt = $pdo->query("SELECT COALESCE(SUM(monthly_total), 0) FROM quotes WHERE status = 'submitted'");
|
|
$stats['total_monthly'] = $stmt->fetchColumn();
|
|
|
|
// This month
|
|
$stmt = $pdo->query("SELECT COUNT(*) FROM quotes WHERE created_at >= DATE_FORMAT(NOW(), '%Y-%m-01')");
|
|
$stats['this_month'] = $stmt->fetchColumn();
|
|
|
|
return $stats;
|
|
}
|
|
|
|
function getQuotes($pdo, $status_filter = '', $search = '') {
|
|
$sql = "SELECT q.*,
|
|
(SELECT COUNT(*) FROM quote_items WHERE quote_id = q.id) as item_count
|
|
FROM quotes q WHERE 1=1";
|
|
$params = [];
|
|
|
|
if ($status_filter) {
|
|
$sql .= " AND q.status = ?";
|
|
$params[] = $status_filter;
|
|
}
|
|
|
|
if ($search) {
|
|
$sql .= " AND (q.company_name LIKE ? OR q.contact_name LIKE ? OR q.contact_email LIKE ?)";
|
|
$params[] = "%$search%";
|
|
$params[] = "%$search%";
|
|
$params[] = "%$search%";
|
|
}
|
|
|
|
$sql .= " ORDER BY q.created_at DESC LIMIT 100";
|
|
|
|
$stmt = $pdo->prepare($sql);
|
|
$stmt->execute($params);
|
|
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
}
|
|
|
|
function getQuoteDetail($pdo, $id) {
|
|
// Get quote
|
|
$stmt = $pdo->prepare("SELECT * FROM quotes WHERE id = ?");
|
|
$stmt->execute([$id]);
|
|
$quote = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
|
|
if (!$quote) return null;
|
|
|
|
// Get items
|
|
$stmt = $pdo->prepare("SELECT * FROM quote_items WHERE quote_id = ? ORDER BY category, created_at");
|
|
$stmt->execute([$id]);
|
|
$quote['items'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
// Get activity
|
|
$stmt = $pdo->prepare("SELECT * FROM quote_activity WHERE quote_id = ? ORDER BY created_at DESC");
|
|
$stmt->execute([$id]);
|
|
$quote['activities'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
|
|
|
return $quote;
|
|
}
|
|
|
|
function showLoginPage($error = null) {
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Quote Admin - Login</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
</head>
|
|
<body class="bg-gray-900 min-h-screen flex items-center justify-center">
|
|
<div class="bg-gray-800 p-8 rounded-lg shadow-xl w-full max-w-md">
|
|
<h1 class="text-2xl font-bold text-white mb-6 text-center">Quote Admin</h1>
|
|
<?php if ($error): ?>
|
|
<div class="bg-red-500/20 border border-red-500 text-red-400 px-4 py-2 rounded mb-4">
|
|
<?= htmlspecialchars($error) ?>
|
|
</div>
|
|
<?php endif; ?>
|
|
<form method="POST">
|
|
<div class="mb-4">
|
|
<label class="block text-gray-400 text-sm mb-2">Password</label>
|
|
<input type="password" name="password" required autofocus
|
|
class="w-full bg-gray-700 border border-gray-600 rounded px-4 py-2 text-white focus:outline-none focus:border-blue-500">
|
|
</div>
|
|
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-2 px-4 rounded transition">
|
|
Login
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</body>
|
|
</html>
|
|
<?php
|
|
exit;
|
|
}
|
|
|
|
function formatMoney($amount) {
|
|
return '$' . number_format((float)$amount, 2);
|
|
}
|
|
|
|
function statusBadge($status) {
|
|
$colors = [
|
|
'draft' => 'bg-gray-500',
|
|
'submitted' => 'bg-blue-500',
|
|
'viewed' => 'bg-purple-500',
|
|
'followed_up' => 'bg-yellow-500',
|
|
'converted' => 'bg-green-500',
|
|
'expired' => 'bg-red-500',
|
|
];
|
|
$color = $colors[$status] ?? 'bg-gray-500';
|
|
return "<span class=\"px-2 py-1 rounded text-xs font-medium text-white $color\">" . ucfirst(str_replace('_', ' ', $status)) . "</span>";
|
|
}
|
|
?>
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Quote Admin Dashboard</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
.stat-card { transition: transform 0.2s; }
|
|
.stat-card:hover { transform: translateY(-2px); }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-900 min-h-screen text-gray-100">
|
|
<!-- Header -->
|
|
<header class="bg-gray-800 border-b border-gray-700">
|
|
<div class="max-w-7xl mx-auto px-4 py-4 flex items-center justify-between">
|
|
<div class="flex items-center gap-4">
|
|
<h1 class="text-xl font-bold text-white">Quote Admin</h1>
|
|
<span class="text-gray-400 text-sm">azcomputerguru.com</span>
|
|
</div>
|
|
<a href="?logout=1" class="text-gray-400 hover:text-white text-sm">Logout</a>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="max-w-7xl mx-auto px-4 py-6">
|
|
<?php if (isset($_GET['updated'])): ?>
|
|
<div class="bg-green-500/20 border border-green-500 text-green-400 px-4 py-2 rounded mb-6">
|
|
Quote status updated successfully.
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
|
<div class="stat-card bg-gray-800 rounded-lg p-4 border border-gray-700">
|
|
<div class="text-gray-400 text-sm">Total Quotes</div>
|
|
<div class="text-3xl font-bold text-white"><?= $stats['total'] ?></div>
|
|
</div>
|
|
<div class="stat-card bg-gray-800 rounded-lg p-4 border border-gray-700">
|
|
<div class="text-gray-400 text-sm">Submitted</div>
|
|
<div class="text-3xl font-bold text-blue-400"><?= $stats['by_status']['submitted'] ?? 0 ?></div>
|
|
</div>
|
|
<div class="stat-card bg-gray-800 rounded-lg p-4 border border-gray-700">
|
|
<div class="text-gray-400 text-sm">Converted</div>
|
|
<div class="text-3xl font-bold text-green-400"><?= $stats['by_status']['converted'] ?? 0 ?></div>
|
|
</div>
|
|
<div class="stat-card bg-gray-800 rounded-lg p-4 border border-gray-700">
|
|
<div class="text-gray-400 text-sm">Monthly Value</div>
|
|
<div class="text-2xl font-bold text-emerald-400"><?= formatMoney($stats['total_monthly']) ?></div>
|
|
</div>
|
|
</div>
|
|
|
|
<?php if ($quote_detail): ?>
|
|
<!-- Quote Detail View -->
|
|
<div class="bg-gray-800 rounded-lg border border-gray-700 mb-6">
|
|
<div class="p-4 border-b border-gray-700 flex items-center justify-between">
|
|
<h2 class="text-lg font-semibold">Quote Details</h2>
|
|
<a href="index.php" class="text-blue-400 hover:text-blue-300 text-sm">← Back to List</a>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="grid md:grid-cols-2 gap-6">
|
|
<!-- Contact Info -->
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-400 mb-3">Contact Information</h3>
|
|
<div class="space-y-2">
|
|
<div><span class="text-gray-500">Company:</span> <span class="text-white"><?= htmlspecialchars($quote_detail['company_name'] ?: '(not provided)') ?></span></div>
|
|
<div><span class="text-gray-500">Contact:</span> <span class="text-white"><?= htmlspecialchars($quote_detail['contact_name'] ?: '(not provided)') ?></span></div>
|
|
<div><span class="text-gray-500">Email:</span> <span class="text-white"><?= htmlspecialchars($quote_detail['contact_email'] ?: '(not provided)') ?></span></div>
|
|
<div><span class="text-gray-500">Phone:</span> <span class="text-white"><?= htmlspecialchars($quote_detail['contact_phone'] ?: '(not provided)') ?></span></div>
|
|
<div><span class="text-gray-500">Employees:</span> <span class="text-white"><?= $quote_detail['employee_count'] ?></span></div>
|
|
</div>
|
|
</div>
|
|
<!-- Quote Summary -->
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-400 mb-3">Quote Summary</h3>
|
|
<div class="space-y-2">
|
|
<div><span class="text-gray-500">Status:</span> <?= statusBadge($quote_detail['status']) ?></div>
|
|
<div><span class="text-gray-500">Monthly Total:</span> <span class="text-2xl font-bold text-emerald-400"><?= formatMoney($quote_detail['monthly_total']) ?></span></div>
|
|
<div><span class="text-gray-500">Setup Total:</span> <span class="text-white"><?= formatMoney($quote_detail['setup_total']) ?></span></div>
|
|
<div><span class="text-gray-500">Created:</span> <span class="text-white"><?= $quote_detail['created_at'] ?></span></div>
|
|
<?php if ($quote_detail['submitted_at']): ?>
|
|
<div><span class="text-gray-500">Submitted:</span> <span class="text-white"><?= $quote_detail['submitted_at'] ?></span></div>
|
|
<?php endif; ?>
|
|
</div>
|
|
<!-- Status Update Form -->
|
|
<form method="POST" class="mt-4">
|
|
<input type="hidden" name="quote_id" value="<?= $quote_detail['id'] ?>">
|
|
<div class="flex gap-2">
|
|
<select name="new_status" class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white text-sm">
|
|
<option value="draft" <?= $quote_detail['status'] === 'draft' ? 'selected' : '' ?>>Draft</option>
|
|
<option value="submitted" <?= $quote_detail['status'] === 'submitted' ? 'selected' : '' ?>>Submitted</option>
|
|
<option value="viewed" <?= $quote_detail['status'] === 'viewed' ? 'selected' : '' ?>>Viewed</option>
|
|
<option value="followed_up" <?= $quote_detail['status'] === 'followed_up' ? 'selected' : '' ?>>Followed Up</option>
|
|
<option value="converted" <?= $quote_detail['status'] === 'converted' ? 'selected' : '' ?>>Converted</option>
|
|
<option value="expired" <?= $quote_detail['status'] === 'expired' ? 'selected' : '' ?>>Expired</option>
|
|
</select>
|
|
<button type="submit" name="update_status" value="1" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm">
|
|
Update Status
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Line Items -->
|
|
<?php if (!empty($quote_detail['items'])): ?>
|
|
<div class="mt-6">
|
|
<h3 class="text-sm font-medium text-gray-400 mb-3">Line Items</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-gray-400 border-b border-gray-700">
|
|
<th class="pb-2">Category</th>
|
|
<th class="pb-2">Product</th>
|
|
<th class="pb-2">Qty</th>
|
|
<th class="pb-2 text-right">Unit Price</th>
|
|
<th class="pb-2 text-right">Monthly</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($quote_detail['items'] as $item): ?>
|
|
<tr class="border-b border-gray-700/50">
|
|
<td class="py-2 text-gray-400"><?= ucfirst(str_replace('_', ' ', $item['category'])) ?></td>
|
|
<td class="py-2 text-white"><?= htmlspecialchars($item['product_name']) ?></td>
|
|
<td class="py-2"><?= $item['quantity'] ?></td>
|
|
<td class="py-2 text-right"><?= formatMoney($item['unit_price']) ?></td>
|
|
<td class="py-2 text-right text-emerald-400"><?= formatMoney($item['monthly_amount']) ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Activity Log -->
|
|
<?php if (!empty($quote_detail['activities'])): ?>
|
|
<div class="mt-6">
|
|
<h3 class="text-sm font-medium text-gray-400 mb-3">Activity Log</h3>
|
|
<div class="space-y-2">
|
|
<?php foreach ($quote_detail['activities'] as $activity): ?>
|
|
<div class="text-sm">
|
|
<span class="text-gray-500"><?= $activity['created_at'] ?></span>
|
|
<span class="text-gray-400 mx-2">|</span>
|
|
<span class="text-white"><?= ucfirst($activity['action']) ?></span>
|
|
<?php if ($activity['step_name']): ?>
|
|
<span class="text-gray-500">(<?= $activity['step_name'] ?>)</span>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php endforeach; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<!-- Filters -->
|
|
<div class="bg-gray-800 rounded-lg border border-gray-700 mb-6">
|
|
<form method="GET" class="p-4 flex flex-wrap gap-4 items-end">
|
|
<div>
|
|
<label class="block text-gray-400 text-sm mb-1">Status</label>
|
|
<select name="status" class="bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white text-sm">
|
|
<option value="">All Statuses</option>
|
|
<option value="draft" <?= $status_filter === 'draft' ? 'selected' : '' ?>>Draft</option>
|
|
<option value="submitted" <?= $status_filter === 'submitted' ? 'selected' : '' ?>>Submitted</option>
|
|
<option value="viewed" <?= $status_filter === 'viewed' ? 'selected' : '' ?>>Viewed</option>
|
|
<option value="followed_up" <?= $status_filter === 'followed_up' ? 'selected' : '' ?>>Followed Up</option>
|
|
<option value="converted" <?= $status_filter === 'converted' ? 'selected' : '' ?>>Converted</option>
|
|
<option value="expired" <?= $status_filter === 'expired' ? 'selected' : '' ?>>Expired</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label class="block text-gray-400 text-sm mb-1">Search</label>
|
|
<input type="text" name="search" value="<?= htmlspecialchars($search) ?>" placeholder="Company, contact, or email..."
|
|
class="w-full bg-gray-700 border border-gray-600 rounded px-3 py-2 text-white text-sm">
|
|
</div>
|
|
<button type="submit" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm">
|
|
Filter
|
|
</button>
|
|
<?php if ($status_filter || $search): ?>
|
|
<a href="index.php" class="text-gray-400 hover:text-white text-sm px-4 py-2">Clear</a>
|
|
<?php endif; ?>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Quotes Table -->
|
|
<div class="bg-gray-800 rounded-lg border border-gray-700 overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="text-left text-gray-400 bg-gray-800/50">
|
|
<th class="px-4 py-3">Date</th>
|
|
<th class="px-4 py-3">Company</th>
|
|
<th class="px-4 py-3">Contact</th>
|
|
<th class="px-4 py-3">Status</th>
|
|
<th class="px-4 py-3">Items</th>
|
|
<th class="px-4 py-3 text-right">Monthly</th>
|
|
<th class="px-4 py-3"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (empty($quotes)): ?>
|
|
<tr>
|
|
<td colspan="7" class="px-4 py-8 text-center text-gray-500">No quotes found</td>
|
|
</tr>
|
|
<?php else: ?>
|
|
<?php foreach ($quotes as $quote): ?>
|
|
<tr class="border-t border-gray-700/50 hover:bg-gray-700/30">
|
|
<td class="px-4 py-3 text-gray-400"><?= date('M j, Y', strtotime($quote['created_at'])) ?></td>
|
|
<td class="px-4 py-3 text-white"><?= htmlspecialchars($quote['company_name'] ?: '(not provided)') ?></td>
|
|
<td class="px-4 py-3">
|
|
<div class="text-white"><?= htmlspecialchars($quote['contact_name'] ?: '-') ?></div>
|
|
<div class="text-gray-500 text-xs"><?= htmlspecialchars($quote['contact_email'] ?: '') ?></div>
|
|
</td>
|
|
<td class="px-4 py-3"><?= statusBadge($quote['status']) ?></td>
|
|
<td class="px-4 py-3 text-gray-400"><?= $quote['item_count'] ?></td>
|
|
<td class="px-4 py-3 text-right text-emerald-400 font-medium"><?= formatMoney($quote['monthly_total']) ?></td>
|
|
<td class="px-4 py-3 text-right">
|
|
<a href="?id=<?= $quote['id'] ?>" class="text-blue-400 hover:text-blue-300">View</a>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</body>
|
|
</html>
|