base setup

This commit is contained in:
2026-01-07 12:09:20 +05:30
commit 0c275efea1
278 changed files with 11228 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
from django.contrib import admin
from .models import SEOConfiguration
# Register your models here.
admin.site.register(SEOConfiguration)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class BackendAdminConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'at_django_boilerplate.backend_admin'

View File

@@ -0,0 +1,38 @@
from django import forms
from .models import SEOConfiguration
class SEOConfigurationForm(forms.ModelForm):
class Meta:
model = SEOConfiguration
fields = "__all__"
# widgets = {
# "home_page_meta_title": forms.Text(attrs={"class": "form-control", "placeholder": "Home Page Meta Title"}),
# "home_page_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Home Page Meta Description"}),
# "about_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "About Page Meta Title"}),
# "about_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "About Page Meta Description"}),
# "tools_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Tools Page Meta Title"}),
# "tools_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Tools Page Meta Description"}),
# "contact_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Contact Page Meta Title"}),
# "contact_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Contact Page Meta Description"}),
# "career_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Career Page Meta Title"}),
# "career_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Career Page Meta Description"}),
# "blog_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Blog Page Meta Title"}),
# "blog_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Blog Page Meta Description"}),
# "term_and_con_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Terms & Conditions Meta Title"}),
# "term_and_con_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Terms & Conditions Meta Description"}),
# "term_and_con_canonical_url": forms.URLInput(attrs={"class": "form-control", "placeholder": "Canonical URL"}),
# "privacy_pol_meta_title": forms.TextInput(attrs={"class": "form-control", "placeholder": "Privacy Policy Meta Title"}),
# "privacy_pol_meta_description": forms.Textarea(attrs={"class": "form-control", "rows": 3, "placeholder": "Privacy Policy Meta Description"}),
# "privacy_pol_canonical_url": forms.URLInput(attrs={"class": "form-control", "placeholder": "Canonical URL"}),
# }

View File

@@ -0,0 +1,38 @@
# Generated by Django 5.2.6 on 2025-12-29 05:20
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='SEOConfiguration',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('home_page_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('home_page_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('about_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('about_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('tools_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('tools_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('contact_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('contact_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('career_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('career_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('blog_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('blog_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('term_and_con_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('term_and_con_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('term_and_con_canonical_url', models.URLField(blank=True, help_text='Canonical URL to avoid duplicate content.', null=True)),
('privacy_pol_meta_title', models.CharField(help_text='SEO title for the page (max 60 characters).', max_length=100)),
('privacy_pol_meta_description', models.CharField(help_text='Meta description for search engines (max 160 characters).', max_length=300)),
('privacy_pol_canonical_url', models.URLField(blank=True, help_text='Canonical URL to avoid duplicate content.', null=True)),
],
),
]

View File

@@ -0,0 +1,30 @@
from django.db import models
class SEOConfiguration(models.Model):
home_page_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
home_page_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
about_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
about_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
tools_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
tools_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
contact_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
contact_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
career_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
career_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters)." )
blog_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
blog_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
term_and_con_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
term_and_con_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
term_and_con_canonical_url = models.URLField(blank=True, null=True, help_text="Canonical URL to avoid duplicate content.")
privacy_pol_meta_title = models.CharField(max_length=100, help_text="SEO title for the page (max 60 characters).")
privacy_pol_meta_description = models.CharField(max_length=300, help_text="Meta description for search engines (max 160 characters).")
privacy_pol_canonical_url = models.URLField(blank=True, null=True, help_text="Canonical URL to avoid duplicate content.")

View File

@@ -0,0 +1,25 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<h2 class="text-2xl font-bold mb-6 text-gray-800">Admin Dashboard</h2>
<div class="flex gap-5">
<a href="{% url 'contact_list' %}"
class="px-6 py-3 border-2 border-blue-600 rounded-xl font-semibold text-blue-600 hover:text-blue-800 transition-colors">
View Contacts
</a>
<a href="{% url 'subscriber_list' %}"
class="px-6 py-3 border-2 border-green-600 rounded-xl font-semibold text-green-600 hover:text-green-800 transition-colors">
View Subscribers
</a>
<a href="{% url 'user_activity' %}"
class="px-6 py-3 border-2 border-green-600 rounded-xl font-semibold text-green-600 hover:text-green-800 transition-colors">
View Activiy
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,188 @@
{% extends 'base.html' %}
{% load static %}
{% block title %}Appointment Leads - Admin{% endblock %}
{% block content %}
<div class="container mx-auto mt-10 px-4 max-w-7xl">
<div class="bg-white shadow-lg rounded-2xl overflow-hidden">
<!-- Header -->
<div class="px-8 py-6 border-b border-gray-200 bg-gradient-to-r from-indigo-50 to-purple-50">
<h2 class="text-3xl font-bold text-gray-800">Appointment Requests</h2>
<p class="text-gray-600 mt-2">Manage appointments: update status, reschedule, and track progress</p>
</div>
{% if appointments %}
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<!-- Table Head -->
<thead class="bg-gray-100">
<tr>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">#</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Ticket ID</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contact</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Expert</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Scheduled For</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-4 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{% for apt in appointments %}
<tr class="hover:bg-gray-50 transition">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">{{ forloop.counter }}</td>
<td class="px-6 py-4 whitespace-nowrap font-medium text-indigo-600">
APP-{{ apt.pk|stringformat:"06d" }}
</td>
<td class="px-6 py-4 whitespace-nowrap font-medium text-gray-900">
{{ apt.full_name }}
</td>
<td class="px-6 py-4 text-sm text-gray-600">
{{ apt.email }}<br>
<small class="text-gray-500">{{ apt.phone }}</small>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
{{ apt.get_meeting_with_display }}
</td>
<td class="px-6 py-4 text-sm">
{{ apt.appointment_datetime|date:"d M Y" }} at {{ apt.appointment_datetime|time:"h:i A" }}
</td>
<!-- Status Badge -->
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-3 py-1 inline-flex text-xs font-semibold rounded-full
{% if apt.status == 'pending' %}bg-yellow-100 text-yellow-800
{% elif apt.status == 'contacted' %}bg-blue-100 text-blue-800
{% elif apt.status == 'in_process' %}bg-purple-100 text-purple-800
{% elif apt.status == 'rescheduled' %}bg-indigo-100 text-indigo-800
{% elif apt.status == 'completed' %}bg-green-100 text-green-800
{% elif apt.status == 'cancelled' %}bg-red-100 text-red-800
{% else %}bg-gray-100 text-gray-800{% endif %}">
{{ apt.get_status_display }}
</span>
</td>
<!-- Actions -->
<td class="px-6 py-4 text-sm">
<button onclick="openModal('modal-{{ apt.id }}')"
class="bg-gradient-to-r from-indigo-600 to-purple-600 text-white px-4 py-2 rounded-lg text-xs font-medium hover:shadow-lg transition">
Manage
</button>
<!-- MODAL -->
<div id="modal-{{ apt.id }}" class="fixed inset-0 bg-black bg-opacity-60 hidden flex items-center justify-center z-50">
<div class="bg-white w-full max-w-lg rounded-2xl shadow-2xl p-8 relative animate-fadeIn">
<button onclick="closeModal('modal-{{ apt.id }}')"
class="absolute top-4 right-6 text-gray-400 hover:text-gray-600 text-3xl">&times;</button>
<h3 class="text-2xl font-bold mb-6 text-gray-800">
Manage Appointment {{ apt.full_name }}
</h3>
<p class="text-sm text-gray-500 mb-6">Ticket: APP-{{ apt.pk|stringformat:"06d" }}</p>
<!-- Update Status -->
<form method="post" class="mb-8">
{% csrf_token %}
<input type="hidden" name="appointment_id" value="{{ apt.id }}">
<input type="hidden" name="action" value="update_status">
<label class="block text-sm font-semibold text-gray-700 mb-2">Update Status</label>
<select name="status" class="w-full border border-gray-300 rounded-lg px-4 py-3 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
{% for value, label in status_choices %}
<option value="{{ value }}" {% if apt.status == value %}selected{% endif %}>
{{ label }}
</option>
{% endfor %}
</select>
<button type="submit" class="mt-4 w-full bg-indigo-600 text-white py-3 rounded-lg font-medium hover:bg-indigo-700 transition">
Update Status
</button>
</form>
<!-- Reschedule -->
<form method="post">
{% csrf_token %}
<input type="hidden" name="appointment_id" value="{{ apt.id }}">
<input type="hidden" name="action" value="reschedule">
<label class="block text-sm font-semibold text-gray-700 mb-2">Reschedule Appointment</label>
<div class="grid grid-cols-2 gap-4 mb-4">
<input type="date" name="new_date"
value="{{ apt.appointment_datetime|date:'Y-m-d' }}"
class="border border-gray-300 rounded-lg px-4 py-3" required>
<input type="time" name="new_time"
value="{{ apt.appointment_datetime|time:'H:i' }}"
class="border border-gray-300 rounded-lg px-4 py-3" required>
</div>
<button type="submit" class="w-full bg-green-600 text-white py-3 rounded-lg font-medium hover:bg-green-700 transition">
Reschedule Appointment
</button>
</form>
<!-- Notes (Read-only) -->
{% if apt.notes %}
<div class="mt-8 p-4 bg-gray-50 rounded-lg">
<p class="text-sm font-medium text-gray-700">User Notes:</p>
<p class="text-sm text-gray-600 mt-1">{{ apt.notes }}</p>
</div>
{% endif %}
</div>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="p-16 text-center">
<p class="text-xl text-gray-500">No appointment requests yet.</p>
</div>
{% endif %}
</div>
</div>
<!-- Modal Scripts -->
<script>
function openModal(id) {
document.getElementById(id).classList.remove('hidden');
document.getElementById(id).classList.add('flex');
}
function closeModal(id) {
document.getElementById(id).classList.add('hidden');
document.getElementById(id).classList.remove('flex');
}
// Close modal when clicking outside
window.addEventListener('click', function(e) {
document.querySelectorAll('[id^="modal-"]').forEach(modal => {
if (e.target === modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
</script>
<style>
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fadeIn { animation: fadeIn 0.3s ease-out; }
</style>
{% endblock %}

View File

@@ -0,0 +1,239 @@
{% extends 'base.html' %}
{% load crispy_forms_tags %}
{% block content %}
<!-- Back -->
<div class="mb-6">
<a href="{% url 'admin_dashboard' %}"
class="inline-block px-4 py-2 border-2 border-gray-800 rounded-xl font-bold text-gray-800 hover:text-gray-900 hover:border-gray-900">
&larr; Admin Dashboard
</a>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
<h1 class="text-3xl font-bold mb-6 text-gray-800 text-center">All Contacts</h1>
<!-- ================= FILTER ================= -->
<!-- ================= FILTER ================= -->
<form method="get" class="mb-6 flex flex-col sm:flex-row gap-3">
<!-- Search -->
<input
type="text"
name="search"
value="{{ request.GET.search }}"
placeholder="Search by name, email, or message..."
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring"
>
<!-- Junk Filter -->
<select
name="status"
class="px-4 py-2 border rounded-lg focus:outline-none focus:ring">
<option value="">All Messages</option>
<option value="inbox"
{% if request.GET.status == "inbox" %}selected{% endif %}>
Inbox
</option>
<option value="junk"
{% if request.GET.status == "junk" %}selected{% endif %}>
Junk
</option>
</select>
<button class="px-6 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Apply
</button>
</form>
<!-- ================= TABLE ================= -->
<div class="overflow-x-auto shadow-lg rounded-lg">
<table class="min-w-[700px] w-full table-auto bg-white divide-y divide-gray-200">
<thead class="bg-indigo-600">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Name</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Message</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Date</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for c in contacts %}
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 font-medium">
{{ c.name }}
</td>
<td class="px-6 py-4 text-gray-600">
{{ c.email }}
</td>
<td class="px-6 py-4 max-w-xs truncate text-gray-700">
{{ c.message }}
</td>
<td class="px-6 py-4 text-gray-500 whitespace-nowrap">
{{ c.created_at|date:"M d, Y H:i" }}
</td>
<!-- ================= ACTIONS ================= -->
<td class="px-6 py-4 flex gap-3">
<!-- View -->
<button
onclick="openModal(
'{{ c.name }}',
'{{ c.email }}',
'{{ c.message|escapejs }}',
'{{ c.created_at|date:"M d, Y H:i" }}'
)"
class="text-indigo-600 hover:text-indigo-800"
title="View">
<i class="fas fa-eye"></i>
</button>
<!-- Junk -->
{% if not c.is_junk %}
<form method="post"
action="{% url 'set_message_to_junk' c.id %}"
onsubmit="return confirmJunk('{{ c.email }}', '{{ c.phone|default:'' }}')">
{% csrf_token %}
<button
type="submit"
class="text-red-600 hover:text-red-800"
title="Mark as junk">
<i class="fas fa-trash"></i>
</button>
</form>
{% else %}
<span class="text-xs text-gray-400">Junk</span>
{% endif %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="5" class="text-center py-10 text-gray-400">
No contacts found.
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- ================= PAGINATION ================= -->
{% if is_paginated %}
<div class="mt-6 flex justify-center items-center gap-2">
{% if page_obj.has_previous %}
<a href="?page={{ page_obj.previous_page_number }}&search={{ request.GET.search }}&status={{ request.GET.status }}"
class="px-3 py-1 border rounded hover:bg-gray-100">
Prev
</a>
{% endif %}
<span class="px-3 py-1 bg-indigo-600 text-white rounded">
{{ page_obj.number }} / {{ page_obj.paginator.num_pages }}
</span>
{% if page_obj.has_next %}
<a href="?page={{ page_obj.next_page_number }}&search={{ request.GET.search }}&status={{ request.GET.status }}"
class="px-3 py-1 border rounded hover:bg-gray-100">
Next
</a>
{% endif %}
</div>
{% endif %}
</div>
<!-- ================= MODAL ================= -->
<div id="contactModal"
class="fixed inset-0 bg-black bg-opacity-50 hidden flex items-center justify-center z-50">
<div class="bg-white rounded-xl shadow-2xl max-w-lg w-full p-6 relative">
<button onclick="closeModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-700 text-2xl">
&times;
</button>
<h2 class="text-2xl font-bold mb-4 text-gray-800">Contact Details</h2>
<div class="space-y-4">
<p><strong>Name:</strong> <span id="modalName"></span></p>
<p><strong>Email:</strong> <span id="modalEmail"></span></p>
<p><strong>Date:</strong> <span id="modalDate"></span></p>
<div>
<p class="font-semibold mb-1">Message</p>
<div id="modalMessage"
class="bg-gray-100 p-3 rounded-lg text-sm whitespace-pre-wrap">
</div>
</div>
</div>
<div class="mt-6 text-right">
<button onclick="closeModal()"
class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">
Close
</button>
</div>
</div>
</div>
<!-- ================= JS ================= -->
<script>
function confirmJunk(email, phone) {
let message = "Set ALL messages";
if (email) {
message += " from email: " + email;
}
if (phone) {
message += email ? " and phone: " + phone : " from phone: " + phone;
}
message += " to junk?\n\nThis will also add the user to the spam list.";
return confirm(message);
}
</script>
<script>
function openModal(name, email, message, date) {
document.getElementById("modalName").innerText = name;
document.getElementById("modalEmail").innerText = email;
document.getElementById("modalMessage").innerText = message;
document.getElementById("modalDate").innerText = date;
document.getElementById("contactModal").classList.remove("hidden");
}
function closeModal() {
document.getElementById("contactModal").classList.add("hidden");
}
</script>
{% endblock %}

View File

@@ -0,0 +1,22 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
{% load static %}
<h2 class="mb-4">SEO Configuration</h2>
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }}">{{ message }}</div>
{% endfor %}
{% endif %}
<form method="post" class="card p-4 shadow-sm">
{% csrf_token %}
{{ form.as_p }}
<button type="submit" class="btn btn-primary">Save</button>
</form>
{% endblock %}

View File

@@ -0,0 +1,60 @@
{% extends 'base.html' %}
{% block content %}
{% load crispy_forms_tags %}
<div class="mb-6">
<a href="{% url 'admin_dashboard' %}"
class="inline-block px-4 py-2 border-2 border-gray-800 rounded-xl font-bold text-gray-800 hover:text-gray-900 hover:border-gray-900 transition-colors">
&larr; Admin Dashboard
</a>
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
<h2 class="text-3xl font-bold mb-6 text-gray-800 text-center">Subscriber List</h2>
<div class="overflow-x-auto shadow-lg rounded-lg">
<table class="min-w-full bg-white divide-y divide-gray-200">
<thead class="bg-indigo-600">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Email</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Subscribed At</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100">
{% for subscriber in subscribers %}
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-gray-800 font-medium">{{ subscriber.id }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-700">{{ subscriber.email }}</td>
<td class="px-6 py-4 whitespace-nowrap text-gray-600">{{ subscriber.subscribed_at|date:"M d, Y H:i" }}</td>
<td class="px-6 py-4 whitespace-nowrap">
{% if subscriber.is_active %}
<span class="text-green-600 font-semibold">✅ Active</span>
{% else %}
<span class="text-red-600 font-semibold">❌ Inactive</span>
{% endif %}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<a href="{% url 'toggle_subscriber' subscriber.id %}"
class="text-indigo-600 hover:text-indigo-800 font-medium">
{% if subscriber.is_active %}Deactivate{% else %}Activate{% endif %}
</a>
</td>
</tr>
{% empty %}
<tr>
<td colspan="6" class="text-center py-8 text-gray-400">No subscribers yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@@ -0,0 +1,21 @@
# urls.py
from django.urls import path
# from .views import (AdminBusinessListView,
# admin_add_business_ajax)
from . import views
urlpatterns = [
# path('admin/businesses/', AdminBusinessListView.as_view(), name='admin-business-list'),
# path('admin/business/add/', admin_add_business_ajax, name='admin_add_business_ajax'),
path("admin_view", views.admin_dashboard, name="admin_dashboard"), # default dashboard
path('admin_view/contact_list/', views.ContactListView.as_view(), name='contact_list'),
path("admin_view/contacts/<uuid:pk>/junk/", views.set_message_to_junk, name="set_message_to_junk"),
path('admin_view/subscriber_list/', views.subscriber_list, name='subscriber_list'),
path('admin_view/subscriber_toggle/<uuid:pk>/', views.toggle_subscriber, name='toggle_subscriber'),
path('appointments/', views.appointment_list, name='appointment_list'),
path("seo/config/", views.SEOConfigView.as_view(), name="seo_config"),
]

View File

@@ -0,0 +1,49 @@
from datetime import timedelta
from django.utils import timezone
# from business.models import Subscription, SubscriptionPackage
# def assign_free_trial_subscription(business):
# """
# Assigns a free trial subscription to a business, if allowed.
# Handles expiration of old trials and avoids duplicates.
# """
# try:
# # Find the first free trial plan
# plan = SubscriptionPackage.objects.filter(
# provider='trial',
# is_free_plan=True,
# free_trial_days__gt=0
# ).first()
# if not plan:
# return {"success": False, "message": "No free trial plan available."}
# # Check for existing active trial
# existing_trial = Subscription.objects.filter(
# business=business,
# plan__provider='trial',
# status='trial'
# ).order_by('-subscribed_on').first()
# if existing_trial and existing_trial.current_period_end > timezone.now():
# return {"success": False, "message": "Active trial already exists."}
# # Mark existing trial as expired (if any)
# if existing_trial:
# existing_trial.status = 'expired'
# existing_trial.save()
# # Create new free trial
# Subscription.objects.create(
# business=business,
# plan=plan,
# status='trial',
# subscribed_on=timezone.now(),
# current_period_end=timezone.now() + timedelta(days=plan.free_trial_days)
# )
# return {"success": True, "message": f"{plan.free_trial_days}-day free trial assigned."}
# except Exception as e:
# return {"success": False, "message": f"Error assigning trial: {str(e)}"}

View File

@@ -0,0 +1,156 @@
from at_django_boilerplate.communications.models import FeedbackModel,AppointmentModel
from at_django_boilerplate.core.models import Subscriber
from django.shortcuts import render, redirect,get_object_or_404
from django.contrib import messages
from django.views import View
from .models import SEOConfiguration
from .forms import SEOConfigurationForm
from django.contrib.auth.decorators import login_required, user_passes_test
from django.contrib.auth import get_user_model
from django.utils.decorators import method_decorator
from django.views.generic import ListView
from django.db.models import Q
from .utils import *
from django.utils import timezone
from django.contrib.auth.mixins import UserPassesTestMixin
from django.http import HttpResponseForbidden
# Keep your custom decorator
def superuser_required(view_func):
def wrapper(request, *args, **kwargs):
if not request.user.is_authenticated:
return redirect('login')
if not request.user.is_superuser:
return HttpResponseForbidden()
return view_func(request, *args, **kwargs)
return wrapper
class SuperuserRequiredMixin(UserPassesTestMixin):
def test_func(self):
return self.request.user.is_superuser
User = get_user_model()
@superuser_required
def admin_dashboard(request):
return render(request, "admin_dashboard.html")
class ContactListView(SuperuserRequiredMixin ,ListView):
model = FeedbackModel
template_name = 'contact_list.html'
context_object_name = 'contacts'
paginate_by = 50
def get_queryset(self):
queryset = FeedbackModel.objects.all().order_by("-created_at")
search = self.request.GET.get("search")
status = self.request.GET.get("status") # NEW
if search:
queryset = queryset.filter(
Q(name__icontains=search) |
Q(email__icontains=search) |
Q(message__icontains=search)
)
# ================= JUNK FILTER =================
if status == "junk":
queryset = queryset.filter(is_junk=True)
elif status == "inbox":
queryset = queryset.filter(is_junk=False)
return queryset
@superuser_required
def set_message_to_junk(request, pk): # Change parameter from 'uuid' to 'pk'
contact = get_object_or_404(FeedbackModel, pk=pk) # Use 'id=pk' instead of 'uuid=uuid'
contact.is_junk = True
contact.save(update_fields=["is_junk"])
messages.success(request, "Message marked as junk.")
return redirect(request.META.get("HTTP_REFERER", "contact_list")) # Better fallback
@superuser_required
# Show all subscribers
def subscriber_list(request):
subscribers = Subscriber.objects.all().order_by("-subscribed_at")
return render(request, "subscriber.html", {"subscribers": subscribers})
@superuser_required
def toggle_subscriber(request, pk): # Changed from pk to uuid
subscriber = get_object_or_404(Subscriber, pk=pk) # Assuming Subscriber also uses UUID as PK
subscriber.is_active = not subscriber.is_active
subscriber.save()
return redirect("subscriber_list")
class SEOConfigView(View):
template_name = "seo_config.html"
def get(self, request):
seo_config = SEOConfiguration.objects.first()
if not seo_config:
seo_config = SEOConfiguration.objects.create() # ensure one record exists
form = SEOConfigurationForm(instance=seo_config)
return render(request, self.template_name, {"form": form})
def post(self, request):
seo_config = SEOConfiguration.objects.first()
form = SEOConfigurationForm(request.POST, instance=seo_config)
if form.is_valid():
form.save()
messages.success(request, "SEO configuration updated successfully.")
return redirect("seo_config")
return render(request, self.template_name, {"form": form})
@superuser_required
def appointment_list(request):
if request.method == "POST":
appointment_id = request.POST.get("appointment_id")
action = request.POST.get("action")
appointment = get_object_or_404(AppointmentModel, id=appointment_id)
if action == "update_status":
new_status = request.POST.get("status")
if new_status in dict(AppointmentModel.STATUS_CHOICES):
appointment.status = new_status
appointment.save()
messages.success(request, f"Status updated to '{appointment.get_status_display()}' for {appointment.full_name}")
elif action == "reschedule":
date_str = request.POST.get("new_date")
time_str = request.POST.get("new_time")
if date_str and time_str:
try:
new_datetime_str = f"{date_str} {time_str}"
new_datetime = timezone.datetime.strptime(new_datetime_str, "%Y-%m-%d %H:%M")
new_datetime = timezone.make_aware(new_datetime)
if new_datetime > timezone.now():
appointment.appointment_datetime = new_datetime
appointment.status = 'rescheduled'
appointment.save()
messages.success(request, f"Appointment rescheduled to {new_datetime.strftime('%d %B %Y, %I:%M %p')}")
else:
messages.error(request, "New time must be in the future.")
except ValueError:
messages.error(request, "Invalid date/time format.")
else:
messages.error(request, "Please select both date and time.")
return redirect('appointment_list')
appointments = AppointmentModel.objects.all().order_by('-appointment_datetime')
status_choices = AppointmentModel.STATUS_CHOICES
return render(request, "appointment_list.html", {
"appointments": appointments,
"status_choices": status_choices,
})