Улучшил работу с заявками на звонок
This commit is contained in:
parent
9a0d64fa00
commit
c3228a8baa
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,4 +1,10 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.html import format_html
|
||||||
|
from django.urls import path
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.http import HttpResponseRedirect
|
||||||
|
from django.contrib import messages
|
||||||
from .models import *
|
from .models import *
|
||||||
|
|
||||||
|
|
||||||
@ -36,13 +42,104 @@ class HomeAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(CallbackRequest)
|
@admin.register(CallbackRequest)
|
||||||
class CallbackAdmin(admin.ModelAdmin):
|
class CallbackAdmin(admin.ModelAdmin):
|
||||||
list_display = ('name', 'phone', 'email', 'time_create', 'is_processed')
|
list_display = ('name', 'phone', 'email', 'time_create', 'is_processed', 'is_read', 'new_badge')
|
||||||
list_display_links = ('name', 'phone')
|
list_display_links = ('name', 'phone')
|
||||||
list_editable = ('is_processed',)
|
list_editable = ('is_processed', 'is_read')
|
||||||
list_filter = ('time_create', 'is_processed')
|
list_filter = ('time_create', 'is_processed', 'is_read')
|
||||||
search_fields = ('name', 'phone', 'email')
|
search_fields = ('name', 'phone', 'email')
|
||||||
readonly_fields = ('time_create',)
|
readonly_fields = ('time_create',)
|
||||||
|
actions = ['mark_as_read', 'mark_as_unread', 'mark_as_processed']
|
||||||
|
|
||||||
|
def new_badge(self, obj):
|
||||||
|
if not obj.is_read:
|
||||||
|
return format_html('<span style="color: red; font-weight: bold;">🆕 НОВАЯ</span>')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
new_badge.short_description = 'Статус'
|
||||||
|
|
||||||
|
def get_queryset(self, request):
|
||||||
|
# Показываем количество непрочитанных в заголовке
|
||||||
|
unread_count = CallbackRequest.objects.filter(is_read=False).count()
|
||||||
|
if unread_count > 0:
|
||||||
|
self.message_user(
|
||||||
|
request,
|
||||||
|
f'У вас {unread_count} непрочитанных заявок!',
|
||||||
|
messages.WARNING
|
||||||
|
)
|
||||||
|
return super().get_queryset(request)
|
||||||
|
|
||||||
|
def mark_as_read(self, request, queryset):
|
||||||
|
updated = queryset.update(is_read=True)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как прочитанные')
|
||||||
|
|
||||||
|
mark_as_read.short_description = "Отметить как прочитанные"
|
||||||
|
|
||||||
|
def mark_as_unread(self, request, queryset):
|
||||||
|
updated = queryset.update(is_read=False)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как непрочитанные')
|
||||||
|
|
||||||
|
mark_as_unread.short_description = "Отметить как непрочитанные"
|
||||||
|
|
||||||
|
def mark_as_processed(self, request, queryset):
|
||||||
|
updated = queryset.update(is_processed=True)
|
||||||
|
self.message_user(request, f'{updated} заявок отмечены как обработанные')
|
||||||
|
|
||||||
|
mark_as_processed.short_description = "Отметить как обработанные"
|
||||||
|
|
||||||
|
# Добавляем кастомное представление для статистики
|
||||||
|
def get_urls(self):
|
||||||
|
urls = super().get_urls()
|
||||||
|
custom_urls = [
|
||||||
|
path('callback-stats/', self.admin_site.admin_view(self.callback_stats), name='callback_stats'),
|
||||||
|
]
|
||||||
|
return custom_urls + urls
|
||||||
|
|
||||||
|
def callback_stats(self, request):
|
||||||
|
today = timezone.now().date()
|
||||||
|
week_ago = today - timezone.timedelta(days=7)
|
||||||
|
|
||||||
|
stats = {
|
||||||
|
'total': CallbackRequest.objects.count(),
|
||||||
|
'today': CallbackRequest.objects.filter(time_create__date=today).count(),
|
||||||
|
'week': CallbackRequest.objects.filter(time_create__date__gte=week_ago).count(),
|
||||||
|
'unread': CallbackRequest.objects.filter(is_read=False).count(),
|
||||||
|
'unprocessed': CallbackRequest.objects.filter(is_processed=False).count(),
|
||||||
|
}
|
||||||
|
|
||||||
|
context = {
|
||||||
|
**self.admin_site.each_context(request),
|
||||||
|
'title': 'Статистика заявок',
|
||||||
|
'stats': stats,
|
||||||
|
}
|
||||||
|
return render(request, 'admin/callback_stats.html', context)
|
||||||
|
|
||||||
|
class ProgrammerAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'photo', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class RecallAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'scan', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class SolutionAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'description', 'implementation')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
|
class HomeAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('id', 'title', 'time_create', 'is_published')
|
||||||
|
list_display_links = ('id', 'title')
|
||||||
|
search_fields = ('title', 'content')
|
||||||
|
list_editable = ('is_published',)
|
||||||
|
list_filter = ('time_create', 'is_published')
|
||||||
|
|
||||||
@admin.register(PageView)
|
@admin.register(PageView)
|
||||||
class PageViewAdmin(admin.ModelAdmin):
|
class PageViewAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.2.7 on 2025-11-14 09:37
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('programmer', '0011_visitor_pageview'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='callbackrequest',
|
||||||
|
name='is_read',
|
||||||
|
field=models.BooleanField(default=False, verbose_name='Прочитано'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
@ -94,6 +94,7 @@ class CallbackRequest(models.Model):
|
|||||||
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
|
question = models.TextField(blank=True, verbose_name='Ваш вопрос') # Сделать необязательным
|
||||||
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
time_create = models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')
|
||||||
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
|
is_processed = models.BooleanField(default=False, verbose_name='Обработано')
|
||||||
|
is_read = models.BooleanField(default=False, verbose_name='Прочитано')
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.name} - {self.phone}"
|
return f"{self.name} - {self.phone}"
|
||||||
|
|||||||
@ -10,6 +10,17 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block userlinks %}
|
{% block userlinks %}
|
||||||
<a href="{% url 'statistics' %}">📊 Статистика</a> /
|
{% load programmer_tags %}
|
||||||
|
|
||||||
|
<!-- Уведомление о новых заявках -->
|
||||||
|
{% get_unread_callbacks as unread_callbacks %}
|
||||||
|
{% if unread_callbacks %}
|
||||||
|
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" style="color: #dc3545; font-weight: bold;">
|
||||||
|
🚨 {{ unread_callbacks }} новых заявок
|
||||||
|
</a> /
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<a href="{% url 'callback_stats' %}">📊 Статистика заявок</a> /
|
||||||
|
<a href="{% url 'statistics' %}">📈 Посещения</a> /
|
||||||
{{ block.super }}
|
{{ block.super }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
45
OneCprogsite/programmer/templates/admin/callback_stats.html
Normal file
45
OneCprogsite/programmer/templates/admin/callback_stats.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% extends "admin/base_site.html" %}
|
||||||
|
|
||||||
|
{% block title %}Статистика заявок{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="module">
|
||||||
|
<h1>📊 Статистика заявок на обратный звонок</h1>
|
||||||
|
|
||||||
|
<div class="stats-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin: 2rem 0;">
|
||||||
|
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||||
|
<h3>📋 Всего заявок</h3>
|
||||||
|
<p style="font-size: 2rem; font-weight: bold; color: #007bff; margin: 0;">{{ stats.total }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||||
|
<h3>📅 Сегодня</h3>
|
||||||
|
<p style="font-size: 2rem; font-weight: bold; color: #28a745; margin: 0;">{{ stats.today }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||||
|
<h3>📈 За неделю</h3>
|
||||||
|
<p style="font-size: 2rem; font-weight: bold; color: #17a2b8; margin: 0;">{{ stats.week }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||||
|
<h3>🆕 Непрочитанные</h3>
|
||||||
|
<p style="font-size: 2rem; font-weight: bold; color: #dc3545; margin: 0;">{{ stats.unread }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card" style="background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center;">
|
||||||
|
<h3>⏳ В обработке</h3>
|
||||||
|
<p style="font-size: 2rem; font-weight: bold; color: #ffc107; margin: 0;">{{ stats.unprocessed }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 2rem;">
|
||||||
|
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="button" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px;">
|
||||||
|
📋 Перейти к списку заявок
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'admin:index' %}" class="button" style="background: #6c757d; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; margin-left: 10px;">
|
||||||
|
🏠 На главную админки
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@ -1,9 +1,29 @@
|
|||||||
{% extends 'admin/base.html' %}
|
{% extends 'admin/base.html' %}
|
||||||
|
{% load programmer_tags %}
|
||||||
|
|
||||||
{% block title %}Статистика посещений{% endblock %}
|
{% block title %}Статистика посещений{% endblock %}
|
||||||
{% block page_title %}Статистика посещений{% endblock %}
|
{% block page_title %}Статистика посещений{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Уведомления о заявках -->
|
||||||
|
{% get_unread_callbacks as unread_callbacks %}
|
||||||
|
{% get_today_callbacks as today_callbacks %}
|
||||||
|
{% if unread_callbacks > 0 %}
|
||||||
|
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
||||||
|
<h4>🚨 Внимание!</h4>
|
||||||
|
<p>
|
||||||
|
У вас <strong>{{ unread_callbacks }}</strong> непрочитанных заявок на обратный звонок
|
||||||
|
{% if today_callbacks > 0 %}
|
||||||
|
({{ today_callbacks }} из них сегодня)
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'admin:programmer_callbackrequest_changelist' %}" class="btn btn-warning btn-sm">
|
||||||
|
📋 Перейти к заявкам
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="stats-grid">
|
<div class="stats-grid">
|
||||||
@ -23,10 +43,21 @@
|
|||||||
<h3>🕒 Всего просмотров</h3>
|
<h3>🕒 Всего просмотров</h3>
|
||||||
<p class="stat-number">{{ total_views }}</p>
|
<p class="stat-number">{{ total_views }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Добавляем статистику по заявкам -->
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>📞 Заявок сегодня</h3>
|
||||||
|
<p class="stat-number">{{ today_callbacks }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>📋 Всего заявок</h3>
|
||||||
|
<p class="stat-number">{% get_unread_callbacks %}/{{ total_callbacks }}</p>
|
||||||
|
<small>(непрочитанные/всего)</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Остальной код статистики остается без изменений -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
|||||||
Binary file not shown.
@ -1,14 +1,14 @@
|
|||||||
from django import template
|
from django import template
|
||||||
from programmer.models import *
|
from ..models import CallbackRequest
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
@register.simple_tag(name='competence')
|
@register.simple_tag
|
||||||
def get_competence():
|
def get_unread_callbacks():
|
||||||
return Competence.objects.all()
|
return CallbackRequest.objects.filter(is_read=False).count()
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
@register.simple_tag(name='recall')
|
def get_today_callbacks():
|
||||||
def get_recall():
|
from django.utils import timezone
|
||||||
return Recall.objects.all()
|
today = timezone.now().date()
|
||||||
|
return CallbackRequest.objects.filter(time_create__date=today).count()
|
||||||
@ -228,6 +228,11 @@ def statistics_view(request):
|
|||||||
# Последние посещения
|
# Последние посещения
|
||||||
recent_views = PageView.objects.select_related().order_by('-timestamp')[:20]
|
recent_views = PageView.objects.select_related().order_by('-timestamp')[:20]
|
||||||
|
|
||||||
|
today = timezone.now().date()
|
||||||
|
total_callbacks = CallbackRequest.objects.count()
|
||||||
|
today_callbacks = CallbackRequest.objects.filter(time_create__date=today).count()
|
||||||
|
unread_callbacks = CallbackRequest.objects.filter(is_read=False).count()
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'today_views': today_views,
|
'today_views': today_views,
|
||||||
'weekly_views': weekly_views,
|
'weekly_views': weekly_views,
|
||||||
@ -235,6 +240,9 @@ def statistics_view(request):
|
|||||||
'unique_visitors': unique_visitors,
|
'unique_visitors': unique_visitors,
|
||||||
'popular_pages': popular_pages,
|
'popular_pages': popular_pages,
|
||||||
'recent_views': recent_views,
|
'recent_views': recent_views,
|
||||||
|
'total_callbacks': total_callbacks,
|
||||||
|
'today_callbacks': today_callbacks,
|
||||||
|
'unread_callbacks': unread_callbacks,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'admin/statistics.html', context)
|
return render(request, 'admin/statistics.html', context)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user