littleshop/LittleShop/Areas/Admin/Views/Activity/Live.cshtml
SysAdmin 034b8facee Implement product multi-buys and variants system
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>
2025-09-21 00:30:12 +01:00

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>
}