Files
DevSuite/templates/clients/partials/client_form.html
T
sankalp 4698351e58 feat(clients/partials): improve client form functionality and validation
Enhancements to the client form include phone number validation and interface improvements. The short code field is now auto-generated and disabled, while the phone input uses a proper widget with real-time validation.

**New Features**
- Added **Phone Number Validation** using libphonenumber-js with real-time feedback
- Implemented **Phone Input Widget** with country code selection

**Enhancements & Refinements**
- **Short Code Field** changed to read-only with "Auto-generated" placeholder
- Updated form styling for **Phone Input** using `join` container class
- Added phone validation script to automatically highlight valid/invalid entries

**Bug Fixes**
- Fixed potential inconsistency in phone number handling by replacing manual input with proper form widget
2025-12-14 08:18:51 +05:45

279 lines
14 KiB
HTML

<div data-stepper="" class="bg-base-100 w-full rounded-lg p-4 shadow-base-300/20 shadow-sm"
id="wizard-validation-horizontal">
<!-- Stepper Nav -->
<ul class="relative flex flex-col gap-2 md:flex-row">
<li class="group flex flex-1 flex-col items-center gap-2 md:flex-row" data-stepper-nav-item='{ "index": 1 }'>
<span class="min-h-7.5 min-w-7.5 inline-flex flex-col items-center gap-2 align-middle text-sm md:flex-row">
<span
class="stepper-active:text-bg-primary stepper-active:shadow-sm shadow-base-300/20 stepper-success:text-bg-primary stepper-success:shadow-sm stepper-completed:text-bg-success stepper-error:text-bg-error text-bg-soft-neutral flex size-7.5 shrink-0 items-center justify-center rounded-full font-medium">
<span class="stepper-success:hidden stepper-error:hidden stepper-completed:hidden text-sm">1</span>
<span class="icon-[tabler--check] stepper-success:block hidden size-4 shrink-0"></span>
<span class="icon-[tabler--x] stepper-error:block hidden size-4 shrink-0"></span>
</span>
<span class="text-base-content text-nowrap font-medium">Basic Info</span>
</span>
<div
class="stepper-success:bg-primary stepper-completed:bg-success bg-base-content/20 h-px w-full group-last:hidden max-md:mt-2 max-md:h-8 max-md:w-px md:flex-1">
</div>
</li>
<li class="group flex flex-1 flex-col items-center gap-2 md:flex-row" data-stepper-nav-item='{ "index": 2 }'>
<span class="min-h-7.5 min-w-7.5 inline-flex flex-col items-center gap-2 align-middle text-sm md:flex-row">
<span
class="stepper-active:text-bg-primary stepper-active:shadow-sm shadow-base-300/20 stepper-success:text-bg-primary stepper-success:shadow-sm stepper-completed:text-bg-success stepper-error:text-bg-error text-bg-soft-neutral flex size-7.5 shrink-0 items-center justify-center rounded-full font-medium">
<span class="stepper-success:hidden stepper-error:hidden stepper-completed:hidden text-sm">2</span>
<span class="icon-[tabler--check] stepper-success:block hidden size-4 shrink-0"></span>
<span class="icon-[tabler--x] stepper-error:block hidden size-4 shrink-0"></span>
</span>
<span class="text-base-content text-nowrap font-medium">Contact Details</span>
</span>
<div
class="stepper-success:bg-primary stepper-completed:bg-success bg-base-content/20 h-px w-full group-last:hidden max-md:mt-2 max-md:h-8 max-md:w-px md:flex-1">
</div>
</li>
<li class="group flex flex-1 flex-col items-center gap-2 md:flex-row" data-stepper-nav-item='{ "index": 3 }'>
<span class="min-h-7.5 min-w-7.5 inline-flex flex-col items-center gap-2 align-middle text-sm md:flex-row">
<span
class="stepper-active:text-bg-primary stepper-active:shadow-sm shadow-base-300/20 stepper-success:text-bg-primary stepper-success:shadow-sm stepper-completed:text-bg-success stepper-error:text-bg-error text-bg-soft-neutral flex size-7.5 shrink-0 items-center justify-center rounded-full font-medium">
<span class="stepper-success:hidden stepper-error:hidden stepper-completed:hidden text-sm">3</span>
<span class="icon-[tabler--check] stepper-success:block hidden size-4 shrink-0"></span>
<span class="icon-[tabler--x] stepper-error:block hidden size-4 shrink-0"></span>
</span>
<span class="text-base-content text-nowrap font-medium">Address</span>
</span>
<div
class="stepper-success:bg-primary stepper-completed:bg-success bg-base-content/20 h-px w-full group-last:hidden max-md:mt-2 max-md:h-8 max-md:w-px md:flex-1">
</div>
</li>
</ul>
<!-- End Stepper Nav -->
<!-- Stepper Content -->
<form method="post" id="wizard-validation-form-horizontal" class="needs-validation mt-5 sm:mt-8">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="alert alert-error mb-4">
{{ form.non_field_errors }}
</div>
{% endif %}
<!-- Step 1: Basic Info -->
<div id="account-details-validation" class="space-y-5" data-stepper-content-item='{ "index": 1 }'>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="label-text" for="{{ form.name.id_for_label }}">Name</label>
<input type="text" name="{{ form.name.name }}" id="{{ form.name.id_for_label }}"
value="{{ form.name.value|default:'' }}" class="input w-full" placeholder="Client Name"
required />
{% if form.name.errors %}
<p class="text-error text-sm mt-1">{{ form.name.errors.0 }}</p>
{% endif %}
</div>
<div>
<label class="label-text" for="{{ form.short_code.id_for_label }}">Short Code</label>
<input type="text" name="{{ form.short_code.name }}" id="{{ form.short_code.id_for_label }}"
value="{{ form.short_code.value|default:'' }}" class="input w-full bg-base-200"
placeholder="Auto-generated" disabled />
{% if form.short_code.errors %}
<p class="text-error text-sm mt-1">{{ form.short_code.errors.0 }}</p>
{% endif %}
</div>
</div>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="label-text" for="{{ form.company_name.id_for_label }}">Company Name</label>
<input type="text" name="{{ form.company_name.name }}" id="{{ form.company_name.id_for_label }}"
value="{{ form.company_name.value|default:'' }}" class="input w-full"
placeholder="Company Ltd." />
{% if form.company_name.errors %}
<p class="text-error text-sm mt-1">{{ form.company_name.errors.0 }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Step 2: Contact Details -->
<div id="personal-info-validation" class="space-y-5" data-stepper-content-item='{ "index": 2 }'
style="display: none">
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
<div>
<label class="label-text" for="{{ form.email.id_for_label }}">Email</label>
<input type="email" name="{{ form.email.name }}" id="{{ form.email.id_for_label }}"
value="{{ form.email.value|default:'' }}" class="input w-full" placeholder="client@example.com"
required />
{% if form.email.errors %}
<p class="text-error text-sm mt-1">{{ form.email.errors.0 }}</p>
{% endif %}
</div>
<div>
<label class="label-text" for="{{ form.phone.id_for_label }}">Phone</label>
<div class="join w-full">
{{ form.phone }}
</div>
{% if form.phone.errors %}
<p class="text-error text-sm mt-1">{{ form.phone.errors.0 }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Step 3: Address -->
<div id="social-links-validation" class="space-y-5" data-stepper-content-item='{ "index": 3}'
style="display: none">
<div>
<label class="label-text" for="{{ form.address.id_for_label }}">Address</label>
<textarea name="{{ form.address.name }}" id="{{ form.address.id_for_label }}" rows="4"
class="textarea w-full" placeholder="Full Address"
required>{{ form.address.value|default:'' }}</textarea>
{% if form.address.errors %}
<p class="text-error text-sm mt-1">{{ form.address.errors.0 }}</p>
{% endif %}
</div>
</div>
<!-- Button Group -->
<div class="mt-5 flex items-center justify-between gap-y-2">
<div class="flex gap-2">
<a href="{% url 'client_list' %}" class="btn btn-ghost">Cancel</a>
<button type="button" class="btn btn-primary btn-prev max-sm:btn-square" data-stepper-back-btn="">
<span class="icon-[tabler--chevron-left] text-primary-content size-5 rtl:rotate-180"></span>
<span class="max-sm:hidden">Back</span>
</button>
</div>
<button type="button" class="btn btn-primary btn-next max-sm:btn-square" data-stepper-next-btn="">
<span class="max-sm:hidden">Next</span>
<span class="icon-[tabler--chevron-right] text-primary-content size-5 rtl:rotate-180"></span>
</button>
<button type="submit" class="btn btn-success" data-stepper-finish-btn="" style="display: none">
Save Client
</button>
</div>
</form>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/libphonenumber-js/1.10.49/libphonenumber-js.min.js"></script>
<script>
(function () {
const stepper = document.getElementById('wizard-validation-horizontal');
if (!stepper) return;
// Phone Validation Logic
const phoneSelect = stepper.querySelector('select[name$="_0"]');
const phoneInput = stepper.querySelector('input[name$="_1"]');
if (phoneSelect && phoneInput) {
const validatePhone = () => {
const country = phoneSelect.value;
const number = phoneInput.value;
// Clear previous validation styles
phoneInput.classList.remove('input-error', 'input-success');
if (!number) return; // Allow empty if not required, or handle required check separately
try {
if (libphonenumber.isValidPhoneNumber(number, country)) {
phoneInput.classList.add('input-success');
} else {
phoneInput.classList.add('input-error');
}
} catch (e) {
console.error("Phone validation error:", e);
phoneInput.classList.add('input-error');
}
};
phoneInput.addEventListener('input', validatePhone);
phoneSelect.addEventListener('change', validatePhone);
}
const navItems = stepper.querySelectorAll('[data-stepper-nav-item]');
const contentItems = stepper.querySelectorAll('[data-stepper-content-item]');
const backBtn = stepper.querySelector('[data-stepper-back-btn]');
const nextBtn = stepper.querySelector('[data-stepper-next-btn]');
const finishBtn = stepper.querySelector('[data-stepper-finish-btn]');
let currentStep = 1;
const totalSteps = 3; // We have 3 steps
function updateStepper() {
// Update Nav
navItems.forEach(item => {
const index = parseInt(JSON.parse(item.dataset.stepperNavItem).index);
const circle = item.querySelector('span > span');
const checkIcon = circle.querySelector('.icon-\\[tabler--check\\]');
const numberSpan = circle.querySelector('span:not([class*="icon"])');
// Reset classes
circle.classList.remove('bg-brand', 'text-white', 'bg-success', 'text-white');
circle.classList.add('text-bg-soft-neutral');
checkIcon.classList.add('hidden');
numberSpan.classList.remove('hidden');
if (index < currentStep) {
// Completed
circle.classList.remove('text-bg-soft-neutral');
circle.classList.add('bg-success', 'text-white');
checkIcon.classList.remove('hidden');
numberSpan.classList.add('hidden');
} else if (index === currentStep) {
// Active
circle.classList.remove('text-bg-soft-neutral');
circle.classList.add('bg-brand', 'text-white');
}
});
// Update Content
contentItems.forEach(item => {
const index = parseInt(JSON.parse(item.dataset.stepperContentItem).index);
if (index === currentStep) {
item.style.display = 'block';
} else {
item.style.display = 'none';
}
});
// Update Buttons
if (currentStep === 1) {
backBtn.style.display = 'none';
nextBtn.style.display = 'inline-flex';
finishBtn.style.display = 'none';
} else if (currentStep === totalSteps) {
backBtn.style.display = 'inline-flex';
nextBtn.style.display = 'none';
finishBtn.style.display = 'inline-flex';
} else {
backBtn.style.display = 'inline-flex';
nextBtn.style.display = 'inline-flex';
finishBtn.style.display = 'none';
}
}
nextBtn.addEventListener('click', () => {
// Simple validation check for current step inputs
const currentContent = stepper.querySelector(`[data-stepper-content-item='{ "index": ${currentStep} }']`);
const inputs = currentContent.querySelectorAll('input, textarea, select');
let valid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
valid = false;
input.reportValidity();
}
});
if (valid && currentStep < totalSteps) {
currentStep++;
updateStepper();
}
});
backBtn.addEventListener('click', () => {
if (currentStep > 1) {
currentStep--;
updateStepper();
}
});
// Initialize
updateStepper();
})();
</script>