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>
161 lines
10 KiB
HTML
161 lines
10 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/modals.html' import modal %}
|
|
|
|
{% block title %}Campaigns{% endblock %}
|
|
|
|
{% block alpine_data %}campaignManager(){% endblock %}
|
|
|
|
{% block content %}
|
|
{{ page_header('Campaign Templates', action_label='New Template', action_onclick='showCreateModal = true', action_icon='plus') }}
|
|
|
|
<!-- Filter by Lead Type -->
|
|
<div x-show="!loading && !error" class="mb-6 p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800">
|
|
<div class="flex flex-wrap gap-2">
|
|
<button type="button" @click="filterLeadType = ''"
|
|
:class="!filterLeadType
|
|
? '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="px-3 py-1.5 text-sm font-medium rounded-lg border transition-colors duration-150">All</button>
|
|
<template x-for="lt in leadTypes" :key="lt.value">
|
|
<button type="button" @click="filterLeadType = lt.value"
|
|
:class="filterLeadType === lt.value
|
|
? '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="px-3 py-1.5 text-sm font-medium rounded-lg border transition-colors duration-150"
|
|
x-text="lt.label"></button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
{{ loading_state('Loading templates...') }}
|
|
{{ error_state('Error loading templates') }}
|
|
|
|
<!-- Templates Grid -->
|
|
<div x-show="!loading && !error" class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
<!-- Empty State -->
|
|
<div x-show="filteredTemplates().length === 0" class="col-span-full py-12 text-center">
|
|
<span x-html="$icon('mail', 'w-12 h-12 mx-auto mb-3 text-gray-300')"></span>
|
|
<p class="font-medium text-gray-600 dark:text-gray-400">No templates found</p>
|
|
<p class="text-xs text-gray-500 mt-1" x-text="filterLeadType ? 'Try a different lead type filter' : 'Create your first campaign template'"></p>
|
|
</div>
|
|
|
|
<template x-for="tpl in filteredTemplates()" :key="tpl.id">
|
|
<div class="p-4 bg-white rounded-lg shadow-xs dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:shadow-md transition-shadow duration-150">
|
|
<div class="flex items-start justify-between mb-3">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-200" x-text="tpl.name"></h3>
|
|
<div class="flex items-center space-x-2 mt-1">
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300"
|
|
x-text="tpl.lead_type.replace('_', ' ')"></span>
|
|
<span class="px-2 py-0.5 text-xs font-medium rounded-full bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400"
|
|
x-text="tpl.channel"></span>
|
|
<span class="text-xs text-gray-400" x-text="tpl.language.toUpperCase()"></span>
|
|
</div>
|
|
</div>
|
|
<span class="w-2.5 h-2.5 rounded-full mt-1" :class="tpl.is_active ? 'bg-green-500' : 'bg-gray-400'"
|
|
:title="tpl.is_active ? 'Active' : 'Inactive'"></span>
|
|
</div>
|
|
<p x-show="tpl.subject_template" class="text-xs text-gray-500 dark:text-gray-400 mb-2 truncate" x-text="'Subject: ' + tpl.subject_template"></p>
|
|
<p class="text-xs text-gray-400 line-clamp-2" x-text="tpl.body_template"></p>
|
|
<div class="flex justify-end mt-3 pt-3 border-t border-gray-100 dark:border-gray-700 space-x-2">
|
|
<button type="button" @click="editTemplate(tpl)"
|
|
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="Edit template">
|
|
<span x-html="$icon('edit', 'w-4 h-4')"></span>
|
|
</button>
|
|
<button type="button" @click="deleteTemplate(tpl.id)"
|
|
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 template">
|
|
<span x-html="$icon('delete', 'w-4 h-4')"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Create/Edit Template Modal -->
|
|
{% call modal('templateModal', 'New Template', show_var='showCreateModal || showEditModal', size='lg', show_footer=false) %}
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
|
Name <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" x-model="templateForm.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-1 sm:grid-cols-3 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Lead Type</label>
|
|
<select x-model="templateForm.lead_type"
|
|
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">
|
|
<template x-for="lt in leadTypes" :key="lt.value">
|
|
<option :value="lt.value" x-text="lt.label"></option>
|
|
</template>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Channel</label>
|
|
<select x-model="templateForm.channel"
|
|
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="email">Email</option>
|
|
<option value="letter">Letter</option>
|
|
<option value="phone_script">Phone Script</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Language</label>
|
|
<select x-model="templateForm.language"
|
|
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="fr">French</option>
|
|
<option value="de">German</option>
|
|
<option value="en">English</option>
|
|
<option value="lb">Luxembourgish</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Subject</label>
|
|
<input type="text" x-model="templateForm.subject_template"
|
|
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">Body</label>
|
|
<textarea x-model="templateForm.body_template" rows="10"
|
|
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 font-mono"></textarea>
|
|
<div class="mt-2 flex flex-wrap gap-1">
|
|
<span class="text-xs text-gray-400">Placeholders:</span>
|
|
<template x-for="ph in placeholders" :key="ph">
|
|
<button type="button" @click="insertPlaceholder(ph)"
|
|
class="text-xs text-purple-600 hover:text-purple-800 dark:text-purple-400 dark:hover:text-purple-300 px-1 py-0.5 rounded hover:bg-purple-50 dark:hover:bg-gray-700 transition-colors"
|
|
x-text="ph"></button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center">
|
|
<input type="checkbox" x-model="templateForm.is_active" id="tpl-active"
|
|
class="w-4 h-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
|
<label for="tpl-active" class="ml-2 text-sm text-gray-700 dark:text-gray-400">Active</label>
|
|
</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; showEditModal = 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="saveTemplate()"
|
|
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 transition-colors duration-150">
|
|
<span x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="showEditModal ? 'Update Template' : 'Create Template'"></span>
|
|
</button>
|
|
</div>
|
|
{% endcall %}
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script defer src="{{ url_for('prospecting_static', path='admin/js/campaigns.js') }}"></script>
|
|
{% endblock %}
|