feat(static): add favicon.ico
Add favicon.ico to the static directory to provide a site icon for better branding and user experience. - **New Features** - Add `favicon.ico` to the static assets for browser tab and bookmark visibility.
This commit is contained in:
6 files changed
+320
-1
No files matched your search
@@ -50,7 +50,6 @@ def logout_view(request):
|
||||
def dashboard(request):
|
||||
# Date ranges
|
||||
today = timezone.now().date()
|
||||
week_ago = today - timedelta(days=7)
|
||||
month_ago = today - timedelta(days=30)
|
||||
|
||||
# Core metrics
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 93 KiB |
@@ -0,0 +1,10 @@
|
||||
{% extends "shared/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
{% include "shared/navbar.html" %}
|
||||
|
||||
<div class="pt-24 px-4 max-w-7xl mx-auto min-h-screen flex flex-col pb-10">
|
||||
{% include "clients/partials/client_form.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% extends "shared/base.html" %}
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
{% include "shared/navbar.html" %}
|
||||
|
||||
<div class="pt-24 px-4 max-w-7xl mx-auto min-h-screen flex flex-col pb-10">
|
||||
{% include "clients/partials/client_list.html" %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,247 @@
|
||||
<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" placeholder="Optional" />
|
||||
{% 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>
|
||||
<!-- Using simple input for phone to avoid widget complexity for now -->
|
||||
<input type="tel" name="{{ form.phone.name }}_0" id="{{ form.phone.id_for_label }}_0"
|
||||
value="{{ form.phone.value.0|default:'' }}" class="input w-full" placeholder="Phone Number" />
|
||||
{% 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>
|
||||
(function () {
|
||||
const stepper = document.getElementById('wizard-validation-horizontal');
|
||||
if (!stepper) return;
|
||||
|
||||
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>
|
||||
@@ -0,0 +1,53 @@
|
||||
<div
|
||||
class="relative overflow-x-auto bg-neutral-primary-soft shadow-xs rounded-base border border-default flex-1 flex flex-col">
|
||||
<table class="w-full text-sm text-left rtl:text-right text-body">
|
||||
<caption class="p-5 text-lg font-medium text-left rtl:text-right text-heading bg-neutral-primary-soft">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
Clients
|
||||
<p class="mt-1.5 text-sm font-normal text-body">Manage your client base, view details, and track
|
||||
interactions.</p>
|
||||
</div>
|
||||
<a href="{% url 'client_create' %}"
|
||||
class="text-white bg-brand hover:bg-brand-strong focus:ring-4 focus:ring-brand-medium font-medium rounded-lg text-sm px-5 py-2.5 focus:outline-none">Add
|
||||
Client</a>
|
||||
</div>
|
||||
</caption>
|
||||
<thead class="text-sm text-body bg-neutral-secondary-medium border-b border-t border-default-medium">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 font-medium">Name</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium">Company</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium">Email</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium">Phone</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium">Short Code</th>
|
||||
<th scope="col" class="px-6 py-3 font-medium"><span class="sr-only">Actions</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for client in clients %}
|
||||
<tr
|
||||
class="bg-neutral-primary-soft border-b border-default hover:bg-neutral-secondary-soft transition-colors">
|
||||
<th scope="row" class="px-6 py-4 font-medium text-heading whitespace-nowrap">
|
||||
{{ client.name }}
|
||||
</th>
|
||||
<td class="px-6 py-4">{{ client.company_name }}</td>
|
||||
<td class="px-6 py-4">{{ client.email }}</td>
|
||||
<td class="px-6 py-4">{{ client.phone }}</td>
|
||||
<td class="px-6 py-4"><span class="badge badge-ghost badge-sm">{{ client.short_code }}</span></td>
|
||||
<td class="px-6 py-4 text-right space-x-2">
|
||||
<a href="{% url 'client_edit' client.pk %}" class="font-medium text-brand hover:underline">Edit</a>
|
||||
<form action="{% url 'client_delete' client.pk %}" method="post" class="inline"
|
||||
onsubmit="return confirm('Are you sure?');">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="font-medium text-error hover:underline">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-4 text-center text-body">No clients found.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
Reference in new issue
Block a user