534 lines
22 KiB
Python
534 lines
22 KiB
Python
import os
|
|
import platform
|
|
|
|
from django import get_version as django_version
|
|
from django.shortcuts import render
|
|
from django.utils import timezone
|
|
from django.views.generic import ListView
|
|
from .models import Customer
|
|
|
|
|
|
def home(request):
|
|
"""Render the landing screen with loader and environment details."""
|
|
host_name = request.get_host().lower()
|
|
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
|
now = timezone.now()
|
|
|
|
context = {
|
|
"project_name": "New Style",
|
|
"agent_brand": agent_brand,
|
|
"django_version": django_version(),
|
|
"python_version": platform.python_version(),
|
|
"current_time": now,
|
|
"host_name": host_name,
|
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
|
|
def article_detail(request):
|
|
"""Render the article detail screen."""
|
|
host_name = request.get_host().lower()
|
|
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
|
now = timezone.now()
|
|
|
|
context = {
|
|
"project_name": "New Style",
|
|
"agent_brand": agent_brand,
|
|
"django_version": django_version(),
|
|
"python_version": platform.python_version(),
|
|
"current_time": now,
|
|
"host_name": host_name,
|
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
|
"project_image__url": os.getenv("PROJECT_IMAGE_URL", ""),
|
|
}
|
|
return render(request, "core/article_detail.html", context)
|
|
|
|
from django.conf import settings
|
|
from django.core.paginator import Paginator
|
|
from django.db.models import Q, Count, Subquery, OuterRef
|
|
from .models import User, ContactHistory, Opportunity, ActivityLog
|
|
from django.shortcuts import get_object_or_404
|
|
from django.views import View
|
|
|
|
class CustomerListView(ListView):
|
|
model = Customer
|
|
template_name = 'core/customer_list.html'
|
|
context_object_name = 'customers'
|
|
paginate_by = 25 # Default page size
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset().select_related('owner')
|
|
|
|
# Filter persistence
|
|
if self.request.GET.get('clear_filters'):
|
|
for key in list(self.request.session.keys()):
|
|
if key.startswith('customer_list_filter_'):
|
|
del self.request.session[key]
|
|
|
|
filter_params = {
|
|
'show_deleted': self.request.GET.get('show_deleted'),
|
|
'q': self.request.GET.get('q'),
|
|
'status': self.request.GET.get('status'),
|
|
'owner': self.request.GET.get('owner'),
|
|
}
|
|
|
|
for key, value in filter_params.items():
|
|
if value is not None:
|
|
self.request.session[f'customer_list_filter_{key}'] = value
|
|
else:
|
|
filter_params[key] = self.request.session.get(f'customer_list_filter_{key}')
|
|
|
|
# Filter by deletion status
|
|
if not filter_params['show_deleted']:
|
|
queryset = queryset.filter(is_deleted=False)
|
|
|
|
# Search
|
|
if filter_params['q']:
|
|
queryset = queryset.filter(
|
|
Q(name__icontains=filter_params['q']) |
|
|
Q(email__icontains=filter_params['q']) |
|
|
Q(id__icontains=filter_params['q'])
|
|
)
|
|
|
|
# Filter
|
|
if filter_params['status']:
|
|
queryset = queryset.filter(status=filter_params['status'])
|
|
|
|
if filter_params['owner']:
|
|
queryset = queryset.filter(owner_id=filter_params['owner'])
|
|
|
|
# Annotate with last contact and opportunity count
|
|
last_contact_subquery = ContactHistory.objects.filter(
|
|
customer=OuterRef('pk')
|
|
).order_by('-created_at').values('created_at')[:1]
|
|
|
|
queryset = queryset.annotate(
|
|
last_contact=Subquery(last_contact_subquery),
|
|
opportunity_count=Count('opportunities', distinct=True)
|
|
).order_by('name')
|
|
|
|
return queryset
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
page_size = self.request.GET.get('page_size', self.paginate_by)
|
|
|
|
paginator = Paginator(self.get_queryset(), page_size)
|
|
page_number = self.request.GET.get('page')
|
|
page_obj = paginator.get_page(page_number)
|
|
|
|
context['page_obj'] = page_obj
|
|
context['page_size'] = int(page_size)
|
|
context['paginator'] = paginator
|
|
context['customer_count'] = paginator.count
|
|
context['statuses'] = Customer.STATUS_CHOICES
|
|
context['owners'] = User.objects.filter(is_staff=True) # Assuming staff are owners
|
|
|
|
# Persist query params
|
|
context['query'] = self.request.session.get('customer_list_filter_q', '')
|
|
context['selected_status'] = self.request.session.get('customer_list_filter_status', '')
|
|
context['selected_owner'] = self.request.session.get('customer_list_filter_owner', '')
|
|
context['show_deleted'] = self.request.session.get('customer_list_filter_show_deleted', False)
|
|
|
|
is_filter_active = any([
|
|
self.request.session.get('customer_list_filter_q'),
|
|
self.request.session.get('customer_list_filter_status'),
|
|
self.request.session.get('customer_list_filter_owner'),
|
|
self.request.session.get('customer_list_filter_show_deleted'),
|
|
self.request.session.get('activity_feed_filter_activity_user'),
|
|
self.request.session.get('activity_feed_filter_activity_action'),
|
|
self.request.session.get('activity_feed_filter_activity_date_range'),
|
|
])
|
|
context['is_filter_active'] = is_filter_active
|
|
|
|
highlighted_customer_ids = []
|
|
updated_customer_id = self.request.session.pop('updated_customer_id', None)
|
|
if updated_customer_id:
|
|
highlighted_customer_ids.append(updated_customer_id)
|
|
|
|
deleted_customer_id = self.request.session.pop('deleted_customer_id', None)
|
|
if deleted_customer_id:
|
|
highlighted_customer_ids.append(deleted_customer_id)
|
|
|
|
restored_customer_id = self.request.session.pop('restored_customer_id', None)
|
|
if restored_customer_id:
|
|
highlighted_customer_ids.append(restored_customer_id)
|
|
|
|
restored_customer_ids = self.request.session.pop('restored_customer_ids', None)
|
|
if restored_customer_ids:
|
|
highlighted_customer_ids.extend([int(cid) for cid in restored_customer_ids])
|
|
|
|
context['highlighted_customer_ids'] = highlighted_customer_ids
|
|
|
|
context['undo_timeout'] = settings.UNDO_TIMEOUT
|
|
context['toast_position'] = settings.TOAST_POSITION
|
|
|
|
# Activity feed filtering
|
|
if self.request.GET.get('clear_filters'):
|
|
for key in list(self.request.session.keys()):
|
|
if key.startswith('activity_feed_filter_'):
|
|
del self.request.session[key]
|
|
|
|
activity_sort = self.request.GET.get('activity_sort', '-timestamp')
|
|
activity_page_size = self.request.GET.get('activity_page_size', 5)
|
|
|
|
activity_filter_params = {
|
|
'activity_user': self.request.GET.get('activity_user'),
|
|
'activity_action': self.request.GET.get('activity_action'),
|
|
'activity_date_range': self.request.GET.get('activity_date_range'),
|
|
'activity_sort': activity_sort,
|
|
'activity_page_size': activity_page_size,
|
|
}
|
|
|
|
for key, value in activity_filter_params.items():
|
|
if value is not None:
|
|
self.request.session[f'activity_feed_filter_{key}'] = value
|
|
else:
|
|
activity_filter_params[key] = self.request.session.get(f'activity_feed_filter_{key}')
|
|
|
|
activity_logs = ActivityLog.objects.select_related('actor').prefetch_related('content_object').all()
|
|
|
|
if activity_filter_params['activity_user']:
|
|
activity_logs = activity_logs.filter(actor_id=activity_filter_params['activity_user'])
|
|
|
|
if activity_filter_params['activity_action']:
|
|
activity_logs = activity_logs.filter(action=activity_filter_params['activity_action'])
|
|
|
|
if activity_filter_params['activity_date_range']:
|
|
try:
|
|
start_date, end_date = activity_filter_params['activity_date_range'].split(' to ')
|
|
activity_logs = activity_logs.filter(timestamp__date__gte=start_date, timestamp__date__lte=end_date)
|
|
except ValueError:
|
|
# Handle cases where the date range is not in the expected format
|
|
pass
|
|
|
|
if activity_filter_params['activity_sort']:
|
|
sort_param = activity_filter_params['activity_sort']
|
|
if sort_param == 'actor':
|
|
activity_logs = activity_logs.order_by('actor__email')
|
|
elif sort_param == '-actor':
|
|
activity_logs = activity_logs.order_by('-actor__email')
|
|
else:
|
|
activity_logs = activity_logs.order_by(sort_param)
|
|
|
|
# Activity summary
|
|
activity_summary = activity_logs.values('action').annotate(count=Count('action'))
|
|
context['activity_summary'] = {item['action']: item['count'] for item in activity_summary}
|
|
|
|
# Paginate activity logs
|
|
activity_page_size = activity_filter_params.get('activity_page_size', 5)
|
|
activity_paginator = Paginator(activity_logs, activity_page_size)
|
|
activity_page_number = self.request.GET.get('activity_page')
|
|
activity_page_obj = activity_paginator.get_page(activity_page_number)
|
|
|
|
context['activity_logs'] = activity_page_obj
|
|
context['activity_paginator'] = activity_paginator
|
|
context['activity_log_count'] = activity_paginator.count
|
|
context['activity_page_size'] = int(activity_page_size)
|
|
|
|
# Get users and actions for filter dropdowns
|
|
user_ids = ActivityLog.objects.values_list('actor_id', flat=True).distinct()
|
|
context['activity_users'] = User.objects.filter(id__in=user_ids)
|
|
context['activity_actions'] = ActivityLog.objects.values_list('action', flat=True).distinct()
|
|
|
|
context['selected_activity_user'] = self.request.session.get('activity_feed_filter_activity_user', '')
|
|
context['selected_activity_action'] = self.request.session.get('activity_feed_filter_activity_action', '')
|
|
context['selected_activity_date_range'] = self.request.session.get('activity_feed_filter_activity_date_range', '')
|
|
context['selected_activity_sort'] = self.request.session.get('activity_feed_filter_activity_sort', '-timestamp')
|
|
|
|
from datetime import timedelta
|
|
twenty_four_hours_ago = timezone.now() - timedelta(hours=24)
|
|
context['recent_activity_count'] = ActivityLog.objects.filter(timestamp__gte=twenty_four_hours_ago).count()
|
|
|
|
return context
|
|
|
|
class CustomerDetailView(View):
|
|
template_name = "core/customer_detail.html"
|
|
|
|
def get(self, request, pk):
|
|
customer = get_object_or_404(
|
|
Customer.objects.select_related('owner', 'tenant')
|
|
.prefetch_related('opportunities', 'leads', 'contacthistory_set', 'notes'),
|
|
pk=pk
|
|
)
|
|
context = {
|
|
"customer": customer,
|
|
"opportunities": customer.opportunities.all(),
|
|
"leads": customer.leads.all(),
|
|
"contact_history": customer.contacthistory_set.all(),
|
|
"notes": customer.notes.all(),
|
|
"opportunity_count": customer.opportunities.count(),
|
|
"lead_count": customer.leads.count(),
|
|
"note_count": customer.notes.count(),
|
|
}
|
|
return render(request, self.template_name, context)
|
|
|
|
from django.urls import reverse_lazy
|
|
from django.http import HttpResponseRedirect
|
|
from django.contrib import messages
|
|
from django.views.generic import DetailView
|
|
from django.views.generic.edit import CreateView, UpdateView, DeleteView
|
|
from .forms import CustomerForm, OpportunityForm
|
|
from django.forms.models import model_to_dict
|
|
|
|
class CustomerUpdateView(UpdateView):
|
|
model = Customer
|
|
form_class = CustomerForm
|
|
template_name = 'core/customer_form.html'
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('customer_list')
|
|
|
|
def form_valid(self, form):
|
|
# Capture previous state for changed fields only
|
|
previous_state = {key: str(getattr(self.object, key)) for key in form.changed_data}
|
|
|
|
response = super().form_valid(form)
|
|
|
|
# Capture new state for changed fields
|
|
changed_data = {key: {'old': previous_state[key], 'new': str(form.cleaned_data[key])} for key in form.changed_data}
|
|
|
|
if form.changed_data:
|
|
undo_data = {
|
|
'customer_id': self.object.pk,
|
|
'previous_state': previous_state,
|
|
'timestamp': timezone.now().isoformat(),
|
|
'session_key': self.request.session.session_key,
|
|
'action': 'update'
|
|
}
|
|
self.request.session['undo_data'] = undo_data
|
|
undo_url = reverse_lazy('customer_undo')
|
|
message = f'{{"message": "Customer updated successfully.", "undo_url": "{undo_url}"}}'
|
|
messages.success(self.request, message)
|
|
|
|
ActivityLog.objects.create(
|
|
actor=self.request.user,
|
|
action=f'updated a {self.object.__class__.__name__}',
|
|
content_object=self.object,
|
|
details=changed_data
|
|
)
|
|
return response
|
|
|
|
def form_invalid(self, form):
|
|
messages.error(self.request, 'Please correct the errors below.')
|
|
return super().form_invalid(form)
|
|
|
|
|
|
class CustomerDeleteView(DeleteView):
|
|
model = Customer
|
|
template_name = 'core/customer_confirm_delete.html'
|
|
success_url = reverse_lazy('customer_list')
|
|
|
|
def form_valid(self, form):
|
|
self.object = self.get_object()
|
|
success_url = self.get_success_url()
|
|
|
|
# Store data required for restoration
|
|
previous_state = model_to_dict(self.object, fields=['name', 'email', 'phone', 'address', 'status', 'owner'])
|
|
# model_to_dict gives owner id, which is what we want.
|
|
|
|
undo_data = {
|
|
'customer_id': self.object.pk,
|
|
'previous_state': previous_state,
|
|
'timestamp': timezone.now().isoformat(),
|
|
'session_key': self.request.session.session_key,
|
|
'action': 'delete'
|
|
}
|
|
self.request.session['undo_data'] = undo_data
|
|
|
|
self.object.delete(user=self.request.user)
|
|
|
|
undo_url = reverse_lazy('customer_undo')
|
|
message = f'{{"message": "Customer deleted successfully.", "undo_url": "{undo_url}"}}'
|
|
messages.success(self.request, message)
|
|
|
|
return HttpResponseRedirect(success_url)
|
|
|
|
|
|
class CustomerUndoView(View):
|
|
def get(self, request):
|
|
undo_data = request.session.get('undo_data')
|
|
|
|
if not undo_data or undo_data.get('session_key') != request.session.session_key:
|
|
messages.error(request, 'Invalid undo request.')
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
|
|
try:
|
|
undo_timestamp = timezone.datetime.fromisoformat(undo_data['timestamp'])
|
|
if (timezone.now() - undo_timestamp).total_seconds() > settings.UNDO_TIMEOUT:
|
|
messages.error(request, 'The undo link has expired.')
|
|
request.session.pop('undo_data', None)
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
except (ValueError, TypeError):
|
|
messages.error(request, 'Invalid undo timestamp.')
|
|
request.session.pop('undo_data', None)
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
|
|
|
|
action = undo_data.get('action')
|
|
customer_id = undo_data.get('customer_id')
|
|
previous_state = undo_data.get('previous_state')
|
|
|
|
try:
|
|
if action == 'update':
|
|
customer = get_object_or_404(Customer, pk=customer_id)
|
|
for key, value in previous_state.items():
|
|
# Handle foreign keys
|
|
if key == 'owner' and value:
|
|
value = get_object_or_404(User, pk=value)
|
|
setattr(customer, key, value)
|
|
customer.save()
|
|
ActivityLog.objects.create(actor=request.user, action='undid update for', content_object=customer)
|
|
messages.success(request, '{"message": "Update undone successfully.", "icon": "success"}')
|
|
request.session['restored_customer_id'] = customer.pk
|
|
|
|
elif action == 'delete':
|
|
# Check if customer is already restored (e.g., from bulk restore)
|
|
if Customer.objects.filter(pk=customer_id, is_deleted=False).exists():
|
|
messages.warning(request, 'Customer already restored.')
|
|
else:
|
|
# If soft-deleted, restore it
|
|
if Customer.objects.filter(pk=customer_id).exists():
|
|
customer = Customer.objects.get(pk=customer_id)
|
|
customer.restore(user=request.user)
|
|
# If hard-deleted, recreate it
|
|
else:
|
|
owner_id = previous_state.pop('owner', None)
|
|
if owner_id:
|
|
previous_state['owner'] = get_object_or_404(User, pk=owner_id)
|
|
customer = Customer.objects.create(pk=customer_id, **previous_state)
|
|
|
|
ActivityLog.objects.create(actor=request.user, action='undid delete for', content_object=customer)
|
|
messages.success(request, '{"message": "1 customer restored successfully.", "icon": "success"}')
|
|
request.session['restored_customer_id'] = customer_id
|
|
|
|
except Exception as e:
|
|
messages.error(request, f"An error occurred during undo: {e}")
|
|
|
|
|
|
request.session.pop('undo_data', None)
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
|
|
|
|
class CustomerRestoreView(View):
|
|
def get(self, request, pk):
|
|
customer = get_object_or_404(Customer, pk=pk)
|
|
customer.restore(user=request.user)
|
|
messages.success(request, '{"message": "1 customer restored successfully.", "icon": "success"}')
|
|
request.session['restored_customer_id'] = pk
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
|
|
class CustomerBulkActionView(View):
|
|
def post(self, request):
|
|
selected_customers = request.POST.getlist('selected_customers')
|
|
action = request.POST.get('action')
|
|
|
|
if action == 'restore':
|
|
restored_count = 0
|
|
restored_ids = []
|
|
for customer_id in selected_customers:
|
|
customer = get_object_or_404(Customer, pk=customer_id)
|
|
customer.restore(user=request.user)
|
|
restored_count += 1
|
|
restored_ids.append(customer_id)
|
|
if restored_count > 0:
|
|
messages.success(request, f'{{"message": "{{restored_count}} customers restored successfully.", "icon": "success"}}')
|
|
request.session['restored_customer_ids'] = restored_ids
|
|
|
|
elif action == 'delete':
|
|
deleted_count = 0
|
|
deleted_ids = []
|
|
for customer_id in selected_customers:
|
|
customer = get_object_or_404(Customer, pk=customer_id)
|
|
customer.delete(user=request.user)
|
|
deleted_count += 1
|
|
deleted_ids.append(customer_id)
|
|
|
|
if deleted_count > 0:
|
|
request.session['bulk_deleted_customer_ids'] = deleted_ids
|
|
undo_url = reverse_lazy('customer_bulk_undo')
|
|
message = f'{{"message": "{deleted_count} customers deleted successfully.", "undo_url": "{undo_url}"}}'
|
|
messages.success(self.request, message)
|
|
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
|
|
|
|
class CustomerBulkUndoView(View):
|
|
def get(self, request):
|
|
customer_ids = request.session.pop('bulk_deleted_customer_ids', [])
|
|
if customer_ids:
|
|
restored_count = 0
|
|
restored_ids = []
|
|
for customer_id in customer_ids:
|
|
customer = get_object_or_404(Customer, pk=customer_id)
|
|
customer.restore(user=request.user)
|
|
restored_count += 1
|
|
restored_ids.append(customer_id)
|
|
|
|
if restored_count > 0:
|
|
messages.success(request, f'{{"message": "{{restored_count}} customers restored successfully.", "icon": "success"}}')
|
|
request.session['restored_customer_ids'] = restored_ids
|
|
|
|
return HttpResponseRedirect(reverse_lazy('customer_list'))
|
|
|
|
|
|
|
|
|
|
class OpportunityListView(ListView):
|
|
model = Opportunity
|
|
template_name = 'core/opportunity_list.html'
|
|
context_object_name = 'opportunities'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
queryset = super().get_queryset().select_related('lead__customer', 'created_by').filter(is_deleted=False)
|
|
return queryset
|
|
|
|
|
|
class OpportunityDetailView(DetailView):
|
|
model = Opportunity
|
|
template_name = 'core/opportunity_detail.html'
|
|
context_object_name = 'opportunity'
|
|
|
|
|
|
class OpportunityCreateView(CreateView):
|
|
model = Opportunity
|
|
form_class = OpportunityForm
|
|
template_name = 'core/opportunity_form.html'
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('opportunity_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def form_valid(self, form):
|
|
form.instance.created_by = self.request.user
|
|
messages.success(self.request, 'Opportunity created successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class OpportunityUpdateView(UpdateView):
|
|
model = Opportunity
|
|
form_class = OpportunityForm
|
|
template_name = 'core/opportunity_form.html'
|
|
|
|
def get_success_url(self):
|
|
return reverse_lazy('opportunity_detail', kwargs={'pk': self.object.pk})
|
|
|
|
def form_valid(self, form):
|
|
form.instance.updated_by = self.request.user
|
|
messages.success(self.request, 'Opportunity updated successfully.')
|
|
return super().form_valid(form)
|
|
|
|
|
|
class OpportunityDeleteView(DeleteView):
|
|
model = Opportunity
|
|
template_name = 'core/opportunity_confirm_delete.html'
|
|
success_url = reverse_lazy('opportunity_list')
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
success_url = self.get_success_url()
|
|
self.object.delete(user=request.user)
|
|
messages.success(request, 'Opportunity deleted successfully.')
|
|
return HttpResponseRedirect(success_url) |