All checks were successful
- Add 55 unit tests for hosting module (hosted site service, client service service, stats service) with full fixture setup - Fix table_empty_state macro: add x_message param for dynamic Alpine.js expressions rendered via x-text instead of server-side Jinja - Fix hosting templates (sites, clients) using message= with Alpine expressions that rendered as literal text - Fix prospecting templates (leads, scan-jobs, prospects) using nonexistent subtitle= param, migrated to x_message= - Align hosting and prospecting admin templates with shared design system Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
7.3 KiB
HTML
132 lines
7.3 KiB
HTML
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/headers.html' import page_header, section_header %}
|
|
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
|
|
|
|
{% block title %}Prospecting Dashboard{% endblock %}
|
|
|
|
{% block alpine_data %}prospectingDashboard(){% endblock %}
|
|
|
|
{% block content %}
|
|
{{ page_header('Prospecting Dashboard') }}
|
|
{{ loading_state('Loading dashboard...') }}
|
|
{{ error_state('Error loading dashboard') }}
|
|
|
|
<div x-show="!loading && !error" class="space-y-6">
|
|
<!-- KPI Cards -->
|
|
<div class="grid gap-6 mb-8 md:grid-cols-2 xl:grid-cols-4">
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-blue-500 bg-blue-100 rounded-full dark:text-blue-100 dark:bg-blue-500">
|
|
<span x-html="$icon('globe', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Total Prospects</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.total_prospects || 0"></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-green-500 bg-green-100 rounded-full dark:text-green-100 dark:bg-green-500">
|
|
<span x-html="$icon('cursor-click', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Top Priority</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.top_priority || 0"></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-orange-500 bg-orange-100 rounded-full dark:text-orange-100 dark:bg-orange-500">
|
|
<span x-html="$icon('chart-bar', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Avg Score</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.avg_score ? stats.avg_score.toFixed(1) : '—'"></p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="p-3 mr-4 text-purple-500 bg-purple-100 rounded-full dark:text-purple-100 dark:bg-purple-500">
|
|
<span x-html="$icon('phone', 'w-5 h-5')"></span>
|
|
</div>
|
|
<div>
|
|
<p class="mb-2 text-sm font-medium text-gray-600 dark:text-gray-400">Offline Leads</p>
|
|
<p class="text-lg font-semibold text-gray-700 dark:text-gray-200" x-text="stats.offline_count || 0"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
{{ section_header('Quick Actions', icon='cursor-click') }}
|
|
<div class="flex flex-wrap gap-3">
|
|
<a href="/admin/prospecting/capture"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple">
|
|
<span x-html="$icon('phone', 'w-4 h-4 mr-2')"></span>
|
|
Quick Capture
|
|
</a>
|
|
<button type="button" @click="runBatchScan()"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-blue-600 border border-transparent rounded-lg hover:bg-blue-700 focus:outline-none">
|
|
<span x-html="$icon('globe-alt', 'w-4 h-4 mr-2')"></span>
|
|
Run Batch Scan
|
|
</button>
|
|
<button type="button" @click="showImportModal = true"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-green-600 border border-transparent rounded-lg hover:bg-green-700 focus:outline-none">
|
|
<span x-html="$icon('upload', 'w-4 h-4 mr-2')"></span>
|
|
Import Domains
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Leads by Tier + By Channel -->
|
|
<div class="grid gap-6 md:grid-cols-2">
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
{{ section_header('Leads by Tier', icon='chart-bar') }}
|
|
<div class="space-y-3">
|
|
<template x-for="tier in ['top_priority', 'quick_win', 'strategic', 'low_priority']" :key="tier">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400 capitalize" x-text="tier.replace('_', ' ')"></span>
|
|
<span class="px-2.5 py-0.5 text-xs font-medium rounded-full"
|
|
:class="tierBadgeClass(tier)"
|
|
x-text="stats.leads_by_tier?.[tier] || 0"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
{{ section_header('By Channel', icon='globe') }}
|
|
<div class="space-y-3">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Digital (Domain Scan)</span>
|
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="stats.digital_count || 0"></span>
|
|
</div>
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400">Offline (Captured)</span>
|
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="stats.offline_count || 0"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Common Issues -->
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
{{ section_header('Common Issues', icon='exclamation') }}
|
|
<div class="space-y-3">
|
|
<template x-for="issue in stats.common_issues || []" :key="issue.flag">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm text-gray-600 dark:text-gray-400" x-text="issueLabel(issue.flag)"></span>
|
|
<div class="flex items-center">
|
|
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700 mr-3">
|
|
<div class="h-2 bg-red-500 rounded-full transition-all duration-300"
|
|
:style="'width: ' + (issue.count / (stats.total_prospects || 1) * 100) + '%'"></div>
|
|
</div>
|
|
<span class="text-xs font-medium text-gray-500 dark:text-gray-400 w-8 text-right" x-text="issue.count"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<p x-show="!stats.common_issues?.length" class="text-sm text-gray-400 text-center py-4">No issues data available</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script defer src="{{ url_for('prospecting_static', path='admin/js/dashboard.js') }}"></script>
|
|
{% endblock %}
|