feat(hosting): prospect search dropdown on site creation form

- Replace raw ID inputs with a live search dropdown that queries
  /admin/prospecting/prospects?search= as you type
- Shows matching prospects with business name, domain, and ID
- Clicking a prospect auto-fills business name, email, phone
- Selected prospect shown as badge with clear button
- Optional merchant ID field for existing merchants
- Remove stale "Create from Prospect" link on sites list (was just
  a link to the prospects page)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 23:14:47 +02:00
parent d4e9fed719
commit 73d453d78a
2 changed files with 82 additions and 22 deletions

View File

@@ -44,25 +44,48 @@
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> 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>
<!-- Prospect or Merchant (at least one required) --> <!-- Prospect Search -->
<div class="pt-4 border-t border-gray-200 dark:border-gray-700"> <div class="pt-4 border-t border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2"> <label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
<div> Link to Prospect <span class="text-red-500">*</span>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1"> </label>
Prospect ID <div class="relative">
</label> <input type="text" x-model="prospectSearch" @input.debounce.300ms="searchProspects()"
<input type="number" x-model.number="form.prospect_id" placeholder="e.g. 2" {# noqa: FE008 #} @focus="showProspectDropdown = true"
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"> placeholder="Search by domain or 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 dark:bg-gray-700 dark:text-gray-300">
<!-- Selected prospect badge -->
<div x-show="selectedProspect" class="absolute right-2 top-1/2 -translate-y-1/2">
<span class="inline-flex items-center px-2 py-0.5 text-xs font-medium rounded bg-teal-100 text-teal-700 dark:bg-teal-900 dark:text-teal-300">
<span x-text="'#' + form.prospect_id"></span>
<button type="button" @click="clearProspect()" class="ml-1 text-teal-500 hover:text-teal-700">&times;</button>
</span>
</div> </div>
<div> <!-- Dropdown -->
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1"> <div x-show="showProspectDropdown && prospectResults.length > 0" @click.away="showProspectDropdown = false"
Merchant ID class="absolute z-10 mt-1 w-full bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg shadow-lg max-h-48 overflow-auto">
</label> <template x-for="p in prospectResults" :key="p.id">
<input type="number" x-model.number="form.merchant_id" placeholder="e.g. 1" {# noqa: FE008 #} <button type="button" @click="selectProspect(p)"
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"> class="w-full px-3 py-2 text-left text-sm hover:bg-gray-100 dark:hover:bg-gray-600 flex justify-between items-center">
<div>
<span class="font-medium text-gray-700 dark:text-gray-200" x-text="p.business_name || p.domain_name"></span>
<span x-show="p.domain_name && p.business_name" class="text-xs text-gray-400 ml-2" x-text="p.domain_name"></span>
</div>
<span class="text-xs text-gray-400" x-text="'#' + p.id"></span>
</button>
</template>
</div> </div>
</div> </div>
<p class="text-xs text-gray-400 mt-1">At least one is required. Prospect ID auto-creates a merchant from prospect data.</p> <p class="text-xs text-gray-400 mt-1">A merchant will be auto-created from the prospect's contact data.</p>
</div>
<!-- Optional: Existing Merchant -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
Or link to existing Merchant ID <span class="text-xs text-gray-400">(optional)</span>
</label>
<input type="number" x-model.number="form.merchant_id" placeholder="Leave empty to auto-create" {# noqa: FE008 #}
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 dark:bg-gray-700 dark:text-gray-300">
</div> </div>
</div> </div>
@@ -101,20 +124,62 @@ function hostingSiteNew() {
contact_phone: '', contact_phone: '',
internal_notes: '', internal_notes: '',
}, },
// Prospect search
prospectSearch: '',
prospectResults: [],
selectedProspect: null,
showProspectDropdown: false,
creating: false, creating: false,
errorMsg: '', errorMsg: '',
get canCreate() { get canCreate() {
return this.form.business_name && (this.form.prospect_id || this.form.merchant_id); return this.form.business_name && (this.form.prospect_id || this.form.merchant_id);
}, },
async searchProspects() {
if (this.prospectSearch.length < 2) { this.prospectResults = []; return; }
try {
var resp = await apiClient.get('/admin/prospecting/prospects?search=' + encodeURIComponent(this.prospectSearch) + '&per_page=10');
this.prospectResults = resp.items || [];
this.showProspectDropdown = true;
} catch (e) {
this.prospectResults = [];
}
},
selectProspect(prospect) {
this.selectedProspect = prospect;
this.form.prospect_id = prospect.id;
this.prospectSearch = prospect.business_name || prospect.domain_name;
this.showProspectDropdown = false;
// Auto-fill form from prospect
if (!this.form.business_name) {
this.form.business_name = prospect.business_name || prospect.domain_name || '';
}
if (!this.form.contact_email && prospect.primary_email) {
this.form.contact_email = prospect.primary_email;
}
if (!this.form.contact_phone && prospect.primary_phone) {
this.form.contact_phone = prospect.primary_phone;
}
},
clearProspect() {
this.selectedProspect = null;
this.form.prospect_id = null;
this.prospectSearch = '';
this.prospectResults = [];
},
async createSite() { async createSite() {
if (!this.canCreate) { if (!this.canCreate) {
this.errorMsg = 'Business name and at least one of Prospect ID or Merchant ID required'; this.errorMsg = 'Business name and a linked prospect or merchant are required';
return; return;
} }
this.creating = true; this.creating = true;
this.errorMsg = ''; this.errorMsg = '';
try { try {
// Clean nulls
var payload = {}; var payload = {};
for (var k in this.form) { for (var k in this.form) {
if (this.form[k] !== null && this.form[k] !== '') payload[k] = this.form[k]; if (this.form[k] !== null && this.form[k] !== '') payload[k] = this.form[k];

View File

@@ -40,11 +40,6 @@
<option value="cancelled">Cancelled</option> <option value="cancelled">Cancelled</option>
</select> </select>
<a href="/admin/prospecting/prospects"
class="inline-flex items-center px-4 py-2 text-sm font-medium leading-5 text-teal-700 dark:text-teal-300 transition-colors duration-150 bg-teal-100 dark:bg-teal-900 border border-transparent rounded-lg hover:bg-teal-200 dark:hover:bg-teal-800 focus:outline-none">
<span x-html="$icon('cursor-click', 'w-4 h-4 mr-2')"></span>
Create from Prospect
</a>
</div> </div>
</div> </div>
</div> </div>