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)