Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,31 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Business, Service, Contact, Call, Booking
|
|
||||||
|
|
||||||
@admin.register(Business)
|
# Register your models here.
|
||||||
class BusinessAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('business_name', 'industry', 'phone_number', 'status')
|
|
||||||
list_filter = ('industry', 'status')
|
|
||||||
search_fields = ('business_name', 'phone_number')
|
|
||||||
|
|
||||||
@admin.register(Service)
|
|
||||||
class ServiceAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'business', 'duration', 'price')
|
|
||||||
list_filter = ('business',)
|
|
||||||
search_fields = ('name', 'description')
|
|
||||||
|
|
||||||
@admin.register(Contact)
|
|
||||||
class ContactAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'phone_number')
|
|
||||||
search_fields = ('name', 'phone_number')
|
|
||||||
|
|
||||||
@admin.register(Call)
|
|
||||||
class CallAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('contact', 'business', 'start_time', 'end_time')
|
|
||||||
list_filter = ('business', 'start_time')
|
|
||||||
search_fields = ('contact__name', 'contact__phone_number')
|
|
||||||
|
|
||||||
@admin.register(Booking)
|
|
||||||
class BookingAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('service', 'contact', 'start_time', 'status')
|
|
||||||
list_filter = ('status', 'service__business')
|
|
||||||
search_fields = ('contact__name', 'contact__phone_number', 'service__name')
|
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
from .models import Booking, Service, Contact
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
def create_booking(contact_phone_number: str, service_name: str, booking_time_str: str) -> str:
|
|
||||||
"""
|
|
||||||
Creates a booking for a given service at a specific time.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
contact = Contact.objects.get(phone_number=contact_phone_number)
|
|
||||||
service = Service.objects.get(name__iexact=service_name)
|
|
||||||
booking_time = datetime.fromisoformat(booking_time_str)
|
|
||||||
|
|
||||||
end_time = booking_time + timedelta(minutes=service.duration)
|
|
||||||
|
|
||||||
booking = Booking.objects.create(
|
|
||||||
contact=contact,
|
|
||||||
service=service,
|
|
||||||
start_time=booking_time,
|
|
||||||
end_time=end_time,
|
|
||||||
status='scheduled',
|
|
||||||
)
|
|
||||||
return f"Booking confirmed for {service.name} at {booking_time.strftime('%Y-%m-%d %H:%M')}."
|
|
||||||
except Contact.DoesNotExist:
|
|
||||||
return "Error: Contact not found."
|
|
||||||
except Service.DoesNotExist:
|
|
||||||
return f"Error: Service '{service_name}' not found."
|
|
||||||
except Exception as e:
|
|
||||||
return f"Error: Could not create booking. {e}"
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-01-20 16:39
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Business',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('business_name', models.CharField(max_length=255)),
|
|
||||||
('industry', models.CharField(choices=[('real_estate', 'Real Estate'), ('clinic_dental', 'Clinic / Dental'), ('salon_spa', 'Salon / Spa'), ('coaching_consulting', 'Coaching / Consulting'), ('custom', 'Custom (Other)')], default='custom', max_length=50)),
|
|
||||||
('phone_number', models.CharField(help_text="Twilio or other provider's phone number", max_length=20)),
|
|
||||||
('agent_name', models.CharField(default='AI Assistant', max_length=100)),
|
|
||||||
('status', models.BooleanField(default=True, help_text='Active or inactive status')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name_plural': 'Businesses',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-01-20 16:56
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Contact',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('phone_number', models.CharField(max_length=20, unique=True)),
|
|
||||||
('name', models.CharField(blank=True, max_length=255)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Call',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('start_time', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('end_time', models.DateTimeField(blank=True, null=True)),
|
|
||||||
('summary', models.TextField(blank=True)),
|
|
||||||
('transcript', models.TextField(blank=True)),
|
|
||||||
('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='core.business')),
|
|
||||||
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='calls', to='core.contact')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Service',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=255)),
|
|
||||||
('description', models.TextField(blank=True)),
|
|
||||||
('duration', models.IntegerField(help_text='Duration in minutes')),
|
|
||||||
('price', models.DecimalField(decimal_places=2, max_digits=10)),
|
|
||||||
('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='services', to='core.business')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Booking',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('start_time', models.DateTimeField()),
|
|
||||||
('end_time', models.DateTimeField()),
|
|
||||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('completed', 'Completed'), ('canceled', 'Canceled')], default='scheduled', max_length=20)),
|
|
||||||
('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='core.contact')),
|
|
||||||
('service', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bookings', to='core.service')),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2026-01-20 17:02
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0002_contact_call_service_booking'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='call',
|
|
||||||
name='conversation_history',
|
|
||||||
field=models.JSONField(default=list),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,67 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Business(models.Model):
|
# Create your models here.
|
||||||
INDUSTRY_CHOICES = [
|
|
||||||
('real_estate', 'Real Estate'),
|
|
||||||
('clinic_dental', 'Clinic / Dental'),
|
|
||||||
('salon_spa', 'Salon / Spa'),
|
|
||||||
('coaching_consulting', 'Coaching / Consulting'),
|
|
||||||
('custom', 'Custom (Other)'),
|
|
||||||
]
|
|
||||||
|
|
||||||
business_name = models.CharField(max_length=255)
|
|
||||||
industry = models.CharField(max_length=50, choices=INDUSTRY_CHOICES, default='custom')
|
|
||||||
phone_number = models.CharField(max_length=20, help_text="Twilio or other provider's phone number")
|
|
||||||
agent_name = models.CharField(max_length=100, default='AI Assistant')
|
|
||||||
status = models.BooleanField(default=True, help_text="Active or inactive status")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.business_name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name_plural = "Businesses"
|
|
||||||
|
|
||||||
class Service(models.Model):
|
|
||||||
business = models.ForeignKey(Business, on_delete=models.CASCADE, related_name='services')
|
|
||||||
name = models.CharField(max_length=255)
|
|
||||||
description = models.TextField(blank=True)
|
|
||||||
duration = models.IntegerField(help_text="Duration in minutes")
|
|
||||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Contact(models.Model):
|
|
||||||
phone_number = models.CharField(max_length=20, unique=True)
|
|
||||||
name = models.CharField(max_length=255, blank=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name or self.phone_number
|
|
||||||
|
|
||||||
class Call(models.Model):
|
|
||||||
business = models.ForeignKey(Business, on_delete=models.CASCADE, related_name='calls')
|
|
||||||
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='calls')
|
|
||||||
start_time = models.DateTimeField(auto_now_add=True)
|
|
||||||
end_time = models.DateTimeField(null=True, blank=True)
|
|
||||||
summary = models.TextField(blank=True)
|
|
||||||
transcript = models.TextField(blank=True)
|
|
||||||
conversation_history = models.JSONField(default=list)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Call with {self.contact} at {self.start_time}"
|
|
||||||
|
|
||||||
class Booking(models.Model):
|
|
||||||
STATUS_CHOICES = [
|
|
||||||
('scheduled', 'Scheduled'),
|
|
||||||
('completed', 'Completed'),
|
|
||||||
('canceled', 'Canceled'),
|
|
||||||
]
|
|
||||||
|
|
||||||
service = models.ForeignKey(Service, on_delete=models.CASCADE, related_name='bookings')
|
|
||||||
contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name='bookings')
|
|
||||||
start_time = models.DateTimeField()
|
|
||||||
end_time = models.DateTimeField()
|
|
||||||
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='scheduled')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.service} booking for {self.contact} at {self.start_time}"
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>{% block title %}Xfront{% endblock %}</title>
|
<title>{% block title %}Knowledge Base{% endblock %}</title>
|
||||||
{% if project_description %}
|
{% if project_description %}
|
||||||
<meta name="description" content="{{ project_description }}">
|
<meta name="description" content="{{ project_description }}">
|
||||||
<meta property="og:description" content="{{ project_description }}">
|
<meta property="og:description" content="{{ project_description }}">
|
||||||
@ -13,90 +13,13 @@
|
|||||||
<meta property="og:image" content="{{ project_image_url }}">
|
<meta property="og:image" content="{{ project_image_url }}">
|
||||||
<meta property="twitter:image" content="{{ project_image_url }}">
|
<meta property="twitter:image" content="{{ project_image_url }}">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css">
|
|
||||||
|
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="b2b-layout">
|
<body>
|
||||||
<div class="sidebar-container">
|
|
||||||
<div class="sidebar">
|
|
||||||
<div class="sidebar-header">
|
|
||||||
<a href="/" class="b2b-logo">Xfront AI</a>
|
|
||||||
</div>
|
|
||||||
<ul class="sidebar-nav">
|
|
||||||
<li><a href="#" data-target="overview-sidebar"><i class="fas fa-chart-pie"></i> Overview</a></li>
|
|
||||||
<li><a href="#" data-target="businesses-sidebar"><i class="fas fa-briefcase"></i> Businesses</a></li>
|
|
||||||
<li><a href="#" data-target="call-system-sidebar"><i class="fas fa-phone-alt"></i> Call System</a></li>
|
|
||||||
<li><a href="#" data-target="appointments-sidebar"><i class="far fa-calendar-alt"></i> Appointments</a></li>
|
|
||||||
<li><a href="#" data-target="knowledge-base-sidebar"><i class="fas fa-book"></i> Knowledge Base</a></li>
|
|
||||||
<li><a href="#" data-target="analytics-sidebar"><i class="fas fa-chart-line"></i> Analytics</a></li>
|
|
||||||
<li><a href="#" data-target="settings-sidebar"><i class="fas fa-cog"></i> Settings</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="secondary-sidebar-container">
|
|
||||||
<div id="overview-sidebar" class="secondary-sidebar active">
|
|
||||||
<h3>Overview</h3>
|
|
||||||
<ul class="secondary-sidebar-nav">
|
|
||||||
<li><a href="#">Dashboard Summary</a></li>
|
|
||||||
<li><a href="#">Recent Activity</a></li>
|
|
||||||
<li><a href="#">Alerts & Notifications</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div id="businesses-sidebar" class="secondary-sidebar">
|
|
||||||
<h3>Businesses</h3>
|
|
||||||
<ul class="secondary-sidebar-nav">
|
|
||||||
<li><a href="#">All Businesses</a></li>
|
|
||||||
<li><a href="#">Add New Business</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="main-content">
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const primaryNavLinks = document.querySelectorAll('.sidebar-nav a');
|
|
||||||
const secondarySidebars = document.querySelectorAll('.secondary-sidebar');
|
|
||||||
|
|
||||||
primaryNavLinks.forEach(link => {
|
|
||||||
link.addEventListener('click', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
primaryNavLinks.forEach(navLink => navLink.classList.remove('active'));
|
|
||||||
this.classList.add('active');
|
|
||||||
|
|
||||||
const targetId = this.getAttribute('data-target');
|
|
||||||
|
|
||||||
secondarySidebars.forEach(sidebar => {
|
|
||||||
if (sidebar.id === targetId) {
|
|
||||||
sidebar.classList.add('active');
|
|
||||||
} else {
|
|
||||||
sidebar.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set initial active state
|
|
||||||
if(primaryNavLinks.length > 0) {
|
|
||||||
primaryNavLinks[0].classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@ -1,58 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ page_title }}{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="main-content-header">
|
|
||||||
<a href="{% url 'dashboard' %}" class="back-link">← Back to Overview</a>
|
|
||||||
<h1>{{ business.business_name }}</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-grid">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Services</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for service in services %}
|
|
||||||
<li class="list-group-item">{{ service.name }} - {{ service.duration }} mins at ${{ service.price }}</li>
|
|
||||||
{% empty %}
|
|
||||||
<li class="list-group-item">No services found.</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Recent Calls</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for call in calls %}
|
|
||||||
<li class="list-group-item">Call from {{ call.contact }} on {{ call.start_time|date:"Y-m-d H:i" }}</li>
|
|
||||||
{% empty %}
|
|
||||||
<li class="list-group-item">No calls found.</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3>Recent Bookings</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for booking in bookings %}
|
|
||||||
<li class="list-group-item">{{ booking.service.name }} for {{ booking.contact }} on {{ booking.start_time|date:"Y-m-d H:i" }}</li>
|
|
||||||
{% empty %}
|
|
||||||
<li class="list-group-item">No bookings found.</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@ -1,45 +1,145 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{% block title %}{{ page_title }}{% endblock %}
|
{% block title %}{{ project_name }}{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg-color-start: #6a11cb;
|
||||||
|
--bg-color-end: #2575fc;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
|
color: var(--text-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
text-align: center;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
|
||||||
|
animation: bg-pan 20s linear infinite;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bg-pan {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 0%;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-position: 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg-color);
|
||||||
|
border: 1px solid var(--card-border-color);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 2.5rem 2rem;
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
-webkit-backdrop-filter: blur(20px);
|
||||||
|
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1.2rem;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border: 4px solid rgba(255, 255, 255, 0.25);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime code {
|
||||||
|
background: rgba(0, 0, 0, 0.25);
|
||||||
|
padding: 0.15rem 0.45rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="main-content-header">
|
<main>
|
||||||
<h1>Overview</h1>
|
<div class="card">
|
||||||
</div>
|
<h1>Analyzing your requirements and generating your app…</h1>
|
||||||
<div class="card">
|
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
|
||||||
<div class="card-header">
|
<span class="sr-only">Loading…</span>
|
||||||
<h3>Businesses</h3>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
|
||||||
<div class="table-responsive">
|
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
|
||||||
<table class="table">
|
<p class="runtime">
|
||||||
<thead>
|
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
|
||||||
<tr>
|
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
|
||||||
<th>Business Name</th>
|
</p>
|
||||||
<th>Industry</th>
|
|
||||||
<th>Services</th>
|
|
||||||
<th>Calls</th>
|
|
||||||
<th>Bookings</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for business in businesses %}
|
|
||||||
<tr>
|
|
||||||
<td><a href="{% url 'business_detail' business.id %}">{{ business.business_name }}</a></td>
|
|
||||||
<td>{{ business.get_industry_display }}</td>
|
|
||||||
<td>{{ business.service_count }}</td>
|
|
||||||
<td>{{ business.call_count }}</td>
|
|
||||||
<td>{{ business.booking_count }}</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="5">No businesses found.</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</div>
|
<footer>
|
||||||
|
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
|
||||||
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -1,10 +1,7 @@
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from .views import dashboard, inbound_call_webhook, business_detail, ai_call_handler
|
from .views import home
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", dashboard, name="dashboard"),
|
path("", home, name="home"),
|
||||||
path("business/<int:business_id>/", business_detail, name="business_detail"),
|
|
||||||
path("webhook/call/inbound/", inbound_call_webhook, name="inbound_call_webhook"),
|
|
||||||
path("webhook/call/handler/<int:call_id>/", ai_call_handler, name="ai_call_handler"),
|
|
||||||
]
|
]
|
||||||
|
|||||||
202
core/views.py
202
core/views.py
@ -1,187 +1,25 @@
|
|||||||
from django.shortcuts import render, get_object_or_404
|
import os
|
||||||
from django.contrib.auth.decorators import login_required
|
import platform
|
||||||
from .models import Business, Service, Call, Booking, Contact
|
|
||||||
from django.db.models import Count
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
import json
|
|
||||||
from twilio.twiml.voice_response import VoiceResponse, Gather
|
|
||||||
from django.urls import reverse
|
|
||||||
from ai.local_ai_api import LocalAIApi
|
|
||||||
from .booking import create_booking
|
|
||||||
|
|
||||||
from datetime import datetime
|
from django import get_version as django_version
|
||||||
|
from django.shortcuts import render
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
#@login_required
|
|
||||||
def dashboard(request):
|
def home(request):
|
||||||
"""Render the main admin dashboard."""
|
"""Render the landing screen with loader and environment details."""
|
||||||
businesses = Business.objects.annotate(
|
host_name = request.get_host().lower()
|
||||||
service_count=Count('services', distinct=True),
|
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
|
||||||
call_count=Count('calls', distinct=True),
|
now = timezone.now()
|
||||||
booking_count=Count('services__bookings', distinct=True)
|
|
||||||
)
|
|
||||||
recent_calls = Call.objects.order_by('-start_time')[:10]
|
|
||||||
upcoming_bookings = Booking.objects.filter(start_time__gte=datetime.now()).order_by('start_time')[:10]
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'businesses': businesses,
|
"project_name": "New Style",
|
||||||
'recent_calls': recent_calls,
|
"agent_brand": agent_brand,
|
||||||
'upcoming_bookings': upcoming_bookings,
|
"django_version": django_version(),
|
||||||
'page_title': 'Dashboard - Xfront'
|
"python_version": platform.python_version(),
|
||||||
|
"current_time": now,
|
||||||
|
"host_name": host_name,
|
||||||
|
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
|
||||||
|
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
|
||||||
}
|
}
|
||||||
return render(request, 'core/index.html', context)
|
return render(request, "core/index.html", context)
|
||||||
|
|
||||||
|
|
||||||
def business_detail(request, business_id):
|
|
||||||
"""Display a detailed view of a business."""
|
|
||||||
business = get_object_or_404(Business, pk=business_id)
|
|
||||||
services = business.services.all()
|
|
||||||
calls = business.calls.all().order_by('-start_time')[:10]
|
|
||||||
bookings = Booking.objects.filter(service__business=business).order_by('-start_time')[:10]
|
|
||||||
|
|
||||||
context = {
|
|
||||||
'business': business,
|
|
||||||
'services': services,
|
|
||||||
'calls': calls,
|
|
||||||
'bookings': bookings,
|
|
||||||
'page_title': f'{business.business_name} - Details'
|
|
||||||
}
|
|
||||||
return render(request, 'core/business_detail.html', context)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def inbound_call_webhook(request):
|
|
||||||
"""Handle inbound calls from Twilio."""
|
|
||||||
if request.method == 'POST':
|
|
||||||
from_number = request.POST.get('From')
|
|
||||||
to_number = request.POST.get('To')
|
|
||||||
|
|
||||||
try:
|
|
||||||
business = Business.objects.get(phone_number=to_number)
|
|
||||||
except Business.DoesNotExist:
|
|
||||||
response = VoiceResponse()
|
|
||||||
response.say("The number you have called is not associated with a business.")
|
|
||||||
return HttpResponse(str(response), content_type='text/xml')
|
|
||||||
|
|
||||||
contact, _ = Contact.objects.get_or_create(phone_number=from_number)
|
|
||||||
|
|
||||||
call = Call.objects.create(
|
|
||||||
business=business,
|
|
||||||
contact=contact,
|
|
||||||
)
|
|
||||||
|
|
||||||
response = VoiceResponse()
|
|
||||||
response.say(f"Hello and welcome to {business.business_name}. Please wait while we connect you to our AI assistant.")
|
|
||||||
response.redirect(reverse('ai_call_handler', args=[call.id]))
|
|
||||||
return HttpResponse(str(response), content_type='text/xml')
|
|
||||||
|
|
||||||
return HttpResponse(status=400)
|
|
||||||
|
|
||||||
tools = [
|
|
||||||
{
|
|
||||||
"type": "function",
|
|
||||||
"function": {
|
|
||||||
"name": "create_booking",
|
|
||||||
"description": "Creates a booking for a given service at a specific time.",
|
|
||||||
"parameters": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"service_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The name of the service to book.",
|
|
||||||
},
|
|
||||||
"booking_time_str": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The desired time for the booking in ISO 8601 format.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["service_name", "booking_time_str"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def ai_call_handler(request, call_id):
|
|
||||||
"""Handle the AI-powered conversation."""
|
|
||||||
call = get_object_or_404(Call, pk=call_id)
|
|
||||||
response = VoiceResponse()
|
|
||||||
|
|
||||||
if 'SpeechResult' in request.POST:
|
|
||||||
user_speech = request.POST['SpeechResult']
|
|
||||||
call.conversation_history.append({'role': 'user', 'content': user_speech})
|
|
||||||
|
|
||||||
services = Service.objects.filter(business=call.business)
|
|
||||||
services_prompt = "\n".join([f"- {s.name}: {s.description}" for s in services])
|
|
||||||
|
|
||||||
# Get AI response
|
|
||||||
ai_response = LocalAIApi.create_response(
|
|
||||||
{
|
|
||||||
"input": [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": f"You are a friendly and helpful AI assistant for {call.business.business_name}. Available services are:\n{services_prompt}"
|
|
||||||
},
|
|
||||||
*call.conversation_history
|
|
||||||
],
|
|
||||||
"tools": tools,
|
|
||||||
"tool_choice": "auto",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"poll_interval": 5,
|
|
||||||
"poll_timeout": 300,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
if ai_response.get("success") and ai_response.get("data", {}).get("output", [{}])[0].get("content", [{}])[0].get("type") == "tool_calls":
|
|
||||||
tool_calls = ai_response["data"]["output"][0]["content"][0]["tool_calls"]
|
|
||||||
call.conversation_history.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
|
|
||||||
|
|
||||||
for tool_call in tool_calls:
|
|
||||||
function_name = tool_call['function']['name']
|
|
||||||
if function_name == 'create_booking':
|
|
||||||
args = json.loads(tool_call['function']['arguments'])
|
|
||||||
result = create_booking(
|
|
||||||
contact_phone_number=call.contact.phone_number,
|
|
||||||
service_name=args.get("service_name"),
|
|
||||||
booking_time_str=args.get("booking_time_str"),
|
|
||||||
)
|
|
||||||
call.conversation_history.append({"role": "tool", "tool_call_id": tool_call['id'], "name": function_name, "content": result})
|
|
||||||
|
|
||||||
# Get AI response after tool call
|
|
||||||
ai_response = LocalAIApi.create_response(
|
|
||||||
{
|
|
||||||
"input": [
|
|
||||||
{
|
|
||||||
"role": "system",
|
|
||||||
"content": f"You are a friendly and helpful AI assistant for {call.business.business_name}. Available services are:\n{services_prompt}"
|
|
||||||
},
|
|
||||||
*call.conversation_history
|
|
||||||
],
|
|
||||||
"tools": tools,
|
|
||||||
"tool_choice": "auto",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"poll_interval": 5,
|
|
||||||
"poll_timeout": 300,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ai_text = LocalAIApi.extract_text(ai_response)
|
|
||||||
else:
|
|
||||||
ai_text = LocalAIApi.extract_text(ai_response)
|
|
||||||
|
|
||||||
if not ai_text:
|
|
||||||
ai_text = "I am sorry, I am having trouble understanding. Please try again."
|
|
||||||
|
|
||||||
call.conversation_history.append({'role': 'assistant', 'content': ai_text})
|
|
||||||
response.say(ai_text)
|
|
||||||
|
|
||||||
else:
|
|
||||||
# First turn of the conversation
|
|
||||||
response.say("How can I help you today?")
|
|
||||||
|
|
||||||
gather = Gather(input='speech', action=reverse('ai_call_handler', args=[call.id]), speechTimeout='auto')
|
|
||||||
response.append(gather)
|
|
||||||
|
|
||||||
call.save()
|
|
||||||
|
|
||||||
return HttpResponse(str(response), content_type='text/xml')
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
Django==5.2.7
|
Django==5.2.7
|
||||||
mysqlclient==2.2.7
|
mysqlclient==2.2.7
|
||||||
python-dotenv==1.1.1
|
python-dotenv==1.1.1
|
||||||
twilio==9.2.2
|
|
||||||
@ -1,177 +1,4 @@
|
|||||||
/*
|
/* Custom styles for the application */
|
||||||
Palette:
|
|
||||||
- Base: #F8F9FA (Light Grey)
|
|
||||||
- Primary (Sidebar/Header): #1a202c (Dark Slate)
|
|
||||||
- Secondary (UI Elements): #718096 (Slate)
|
|
||||||
- Accent (Buttons, Highlights): #3182ce (Vibrant Blue)
|
|
||||||
*/
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--base-bg: #F8F9FA;
|
|
||||||
--primary-bg: #1a202c;
|
|
||||||
--secondary-color: #718096;
|
|
||||||
--accent-color: #3182ce;
|
|
||||||
--text-color: #4A5568;
|
|
||||||
--heading-color: #2D3748;
|
|
||||||
--white-color: #FFFFFF;
|
|
||||||
--border-color: #E2E8F0;
|
|
||||||
--sidebar-width: 260px;
|
|
||||||
--secondary-sidebar-width: 240px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
background-color: #f7fafc;
|
|
||||||
color: var(--text-color);
|
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b2b-layout {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar-container {
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: var(--primary-bg);
|
|
||||||
color: var(--white-color);
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Secondary Sidebar */
|
|
||||||
.secondary-sidebar-container {
|
|
||||||
width: var(--secondary-sidebar-width);
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: var(--sidebar-width);
|
|
||||||
background-color: #edf2f7;
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
padding: 1.5rem;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-nav {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-nav li a {
|
|
||||||
display: block;
|
|
||||||
padding: 0.75rem 0;
|
|
||||||
color: #4a5568;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-nav li a:hover {
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content */
|
|
||||||
.main-content {
|
|
||||||
margin-left: calc(var(--sidebar-width) + var(--secondary-sidebar-width));
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content-header {
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card Grid & Card */
|
|
||||||
.card-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: var(--white-color);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 1.5rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
:root {
|
|
||||||
--sidebar-width: 220px;
|
|
||||||
--secondary-sidebar-width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root {
|
|
||||||
--sidebar-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: var(--sidebar-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav li a {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav li a span {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-header .b2b-logo {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,177 +1,21 @@
|
|||||||
/*
|
|
||||||
Palette:
|
|
||||||
- Base: #F8F9FA (Light Grey)
|
|
||||||
- Primary (Sidebar/Header): #1a202c (Dark Slate)
|
|
||||||
- Secondary (UI Elements): #718096 (Slate)
|
|
||||||
- Accent (Buttons, Highlights): #3182ce (Vibrant Blue)
|
|
||||||
*/
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--base-bg: #F8F9FA;
|
--bg-color-start: #6a11cb;
|
||||||
--primary-bg: #1a202c;
|
--bg-color-end: #2575fc;
|
||||||
--secondary-color: #718096;
|
--text-color: #ffffff;
|
||||||
--accent-color: #3182ce;
|
--card-bg-color: rgba(255, 255, 255, 0.01);
|
||||||
--text-color: #4A5568;
|
--card-border-color: rgba(255, 255, 255, 0.1);
|
||||||
--heading-color: #2D3748;
|
|
||||||
--white-color: #FFFFFF;
|
|
||||||
--border-color: #E2E8F0;
|
|
||||||
--sidebar-width: 260px;
|
|
||||||
--secondary-sidebar-width: 240px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
margin: 0;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
background-color: #f7fafc;
|
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
margin: 0;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.b2b-layout {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
/* Sidebar */
|
|
||||||
.sidebar-container {
|
|
||||||
width: var(--sidebar-width);
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
background-color: var(--primary-bg);
|
|
||||||
color: var(--white-color);
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Secondary Sidebar */
|
|
||||||
.secondary-sidebar-container {
|
|
||||||
width: var(--secondary-sidebar-width);
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
top: 0;
|
|
||||||
left: var(--sidebar-width);
|
|
||||||
background-color: #edf2f7;
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
padding: 1.5rem;
|
|
||||||
z-index: 999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar.active {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar h3 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
color: #4a5568;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-nav {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-nav li a {
|
|
||||||
display: block;
|
|
||||||
padding: 0.75rem 0;
|
|
||||||
color: #4a5568;
|
|
||||||
text-decoration: none;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 5px;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-nav li a:hover {
|
|
||||||
background-color: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Main Content */
|
|
||||||
.main-content {
|
|
||||||
margin-left: calc(var(--sidebar-width) + var(--secondary-sidebar-width));
|
|
||||||
flex-grow: 1;
|
|
||||||
padding: 2.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content-header {
|
|
||||||
padding-bottom: 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content-header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card Grid & Card */
|
|
||||||
.card-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: var(--white-color);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
transition: box-shadow 0.3s ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover {
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
padding: 1.25rem 1.5rem;
|
|
||||||
border-bottom: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header h3 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 1.5rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
:root {
|
|
||||||
--sidebar-width: 220px;
|
|
||||||
--secondary-sidebar-width: 200px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
:root {
|
|
||||||
--sidebar-width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.secondary-sidebar-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
margin-left: var(--sidebar-width);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-nav li a {
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
.sidebar-nav li a span {
|
text-align: center;
|
||||||
display: none;
|
overflow: hidden;
|
||||||
}
|
position: relative;
|
||||||
|
|
||||||
.sidebar-header .b2b-logo {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user