- 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>
199 lines
11 KiB
HTML
199 lines
11 KiB
HTML
{% extends "admin/base.html" %}
|
|
{% from 'shared/macros/headers.html' import page_header %}
|
|
|
|
{% block title %}New Hosted Site{% endblock %}
|
|
|
|
{% block alpine_data %}hostingSiteNew(){% endblock %}
|
|
|
|
{% block content %}
|
|
{{ page_header('New Hosted Site', back_url='/admin/hosting/sites', back_label='Back') }}
|
|
|
|
<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">
|
|
<div class="space-y-5">
|
|
<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="form.business_name" placeholder="Acme Luxembourg SARL" autofocus
|
|
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 gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Contact Name</label>
|
|
<input type="text" x-model="form.contact_name" placeholder="Jean Dupont"
|
|
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">Contact Email</label>
|
|
<input type="email" x-model="form.contact_email" placeholder="contact@acme.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>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">Contact Phone</label>
|
|
<input type="tel" x-model="form.contact_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">Internal Notes</label>
|
|
<textarea x-model="form.internal_notes" rows="3" placeholder="Any internal notes..."
|
|
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>
|
|
|
|
<!-- Prospect Search -->
|
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-400 mb-1">
|
|
Link to Prospect <span class="text-red-500">*</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input type="text" x-model="prospectSearch" @input.debounce.300ms="searchProspects()"
|
|
@focus="showProspectDropdown = true"
|
|
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">×</button>
|
|
</span>
|
|
</div>
|
|
<!-- Dropdown -->
|
|
<div x-show="showProspectDropdown && prospectResults.length > 0" @click.away="showProspectDropdown = false"
|
|
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">
|
|
<template x-for="p in prospectResults" :key="p.id">
|
|
<button type="button" @click="selectProspect(p)"
|
|
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>
|
|
<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>
|
|
|
|
<!-- Footer Actions -->
|
|
<div class="flex flex-col sm:flex-row justify-end mt-8 pt-4 border-t border-gray-200 dark:border-gray-700 gap-3">
|
|
<a href="/admin/hosting/sites"
|
|
class="inline-flex items-center justify-center 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
|
|
</a>
|
|
<button type="button" @click="createSite()"
|
|
:disabled="!canCreate || creating"
|
|
class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium leading-5 text-white transition-colors duration-150 bg-teal-600 border border-transparent rounded-lg hover:bg-teal-700 focus:outline-none focus:shadow-outline-purple disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<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 Site'"></span>
|
|
</button>
|
|
</div>
|
|
|
|
<div x-show="errorMsg" class="mt-4 p-3 text-sm text-red-700 bg-red-100 rounded-lg dark:bg-red-900 dark:text-red-300" x-text="errorMsg"></div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_scripts %}
|
|
<script>
|
|
function hostingSiteNew() {
|
|
return {
|
|
...data(),
|
|
currentPage: 'hosting-sites',
|
|
form: {
|
|
business_name: '',
|
|
prospect_id: null,
|
|
merchant_id: null,
|
|
contact_name: '',
|
|
contact_email: '',
|
|
contact_phone: '',
|
|
internal_notes: '',
|
|
},
|
|
// Prospect search
|
|
prospectSearch: '',
|
|
prospectResults: [],
|
|
selectedProspect: null,
|
|
showProspectDropdown: false,
|
|
|
|
creating: false,
|
|
errorMsg: '',
|
|
|
|
get canCreate() {
|
|
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() {
|
|
if (!this.canCreate) {
|
|
this.errorMsg = 'Business name and a linked prospect or merchant are required';
|
|
return;
|
|
}
|
|
this.creating = true;
|
|
this.errorMsg = '';
|
|
try {
|
|
var payload = {};
|
|
for (var k in this.form) {
|
|
if (this.form[k] !== null && this.form[k] !== '') payload[k] = this.form[k];
|
|
}
|
|
const site = await apiClient.post('/admin/hosting/sites', payload);
|
|
window.location.href = '/admin/hosting/sites/' + site.id;
|
|
} catch (e) {
|
|
this.errorMsg = e.message || 'Failed to create site';
|
|
} finally {
|
|
this.creating = false;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|