Some checks failed
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>
227 lines
12 KiB
HTML
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 %}
|