- Trash icon button in Actions column with confirmation dialog
- Calls DELETE /admin/prospecting/prospects/{id} (existing endpoint)
- Reloads list after successful deletion
- Toast notification on success/failure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
246 lines
15 KiB
HTML
246 lines
15 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_wrapper, table_header, table_empty_state %}
|
|
{% from 'shared/macros/pagination.html' import pagination %}
|
|
{% from 'shared/macros/modals.html' import modal %}
|
|
|
|
{% 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') }}
|
|
|
|
{{ loading_state('Loading prospects...') }}
|
|
{{ error_state('Error loading prospects') }}
|
|
|
|
<!-- Search and Filters Bar -->
|
|
<div x-show="!loading && !error" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
|
<!-- Search Input -->
|
|
<div class="flex-1 max-w-md">
|
|
<div class="relative">
|
|
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
|
|
<span x-html="$icon('search', 'w-5 h-5 text-gray-400')"></span>
|
|
</span>
|
|
<input type="text" x-model="search" @input.debounce.300ms="pagination.page = 1; loadProspects()"
|
|
placeholder="Search by domain or business name..."
|
|
class="w-full pl-10 pr-4 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none dark:bg-gray-700 dark:text-gray-300">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="flex flex-wrap gap-3">
|
|
<select x-model="filterChannel" @change="pagination.page = 1; loadProspects()"
|
|
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none">
|
|
<option value="">All Channels</option>
|
|
<option value="digital">Digital</option>
|
|
<option value="offline">Offline</option>
|
|
</select>
|
|
|
|
<select x-model="filterStatus" @change="pagination.page = 1; loadProspects()"
|
|
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none">
|
|
<option value="">All Status</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>
|
|
|
|
<select x-model="filterTier" @change="pagination.page = 1; loadProspects()"
|
|
class="px-4 py-2 text-sm text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none">
|
|
<option value="">All Tiers</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>
|
|
|
|
<!-- Prospects Table -->
|
|
<div x-show="!loading && !error">
|
|
{% call table_wrapper() %}
|
|
{{ table_header(['Business / Domain', 'Channel', 'Status', 'Score', 'Tier', 'Contact', 'Actions']) }}
|
|
<tbody class="bg-white divide-y dark:divide-gray-700 dark:bg-gray-800">
|
|
{{ table_empty_state(7, title='No prospects found', x_message="search || filterChannel || filterStatus || filterTier ? 'Try adjusting your search or filters' : 'Create your first prospect to get started'", show_condition='prospects.length === 0', icon='user-group') }}
|
|
<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 transition-colors">
|
|
<!-- Business / Domain with Avatar -->
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center text-sm">
|
|
<div class="relative hidden w-8 h-8 mr-3 rounded-full md:block">
|
|
<div class="absolute inset-0 rounded-full bg-purple-100 dark:bg-purple-600 flex items-center justify-center">
|
|
<span class="text-xs font-semibold text-purple-600 dark:text-purple-100"
|
|
x-text="(p.business_name || p.domain_name)?.charAt(0).toUpperCase() || '?'"></span>
|
|
</div>
|
|
</div>
|
|
<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>
|
|
|
|
<!-- Channel Badge -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<span class="px-2.5 py-0.5 text-xs font-medium 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>
|
|
|
|
<!-- Status Badge -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<span class="px-2.5 py-0.5 text-xs font-medium rounded-full"
|
|
:class="statusBadgeClass(p.status)"
|
|
x-text="p.status"></span>
|
|
</td>
|
|
|
|
<!-- Score -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<span x-text="p.score?.score ?? '—'" class="font-semibold"
|
|
:class="scoreColor(p.score?.score)"></span>
|
|
</td>
|
|
|
|
<!-- Tier Badge -->
|
|
<td class="px-4 py-3 text-sm">
|
|
<span x-show="p.score?.lead_tier"
|
|
class="px-2.5 py-0.5 text-xs font-medium 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>
|
|
|
|
<!-- Contact -->
|
|
<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>
|
|
|
|
<!-- Actions -->
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center space-x-2 text-sm">
|
|
<a :href="'/admin/prospecting/prospects/' + p.id"
|
|
class="flex items-center justify-center p-2 text-purple-600 rounded-lg hover:bg-purple-50 dark:text-purple-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
|
title="View details">
|
|
<span x-html="$icon('eye', 'w-5 h-5')"></span>
|
|
</a>
|
|
<button type="button" @click="deleteProspect(p)"
|
|
class="flex items-center justify-center p-2 text-red-600 rounded-lg hover:bg-red-50 dark:text-red-400 dark:hover:bg-gray-700 focus:outline-none transition-colors"
|
|
title="Delete">
|
|
<span x-html="$icon('trash', 'w-5 h-5')"></span>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
{% endcall %}
|
|
|
|
{{ pagination() }}
|
|
</div>
|
|
|
|
<!-- Create Prospect Modal -->
|
|
{% call modal('createProspectModal', 'New Prospect', show_var='showCreateModal', size='md', show_footer=false) %}
|
|
<!-- Channel Toggle -->
|
|
<div class="flex mb-5 space-x-2">
|
|
<button type="button" @click="newProspect.channel = 'digital'"
|
|
:class="newProspect.channel === 'digital'
|
|
? 'bg-blue-600 text-white border-blue-600'
|
|
: 'bg-white text-gray-700 border-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'"
|
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-lg border transition-colors duration-150">
|
|
<span x-html="$icon('globe', 'w-4 h-4 inline mr-1')"></span>
|
|
Digital (Domain)
|
|
</button>
|
|
<button type="button" @click="newProspect.channel = 'offline'"
|
|
:class="newProspect.channel === 'offline'
|
|
? 'bg-purple-600 text-white border-purple-600'
|
|
: 'bg-white text-gray-700 border-gray-300 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-600'"
|
|
class="flex-1 px-4 py-2 text-sm font-medium rounded-lg border transition-colors duration-150">
|
|
<span x-html="$icon('office-building', 'w-4 h-4 inline mr-1')"></span>
|
|
Offline (Manual)
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Digital Fields -->
|
|
<div x-show="newProspect.channel === 'digital'" class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
|
Domain Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" x-model="newProspect.domain_name" placeholder="example.lu"
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray 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="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
|
Business Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" x-model="newProspect.business_name" placeholder="Business name"
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300">
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Phone</label>
|
|
<input type="tel" x-model="newProspect.phone" placeholder="+352..."
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Email</label>
|
|
<input type="email" x-model="newProspect.email" placeholder="contact@..."
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">City</label>
|
|
<input type="text" x-model="newProspect.city" placeholder="Luxembourg"
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Source</label>
|
|
<select x-model="newProspect.source"
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray 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="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Notes</label>
|
|
<textarea x-model="newProspect.notes" rows="3" placeholder="Any notes about this business..."
|
|
class="w-full px-3 py-2 text-sm border border-gray-300 dark:border-gray-600 rounded-lg focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray dark:bg-gray-700 dark:text-gray-300"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer Actions -->
|
|
<div class="flex justify-end mt-6 pt-4 border-t border-gray-200 dark:border-gray-700 space-x-3">
|
|
<button type="button" @click="showCreateModal = false"
|
|
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors">
|
|
Cancel
|
|
</button>
|
|
<button type="button" @click="createProspect()"
|
|
:disabled="creating"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-purple-600 border border-transparent rounded-lg hover:bg-purple-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed transition-colors duration-150">
|
|
<span x-show="!creating" x-html="$icon('plus', 'w-4 h-4 mr-2')"></span>
|
|
<span x-show="creating" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="creating ? 'Creating...' : 'Create Prospect'"></span>
|
|
</button>
|
|
</div>
|
|
{% endcall %}
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script defer src="{{ url_for('prospecting_static', path='admin/js/prospects.js') }}"></script>
|
|
{% endblock %}
|