Files
orion/app/modules/prospecting/templates/prospecting/admin/prospects.html
Samir Boulahtit 6d6eba75bf
Some checks failed
CI / pytest (push) Failing after 48m31s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
CI / ruff (push) Successful in 11s
CI / validate (push) Successful in 23s
CI / dependency-scanning (push) Successful in 28s
feat(prospecting): add complete prospecting module for lead discovery and scoring
Migrates scanning pipeline from marketing-.lu-domains app into Orion module.
Supports digital (domain scan) and offline (manual capture) lead channels
with enrichment, scoring, campaign management, and interaction tracking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 00:59:47 +01:00

227 lines
12 KiB
HTML

{% extends "admin/base.html" %}
{% from 'shared/macros/headers.html' import page_header %}
{% from 'shared/macros/alerts.html' import loading_state, error_state %}
{% from 'shared/macros/tables.html' import table_header, table_empty %}
{% from 'shared/macros/pagination.html' import pagination_controls %}
{% block title %}Prospects{% endblock %}
{% block alpine_data %}prospectsList(){% endblock %}
{% block content %}
{{ page_header('Prospects', action_label='New Prospect', action_onclick='showCreateModal = true', action_icon='plus') }}
<!-- Filters -->
<div class="mb-6 p-4 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="grid gap-4 md:grid-cols-4">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Search</label>
<input type="text" x-model="search" @input.debounce.300ms="loadProspects()"
placeholder="Domain or business name..."
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300 focus:border-purple-400 focus:ring-purple-300">
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Channel</label>
<select x-model="filterChannel" @change="loadProspects()"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
<option value="">All</option>
<option value="digital">Digital</option>
<option value="offline">Offline</option>
</select>
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Status</label>
<select x-model="filterStatus" @change="loadProspects()"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
<option value="">All</option>
<option value="pending">Pending</option>
<option value="active">Active</option>
<option value="contacted">Contacted</option>
<option value="converted">Converted</option>
<option value="inactive">Inactive</option>
</select>
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Tier</label>
<select x-model="filterTier" @change="loadProspects()"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
<option value="">All</option>
<option value="top_priority">Top Priority</option>
<option value="quick_win">Quick Win</option>
<option value="strategic">Strategic</option>
<option value="low_priority">Low Priority</option>
</select>
</div>
</div>
</div>
{{ loading_state('Loading prospects...') }}
{{ error_state('Error loading prospects') }}
<!-- Prospects Table -->
<div x-show="!loading && !error" class="w-full overflow-hidden rounded-lg shadow">
<div class="w-full overflow-x-auto">
<table class="w-full whitespace-nowrap">
<thead>
<tr class="text-xs font-semibold tracking-wide text-left text-gray-500 uppercase border-b dark:border-gray-700 bg-gray-50 dark:text-gray-400 dark:bg-gray-800">
<th class="px-4 py-3">Business / Domain</th>
<th class="px-4 py-3">Channel</th>
<th class="px-4 py-3">Status</th>
<th class="px-4 py-3">Score</th>
<th class="px-4 py-3">Tier</th>
<th class="px-4 py-3">Contact</th>
<th class="px-4 py-3">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
<template x-for="p in prospects" :key="p.id">
<tr class="text-gray-700 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-4 py-3">
<div class="flex items-center text-sm">
<div>
<p class="font-semibold" x-text="p.business_name || p.domain_name"></p>
<p class="text-xs text-gray-600 dark:text-gray-400" x-show="p.domain_name && p.business_name" x-text="p.domain_name"></p>
</div>
</div>
</td>
<td class="px-4 py-3 text-sm">
<span class="px-2 py-1 text-xs font-semibold rounded-full"
:class="p.channel === 'digital' ? 'text-blue-700 bg-blue-100 dark:text-blue-100 dark:bg-blue-700' : 'text-purple-700 bg-purple-100 dark:text-purple-100 dark:bg-purple-700'"
x-text="p.channel"></span>
</td>
<td class="px-4 py-3 text-sm">
<span class="px-2 py-1 text-xs font-semibold rounded-full"
:class="statusBadgeClass(p.status)"
x-text="p.status"></span>
</td>
<td class="px-4 py-3 text-sm">
<span x-text="p.score?.score ?? '—'" class="font-semibold"
:class="scoreColor(p.score?.score)"></span>
</td>
<td class="px-4 py-3 text-sm">
<span x-show="p.score?.lead_tier"
class="px-2 py-1 text-xs font-semibold rounded-full"
:class="tierBadgeClass(p.score?.lead_tier)"
x-text="p.score?.lead_tier?.replace('_', ' ')"></span>
<span x-show="!p.score?.lead_tier" class="text-gray-400"></span>
</td>
<td class="px-4 py-3 text-sm">
<template x-if="p.primary_email">
<span class="text-xs" x-text="p.primary_email"></span>
</template>
<span x-show="!p.primary_email" class="text-gray-400"></span>
</td>
<td class="px-4 py-3 text-sm">
<a :href="'/admin/prospecting/prospects/' + p.id"
class="text-purple-600 hover:text-purple-900 dark:text-purple-400 dark:hover:text-purple-300">
View
</a>
</td>
</tr>
</template>
</tbody>
</table>
</div>
{{ table_empty('No prospects found') }}
</div>
{{ pagination_controls() }}
<!-- Create Prospect Modal -->
<div x-show="showCreateModal" x-cloak
class="fixed inset-0 z-30 flex items-end bg-black bg-opacity-50 sm:items-center sm:justify-center"
@click.self="showCreateModal = false">
<div class="w-full px-6 py-4 overflow-hidden bg-white rounded-t-lg dark:bg-gray-800 sm:rounded-lg sm:m-4 sm:max-w-xl"
@keydown.escape.window="showCreateModal = false">
<header class="flex justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-700 dark:text-gray-200">New Prospect</h3>
<button @click="showCreateModal = false" class="text-gray-400 hover:text-gray-600">
<span x-html="$icon('x', 'w-5 h-5')"></span>
</button>
</header>
<!-- Channel Toggle -->
<div class="flex mb-4 space-x-2">
<button @click="newProspect.channel = 'digital'"
:class="newProspect.channel === 'digital' ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
class="flex-1 px-4 py-2 text-sm font-medium rounded-lg">
Digital (Domain)
</button>
<button @click="newProspect.channel = 'offline'"
:class="newProspect.channel === 'offline' ? 'bg-purple-600 text-white' : 'bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300'"
class="flex-1 px-4 py-2 text-sm font-medium rounded-lg">
Offline (Manual)
</button>
</div>
<!-- Digital Fields -->
<div x-show="newProspect.channel === 'digital'" class="space-y-4">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Domain Name</label>
<input type="text" x-model="newProspect.domain_name" placeholder="example.lu"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
</div>
</div>
<!-- Offline Fields -->
<div x-show="newProspect.channel === 'offline'" class="space-y-4">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Business Name *</label>
<input type="text" x-model="newProspect.business_name" placeholder="Business name"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Phone</label>
<input type="tel" x-model="newProspect.phone" placeholder="+352..."
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Email</label>
<input type="email" x-model="newProspect.email" placeholder="contact@..."
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
</div>
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">City</label>
<input type="text" x-model="newProspect.city" placeholder="Luxembourg"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Source</label>
<select x-model="newProspect.source"
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300">
<option value="street">Street</option>
<option value="networking_event">Networking Event</option>
<option value="referral">Referral</option>
<option value="other">Other</option>
</select>
</div>
<div>
<label class="text-sm text-gray-600 dark:text-gray-400">Notes</label>
<textarea x-model="newProspect.notes" rows="3" placeholder="Any notes about this business..."
class="w-full mt-1 text-sm rounded-lg border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-300"></textarea>
</div>
</div>
<footer class="flex justify-end mt-6 space-x-3">
<button @click="showCreateModal = false"
class="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-200 rounded-lg hover:bg-gray-300 dark:text-gray-300 dark:bg-gray-700">
Cancel
</button>
<button @click="createProspect()"
:disabled="creating"
class="px-4 py-2 text-sm font-medium text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50">
<span x-show="!creating">Create</span>
<span x-show="creating">Creating...</span>
</button>
</footer>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script defer src="{{ url_for('prospecting_static', path='admin/js/prospects.js') }}"></script>
{% endblock %}