Major restructuring of product variations: - Renamed ProductVariation to ProductMultiBuy for quantity-based pricing (e.g., "3 for £25") - Added new ProductVariant model for string-based options (colors, flavors) - Complete separation of multi-buy pricing from variant selection Features implemented: - Multi-buy deals with automatic price-per-unit calculation - Product variants for colors/flavors/sizes with stock tracking - TeleBot checkout supports both multi-buys and variant selection - Shopping cart correctly calculates multi-buy bundle prices - Order system tracks selected variants and multi-buy choices - Real-time bot activity monitoring with SignalR - Public bot directory page with QR codes for Telegram launch - Admin dashboard shows multi-buy and variant metrics Technical changes: - Updated all DTOs, services, and controllers - Fixed cart total calculation for multi-buy bundles - Comprehensive test coverage for new functionality - All existing tests passing with new features Database changes: - Migrated ProductVariations to ProductMultiBuys - Added ProductVariants table - Updated OrderItems to track variants 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
317 lines
10 KiB
Plaintext
317 lines
10 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Live Bot Activity";
|
|
}
|
|
|
|
<div class="container-fluid">
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<h2><i class="bi bi-activity"></i> Live Bot Activity Monitor</h2>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Stats Cards -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-success">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Active Users</h5>
|
|
<h2 class="display-4" id="activeUsers">0</h2>
|
|
<small>Right now</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-info">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Product Views</h5>
|
|
<h2 class="display-4" id="productViews">0</h2>
|
|
<small>Last 5 minutes</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-warning">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Active Carts</h5>
|
|
<h2 class="display-4" id="activeCarts">0</h2>
|
|
<small>With items</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-white bg-primary">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Cart Value</h5>
|
|
<h2 class="display-4">£<span id="cartValue">0</span></h2>
|
|
<small>Total in carts</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Users List -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-light">
|
|
<h5 class="mb-0"><i class="bi bi-people-fill"></i> Currently Active Users</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="activeUsersList" class="d-flex flex-wrap gap-2">
|
|
<!-- User badges will be inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Live Activity Feed -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-light d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="bi bi-lightning-fill"></i> Real-Time Activity Feed</h5>
|
|
<span class="badge bg-success pulse" id="connectionStatus">
|
|
<i class="bi bi-wifi"></i> Connected
|
|
</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div id="activityFeed" class="activity-feed" style="max-height: 500px; overflow-y: auto;">
|
|
<!-- Activities will be inserted here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.activity-item {
|
|
padding: 10px;
|
|
border-left: 3px solid #007bff;
|
|
margin-bottom: 10px;
|
|
background: #f8f9fa;
|
|
border-radius: 4px;
|
|
animation: slideIn 0.3s ease;
|
|
}
|
|
|
|
.activity-item.new {
|
|
animation: highlight 1s ease;
|
|
}
|
|
|
|
@@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateX(-20px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateX(0);
|
|
}
|
|
}
|
|
|
|
@@keyframes highlight {
|
|
0% {
|
|
background-color: #fff3cd;
|
|
}
|
|
100% {
|
|
background-color: #f8f9fa;
|
|
}
|
|
}
|
|
|
|
@@keyframes pulse {
|
|
0% {
|
|
opacity: 1;
|
|
}
|
|
50% {
|
|
opacity: 0.5;
|
|
}
|
|
100% {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
.user-badge {
|
|
display: inline-block;
|
|
padding: 5px 10px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border-radius: 20px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
animation: fadeIn 0.5s ease;
|
|
}
|
|
|
|
@@keyframes fadeIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: scale(0.8);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
}
|
|
|
|
.activity-type-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 12px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
margin-right: 8px;
|
|
}
|
|
|
|
.type-viewproduct {
|
|
background-color: #e3f2fd;
|
|
color: #1976d2;
|
|
}
|
|
|
|
.type-addtocart {
|
|
background-color: #fff3e0;
|
|
color: #f57c00;
|
|
}
|
|
|
|
.type-checkout {
|
|
background-color: #e8f5e9;
|
|
color: #388e3c;
|
|
}
|
|
|
|
.type-browse {
|
|
background-color: #f3e5f5;
|
|
color: #7b1fa2;
|
|
}
|
|
</style>
|
|
|
|
@section Scripts {
|
|
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
|
|
<script>
|
|
const connection = new signalR.HubConnectionBuilder()
|
|
.withUrl("/activityHub")
|
|
.configureLogging(signalR.LogLevel.Information)
|
|
.build();
|
|
|
|
let activityCount = 0;
|
|
const maxActivities = 50;
|
|
|
|
// Connection status handling
|
|
connection.onclose(() => {
|
|
document.getElementById('connectionStatus').innerHTML = '<i class="bi bi-wifi-off"></i> Disconnected';
|
|
document.getElementById('connectionStatus').classList.remove('bg-success');
|
|
document.getElementById('connectionStatus').classList.add('bg-danger');
|
|
setTimeout(() => startConnection(), 5000);
|
|
});
|
|
|
|
// Handle initial summary
|
|
connection.on("InitialSummary", (summary) => {
|
|
updateStats(summary);
|
|
updateActivityFeed(summary.recentActivities);
|
|
});
|
|
|
|
// Handle new activity
|
|
connection.on("NewActivity", (activity) => {
|
|
addActivity(activity, true);
|
|
});
|
|
|
|
// Handle summary updates
|
|
connection.on("SummaryUpdate", (summary) => {
|
|
updateStats(summary);
|
|
});
|
|
|
|
function updateStats(summary) {
|
|
document.getElementById('activeUsers').textContent = summary.activeUsers;
|
|
document.getElementById('productViews').textContent = summary.productViewsLast5Min;
|
|
document.getElementById('activeCarts').textContent = summary.cartsActiveNow;
|
|
document.getElementById('cartValue').textContent = summary.totalValueInCartsNow.toFixed(2);
|
|
|
|
// Update active users list
|
|
const usersList = document.getElementById('activeUsersList');
|
|
usersList.innerHTML = '';
|
|
summary.activeUserNames.forEach(name => {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'user-badge';
|
|
badge.innerHTML = `<i class="bi bi-person-circle"></i> ${name}`;
|
|
usersList.appendChild(badge);
|
|
});
|
|
}
|
|
|
|
function updateActivityFeed(activities) {
|
|
const feed = document.getElementById('activityFeed');
|
|
feed.innerHTML = '';
|
|
activities.forEach(activity => addActivity(activity, false));
|
|
}
|
|
|
|
function addActivity(activity, isNew) {
|
|
const feed = document.getElementById('activityFeed');
|
|
|
|
// Create activity element
|
|
const item = document.createElement('div');
|
|
item.className = 'activity-item' + (isNew ? ' new' : '');
|
|
|
|
const typeClass = 'type-' + activity.activityType.toLowerCase().replace(/\s+/g, '');
|
|
const typeBadge = `<span class="activity-type-badge ${typeClass}">${activity.activityType}</span>`;
|
|
|
|
const time = new Date(activity.timestamp).toLocaleTimeString();
|
|
|
|
let icon = getActivityIcon(activity.activityType);
|
|
|
|
item.innerHTML = `
|
|
<div class="d-flex justify-content-between align-items-start">
|
|
<div>
|
|
${typeBadge}
|
|
<strong>${icon} ${activity.userDisplayName}</strong>
|
|
<span class="text-muted">- ${activity.activityDescription}</span>
|
|
${activity.productName ? `<br><small class="text-info">Product: ${activity.productName}</small>` : ''}
|
|
${activity.value ? `<br><small class="text-success">Value: £${activity.value.toFixed(2)}</small>` : ''}
|
|
</div>
|
|
<small class="text-muted">${time}</small>
|
|
</div>
|
|
`;
|
|
|
|
// Add to top of feed
|
|
feed.insertBefore(item, feed.firstChild);
|
|
|
|
// Limit number of activities shown
|
|
activityCount++;
|
|
if (activityCount > maxActivities) {
|
|
feed.removeChild(feed.lastChild);
|
|
activityCount--;
|
|
}
|
|
}
|
|
|
|
function getActivityIcon(type) {
|
|
const icons = {
|
|
'ViewProduct': '👁️',
|
|
'AddToCart': '🛒',
|
|
'Checkout': '💳',
|
|
'Browse': '🔍',
|
|
'RemoveFromCart': '❌',
|
|
'UpdateCart': '✏️',
|
|
'OrderComplete': '✅',
|
|
'StartSession': '👋',
|
|
'EndSession': '👋'
|
|
};
|
|
return icons[type] || '📍';
|
|
}
|
|
|
|
async function startConnection() {
|
|
try {
|
|
await connection.start();
|
|
document.getElementById('connectionStatus').innerHTML = '<i class="bi bi-wifi"></i> Connected';
|
|
document.getElementById('connectionStatus').classList.remove('bg-danger');
|
|
document.getElementById('connectionStatus').classList.add('bg-success');
|
|
} catch (err) {
|
|
console.error(err);
|
|
setTimeout(() => startConnection(), 5000);
|
|
}
|
|
}
|
|
|
|
// Start the connection
|
|
startConnection();
|
|
</script>
|
|
} |