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>
153 lines
9.6 KiB
HTML
153 lines
9.6 KiB
HTML
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/headers.html' import page_header %}
|
|
|
|
{% block title %}Quick Capture{% endblock %}
|
|
|
|
{% block alpine_data %}quickCapture(){% endblock %}
|
|
|
|
{% block content %}
|
|
{{ page_header('Quick Capture', subtitle='Capture offline prospects on the go') }}
|
|
|
|
<!-- Success Message -->
|
|
<div x-show="saved" x-transition
|
|
class="mb-4 p-4 bg-green-100 text-green-700 rounded-lg dark:bg-green-900 dark:text-green-300 max-w-2xl mx-auto">
|
|
<span x-html="$icon('check-circle', 'w-5 h-5 inline mr-2')"></span>
|
|
<span x-text="'Prospect saved: ' + lastSaved"></span>
|
|
</div>
|
|
|
|
<div class="max-w-2xl mx-auto">
|
|
<div class="px-4 py-5 sm:p-6 bg-white rounded-lg shadow-md dark:bg-gray-800">
|
|
|
|
<!-- Business Name -->
|
|
<div class="mb-5">
|
|
<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="form.business_name" required autofocus
|
|
class="w-full px-3 py-2.5 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"
|
|
placeholder="e.g. Boulangerie Schmidt">
|
|
</div>
|
|
|
|
<!-- Phone + Email Row -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-5">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Phone</label>
|
|
<input type="tel" x-model="form.phone" placeholder="+352..."
|
|
class="w-full px-3 py-2.5 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="form.email" placeholder="contact@..."
|
|
class="w-full px-3 py-2.5 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>
|
|
|
|
<!-- Address -->
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Address</label>
|
|
<input type="text" x-model="form.address" placeholder="Street and number"
|
|
class="w-full px-3 py-2.5 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>
|
|
|
|
<!-- City + Postal Code -->
|
|
<div class="grid grid-cols-2 gap-4 mb-5">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">City</label>
|
|
<input type="text" x-model="form.city" placeholder="Luxembourg"
|
|
class="w-full px-3 py-2.5 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">Postal Code</label>
|
|
<input type="text" x-model="form.postal_code" placeholder="L-1234"
|
|
class="w-full px-3 py-2.5 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>
|
|
|
|
<!-- Source -->
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">Source</label>
|
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
<template x-for="src in sources" :key="src.value">
|
|
<button type="button" @click="form.source = src.value"
|
|
:class="form.source === src.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-2 text-sm font-medium rounded-lg border transition-colors duration-150"
|
|
x-text="src.label"></button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-2">Tags</label>
|
|
<div class="flex flex-wrap gap-2">
|
|
<template x-for="tag in availableTags" :key="tag">
|
|
<button type="button" @click="toggleTag(tag)"
|
|
:class="form.tags.includes(tag)
|
|
? 'bg-purple-100 text-purple-700 border-purple-300 dark:bg-purple-900 dark:text-purple-300 dark:border-purple-700'
|
|
: 'bg-gray-100 text-gray-600 border-gray-200 dark:bg-gray-700 dark:text-gray-400 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'"
|
|
class="px-3 py-1.5 text-xs font-medium rounded-full border transition-colors duration-150"
|
|
x-text="tag"></button>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes -->
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Notes</label>
|
|
<textarea x-model="form.notes" rows="3" placeholder="Quick notes..."
|
|
class="w-full px-3 py-2.5 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>
|
|
|
|
<!-- Location + Submit Row -->
|
|
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
|
|
<button type="button" @click="getLocation()"
|
|
:disabled="gettingLocation"
|
|
class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium leading-5 text-gray-700 dark:text-gray-300 transition-colors duration-150 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:shadow-outline-gray disabled:opacity-50">
|
|
<span x-html="$icon('location-marker', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="gettingLocation ? 'Getting location...' : (form.location_lat ? 'Location saved' : 'Get Location')"></span>
|
|
</button>
|
|
<span x-show="form.location_lat" class="text-xs text-green-600 dark:text-green-400 self-center">
|
|
<span x-text="form.location_lat?.toFixed(4)"></span>, <span x-text="form.location_lng?.toFixed(4)"></span>
|
|
</span>
|
|
|
|
<div class="sm:ml-auto">
|
|
<button type="button" @click="submitCapture()"
|
|
:disabled="submitting || !form.business_name"
|
|
class="w-full sm:w-auto inline-flex items-center justify-center px-6 py-2.5 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 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<span x-show="!submitting" x-html="$icon('check', 'w-4 h-4 mr-2')"></span>
|
|
<span x-show="submitting" x-html="$icon('spinner', 'w-4 h-4 mr-2')"></span>
|
|
<span x-text="submitting ? 'Saving...' : 'Save & Capture Next'"></span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Captures -->
|
|
<div x-show="recentCaptures.length > 0" class="mt-6">
|
|
<h3 class="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-3">
|
|
Recent Captures (<span x-text="recentCaptures.length"></span>)
|
|
</h3>
|
|
<div class="space-y-2">
|
|
<template x-for="cap in recentCaptures" :key="cap.id">
|
|
<div class="flex items-center justify-between p-3 bg-white rounded-lg shadow-xs dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center space-x-3">
|
|
<div class="flex items-center justify-center w-8 h-8 rounded-full bg-purple-100 dark:bg-purple-900">
|
|
<span class="text-sm font-semibold text-purple-600 dark:text-purple-300"
|
|
x-text="cap.business_name.charAt(0).toUpperCase()"></span>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-700 dark:text-gray-300" x-text="cap.business_name"></span>
|
|
</div>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400" x-text="cap.city || cap.source"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script defer src="{{ url_for('prospecting_static', path='admin/js/capture.js') }}"></script>
|
|
{% endblock %}
|