Files
moto-adv-website/app/templates/admin/manage_chats.html
ske087 30bd4c62ad Major Feature Update: Modern Chat System & Admin Management
Features Added:
🔥 Modern Chat System:
- Real-time messaging with modern Tailwind CSS design
- Post-linked discussions for adventure sharing
- Chat categories (general, technical-support, adventure-planning)
- Mobile-responsive interface with gradient backgrounds
- JavaScript polling for live message updates

🎯 Comprehensive Admin Panel:
- Chat room management with merge capabilities
- Password reset system with email templates
- User management with admin controls
- Chat statistics and analytics dashboard
- Room binding to posts and categorization

�� Mobile API Integration:
- RESTful API endpoints at /api/v1/chat
- Session-based authentication for mobile apps
- Comprehensive endpoints for rooms, messages, users
- Mobile app compatibility (React Native, Flutter)

🛠️ Technical Improvements:
- Enhanced database models with ChatRoom categories
- Password reset token system with email verification
- Template synchronization fixes for Docker deployment
- Migration scripts for database schema updates
- Improved error handling and validation

🎨 UI/UX Enhancements:
- Modern card-based layouts matching app design
- Consistent styling across chat and admin interfaces
- Mobile-optimized touch interactions
- Professional gradient designs and glass morphism effects

📚 Documentation:
- Updated README with comprehensive API documentation
- Added deployment instructions for Docker (port 8100)
- Configuration guide for production environments
- Mobile integration examples and endpoints

This update transforms the platform into a comprehensive motorcycle adventure community with modern chat capabilities and professional admin management tools.
2025-08-10 00:22:33 +03:00

604 lines
27 KiB
HTML

{% extends "admin/base.html" %}
{% block title %}Manage Chats - Admin{% endblock %}
{% block admin_content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pb-2 mb-3 border-bottom">
<h1 class="h2">
<i class="fas fa-comments me-2"></i>Manage Chat Rooms
</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<button type="button" class="btn btn-sm btn-outline-secondary me-2" onclick="location.reload()">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button type="button" class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#createRoomModal">
<i class="fas fa-plus"></i> Create Room
</button>
</div>
</div>
<!-- Statistics Cards -->
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-primary h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-primary text-uppercase mb-1">Total Rooms</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ total_rooms }}</div>
</div>
<div class="col-auto">
<i class="fas fa-comments fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-success h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-success text-uppercase mb-1">Linked to Posts</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ linked_rooms }}</div>
</div>
<div class="col-auto">
<i class="fas fa-link fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-warning h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-warning text-uppercase mb-1">Active Today</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ active_today }}</div>
</div>
<div class="col-auto">
<i class="fas fa-clock fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-4">
<div class="card border-left-info h-100 py-2">
<div class="card-body">
<div class="row no-gutters align-items-center">
<div class="col me-2">
<div class="text-xs fw-bold text-info text-uppercase mb-1">Total Messages</div>
<div class="h5 mb-0 fw-bold text-gray-800">{{ total_messages }}</div>
</div>
<div class="col-auto">
<i class="fas fa-comment fa-2x text-gray-300"></i>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters and Search -->
<div class="card mb-4">
<div class="card-body">
<form method="GET" class="row g-3">
<div class="col-md-3">
<label for="category" class="form-label">Category</label>
<select class="form-select" id="category" name="category">
<option value="">All Categories</option>
<option value="general" {{ 'selected' if request.args.get('category') == 'general' }}>General</option>
<option value="technical" {{ 'selected' if request.args.get('category') == 'technical' }}>Technical</option>
<option value="maintenance" {{ 'selected' if request.args.get('category') == 'maintenance' }}>Maintenance</option>
<option value="routes" {{ 'selected' if request.args.get('category') == 'routes' }}>Routes</option>
<option value="events" {{ 'selected' if request.args.get('category') == 'events' }}>Events</option>
<option value="safety" {{ 'selected' if request.args.get('category') == 'safety' }}>Safety</option>
<option value="gear" {{ 'selected' if request.args.get('category') == 'gear' }}>Gear & Equipment</option>
<option value="social" {{ 'selected' if request.args.get('category') == 'social' }}>Social</option>
</select>
</div>
<div class="col-md-3">
<label for="status" class="form-label">Status</label>
<select class="form-select" id="status" name="status">
<option value="">All Status</option>
<option value="active" {{ 'selected' if request.args.get('status') == 'active' }}>Active</option>
<option value="inactive" {{ 'selected' if request.args.get('status') == 'inactive' }}>Inactive</option>
<option value="linked" {{ 'selected' if request.args.get('status') == 'linked' }}>Linked to Post</option>
<option value="unlinked" {{ 'selected' if request.args.get('status') == 'unlinked' }}>Not Linked</option>
</select>
</div>
<div class="col-md-4">
<label for="search" class="form-label">Search</label>
<input type="text" class="form-control" id="search" name="search"
placeholder="Search room name or description..."
value="{{ request.args.get('search', '') }}">
</div>
<div class="col-md-2">
<label class="form-label">&nbsp;</label>
<div class="d-grid">
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Filter
</button>
</div>
</div>
</form>
</div>
</div>
<!-- Chat Rooms Table -->
<div class="card">
<div class="card-header">
<h6 class="m-0 fw-bold text-primary">Chat Rooms</h6>
</div>
<div class="card-body">
{% if chat_rooms %}
<div class="table-responsive">
<table class="table table-bordered table-hover">
<thead class="table-light">
<tr>
<th>Room Name</th>
<th>Category</th>
<th>Created By</th>
<th>Linked Post</th>
<th>Messages</th>
<th>Last Activity</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for room in chat_rooms %}
<tr>
<td>
<div class="d-flex align-items-center">
<div>
<a href="{{ url_for('chat.room', room_id=room.id) }}"
class="fw-bold text-decoration-none" target="_blank">
{{ room.name }}
</a>
{% if room.description %}
<div class="small text-muted">{{ room.description[:100] }}{% if room.description|length > 100 %}...{% endif %}</div>
{% endif %}
</div>
</div>
</td>
<td>
<span class="badge bg-{{ 'success' if room.category == 'general' else 'info' if room.category == 'technical' else 'warning' if room.category == 'maintenance' else 'primary' }}">
{{ room.category.title() if room.category else 'Uncategorized' }}
</span>
</td>
<td>
<a href="{{ url_for('admin.user_detail', user_id=room.created_by.id) }}" class="text-decoration-none">
{{ room.created_by.nickname }}
</a>
</td>
<td>
{% if room.related_post %}
<a href="{{ url_for('admin.post_detail', post_id=room.related_post.id) }}"
class="text-decoration-none">
{{ room.related_post.title[:30] }}{% if room.related_post.title|length > 30 %}...{% endif %}
</a>
{% else %}
<span class="text-muted">Not linked</span>
{% endif %}
</td>
<td>
<span class="badge bg-secondary">{{ room.message_count or 0 }}</span>
</td>
<td>
{% if room.last_activity %}
<small>{{ room.last_activity.strftime('%Y-%m-%d %H:%M') }}</small>
{% else %}
<small class="text-muted">Never</small>
{% endif %}
</td>
<td>
<div class="btn-group" role="group">
<button type="button" class="btn btn-sm btn-outline-primary dropdown-toggle"
data-bs-toggle="dropdown">
Actions
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="#"
onclick="editRoom({{ room.id }}, '{{ room.name }}', '{{ room.description or '' }}', '{{ room.category or '' }}', {{ room.related_post.id if room.related_post else 'null' }})">
<i class="fas fa-edit"></i> Edit Room
</a>
</li>
<li>
<a class="dropdown-item" href="#"
onclick="linkToPost({{ room.id }}, '{{ room.name }}')">
<i class="fas fa-link"></i> Link to Post
</a>
</li>
<li>
<a class="dropdown-item" href="#"
onclick="mergeRoom({{ room.id }}, '{{ room.name }}')">
<i class="fas fa-compress-arrows-alt"></i> Merge Room
</a>
</li>
<li><hr class="dropdown-divider"></li>
<li>
<a class="dropdown-item text-danger" href="#"
onclick="deleteRoom({{ room.id }}, '{{ room.name }}')">
<i class="fas fa-trash"></i> Delete Room
</a>
</li>
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if pagination %}
<nav>
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.manage_chats', page=pagination.prev_num, **request.args) }}">Previous</a>
</li>
{% endif %}
{% for page_num in pagination.iter_pages() %}
{% if page_num %}
{% if page_num != pagination.page %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.manage_chats', page=page_num, **request.args) }}">{{ page_num }}</a>
</li>
{% else %}
<li class="page-item active">
<span class="page-link">{{ page_num }}</span>
</li>
{% endif %}
{% else %}
<li class="page-item disabled">
<span class="page-link">...</span>
</li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item">
<a class="page-link" href="{{ url_for('admin.manage_chats', page=pagination.next_num, **request.args) }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
{% else %}
<div class="text-center py-4">
<i class="fas fa-comments fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No chat rooms found</h5>
<p class="text-muted">Create a new room or adjust your filters.</p>
</div>
{% endif %}
</div>
</div>
<!-- Edit Room Modal -->
<div class="modal fade" id="editRoomModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit Chat Room</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editRoomForm">
<div class="modal-body">
<input type="hidden" id="editRoomId">
<div class="mb-3">
<label for="editRoomName" class="form-label">Room Name</label>
<input type="text" class="form-control" id="editRoomName" required>
</div>
<div class="mb-3">
<label for="editRoomDescription" class="form-label">Description</label>
<textarea class="form-control" id="editRoomDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editRoomCategory" class="form-label">Category</label>
<select class="form-select" id="editRoomCategory">
<option value="">Select Category</option>
<option value="general">General</option>
<option value="technical">Technical</option>
<option value="maintenance">Maintenance</option>
<option value="routes">Routes</option>
<option value="events">Events</option>
<option value="safety">Safety</option>
<option value="gear">Gear & Equipment</option>
<option value="social">Social</option>
</select>
</div>
<div class="mb-3">
<label for="editLinkedPost" class="form-label">Linked Post</label>
<select class="form-select" id="editLinkedPost">
<option value="">No linked post</option>
{% for post in available_posts %}
<option value="{{ post.id }}">{{ post.title }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<!-- Link to Post Modal -->
<div class="modal fade" id="linkPostModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Link Chat Room to Post</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="linkPostForm">
<div class="modal-body">
<input type="hidden" id="linkRoomId">
<div class="alert alert-info">
<i class="fas fa-info-circle"></i>
Linking a chat room to a post will make it appear in the post's discussion section.
</div>
<div class="mb-3">
<label for="linkRoomName" class="form-label">Room Name</label>
<input type="text" class="form-control" id="linkRoomName" readonly>
</div>
<div class="mb-3">
<label for="linkPostSelect" class="form-label">Select Post</label>
<select class="form-select" id="linkPostSelect" required>
<option value="">Choose a post...</option>
{% for post in available_posts %}
<option value="{{ post.id }}">{{ post.title }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-success">Link to Post</button>
</div>
</form>
</div>
</div>
</div>
<!-- Merge Room Modal -->
<div class="modal fade" id="mergeRoomModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Merge Chat Room</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="mergeRoomForm">
<div class="modal-body">
<input type="hidden" id="mergeSourceRoomId">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle"></i>
<strong>Warning:</strong> This action will merge all messages from the source room into the target room.
The source room will be deleted. This cannot be undone.
</div>
<div class="row">
<div class="col-md-6">
<h6>Source Room (will be deleted)</h6>
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title" id="mergeSourceRoomName"></h6>
<p class="card-text small" id="mergeSourceRoomInfo"></p>
</div>
</div>
</div>
<div class="col-md-6">
<h6>Target Room (messages will be merged here)</h6>
<select class="form-select" id="mergeTargetRoom" required>
<option value="">Select target room...</option>
</select>
<div id="mergeTargetRoomPreview" class="mt-2" style="display: none;">
<div class="card bg-light">
<div class="card-body">
<h6 class="card-title" id="mergeTargetRoomName"></h6>
<p class="card-text small" id="mergeTargetRoomInfo"></p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-danger">Merge Rooms</button>
</div>
</form>
</div>
</div>
</div>
<script>
// Edit room functionality
function editRoom(roomId, name, description, category, linkedPostId) {
document.getElementById('editRoomId').value = roomId;
document.getElementById('editRoomName').value = name;
document.getElementById('editRoomDescription').value = description;
document.getElementById('editRoomCategory').value = category;
document.getElementById('editLinkedPost').value = linkedPostId || '';
const modal = new bootstrap.Modal(document.getElementById('editRoomModal'));
modal.show();
}
// Link to post functionality
function linkToPost(roomId, roomName) {
document.getElementById('linkRoomId').value = roomId;
document.getElementById('linkRoomName').value = roomName;
const modal = new bootstrap.Modal(document.getElementById('linkPostModal'));
modal.show();
}
// Merge room functionality
function mergeRoom(roomId, roomName) {
document.getElementById('mergeSourceRoomId').value = roomId;
document.getElementById('mergeSourceRoomName').textContent = roomName;
// Load available rooms for merging
fetch(`/admin/api/chat-rooms?exclude=${roomId}`)
.then(response => response.json())
.then(data => {
const select = document.getElementById('mergeTargetRoom');
select.innerHTML = '<option value="">Select target room...</option>';
data.rooms.forEach(room => {
const option = document.createElement('option');
option.value = room.id;
option.textContent = `${room.name} (${room.category}) - ${room.message_count} messages`;
option.dataset.roomData = JSON.stringify(room);
select.appendChild(option);
});
});
const modal = new bootstrap.Modal(document.getElementById('mergeRoomModal'));
modal.show();
}
// Delete room functionality
function deleteRoom(roomId, roomName) {
if (confirm(`Are you sure you want to delete the room "${roomName}"? This action cannot be undone.`)) {
fetch(`/admin/api/chat-rooms/${roomId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error deleting room: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to delete room');
});
}
}
// Form submissions
document.getElementById('editRoomForm').addEventListener('submit', function(e) {
e.preventDefault();
const roomId = document.getElementById('editRoomId').value;
const formData = {
name: document.getElementById('editRoomName').value,
description: document.getElementById('editRoomDescription').value,
category: document.getElementById('editRoomCategory').value,
related_post_id: document.getElementById('editLinkedPost').value || null
};
fetch(`/admin/api/chat-rooms/${roomId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error updating room: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to update room');
});
});
document.getElementById('linkPostForm').addEventListener('submit', function(e) {
e.preventDefault();
const roomId = document.getElementById('linkRoomId').value;
const postId = document.getElementById('linkPostSelect').value;
fetch(`/admin/api/chat-rooms/${roomId}/link-post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ post_id: postId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Error linking room to post: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to link room to post');
});
});
document.getElementById('mergeRoomForm').addEventListener('submit', function(e) {
e.preventDefault();
const sourceRoomId = document.getElementById('mergeSourceRoomId').value;
const targetRoomId = document.getElementById('mergeTargetRoom').value;
fetch(`/admin/api/chat-rooms/${sourceRoomId}/merge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ target_room_id: targetRoomId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Rooms merged successfully!');
location.reload();
} else {
alert('Error merging rooms: ' + data.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Failed to merge rooms');
});
});
// Target room preview
document.getElementById('mergeTargetRoom').addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
const preview = document.getElementById('mergeTargetRoomPreview');
if (selectedOption.value && selectedOption.dataset.roomData) {
const roomData = JSON.parse(selectedOption.dataset.roomData);
document.getElementById('mergeTargetRoomName').textContent = roomData.name;
document.getElementById('mergeTargetRoomInfo').textContent =
`Category: ${roomData.category} | Messages: ${roomData.message_count}`;
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
});
</script>
{% endblock %}