149 lines
6.6 KiB
Python
149 lines
6.6 KiB
Python
from django.contrib import messages
|
|
from django.db.models import Q
|
|
from django.shortcuts import get_object_or_404, redirect, render
|
|
from django.utils import timezone
|
|
|
|
from .forms import ActivityForm, QuoteLineForm, StageUpdateForm, WorkspaceIntakeForm
|
|
from .models import SalesWorkspace
|
|
|
|
|
|
def _base_shell_context():
|
|
return {
|
|
"project_name": "FlowDesk Sales",
|
|
"project_tagline": "Compact sales workspace for fast lead-to-quote execution",
|
|
"meta_description": (
|
|
"Single-screen sales workspace with compact navigation, opportunity tracking, "
|
|
"quotation drafting, projects, and agenda panels."
|
|
),
|
|
"nav_items": [
|
|
{"icon": "bi-grid-1x2-fill", "label": "Workspace", "url": "/"},
|
|
{"icon": "bi-people-fill", "label": "Contacts", "url": "/"},
|
|
{"icon": "bi-graph-up-arrow", "label": "Opportunities", "url": "/"},
|
|
{"icon": "bi-receipt-cutoff", "label": "Quotations", "url": "/"},
|
|
{"icon": "bi-kanban-fill", "label": "Projects", "url": "/"},
|
|
{"icon": "bi-calendar3", "label": "Agenda", "url": "/"},
|
|
{"icon": "bi-shield-lock-fill", "label": "Admin", "url": "/admin/"},
|
|
],
|
|
}
|
|
|
|
|
|
def _workspace_queryset(query=None):
|
|
queryset = SalesWorkspace.objects.prefetch_related("quote_lines", "activities")
|
|
if query:
|
|
queryset = queryset.filter(
|
|
Q(customer_name__icontains=query)
|
|
| Q(project_name__icontains=query)
|
|
| Q(project_number__icontains=query)
|
|
| Q(zipcode__icontains=query)
|
|
| Q(address__icontains=query)
|
|
| Q(opportunity_title__icontains=query)
|
|
)
|
|
return queryset
|
|
|
|
|
|
def home(request):
|
|
query = request.GET.get("q", "").strip()
|
|
|
|
if request.method == "POST":
|
|
create_form = WorkspaceIntakeForm(request.POST, prefix="workspace")
|
|
if create_form.is_valid():
|
|
workspace = create_form.save(commit=False)
|
|
workspace.stage = SalesWorkspace.Stage.NEW
|
|
workspace.save()
|
|
if workspace.next_meeting_at:
|
|
workspace.activities.create(
|
|
title="Initial meeting",
|
|
activity_type="meeting",
|
|
due_at=workspace.next_meeting_at,
|
|
owner=workspace.contact_name or "Sales",
|
|
notes="Auto-created from workspace intake.",
|
|
)
|
|
messages.success(request, f"{workspace.customer_name} workspace created.")
|
|
return redirect("workspace_detail", pk=workspace.pk)
|
|
messages.error(request, "Please review the highlighted fields and try again.")
|
|
else:
|
|
create_form = WorkspaceIntakeForm(prefix="workspace")
|
|
|
|
workspaces = list(_workspace_queryset(query)[:8])
|
|
upcoming_items = []
|
|
for workspace in workspaces:
|
|
upcoming_items.extend([item for item in workspace.activities.all() if not item.is_done])
|
|
upcoming_items.sort(key=lambda item: item.due_at)
|
|
|
|
all_workspaces = _workspace_queryset()
|
|
active_workspaces = [item for item in all_workspaces if item.stage not in {SalesWorkspace.Stage.WON, SalesWorkspace.Stage.LOST}]
|
|
context = {
|
|
**_base_shell_context(),
|
|
"meta_title": "FlowDesk Sales Workspace",
|
|
"search_query": query,
|
|
"create_form": create_form,
|
|
"workspaces": workspaces,
|
|
"workspace_count": all_workspaces.count(),
|
|
"active_count": len(active_workspaces),
|
|
"won_count": sum(1 for item in all_workspaces if item.stage == SalesWorkspace.Stage.WON),
|
|
"pipeline_total": sum((item.estimated_value for item in active_workspaces), 0),
|
|
"upcoming_items": upcoming_items[:5],
|
|
"stage_choices": SalesWorkspace.Stage.choices,
|
|
"layout_choices": SalesWorkspace.LayoutTemplate.choices,
|
|
}
|
|
return render(request, "core/index.html", context)
|
|
|
|
|
|
def workspace_detail(request, pk):
|
|
workspace = get_object_or_404(_workspace_queryset(), pk=pk)
|
|
context = {
|
|
**_base_shell_context(),
|
|
"meta_title": f"{workspace.customer_name} · Workspace",
|
|
"meta_description": f"Compact sales workspace for {workspace.customer_name} and project {workspace.project_name or workspace.project_number or workspace.opportunity_title}.",
|
|
"workspace": workspace,
|
|
"stage_form": StageUpdateForm(prefix="stage", current_stage=workspace.stage),
|
|
"quote_form": QuoteLineForm(prefix="quote"),
|
|
"activity_form": ActivityForm(prefix="activity"),
|
|
"quote_lines": workspace.quote_lines.all(),
|
|
"activities": workspace.activities.all(),
|
|
"upcoming_activities": [item for item in workspace.activities.all() if not item.is_done],
|
|
"completed_activities": [item for item in workspace.activities.all() if item.is_done],
|
|
}
|
|
return render(request, "core/workspace_detail.html", context)
|
|
|
|
|
|
def update_stage(request, pk):
|
|
workspace = get_object_or_404(SalesWorkspace, pk=pk)
|
|
form = StageUpdateForm(request.POST, prefix="stage", current_stage=workspace.stage)
|
|
if request.method == "POST" and form.is_valid():
|
|
workspace.stage = form.cleaned_data["stage"]
|
|
workspace.save(update_fields=["stage", "updated_at"])
|
|
messages.success(request, f"Stage updated to {workspace.get_stage_display()}.")
|
|
else:
|
|
messages.error(request, "Could not update the stage. Please choose a valid value.")
|
|
return redirect("workspace_detail", pk=workspace.pk)
|
|
|
|
|
|
def add_quote_line(request, pk):
|
|
workspace = get_object_or_404(SalesWorkspace, pk=pk)
|
|
form = QuoteLineForm(request.POST, prefix="quote")
|
|
if request.method == "POST" and form.is_valid():
|
|
quote_line = form.save(commit=False)
|
|
quote_line.workspace = workspace
|
|
quote_line.save()
|
|
if workspace.stage in {SalesWorkspace.Stage.NEW, SalesWorkspace.Stage.QUALIFIED}:
|
|
workspace.stage = SalesWorkspace.Stage.PROPOSAL
|
|
workspace.save(update_fields=["stage", "updated_at"])
|
|
messages.success(request, f"Added quote line: {quote_line.product_name}.")
|
|
else:
|
|
messages.error(request, "Please correct the quote line fields and try again.")
|
|
return redirect("workspace_detail", pk=workspace.pk)
|
|
|
|
|
|
def add_activity(request, pk):
|
|
workspace = get_object_or_404(SalesWorkspace, pk=pk)
|
|
form = ActivityForm(request.POST, prefix="activity")
|
|
if request.method == "POST" and form.is_valid():
|
|
activity = form.save(commit=False)
|
|
activity.workspace = workspace
|
|
activity.save()
|
|
messages.success(request, f"Activity scheduled: {activity.title}.")
|
|
else:
|
|
messages.error(request, "Please correct the activity fields and try again.")
|
|
return redirect("workspace_detail", pk=workspace.pk)
|