Compare commits

...

8 Commits

Author SHA1 Message Date
5880de54cd phoenix in opportunity 2025-06-15 16:50:47 +03:00
ceb6ddf902 Merge branch 'main' of http://10.10.1.136:3000/ismail/haikal into frontend 2025-06-15 16:47:38 +03:00
f1277a2ed4 phoenix btn added 2025-06-15 16:40:40 +03:00
Marwan Alwali
85793dfba7 update 2025-06-15 15:57:32 +03:00
Marwan Alwali
008ada38ae update translation 2025-06-13 02:43:50 +03:00
Marwan Alwali
c212a65185 added HaikalBot logic and Tours 2025-06-13 01:58:40 +03:00
7d2b70e950 button ui 2025-06-12 21:01:13 +03:00
cba8a39d1b Merge pull request 'Audit log changes' (#70) from frontend into main
Reviewed-on: #70
2025-06-12 17:20:17 +03:00
202 changed files with 13224 additions and 2007 deletions

BIN
.DS_Store vendored

Binary file not shown.

6
.idea/sqldialects.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/haikalbot/management/commands/generate_support_yaml.py" dialect="GenericSQL" />
</component>
</project>

View File

@ -106,8 +106,9 @@ def car_list(request):
page = request.GET.get("page", 1)
per_page = 10
cars = inventory_models.Car.objects.filter(dealer=dealer).values(
cars = inventory_models.Car.objects.all().values(
"vin",
"year",
"id_car_make__name",
"id_car_model__name",
"status"

View File

@ -28,6 +28,7 @@ urlpatterns += i18n_patterns(
path('appointment/', include('appointment.urls')),
path('plans/', include('plans.urls')),
path("schema/", Schema.as_view()),
path('tours/', include('tours.urls')),
# path('', include(tf_urls)),
)

0
database.sqlite Normal file
View File

2950
haikal_kb.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@ from sqlalchemy.orm import relationship
logger = logging.getLogger(__name__)
# Configuration settings
LLM_MODEL = getattr(settings, 'MODEL_ANALYZER_LLM_MODEL', 'qwen:7b-chat')
LLM_MODEL = getattr(settings, 'MODEL_ANALYZER_LLM_MODEL', 'qwen3:8b')
LLM_TEMPERATURE = getattr(settings, 'MODEL_ANALYZER_LLM_TEMPERATURE', 0.3)
LLM_MAX_TOKENS = getattr(settings, 'MODEL_ANALYZER_LLM_MAX_TOKENS', 2048)
CACHE_TIMEOUT = getattr(settings, 'MODEL_ANALYZER_CACHE_TIMEOUT', 3600)
@ -753,12 +753,14 @@ def analyze_prompt(prompt: str) -> Dict[str, Any]:
"""
# Detect language
language = "ar" if bool(re.search(r'[\u0600-\u06FF]', prompt)) else "en"
filtered_apps = ['inventory', 'django_ledger', 'appointments', 'plans']
filtered_apps = ['inventory']
try:
analyzer = DjangoModelAnalyzer()
model_structure = get_all_model_structures(filtered_apps=filtered_apps)
print(model_structure)
analysis = analyzer.analyze_prompt(prompt, model_structure)
print(analysis)
if not analysis or not analysis.app_label or not analysis.model_name:
return {

801
haikalbot/haikal_agent.py Normal file
View File

@ -0,0 +1,801 @@
import asyncio
import sqlite3
import json
import re
import logging
from typing import List, Dict, Any, Optional, Union
from dataclasses import dataclass, asdict
from enum import Enum
import os
from functools import reduce
import operator
# Pydantic and AI imports
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
# Optional Django imports (if available)
try:
from django.apps import apps
from django.db import models, connection
from django.db.models import QuerySet, Q, F, Sum, Avg, Count, Max, Min
from django.core.exceptions import FieldDoesNotExist
from django.conf import settings
DJANGO_AVAILABLE = True
except ImportError:
DJANGO_AVAILABLE = False
# Optional database drivers
try:
import psycopg2
POSTGRESQL_AVAILABLE = True
except ImportError:
POSTGRESQL_AVAILABLE = False
try:
import pymysql
MYSQL_AVAILABLE = True
except ImportError:
MYSQL_AVAILABLE = False
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configuration
class DatabaseConfig:
LLM_MODEL = settings.MODEL_ANALYZER_LLM_MODEL
LLM_BASE_URL = "http://localhost:11434/v1"
LLM_TEMPERATURE = 0.3
MAX_RESULTS = 1000
SUPPORTED_CHART_TYPES = ["bar", "line", "pie", "doughnut", "radar", "scatter"]
class DatabaseType(Enum):
SQLITE = "sqlite"
POSTGRESQL = "postgresql"
MYSQL = "mysql"
@dataclass
class DatabaseConnection:
db_type: DatabaseType
connection_string: str
database_name: Optional[str] = None
host: Optional[str] = None
port: Optional[int] = None
user: Optional[str] = None
password: Optional[str] = None
schema_info: Optional[Dict] = None
@dataclass
class QueryResult:
status: str
data: Union[List[Dict], Dict]
metadata: Dict[str, Any]
chart_data: Optional[Dict] = None
language: str = "en"
error: Optional[str] = None
def to_dict(self):
"""Convert to dictionary for JSON serialization."""
return asdict(self)
class DatabaseSchema(BaseModel):
tables: Dict[str, List[Dict[str, Any]]] = Field(
description="Database schema with table names as keys and column info as values"
)
relationships: Optional[List[Dict[str, Any]]] = Field(
default=None,
description="Foreign key relationships between tables"
)
class InsightRequest(BaseModel):
prompt: str = Field(description="Natural language query from user")
database_path: Optional[str] = Field(default=None, description="Path to database file (for SQLite)")
chart_type: Optional[str] = Field(default=None, description="Preferred chart type")
limit: Optional[int] = Field(default=1000, description="Maximum number of results")
language: Optional[str] = Field(default="auto", description="Response language preference")
use_django: Optional[bool] = Field(default=True, description="Use Django database if available")
class DatabaseInsightSystem:
def __init__(self, config: DatabaseConfig = None):
self.config = config or DatabaseConfig()
self.model = OpenAIModel(
model_name=self.config.LLM_MODEL,
provider=OpenAIProvider(base_url=self.config.LLM_BASE_URL)
)
self.db_connection = None
self._setup_agents()
def _setup_agents(self):
"""Initialize the AI agents for schema analysis and query generation."""
# Query generation and execution agent
self.query_agent = Agent(
self.model,
deps_type=DatabaseSchema,
output_type=str,
system_prompt="""You are an intelligent database query generator and analyst.
Given a natural language prompt and database schema, you must:
1. ANALYZE the user's request in English or Arabic
2. IDENTIFY relevant tables and columns from the schema
3. GENERATE appropriate SQL query or analysis approach
4. DETERMINE if aggregation, grouping, or joins are needed
5. SUGGEST appropriate visualization type
6. EXECUTE the query and provide insights
Response format should be JSON:
{
"analysis": "Brief analysis of the request",
"query_type": "select|aggregate|join|complex",
"sql_query": "Generated SQL query",
"chart_suggestion": "bar|line|pie|etc",
"expected_fields": ["field1", "field2"],
"language": "en|ar"
}
Handle both English and Arabic prompts. For Arabic text, respond in Arabic.
Focus on providing actionable insights, not just raw data."""
)
def _get_django_database_config(self) -> Optional[DatabaseConnection]:
"""Extract database configuration from Django settings."""
if not DJANGO_AVAILABLE:
return None
try:
# Get default database configuration
db_config = settings.DATABASES.get('default', {})
if not db_config:
logger.warning("No default database configuration found in Django settings")
return None
engine = db_config.get('ENGINE', '')
db_name = db_config.get('NAME', '')
host = db_config.get('HOST', 'localhost')
port = db_config.get('PORT', None)
user = db_config.get('USER', '')
password = db_config.get('PASSWORD', '')
# Determine database type from engine
if 'sqlite' in engine.lower():
db_type = DatabaseType.SQLITE
connection_string = db_name # For SQLite, NAME is the file path
elif 'postgresql' in engine.lower():
db_type = DatabaseType.POSTGRESQL
port = port or 5432
connection_string = f"postgresql://{user}:{password}@{host}:{port}/{db_name}"
elif 'mysql' in engine.lower():
db_type = DatabaseType.MYSQL
port = port or 3306
connection_string = f"mysql://{user}:{password}@{host}:{port}/{db_name}"
else:
logger.warning(f"Unsupported database engine: {engine}")
return None
return DatabaseConnection(
db_type=db_type,
connection_string=connection_string,
database_name=db_name,
host=host,
port=port,
user=user,
password=password
)
except Exception as e:
logger.error(f"Failed to get Django database config: {e}")
return None
def analyze_database_schema_sync(self, request: InsightRequest) -> DatabaseSchema:
"""Synchronous wrapper for schema analysis."""
return asyncio.run(self.analyze_database_schema(request))
async def analyze_database_schema(self, request: InsightRequest) -> DatabaseSchema:
"""Extract and analyze database schema."""
try:
# Try Django first if available and requested
if request.use_django and DJANGO_AVAILABLE:
django_config = self._get_django_database_config()
if django_config:
self.db_connection = django_config
return await self._analyze_django_schema()
# Fallback to direct database connection
if request.database_path:
# Assume SQLite for direct file path
self.db_connection = DatabaseConnection(
db_type=DatabaseType.SQLITE,
connection_string=request.database_path
)
return await self._analyze_sqlite_schema(request.database_path)
raise ValueError("No database configuration available")
except Exception as e:
logger.error(f"Schema analysis failed: {e}")
raise
async def _analyze_sqlite_schema(self, db_path: str) -> DatabaseSchema:
"""Analyze SQLite database schema."""
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Get table names
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = [row[0] for row in cursor.fetchall()]
schema_data = {}
relationships = []
for table in tables:
# Get column information
cursor.execute(f"PRAGMA table_info({table})")
columns = []
for col in cursor.fetchall():
columns.append({
"name": col[1],
"type": col[2],
"notnull": bool(col[3]),
"default_value": col[4],
"primary_key": bool(col[5])
})
schema_data[table] = columns
# Get foreign key relationships
cursor.execute(f"PRAGMA foreign_key_list({table})")
for fk in cursor.fetchall():
relationships.append({
"from_table": table,
"from_column": fk[3],
"to_table": fk[2],
"to_column": fk[4]
})
conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships)
except Exception as e:
logger.error(f"SQLite schema analysis failed: {e}")
raise
async def _analyze_django_schema(self) -> DatabaseSchema:
"""Analyze Django models schema."""
if not DJANGO_AVAILABLE:
raise ImportError("Django is not available")
schema_data = {}
relationships = []
for model in apps.get_models():
table_name = model._meta.db_table
columns = []
for field in model._meta.get_fields():
if not field.is_relation:
columns.append({
"name": field.name,
"type": field.get_internal_type(),
"notnull": not getattr(field, 'null', True),
"primary_key": getattr(field, 'primary_key', False)
})
else:
# Handle relationships
if hasattr(field, 'related_model') and field.related_model:
relationships.append({
"from_table": table_name,
"from_column": field.name,
"to_table": field.related_model._meta.db_table,
"relationship_type": field.get_internal_type()
})
schema_data[table_name] = columns
return DatabaseSchema(tables=schema_data, relationships=relationships)
async def _analyze_postgresql_schema(self, connection_string: str) -> DatabaseSchema:
"""Analyze PostgreSQL database schema."""
if not POSTGRESQL_AVAILABLE:
raise ImportError("psycopg2 is not available")
try:
import psycopg2
from psycopg2.extras import RealDictCursor
conn = psycopg2.connect(connection_string)
cursor = conn.cursor(cursor_factory=RealDictCursor)
# Get table names
cursor.execute("""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
""")
tables = [row['table_name'] for row in cursor.fetchall()]
schema_data = {}
relationships = []
for table in tables:
# Get column information
cursor.execute("""
SELECT column_name, data_type, is_nullable, column_default
FROM information_schema.columns
WHERE table_name = %s
ORDER BY ordinal_position
""", (table,))
columns = []
for col in cursor.fetchall():
columns.append({
"name": col['column_name'],
"type": col['data_type'],
"notnull": col['is_nullable'] == 'NO',
"default_value": col['column_default'],
"primary_key": False # Will be updated below
})
# Get primary key information
cursor.execute("""
SELECT column_name
FROM information_schema.key_column_usage
WHERE table_name = %s
AND constraint_name LIKE '%_pkey'
""", (table,))
pk_columns = [row['column_name'] for row in cursor.fetchall()]
for col in columns:
if col['name'] in pk_columns:
col['primary_key'] = True
schema_data[table] = columns
# Get foreign key relationships
cursor.execute("""
SELECT kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY'
AND tc.table_name = %s
""", (table,))
for fk in cursor.fetchall():
relationships.append({
"from_table": table,
"from_column": fk['column_name'],
"to_table": fk['foreign_table_name'],
"to_column": fk['foreign_column_name']
})
conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships)
except Exception as e:
logger.error(f"PostgreSQL schema analysis failed: {e}")
raise
async def _analyze_mysql_schema(self, connection_string: str) -> DatabaseSchema:
"""Analyze MySQL database schema."""
if not MYSQL_AVAILABLE:
raise ImportError("pymysql is not available")
try:
import pymysql
# Parse connection string to get connection parameters
# Format: mysql://user:password@host:port/database
import urllib.parse
parsed = urllib.parse.urlparse(connection_string)
conn = pymysql.connect(
host=parsed.hostname,
port=parsed.port or 3306,
user=parsed.username,
password=parsed.password,
database=parsed.path[1:], # Remove leading slash
cursorclass=pymysql.cursors.DictCursor
)
cursor = conn.cursor()
# Get table names
cursor.execute("SHOW TABLES")
tables = [list(row.values())[0] for row in cursor.fetchall()]
schema_data = {}
relationships = []
for table in tables:
# Get column information
cursor.execute(f"DESCRIBE {table}")
columns = []
for col in cursor.fetchall():
columns.append({
"name": col['Field'],
"type": col['Type'],
"notnull": col['Null'] == 'NO',
"default_value": col['Default'],
"primary_key": col['Key'] == 'PRI'
})
schema_data[table] = columns
# Get foreign key relationships
cursor.execute(f"""
SELECT
COLUMN_NAME,
REFERENCED_TABLE_NAME,
REFERENCED_COLUMN_NAME
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE TABLE_NAME = '{table}'
AND REFERENCED_TABLE_NAME IS NOT NULL
""")
for fk in cursor.fetchall():
relationships.append({
"from_table": table,
"from_column": fk['COLUMN_NAME'],
"to_table": fk['REFERENCED_TABLE_NAME'],
"to_column": fk['REFERENCED_COLUMN_NAME']
})
conn.close()
return DatabaseSchema(tables=schema_data, relationships=relationships)
except Exception as e:
logger.error(f"MySQL schema analysis failed: {e}")
raise
def _detect_language(self, text: str) -> str:
"""Detect if text is Arabic or English."""
arabic_chars = re.findall(r'[\u0600-\u06FF]', text)
return "ar" if len(arabic_chars) > len(text) * 0.3 else "en"
def _execute_query_sync(self, query: str) -> List[Dict]:
"""Synchronous wrapper for query execution."""
return asyncio.run(self._execute_query(query))
async def _execute_query(self, query: str) -> List[Dict]:
"""Execute query based on the current database connection."""
if not self.db_connection:
raise ValueError("No database connection established")
if self.db_connection.db_type == DatabaseType.SQLITE:
return await self._execute_sqlite_query(self.db_connection.connection_string, query)
# elif self.db_connection.db_type == DatabaseType.DJANGO and DJANGO_AVAILABLE:
# return await self._execute_django_query(query)
elif self.db_connection.db_type == DatabaseType.POSTGRESQL:
return await self._execute_postgresql_query(self.db_connection.connection_string, query)
elif self.db_connection.db_type == DatabaseType.MYSQL:
return await self._execute_mysql_query(self.db_connection.connection_string, query)
else:
raise ValueError(f"Unsupported database type: {self.db_connection.db_type}")
async def _execute_sqlite_query(self, db_path: str, query: str) -> List[Dict]:
"""Execute SQL query on SQLite database."""
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(query)
# Get column names
columns = [description[0] for description in cursor.description]
# Fetch results and convert to dictionaries
results = cursor.fetchall()
data = [dict(zip(columns, row)) for row in results]
conn.close()
return data
except Exception as e:
logger.error(f"SQLite query execution failed: {e}")
raise
async def _execute_django_query(self, query: str) -> List[Dict]:
"""Execute raw SQL query using Django's database connection."""
try:
from django.db import connection
with connection.cursor() as cursor:
cursor.execute(query)
columns = [col[0] for col in cursor.description]
results = cursor.fetchall()
data = [dict(zip(columns, row)) for row in results]
return data
except Exception as e:
logger.error(f"Django query execution failed: {e}")
raise
async def _execute_postgresql_query(self, connection_string: str, query: str) -> List[Dict]:
"""Execute SQL query on PostgreSQL database."""
try:
import psycopg2
from psycopg2.extras import RealDictCursor
conn = psycopg2.connect(connection_string)
cursor = conn.cursor(cursor_factory=RealDictCursor)
cursor.execute(query)
results = cursor.fetchall()
data = [dict(row) for row in results]
conn.close()
return data
except Exception as e:
logger.error(f"PostgreSQL query execution failed: {e}")
raise
async def _execute_mysql_query(self, connection_string: str, query: str) -> List[Dict]:
"""Execute SQL query on MySQL database."""
try:
import pymysql
import urllib.parse
parsed = urllib.parse.urlparse(connection_string)
conn = pymysql.connect(
host=parsed.hostname,
port=parsed.port or 3306,
user=parsed.username,
password=parsed.password,
database=parsed.path[1:],
cursorclass=pymysql.cursors.DictCursor
)
cursor = conn.cursor()
cursor.execute(query)
results = cursor.fetchall()
conn.close()
return results
except Exception as e:
logger.error(f"MySQL query execution failed: {e}")
raise
def _prepare_chart_data(self, data: List[Dict], chart_type: str, fields: List[str]) -> Optional[Dict]:
"""Prepare data for chart visualization."""
if not data or not fields:
return None
chart_type = chart_type.lower()
if chart_type not in self.config.SUPPORTED_CHART_TYPES:
chart_type = "bar"
try:
# Extract labels and values
labels = []
datasets = []
if len(fields) >= 1:
labels = [str(item.get(fields[0], "")) for item in data]
if chart_type in ["pie", "doughnut"]:
# Single dataset for pie charts
values = []
for item in data:
if len(fields) > 1:
try:
value = float(item.get(fields[1], 0) or 0)
except (ValueError, TypeError):
value = 1
values.append(value)
else:
values.append(1)
return {
"type": chart_type,
"labels": labels,
"data": values,
"backgroundColor": [
f"rgba({50 + i * 30}, {100 + i * 25}, {200 + i * 20}, 0.7)"
for i in range(len(values))
]
}
else:
# Multiple datasets for other chart types
for i, field in enumerate(fields[1:], 1):
try:
dataset_values = []
for item in data:
try:
value = float(item.get(field, 0) or 0)
except (ValueError, TypeError):
value = 0
dataset_values.append(value)
datasets.append({
"label": field,
"data": dataset_values,
"backgroundColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 0.6)",
"borderColor": f"rgba({50 + i * 40}, {100 + i * 30}, 235, 1.0)",
"borderWidth": 2
})
except Exception as e:
logger.warning(f"Error processing field {field}: {e}")
return {
"type": chart_type,
"labels": labels,
"datasets": datasets
}
except Exception as e:
logger.error(f"Chart preparation failed: {e}")
return None
def get_insights_sync(self, request: InsightRequest) -> Dict[str, Any]:
"""Synchronous wrapper for get_insights - for Django views."""
try:
result = asyncio.run(self.get_insights(request))
return result.to_dict()
except Exception as e:
logger.error(f"Synchronous insight generation failed: {e}")
return {
"status": "error",
"data": [],
"metadata": {},
"error": str(e),
"language": "en"
}
async def get_insights(self, request: InsightRequest) -> QueryResult:
"""Main method to get database insights from natural language prompt."""
try:
# Detect language
language = self._detect_language(request.prompt) if request.language == "auto" else request.language
# Analyze database schema
schema = await self.analyze_database_schema(request)
# Generate query plan using AI
query_response = await self.query_agent.run(
f"User prompt: {request.prompt}\nLanguage: {language}",
database_schema=schema
)
# Parse AI response
try:
query_plan = json.loads(query_response.output)
except json.JSONDecodeError:
# Fallback: extract SQL from response
sql_match = re.search(r'SELECT.*?;', query_response.output, re.IGNORECASE | re.DOTALL)
if sql_match:
query_plan = {
"sql_query": sql_match.group(0),
"chart_suggestion": "bar",
"expected_fields": [],
"language": language
}
else:
raise ValueError("Could not parse AI response")
# Execute query
sql_query = query_plan.get("sql_query", "")
if not sql_query:
raise ValueError("No SQL query generated")
data = await self._execute_query(sql_query)
# Prepare chart data
chart_data = None
chart_type = request.chart_type or query_plan.get("chart_suggestion", "bar")
expected_fields = query_plan.get("expected_fields", [])
if data and expected_fields:
chart_data = self._prepare_chart_data(data, chart_type, expected_fields)
elif data:
# Use first few fields if no specific fields suggested
available_fields = list(data[0].keys()) if data else []
chart_data = self._prepare_chart_data(data, chart_type, available_fields[:3])
# Prepare result
return QueryResult(
status="success",
data=data[:request.limit] if data else [],
metadata={
"total_count": len(data) if data else 0,
"query": sql_query,
"analysis": query_plan.get("analysis", ""),
"fields": expected_fields or (list(data[0].keys()) if data else []),
"database_type": self.db_connection.db_type.value if self.db_connection else "unknown"
},
chart_data=chart_data,
language=language
)
except Exception as e:
logger.error(f"Insight generation failed: {e}")
return QueryResult(
status="error",
data=[],
metadata={},
error=str(e),
language=language if 'language' in locals() else "en"
)
# # Static method for Django view compatibility
# @staticmethod
# def get_insights(django_request, prompt: str, **kwargs) -> Dict[str, Any]:
# """
# Static method compatible with your Django view.
# This method signature matches what your view is calling.
#
# Args:
# django_request: Django HttpRequest object (not used but kept for compatibility)
# prompt: Natural language query string
# **kwargs: Additional parameters
#
# Returns:
# Dictionary with query results
# """
# try:
# # Create system instance
# system = DatabaseInsightSystem()
#
# # Extract language from Django request if available
# language = "auto"
# if hasattr(django_request, 'LANGUAGE_CODE'):
# language = django_request.LANGUAGE_CODE
#
# # Create insight request
# insight_request = InsightRequest(
# prompt=prompt,
# language=language,
# use_django=True,
# **kwargs
# )
#
# # Get insights synchronously
# return system.get_insights_sync(insight_request)
#
# except Exception as e:
# logger.error(f"Static get_insights failed: {e}")
# return {
# "status": "error",
# "data": [],
# "metadata": {},
# "error": str(e),
# "language": language if 'language' in locals() else "en"
# }
# Convenience function for Django views (alternative approach)
def analyze_prompt_sync(prompt: str, **kwargs) -> Dict[str, Any]:
"""
Synchronous function to analyze a prompt and return insights.
Perfect for Django views.
Args:
prompt: Natural language query
**kwargs: Additional parameters for InsightRequest
Returns:
Dictionary with query results
"""
system = DatabaseInsightSystem()
request = InsightRequest(prompt=prompt, **kwargs)
return system.get_insights_sync(request)

File diff suppressed because it is too large Load Diff

View File

@ -5,9 +5,11 @@ import importlib
import yaml
import os
from django.conf import settings
from django.template.loaders.app_directories import get_app_template_dirs
class Command(BaseCommand):
help = "Generate YAML support knowledge base from Django views and models"
help = "Generate YAML support knowledge base from Django views, models, and templates"
def handle(self, *args, **kwargs):
output_file = "haikal_kb.yaml"
@ -18,6 +20,8 @@ class Command(BaseCommand):
"generated_from": "Django",
},
"features": {},
"user_workflows": {}, # New section for step-by-step instructions
"templates": {},
"glossary": {}
}
@ -41,6 +45,49 @@ class Command(BaseCommand):
all_models.append((model._meta.app_label, model.__name__, extract_doc(model)))
return all_models
def get_all_templates():
template_dirs = get_app_template_dirs('templates')
templates = []
for template_dir in template_dirs:
app_name = os.path.basename(os.path.dirname(os.path.dirname(template_dir)))
for root, dirs, files in os.walk(template_dir):
for file in files:
if file.endswith(('.html', '.htm', '.txt')):
rel_path = os.path.relpath(os.path.join(root, file), template_dir)
with open(os.path.join(root, file), 'r', encoding='utf-8', errors='ignore') as f:
try:
content = f.read()
# Extract template comment documentation if it exists
doc = ""
if '{# DOC:' in content and '#}' in content:
doc_parts = content.split('{# DOC:')
for part in doc_parts[1:]:
if '#}' in part:
doc += part.split('#}')[0].strip() + "\n"
except Exception as e:
self.stdout.write(self.style.WARNING(f"Error reading {rel_path}: {e}"))
continue
templates.append((app_name, rel_path, doc.strip()))
return templates
# Look for workflow documentation files
def get_workflow_docs():
workflows = {}
workflow_dir = os.path.join(settings.BASE_DIR, 'docs', 'workflows')
if os.path.exists(workflow_dir):
for file in os.listdir(workflow_dir):
if file.endswith('.yaml') or file.endswith('.yml'):
try:
with open(os.path.join(workflow_dir, file), 'r') as f:
workflow_data = yaml.safe_load(f)
for workflow_name, workflow_info in workflow_data.items():
workflows[workflow_name] = workflow_info
except Exception as e:
self.stdout.write(self.style.WARNING(f"Error reading workflow file {file}: {e}"))
return workflows
# Extract views
for app, mod in get_all_views_modules():
for name, obj in inspect.getmembers(mod, inspect.isfunction):
@ -52,6 +99,25 @@ class Command(BaseCommand):
"type": "view_function"
}
# Look for @workflow decorator or WORKFLOW tag in docstring
if hasattr(obj, 'workflow_steps') or 'WORKFLOW:' in doc:
workflow_name = name.replace('_', ' ').title()
steps = []
if hasattr(obj, 'workflow_steps'):
steps = obj.workflow_steps
elif 'WORKFLOW:' in doc:
workflow_section = doc.split('WORKFLOW:')[1].strip()
steps_text = workflow_section.split('\n')
steps = [step.strip() for step in steps_text if step.strip()]
if steps:
kb["user_workflows"][workflow_name] = {
"description": f"How to {name.replace('_', ' ')}",
"steps": steps,
"source": f"{app}.views.{name}"
}
# Extract models
for app, name, doc in get_all_model_classes():
if doc:
@ -61,6 +127,52 @@ class Command(BaseCommand):
"type": "model_class"
}
# Extract templates
for app, template_path, doc in get_all_templates():
template_id = f"{app}:{template_path}"
if doc: # Only include templates with documentation
kb["templates"][template_id] = {
"description": doc,
"path": template_path,
"app": app
}
# Add workflow documentation
kb["user_workflows"].update(get_workflow_docs())
# Add manual workflow examples if no workflows were found
if not kb["user_workflows"]:
kb["user_workflows"] = {
"Add New Car": {
"description": "How to add a new car to the inventory",
"steps": [
"Navigate to the Inventory section by clicking 'Inventory' in the main menu",
"Click the 'Add Car' button in the top right corner",
"Enter the VIN number or scan it using the barcode scanner",
"Select the car make from the dropdown menu",
"Select the car series from the available options",
"Select the trim level for the car",
"Fill in additional details like color, mileage, and price",
"Click 'Save' to add the car to inventory, or 'Save & Add Another' to continue adding cars"
],
"source": "manual_documentation"
},
"Create New Invoice": {
"description": "How to create a new invoice",
"steps": [
"Navigate to the Finance section by clicking 'Finance' in the main menu",
"Click the 'Invoices' tab",
"Click the 'Create New Invoice' button",
"Select a customer from the dropdown or click 'Add New Customer'",
"Select the car(s) to include in the invoice",
"Add any additional services or parts by clicking 'Add Item'",
"Set the payment terms and due date",
"Click 'Save Draft' to save without finalizing, or 'Finalize Invoice' to complete"
],
"source": "manual_documentation"
}
}
with open(output_file, "w", encoding="utf-8") as f:
yaml.dump(kb, f, allow_unicode=True, sort_keys=False)

View File

@ -0,0 +1,50 @@
# Generated by Django 5.2.1 on 2025-06-12 16:25
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('inventory', '__first__'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AnalysisCache',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('prompt_hash', models.CharField(db_index=True, max_length=64)),
('dealer_id', models.IntegerField(blank=True, db_index=True, null=True)),
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
('updated_at', models.DateTimeField(auto_now=True)),
('expires_at', models.DateTimeField(db_index=True)),
('result', models.JSONField()),
('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name_plural': 'Analysis caches',
'indexes': [models.Index(fields=['prompt_hash', 'dealer_id'], name='haikalbot_a_prompt__b98e1e_idx'), models.Index(fields=['expires_at'], name='haikalbot_a_expires_e790cd_idx')],
},
),
migrations.CreateModel(
name='ChatLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('user_message', models.TextField()),
('chatbot_response', models.TextField()),
('timestamp', models.DateTimeField(auto_now_add=True, db_index=True)),
('dealer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chatlogs', to='inventory.dealer')),
],
options={
'ordering': ['-timestamp'],
'indexes': [models.Index(fields=['dealer', 'timestamp'], name='haikalbot_c_dealer__6f8d63_idx')],
},
),
]

View File

@ -1,19 +0,0 @@
from langchain.document_loaders import TextLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain.chat_models import ChatOpenAI
from langchain.chains import RetrievalQA
# Load YAML doc
loader = TextLoader("haikal_kb.yaml")
index = VectorstoreIndexCreator().from_loaders([loader])
# Setup QA chain
qa = RetrievalQA.from_chain_type(
llm=ChatOpenAI(model="gpt-3.5-turbo", temperature=0),
retriever=index.vectorstore.as_retriever()
)
# Ask a question
query = "How do I add a new invoice?"
response = qa.run(query)
print("Answer:", response)

View File

@ -0,0 +1,77 @@
from langchain_community.document_loaders import TextLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.prompts import PromptTemplate
# from django.conf import settings
# Load YAML doc
loader = TextLoader("haikal_kb.yaml")
# Create embeddings model
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# Create an instance of VectorstoreIndexCreator with the embeddings
index_creator = VectorstoreIndexCreator(embedding=embeddings)
# Then call the from_loaders method on the instance
index = index_creator.from_loaders([loader])
# Create LLM instance
llm = Ollama(model="qwen3:8b", temperature=0.3)
# Define a custom prompt template for instructional responses
template = """
You are Haikal, an assistant for the car inventory management system.
Your goal is to provide clear step-by-step instructions for users to complete tasks.
Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say you don't know. Don't try to make up an answer.
Context:
{context}
Question: {question}
Provide a clear step-by-step guide with numbered instructions. Include:
1. Where to click in the interface
2. What to enter or select
3. Any buttons to press to complete the action
4. Any alternatives or shortcuts if available
Helpful Step-by-Step Instructions:"""
PROMPT = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
# Setup QA chain
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=index.vectorstore.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt": PROMPT}
)
# Function to run a query
def ask_haikal(query):
response = qa.invoke({"query": query})
print("\n" + "="*50)
print(f"Question: {query}")
print("="*50)
print("\nAnswer:")
print(response["result"])
print("\nSources:")
for doc in response["source_documents"]:
print(f"- {doc.metadata.get('source', 'Unknown source')}")
print("="*50)
return response["result"]
# # Example query
# if __name__ == "__main__":
# query = "How do I add a new car to the inventory? answer in Arabic"
# ask_haikal(query)

View File

@ -4,12 +4,12 @@ from django.shortcuts import render
from django.utils.translation import gettext as _
from django.views import View
import logging
from .ai_agent import analyze_prompt
# from .haikal_agent import DatabaseInsightSystem, analyze_prompt_sync
from .utils.export import export_to_excel, export_to_csv
logger = logging.getLogger(__name__)
# analyze_prompt_ai = DatabaseInsightSystem
class HaikalBot(LoginRequiredMixin, View):
def get(self, request, *args, **kwargs):
@ -33,11 +33,9 @@ class HaikalBot(LoginRequiredMixin, View):
if not prompt:
error_msg = _("Prompt is required.") if language != "ar" else "الاستعلام مطلوب."
return JsonResponse({"status": "error", "error": error_msg}, status=400)
try:
result = analyze_prompt(prompt)
# Handle export requests if data is available
if export and result.get("status") == "success" and result.get("data"):
try:
if export == "excel":

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -6,8 +6,10 @@ alabaster==1.0.0
albucore==0.0.24
albumentations==2.0.7
annotated-types==0.7.0
anthropic==0.52.2
anyio==4.9.0
arabic-reshaper==3.0.0
argcomplete==3.6.2
arrow==1.3.0
asgiref==3.8.1
astor==0.8.1
@ -19,13 +21,17 @@ beautifulsoup4==4.13.4
bleach==6.2.0
blessed==1.21.0
blinker==1.9.0
boto3==1.38.29
botocore==1.38.29
Brotli==1.1.0
cachetools==5.5.2
cattrs==24.1.3
certifi==2025.4.26
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.4.2
click==8.2.1
cohere==5.15.0
colorama==0.4.6
commonmark==0.9.1
contourpy==1.3.2
@ -55,6 +61,7 @@ django-cors-headers==4.7.0
django-countries==7.6.1
django-crispy-forms==2.4
django-debug-toolbar==5.2.0
django-easy-audit==1.3.7
django-extensions==4.1
django-filter==25.1
django-formtools==2.5.1
@ -94,7 +101,11 @@ docutils==0.21.2
easy-thumbnails==2.10
emoji==2.14.1
et_xmlfile==2.0.0
eval_type_backport==0.2.2
executing==2.2.0
Faker==37.3.0
fasta2a==0.2.14
fastavro==1.11.1
filelock==3.18.0
fire==0.7.0
fonttools==4.58.0
@ -102,28 +113,37 @@ fpdf==1.7.2
fpdf2==2.8.3
frozenlist==1.6.0
fsspec==2025.5.1
google-auth==2.40.2
google-genai==1.18.0
googleapis-common-protos==1.70.0
gprof2dot==2025.4.14
graphqlclient==0.2.4
greenlet==3.2.2
griffe==1.7.3
groq==0.26.0
h11==0.16.0
h2==4.2.0
hf-xet==1.1.3
hpack==4.1.0
hstspreload==2025.1.1
httpcore==1.0.9
httpx==0.28.1
httpx-sse==0.4.0
huggingface-hub==0.32.4
hyperframe==6.1.0
icalendar==6.3.1
idna==3.10
imageio==2.37.0
imagesize==1.4.1
imgaug==0.4.0
importlib_metadata==8.7.0
iso4217==1.12.20240625
isodate==0.7.2
isort==6.0.1
itsdangerous==2.2.0
Jinja2==3.1.6
jiter==0.10.0
jmespath==1.0.1
joblib==1.5.1
jsonpatch==1.33
jsonpointer==3.0.0
@ -132,12 +152,15 @@ kiwisolver==1.4.8
langchain==0.3.25
langchain-community==0.3.24
langchain-core==0.3.61
langchain-ollama==0.3.3
langchain-text-splitters==0.3.8
langsmith==0.3.42
lazy_loader==0.4
ledger==1.0.1
libretranslatepy==2.1.4
lmdb==1.6.2
logfire==3.18.0
logfire-api==3.17.0
luhnchecker==0.0.12
lxml==5.4.0
Markdown==3.8
@ -146,7 +169,9 @@ MarkupSafe==3.0.2
marshmallow==3.26.1
matplotlib==3.10.3
mccabe==0.7.0
mcp==1.9.2
mdurl==0.1.2
mistralai==1.8.1
MouseInfo==0.1.3
mpmath==1.3.0
multidict==6.4.4
@ -158,11 +183,19 @@ num2words==0.5.14
numpy==2.2.6
oauthlib==3.2.2
ofxtools==0.9.5
ollama==0.4.8
openai==1.82.0
opencv-contrib-python==4.11.0.86
opencv-python==4.11.0.86
opencv-python-headless==4.11.0.86
openpyxl==3.1.5
opentelemetry-api==1.34.0
opentelemetry-exporter-otlp-proto-common==1.34.0
opentelemetry-exporter-otlp-proto-http==1.34.0
opentelemetry-instrumentation==0.55b0
opentelemetry-proto==1.34.0
opentelemetry-sdk==1.34.0
opentelemetry-semantic-conventions==0.55b0
opt_einsum==3.4.0
orjson==3.10.18
outcome==1.3.0.post0
@ -174,18 +207,25 @@ phonenumbers==8.13.42
pillow==10.4.0
platformdirs==4.3.8
prometheus_client==0.22.0
prompt_toolkit==3.0.51
propcache==0.3.1
protobuf==6.31.0
protobuf==5.29.5
psycopg==3.2.9
psycopg-binary==3.2.9
psycopg-c==3.2.9
psycopg2-binary==2.9.10
py-moneyed==3.0
pyasn1==0.6.1
pyasn1_modules==0.4.2
PyAutoGUI==0.9.54
pyclipper==1.3.0.post6
pycodestyle==2.13.0
pycparser==2.22
pydantic==2.11.5
pydantic-ai==0.2.14
pydantic-ai-slim==0.2.14
pydantic-evals==0.2.14
pydantic-graph==0.2.14
pydantic-settings==2.9.1
pydantic_core==2.33.2
pydotplus==2.0.2
@ -212,6 +252,7 @@ python-bidi==0.6.6
python-dateutil==2.9.0.post0
python-docx==1.1.2
python-dotenv==1.1.0
python-multipart==0.0.20
python-openid==2.2.5
python-slugify==8.0.4
python-stdnum==2.1
@ -234,12 +275,16 @@ requests-oauthlib==2.0.0
requests-toolbelt==1.0.0
rfc3986==2.0.0
rich==14.0.0
rsa==4.9.1
rubicon-objc==0.5.0
s3transfer==0.13.0
sacremoses==0.1.1
safetensors==0.5.3
scikit-image==0.25.2
scikit-learn==1.6.1
scipy==1.15.3
selenium==4.33.0
sentence-transformers==4.1.0
sentencepiece==0.2.0
shapely==2.1.1
simsimd==6.2.1
@ -251,7 +296,9 @@ sortedcontainers==2.4.0
soupsieve==2.7
SQLAlchemy==2.0.41
sqlparse==0.5.3
sse-starlette==2.3.6
stanza==1.10.1
starlette==0.47.0
stringzilla==3.12.5
suds==1.2.0
swapper==1.3.0
@ -264,14 +311,17 @@ threadpoolctl==3.6.0
tifffile==2025.5.24
tinycss2==1.4.0
tinyhtml5==2.0.0
tokenizers==0.21.1
tomli==2.2.1
tomlkit==0.13.2
torch==2.7.0
tqdm==4.67.1
transformers==4.52.4
trio==0.30.0
trio-websocket==0.12.2
twilio==9.6.1
types-python-dateutil==2.9.0.20250516
types-requests==2.32.0.20250602
typing-inspect==0.9.0
typing-inspection==0.4.1
typing_extensions==4.13.2
@ -279,6 +329,7 @@ tzdata==2025.2
Unidecode==1.4.0
upgrade-requirements==1.7.0
urllib3==2.4.0
uvicorn==0.34.3
vin==0.6.2
vininfo==1.8.0
vishap==0.1.5
@ -287,10 +338,13 @@ wcwidth==0.2.13
weasyprint==65.1
webencodings==0.5.1
websocket-client==1.8.0
websockets==15.0.1
Werkzeug==3.1.3
wikipedia==1.4.0
wrapt==1.17.2
wsproto==1.2.0
xmlsec==1.3.15
yarl==1.20.0
zipp==3.22.0
zopfli==0.2.3.post1
zstandard==0.23.0

77
run_haikal_qa.py Normal file
View File

@ -0,0 +1,77 @@
from langchain_community.document_loaders import TextLoader
from langchain.indexes import VectorstoreIndexCreator
from langchain_community.llms import Ollama
from langchain.chains import RetrievalQA
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain.prompts import PromptTemplate
# from django.conf import settings
# Load YAML doc
loader = TextLoader("haikal_kb.yaml")
# Create embeddings model
embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2")
# Create an instance of VectorstoreIndexCreator with the embeddings
index_creator = VectorstoreIndexCreator(embedding=embeddings)
# Then call the from_loaders method on the instance
index = index_creator.from_loaders([loader])
# Create LLM instance
llm = Ollama(model="qwen3:8b", temperature=0.3)
# Define a custom prompt template for instructional responses
template = """
You are Haikal, an assistant for the car inventory management system.
Your goal is to provide clear step-by-step instructions for users to complete tasks.
Use the following pieces of context to answer the question at the end.
If you don't know the answer, just say you don't know. Don't try to make up an answer.
Context:
{context}
Question: {question}
Provide a clear step-by-step guide with numbered instructions. Include:
1. Where to click in the interface
2. What to enter or select
3. Any buttons to press to complete the action
4. Any alternatives or shortcuts if available
Helpful Step-by-Step Instructions:"""
PROMPT = PromptTemplate(
template=template,
input_variables=["context", "question"]
)
# Setup QA chain
qa = RetrievalQA.from_chain_type(
llm=llm,
chain_type="stuff",
retriever=index.vectorstore.as_retriever(),
return_source_documents=True,
chain_type_kwargs={"prompt": PROMPT}
)
# Function to run a query
def ask_haikal(query):
response = qa.invoke({"query": query})
print("\n" + "="*50)
print(f"Question: {query}")
print("="*50)
print("\nAnswer:")
print(response["result"])
print("\nSources:")
for doc in response["source_documents"]:
print(f"- {doc.metadata.get('source', 'Unknown source')}")
print("="*50)
return response["result"]
# Example query
if __name__ == "__main__":
query = "How do I add a new car to the inventory? answer in Arabic"
ask_haikal(query)

159
sql_agent.py Normal file
View File

@ -0,0 +1,159 @@
import asyncio
import sqlite3
import json
from typing import List, Dict
from pydantic import BaseModel, Field
from pydantic_ai import Agent, RunContext
from pydantic_ai.models.openai import OpenAIModel
from pydantic_ai.providers.openai import OpenAIProvider
import os
import logfire
logfire.configure(send_to_logfire='if-token-present')
logfire.instrument_pydantic_ai()
# Define the OpenAI model (replace with your actual model if needed)
model = OpenAIModel(
model_name="qwen2.5:14b", # Or your preferred model
provider=OpenAIProvider(base_url='http://localhost:11434/v1') # Or your provider
)
class DatabaseSchema(BaseModel):
tables: Dict[str, List[Dict[str, str]]] = Field(
description="A dictionary where keys are table names and values are lists of column dictionaries (name, type)")
# Agent to get the database schema
schema_agent = Agent(
model,
deps_type=str,
output_type=str,
system_prompt="""You are a helpful assistant that extracts the schema of a SQLite database.
When the user provides a database path, use the <tool>get_database_schema</tool> to retrieve the schema.
Your ONLY response should be the raw JSON string representing the database schema. Do not include any other text.
The JSON should be a dictionary where keys are table names, and values are lists of column dictionaries.
Each column dictionary should include 'name', 'type', 'notnull', 'dflt_value', and 'pk' keys.
If there is an error, return a JSON string containing an "error" key with a list of error messages."""
)
@schema_agent.tool
async def get_database_schema(ctx: RunContext[str], db_path: str) -> str:
"""Retrieves the schema of the SQLite database and returns it as a JSON string."""
print(f"Database path: {db_path}")
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table';")
tables = [row[0] for row in cursor.fetchall()]
print(tables)
schema = {}
for table in tables:
cursor.execute(f"PRAGMA table_info({table})")
columns = [
{
"name": col[1],
"type": col[2],
"notnull": col[3],
"dflt_value": col[4],
"pk": col[5],
}
for col in cursor.fetchall()
]
schema[table] = columns
print(schema)
conn.close()
return json.dumps(schema)
except sqlite3.Error as e:
error_json = json.dumps({"error": [str(e)]})
return error_json
except Exception as e:
error_json = json.dumps({"error": [str(e)]})
return error_json
# Agent to generate and execute SQL queries
sql_agent = Agent(
model,
deps_type=DatabaseSchema,
output_type=str,
system_prompt="""You are a highly precise SQL query generator for a SQLite database.
You are given the EXACT database schema, which is a dictionary where keys are table names and values are lists of column dictionaries (with 'name' and 'type').
Your ABSOLUTE priority is to generate SQL queries that ONLY use the table and column names exactly as they appear in this schema to answer the user's question.
Follow these strict steps:
1. **Analyze User Question:** Understand the user's request.
2. **Match Schema EXACTLY:** Identify the specific table(s) and column(s) in the provided schema whose names EXACTLY match the entities and information requested in the user's question.
3. **Generate STRICT SQL:** Construct a valid SQL query that selects the identified column(s) from the identified table(s). You MUST use the exact names from the schema. Do not use aliases or make any assumptions about naming conventions. Aim for the simplest possible query.
4. **Execute Query:** Use the <tool>execute_sql_query</tool> to run your generated SQL.
5. **Return interactive Answer as if you are a sports person:** Provide a direct and simple answer to the user's question based on the query results.
6. **No Results:** If the query returns empty list, respond with: 'No matching entries found.'
7. **Error Handling:** If there's any error in generating or executing the SQL, return a JSON string with an "error" key and a list of error messages.
"""
)
# Example:
# Schema: {'Country': [{'name': 'id', 'type': 'INTEGER'}, {'name': 'name', 'type': 'TEXT'}]}
# User Question: "What are the country names?"
# Generated SQL: SELECT name FROM Country;
# Expected Answer: The countries are Belgium, England, France, ...
@sql_agent.tool
async def execute_sql_query(ctx: RunContext[DatabaseSchema], query: str) -> str:
"""Executes the SQL query and returns a simple string answer."""
db_path = os.path.join(os.getcwd(), 'db.sqlite3')
print(query)
try:
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
cursor.execute(query)
results = cursor.fetchall()
columns = [description[0] for description in cursor.description]
rows = [dict(zip(columns, row)) for row in results]
conn.close()
print(rows)
return rows
except Exception as e:
print(e)
async def main():
db_path = os.path.join(os.getcwd(), 'db.sqlite3')
print(f"Database path: {db_path}")
user_question = "how many cars do we have in the inventory"
# 1. Get the database schema
schema_result = await schema_agent.run(db_path)
print("Schema Agent Response:", schema_result)
print("Schema Agent Output:", schema_result.output)
if "error" in schema_result.output:
print(f"Error getting schema: {schema_result.output}")
return
try:
schema_data = json.loads(schema_result.output)
database_schema = DatabaseSchema(tables=schema_data)
print("Parsed Database Schema:", database_schema)
# 2. Use the schema to answer the user question
sql_response = await sql_agent.run(user_question, database_schema=database_schema.tables)
print("SQL Agent Response:", sql_response)
print("SQL Agent Output:", sql_response.output)
if "error" in sql_response.output:
print(f"Error executing SQL: {sql_response.output}")
except json.JSONDecodeError:
print(f"Error: Could not parse schema agent response as JSON: {schema_result.output}")
if __name__ == "__main__":
asyncio.run(main())

View File

@ -0,0 +1,61 @@
{
"name": "Add New Car",
"description": "How to add a new car to the inventory",
"steps": [
{
"title": "Step 1",
"intro": "Navigate to the Inventory section by clicking 'Inventory' in the main menu",
"position": "bottom",
"element": "#inventory-nav",
"click": "#inventory-nav"
},
{
"title": "Step 2",
"intro": "Click the 'Inventory' button in the top right corner",
"position": "bottom",
"element": ".parent-wrapper label-1"
},
{
"title": "Step 3",
"intro": "Click the 'Add Car' button in the top right corner",
"position": "bottom",
"element": "#btn-add-car"
},
{
"title": "Step 4",
"intro": "Enter the VIN number or scan it using the barcode scanner",
"position": "bottom",
"element": "#nv-inventory"
},
{
"title": "Step 5",
"intro": "Select the car make from the dropdown menu",
"position": "bottom",
"element": "#make-select, select[name='make'], .make-field"
},
{
"title": "Step 6",
"intro": "Select the car series from the available options",
"position": "bottom",
"element": "#series-select, select[name='series'], .series-field"
},
{
"title": "Step 7",
"intro": "Select the trim level for the car",
"position": "bottom",
"element": "#trim-select, select[name='trim'], .trim-field"
},
{
"title": "Step 8",
"intro": "Fill in additional details like color, mileage, and price",
"position": "bottom",
"element": "#price-input, input[name='price'], .price-field"
},
{
"title": "Step 9",
"intro": "Click 'Save' to add the car to inventory, or 'Save & Add Another' to continue adding cars",
"position": "bottom",
"element": "#inventory-menu, .inventory-nav, nav .inventory"
}
]
}

View File

@ -0,0 +1,52 @@
{
"name": "Create New Invoice",
"description": "How to create a new invoice",
"steps": [
{
"title": "Step 1",
"intro": "Navigate to the Finance section by clicking 'Finance' in the main menu",
"position": "bottom",
"element": "#finance-menu, .finance-nav, nav .finance"
},
{
"title": "Step 2",
"intro": "Click the 'Invoices' tab",
"position": "bottom",
"element": "#invoice-section, .invoice-tab, #create-invoice"
},
{
"title": "Step 3",
"intro": "Click the 'Create New Invoice' button",
"position": "bottom",
"element": "#invoice-section, .invoice-tab, #create-invoice"
},
{
"title": "Step 4",
"intro": "Select a customer from the dropdown or click 'Add New Customer'",
"position": "bottom",
"element": "#customer-select, select[name='customer'], .customer-field"
},
{
"title": "Step 5",
"intro": "Select the car(s) to include in the invoice",
"position": "bottom",
"element": "#invoice-section, .invoice-tab, #create-invoice"
},
{
"title": "Step 6",
"intro": "Add any additional services or parts by clicking 'Add Item'",
"position": "bottom"
},
{
"title": "Step 7",
"intro": "Set the payment terms and due date",
"position": "bottom"
},
{
"title": "Step 8",
"intro": "Click 'Save Draft' to save without finalizing, or 'Finalize Invoice' to complete",
"position": "bottom",
"element": "button[type='submit'], .btn-save, #save-button"
}
]
}

View File

@ -0,0 +1,163 @@
/**
* Help Button Component
* Provides context-aware help based on the current page
*/
class HelpButton {
constructor(options = {}) {
this.options = Object.assign({
position: 'bottom-right',
icon: 'question-circle',
text: 'Help',
autoDetect: true
}, options);
this.pageToTourMap = {
'/inventory/': 'inventory_overview',
'/inventory/add/': 'add_new_car',
'/inventory/edit/': 'edit_car',
'/finance/invoices/': 'manage_invoices',
'/finance/invoices/create/': 'create_new_invoice',
'/customers/': 'manage_customers',
'/customers/add/': 'add_new_customer'
};
this.render();
this.attachEvents();
}
render() {
// Create the help button
const button = document.createElement('div');
button.className = `help-button ${this.options.position}`;
button.innerHTML = `
<button class="btn btn-phoenix-primary" id="context-help-btn"
data-bs-toggle="tooltip" title="Get help for this page">
<i class="bi bi-${this.options.icon}"></i>
HELP
</button>
`;
// Add styles
const style = document.createElement('style');
style.textContent = `
.help-button {
position: fixed;
z-index: 1000;
}
.help-button.bottom-right {
bottom: 20px;
right: 20px;
}
.help-button.bottom-left {
bottom: 20px;
left: 20px;
}
.help-button.top-right {
top: 20px;
right: 20px;
}
.help-button.top-left {
top: 20px;
left: 20px;
}
`;
document.head.appendChild(style);
document.body.appendChild(button);
}
attachEvents() {
const helpButton = document.getElementById('context-help-btn');
if (!helpButton) return;
helpButton.addEventListener('click', () => {
this.showContextHelp();
});
// Initialize tooltip
new bootstrap.Tooltip(helpButton);
}
showContextHelp() {
// Detect current page and show appropriate tour
if (this.options.autoDetect) {
const currentPath = window.location.pathname;
let tourSlug = null;
// Find the best match for the current path
for (const [path, slug] of Object.entries(this.pageToTourMap)) {
if (currentPath.includes(path)) {
tourSlug = slug;
break;
}
}
if (tourSlug) {
window.tourManager.loadTour(tourSlug);
return;
}
}
// If no specific tour found or autoDetect is off, show help menu
this.showHelpMenu();
}
showHelpMenu() {
// Create a modal with available help options
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'helpModal';
modal.setAttribute('tabindex', '-1');
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Help & Guides</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="list-group">
<a href="#" class="list-group-item list-group-item-action"
onclick="window.tourManager.loadTour('add_new_car');">
<i class="bi bi-plus-circle me-2"></i> How to Add a New Car
</a>
<a href="#" class="list-group-item list-group-item-action"
onclick="window.tourManager.loadTour('create_new_invoice');">
<i class="bi bi-file-text me-2"></i> How to Create an Invoice
</a>
<a href="#" class="list-group-item list-group-item-action"
onclick="window.tourManager.loadTour('manage_customers');">
<i class="bi bi-people me-2"></i> How to Manage Customers
</a>
<div class="dropdown-divider"></div>
<a href="/tours/" class="list-group-item list-group-item-action">
<i class="bi bi-collection me-2"></i> View All Guides
</a>
<a href="/support/" class="list-group-item list-group-item-action">
<i class="bi bi-headset me-2"></i> Contact Support
</a>
</div>
</div>
</div>
</div>
`;
document.body.appendChild(modal);
const modalInstance = new bootstrap.Modal(modal);
modalInstance.show();
// Remove modal from DOM after it's hidden
modal.addEventListener('hidden.bs.modal', () => {
modal.remove();
});
}
}
// Initialize help button on all pages
document.addEventListener('DOMContentLoaded', () => {
window.helpButton = new HelpButton();
});
// export { HelpButton };

View File

@ -0,0 +1,134 @@
/**
* Tour Manager for Car Inventory System
* Uses IntroJS to provide guided tours of the application
*/
function getCsrfToken(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== "") {
const cookies = document.cookie.split(";");
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.substring(0, name.length + 1) === name + "=") {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
class TourManager {
constructor() {
this.introJs = introJs();
this.currentTour = null;
this.tourData = null;
this.tourSlug = null;
// Configure IntroJS defaults
this.introJs.setOptions({
showStepNumbers: true,
showBullets: true,
showProgress: true,
scrollToElement: true,
disableInteraction: false,
doneLabel: 'Finish',
nextLabel: 'Next →',
prevLabel: '← Back',
exitOnEsc: true,
exitOnOverlayClick: false
});
// Set up event listeners
this.introJs.oncomplete(() => this.onTourComplete());
this.introJs.onexit(() => this.onTourExit());
}
/**
* Load and start a tour by its slug
* @param {string} slug - The tour slug
*/
async loadTour(slug) {
try {
this.tourSlug = slug;
const response = await fetch(`/tours/data/${slug}/`);
if (!response.ok) {
throw new Error('Failed to load tour data');
}
const data = await response.json();
this.tourData = data.tour;
// If user already completed this tour, ask if they want to repeat
if (data.completed && !confirm('You have already completed this guide. Would you like to view it again?')) {
return;
}
this.startTour();
} catch (error) {
console.error('Error loading tour:', error);
alert('Failed to load the interactive guide. Please try again later.');
}
}
/**
* Start the currently loaded tour
*/
startTour() {
if (!this.tourData) {
console.error('No tour data loaded');
return;
}
this.introJs.setOptions({
steps: this.tourData.steps
});
this.introJs.start();
}
/**
* Handle tour completion
*/
onTourComplete() {
if (!this.tourSlug) return;
// Mark the tour as completed on the server
fetch(`/tours/complete/${this.tourSlug}/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
}
}).catch(error => {
console.error('Error marking tour as completed:', error);
});
// Show success message
alert('Congratulations! You have completed the guide.');
}
/**
* Handle tour exit (without completion)
*/
onTourExit() {
console.log('Tour exited');
}
/**
* Get CSRF token from cookies
*/
}
// Initialize the tour manager
window.tourManager = new TourManager();
// Function to start a tour from a link
function startTour(slug) {
window.tourManager.loadTour(slug);
return false; // Prevent default link action
}
// Export for use in other modules
// export { startTour };

File diff suppressed because it is too large Load Diff

View File

@ -68,7 +68,7 @@
<h2 class="text-body-secondary fw-bolder mb-3">Access Forbidden!</h2>
<p class="text-body mb-5">
Halt! Thou art endeavouring to trespass upon a realm not granted unto thee.<br class="d-none d-md-block d-lg-none" />granted unto thee.
</p><a class="btn btn-lg btn-primary" href="{% url 'home' %}">Go Home</a>
</p><a class="btn btn-lg btn-phoenix-primary" href="{% url 'home' %}">Go Home</a>
</div>
</div>
</div>

View File

@ -66,7 +66,7 @@
<div class="col-12 col-lg-6 text-center text-lg-start">
<img class="img-fluid mb-6 w-50 w-lg-75 d-dark-none" src="{% static 'images/spot-illustrations/500.png' %}" alt="" />
<h2 class="text-body-secondary fw-bolder mb-3">Page Missing!</h2>
<p class="text-body mb-5">But no worries! Our ostrich is looking everywhere <br class="d-none d-sm-block" />while you wait safely. </p><a class="btn btn-lg btn-primary" href="{% url 'home' %}">Go Home</a>
<p class="text-body mb-5">But no worries! Our ostrich is looking everywhere <br class="d-none d-sm-block" />while you wait safely. </p><a class="btn btn-lg btn-phoenix-primary" href="{% url 'home' %}">Go Home</a>
</div>
</div>
</div>

View File

@ -66,7 +66,7 @@
<div class="col-12 col-lg-6 text-center text-lg-start">
<img class="img-fluid mb-6 w-50 w-lg-75 d-dark-none" src="{% static 'images/spot-illustrations/404.png' %}" alt="" />
<h2 class="text-body-secondary fw-bolder mb-3">Unknow error!</h2>
<p class="text-body mb-5">But relax! Our cat is here to play you some music.</p><a class="btn btn-lg btn-primary" href="{% url 'home' %}">Go Home</a>
<p class="text-body mb-5">But relax! Our cat is here to play you some music.</p><a class="btn btn-lg btn-phoenix-primary" href="{% url 'home' %}">Go Home</a>
</div>
</div>
</div>

View File

@ -32,7 +32,7 @@
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
<button type="submit" class="btn btn-primary btn-sm w-100">{% trans "Sign In" %}</button>
<button type="submit" class="btn btn-phoenix-primary btn-sm w-100">{% trans "Sign In" %}</button>
</form>
{% element button type="submit" form="logout-from-stage" tags="link" %}
{% translate "Cancel" %}

View File

@ -46,9 +46,9 @@
{% endfor %}
</div>
<div class="mt-2 mb-6">
<button type="submit" name="action_primary" class="btn btn-sm btn-primary">{% trans 'Make Primary' %}</button>
<button type="submit" name="action_send" class="btn btn-sm btn-secondary">{% trans 'Re-send Verification' %}</button>
<button type="submit" name="action_remove" class="btn btn-sm btn-danger delete">{% trans 'Remove' %}</button>
<button type="submit" name="action_primary" class="btn btn-sm btn-phoenix-primary">{% trans 'Make Primary' %}</button>
<button type="submit" name="action_send" class="btn btn-sm btn-phoenix-secondary">{% trans 'Re-send Verification' %}</button>
<button type="submit" name="action_remove" class="btn btn-sm btn-phoenix-danger delete">{% trans 'Remove' %}</button>
</div>
@ -65,7 +65,7 @@
<form action="{{ action_url }}" method="POST" class="form email add">
{% csrf_token %}
{{ form|crispy }}
<button class="btn btn-sn btn-success w-100" type="submit" name="action_add">
<button class="btn btn-sm btn-phoenix-success w-100" type="submit" name="action_add">
{% trans "Add Email" %}
</button>
</form>

View File

@ -74,7 +74,7 @@
<div class="position-relative" data-password="data-password">
<input class="form-control mb-3" id="password" type="password" placeholder="Enter Password" data-password-input="data-password-input" />
<button class="btn px-3 py-0 h-100 position-absolute top-0 end-0 fs-7 text-body-tertiary" data-password-toggle="data-password-toggle"><span class="uil uil-eye show"></span><span class="uil uil-eye-slash hide"></span></button>
</div><a class="btn btn-primary w-100" href="../../../index.html">Sign In</a>
</div><a class="btn btn-phoenix-primary w-100" href="../../../index.html">Sign In</a>
</div>
</div>
</div>

View File

@ -43,7 +43,7 @@
<input type="checkbox" name="remember" id="id_remember" class="form-check-input">
<label class="form-check-label mb-0 fs-9" for="id_remember">{{ _("Remember Me")}}</label>
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">{% trans "Sign In" %}</button>
<button type="submit" class="btn btn-phoenix-primary btn-sm w-100">{% trans "Sign In" %}</button>
<div class="text-start mt-1">
<a class="fs-9" href="{% url 'account_reset_password' %}">{{ _("Forgot Password?")}}</a>
</div>

View File

@ -16,7 +16,7 @@
{% csrf_token %}
{{ redirect_field }}
<div class="d-grid gap-2 mt-3">
<button type="submit" class="btn btn-danger">
<button type="submit" class="btn btn-phoenix-danger">
<span data-feather="log-out"></span> {{ _("Sign Out") }}
</button>
</div>

View File

@ -25,9 +25,9 @@
<div class="d-flex align-items-center gap-2 mb-3">
<input class="form-control px-2 text-center" type="number" name="otp_code" required maxlength="6" />
</div>
<Button class="btn btn-primary w-100 mb-5" type="submit">
<button class="btn btn-phoenix-primary w-100 mb-5" type="submit">
{{ _("Verify") }}
</Button>
</button>
<a class="fs-9" href="">{{ _("Didnt receive the code") }}</a>
</form>
</div>

View File

@ -23,7 +23,7 @@
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
<button type="submit" class="btn btn-primary btn-sm w-100">{% trans "Change Password" %}</button>
<button type="submit" class="btn btn-phoenix-primary btn-sm w-100">{% trans "Change Password" %}</button>
<div class="text-start mt-1">
<a class="fs-9" href="{% url 'account_reset_password' %}">{{ _("Forgot Password?")}}</a>
</div>

View File

@ -30,7 +30,7 @@
<form method="post" action="{% url 'account_reset_password' %}" class="form needs-validation" novalidate>
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary btn-sm w-100">{% trans 'Reset My Password' %}</button>
<button type="submit" class="btn btn-phoenix-primary btn-sm w-100">{% trans 'Reset My Password' %}</button>
</form>
<p class="fs-9 mt-4">
{% blocktrans %}Please contact us if you have any trouble resetting your password.{% endblocktrans %}

View File

@ -37,7 +37,7 @@
{% csrf_token %}
{{ redirect_field }}
{{ form|crispy }}
<button type="submit" class="btn btn-primary btn-sm w-100">{% trans 'Change Password' %}</button>
<button type="submit" class="btn btn-phoenix-primary btn-sm w-100">{% trans 'Change Password' %}</button>
</form>
{% endif %}
</div>

View File

@ -76,7 +76,7 @@
<input class="form-check-input" id="termsService" type="checkbox" />
<label class="form-label fs-9 text-transform-none" for="termsService">I accept the <a href="">terms </a>and <a href="">privacy policy</a></label>
</div>
<button type="submit" class="btn btn-primary w-100 mb-3">{{ _("Sign Up") }}</button>
<button type="submit" class="btn btn-phoenix-primary w-100 mb-3">{{ _("Sign Up") }}</button>
<div class="text-center">{% trans 'Already have an account?' %}<a class="fw-bold" href="{% url 'account_login' %}"> {{ _("Sign In") }}</a></div>
</form>
{% endif %}

View File

@ -42,7 +42,7 @@
<div class="text-start mb-6">
<div>
<button type="submit" class="btn btn-primary">Update</button>
<button type="submit" class="btn btn-phoenix-primary">Update</button>
</div>
</div>
</div>

View File

@ -8,8 +8,8 @@
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-center">
<button class="btn btn-primary mx-2" type="submit">Activate</button>
<a class="btn btn-secondary mx-2" href="{% url 'user_management' %}">Cancel</a>
<button class="btn btn-phoenix-primary mx-2" type="submit">Activate</button>
<a class="btn btn-phoenix-danger mx-2" href="{% url 'user_management' %}">Cancel</a>
</div>
</form>
</div>

View File

@ -8,8 +8,8 @@
<form method="post">
{% csrf_token %}
<div class="d-flex justify-content-center">
<button class="btn btn-danger mx-2" type="submit"><i class="fas fa-trash me-2"></i> Delete Permenantly</button>
<a class="btn btn-secondary mx-2" href="{% url 'user_management' %}"><i class="fas fa-ban me-2"></i>Cancel</a>
<button class="btn btn-phoenix-danger mx-2" type="submit"><i class="fas fa-trash me-2"></i> Delete Permenantly</button>
<a class="btn btn-phoenix-secondary mx-2" href="{% url 'user_management' %}"><i class="fas fa-ban me-2"></i>Cancel</a>
</div>
</form>
</div>

View File

@ -81,7 +81,8 @@
</div>
<div class="col-md-6">
<div class="d-flex align-items-center">
<i class="fas fa-dollar-sign me-2 text-primary"></i>
<span class="icon-saudi_riyal text-primary"></span>
<strong class="me-2">{% trans 'Service price' %}:</strong> {{ appointment.get_appointment_amount_to_pay_text }}
</div>
</div>

View File

@ -22,7 +22,7 @@
<label>{% trans 'Code' %}:
<input type="text" name="code" placeholder="X1Y2Z3" required>
</label>
<button class="btn btn-primary" type="submit">{% trans 'Submit' %}</button>
<button class="btn btn-phoenix-primary" type="submit">{% trans 'Submit' %}</button>
</form>
</div>
</div>

View File

@ -66,7 +66,7 @@
</div>
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
<button type="submit" class="btn btn-phoenix-primary">{% trans "Submit" %}</button>
</form>
<div class="row-form-errors" style="margin: 10px 0">
{% if days_off_form.errors %}

View File

@ -67,7 +67,7 @@
{{ form.work_on_sunday.label_tag }}
</div>
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
<button type="submit" class="btn btn-phoenix-primary">{% trans 'Save' %}</button>
</form>
<div class="messages" style="margin: 20px 0">
{% if messages %}

View File

@ -97,12 +97,12 @@
<div>${{ ar.get_service_price }}</div>
</div>
<div class="payment-options">
<button type="submit" class="btn btn-dark btn-pay-full" name="payment_type"
<button type="submit" class="btn btn-phoenix-primary btn-pay-full" name="payment_type"
value="full">
{% trans "Pay" %}
</button>
{% if ar.accepts_down_payment %}
<button type="submit" class="btn btn-dark btn-pay-down-payment"
<button type="submit" class="btn btn-phoenix-primary btn-pay-down-payment"
name="payment_type"
value="down">
{% trans "Down Payment" %} (${{ ar.get_service_down_payment }})
@ -111,13 +111,13 @@
</div>
</div>
{% else %}
<button type="submit" class="btn btn-dark btn-submit-appointment" name="payment_type"
<button type="submit" class="btn btn-phoenix-primary btn-submit-appointment" name="payment_type"
value="full">
{% trans "Finish" %}
</button>
{% endif %}
{% else %}
<button type="submit" class="btn btn-dark btn-submit-appointment" name="payment_type"
<button type="submit" class="btn btn-phoenix-primary btn-submit-appointment" name="payment_type"
value="full">
{% trans "Finish" %}
</button>

View File

@ -40,6 +40,7 @@
<link href="{% static 'vendors/flatpickr/flatpickr.min.css' %}" rel="stylesheet">
<link href="{% static 'css/custom.css' %}" rel="stylesheet">
<link rel="stylesheet" href="https://unicons.iconscout.com/release/v4.0.8/css/line.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/7.2.0/introjs.css" integrity="sha512-4OzqLjfh1aJa7M33b5+h0CSx0Q3i9Qaxlrr1T/Z+Vz+9zs5A7GM3T3MFKXoreghi3iDOSbkPMXiMBhFO7UBW/g==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{% if LANGUAGE_CODE == 'ar' %}
<link href="{% static 'css/theme-rtl.min.css' %}" type="text/css" rel="stylesheet" id="style-rtl">
<link href="{% static 'css/user-rtl.min.css' %}" type="text/css" rel="stylesheet" id="user-style-rtl">
@ -76,6 +77,7 @@
{% endblock period_navigation %}
{% block content %}
{% endblock content%}
{% block body %}
{% endblock body%}
@ -96,6 +98,11 @@
<script src="{% static 'vendors/anchorjs/anchor.min.js' %}"></script>
<script src="{% static 'vendors/is/is.min.js' %}"></script>
<script src="{% static 'vendors/fontawesome/all.min.js' %}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/intro.js/7.2.0/intro.js" integrity="sha512-f26fxKZJiF0AjutUaQHNJ5KnXSisqyUQ3oyfaoen2apB1wLa5ccW3lmtaRe2jdP5kh4LF2gAHP9xQbx7wYhU5w==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Tour Manager -->
<script src="{% static 'js/tours/tour-manager.js' %}"></script>
<script src="{% static 'js/tours/help-button.js' %}"></script>
<script src="{% static 'vendors/lodash/lodash.min.js' %}"></script>
<script src="{% static 'vendors/list.js/list.min.js' %}"></script>
<script src="{% static 'vendors/feather-icons/feather.min.js' %}"></script>
@ -115,6 +122,7 @@
<script src="{% static 'vendors/flatpickr/flatpickr.min.js' %}"></script>
<script>
{% if entity_slug %}
let entitySlug = "{{ view.kwargs.entity_slug }}"
{% endif %}

View File

@ -36,11 +36,11 @@
<div class="d-grid gap-2">
<button type="submit"
id="djl-bill-create-button"
class="btn btn-primary btn-lg">{% trans 'Create' %}
class="btn btn-phoenix-primary btn-lg">{% trans 'Create' %}
</button>
<a href="{{request.META.HTTP_REFERER}}"
id="djl-bill-create-back-button"
class="btn btn-outline-secondary">{% trans 'Cancel' %}</a>
class="btn btn-phoenix-secondary">{% trans 'Cancel' %}</a>
</div>
</div>
</form>

View File

@ -24,23 +24,29 @@
.text-xxs {
font-size: 0.6rem;
}
#djl-vendor-card-widget{
height:30rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row g-4">
<div class="row g-4" >
<!-- Left Sidebar -->
<div class="col-lg-4">
<div class="card h-100 shadow-sm">
<div class="card shadow-sm">
<div class="card-body">
{% include 'bill/includes/card_bill.html' with bill=bill entity_slug=view.kwargs.entity_slug style='bill-detail' %}
<hr class="my-4">
{% include 'django_ledger/vendor/includes/card_vendor.html' with vendor=bill.vendor %}
<div class="d-grid mt-4">
<a href="{% url 'django_ledger:bill-list' entity_slug=view.kwargs.entity_slug %}"
class="btn btn-outline-primary">
class="btn btn-phoenix-primary">
<i class="fas fa-arrow-left me-1"></i> {% trans 'Bill List' %}
</a>
</div>
@ -49,7 +55,7 @@
</div>
<!-- Main Content -->
<div class="col-lg-8">
<div class="col-lg-8 ">
{% if bill.is_configured %}
<div class="card mb-4 shadow-sm">
<div class="card-body">
@ -127,7 +133,7 @@
<!-- Bill Items Card -->
<div class="card mb-4 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<div class="d-flex align-items-center mb-1">
<i class="fas fa-receipt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Items' %}</h5>
</div>
@ -135,49 +141,49 @@
<div class="card-body px-0 pt-0 pb-2">
<div class="table-responsive">
<table class="table table-hover align-items-center mb-0">
<thead class="table-light">
<thead >
<tr>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">{% trans 'Item' %}</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7">{% trans 'Entity Unit' %}</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 text-end">{% trans 'Unit Cost' %}</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 text-end">{% trans 'Quantity' %}</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 text-end">{% trans 'Total' %}</th>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 text-end">{% trans 'PO' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Item' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Entity Unit' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Unit Cost' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Quantity' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'Total' %}</th>
<th class="sort white-space-nowrap align-middle" scope="col">{% trans 'PO' %}</th>
</tr>
</thead>
<tbody>
{% for bill_item in itemtxs_qs %}
<tr>
<td>
<td class="align-middle white-space-nowrap">
<div class="d-flex px-2 py-1">
<div class="d-flex flex-column justify-content-center">
<h6 class="mb-0 text-sm">{{ bill_item.item_model }}</h6>
</div>
</div>
</td>
<td>
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">
{% if bill_item.entity_unit %}
{{ bill_item.entity_unit }}
{% endif %}
</span>
</td>
<td class="text-end">
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">
{% currency_symbol %}{{ bill_item.unit_cost | currency_format }}
</span>
</td>
<td class="text-end">
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">{{ bill_item.quantity }}</span>
</td>
<td class="text-end">
<td class="align-middle white-space-nowrap">
<span class="text-xs font-weight-bold">
{% currency_symbol %}{{ bill_item.total_amount | currency_format }}
</span>
</td>
<td class="text-end">
<td class="align-middle white-space-nowrap">
{% if bill_item.po_model_id %}
<a class="btn btn-sm btn-outline-info"
<a class="btn btn-sm btn-phoenix-primary"
href="{% url 'purchase_order_detail' bill_item.po_model_id %}">
{% trans 'View PO' %}
</a>
@ -209,15 +215,15 @@
<div class="col-12">
<div class="d-flex justify-content-center gap-2 flex-wrap">
<a href="{% url 'django_ledger:ledger-bs' entity_slug=view.kwargs.entity_slug ledger_pk=bill.ledger_id %}"
class="btn btn-outline-info">
class="btn btn-phoenix-info">
{% trans 'Balance Sheet' %}
</a>
<a href="{% url 'django_ledger:ledger-ic' entity_slug=view.kwargs.entity_slug ledger_pk=bill.ledger_id %}"
class="btn btn-outline-info">
class="btn btn-phoenix-info">
{% trans 'Income Statement' %}
</a>
<a href="{% url 'django_ledger:ledger-cf' entity_slug=view.kwargs.entity_slug ledger_pk=bill.ledger_id %}"
class="btn btn-outline-info">
class="btn btn-phoenix-info">
{% trans 'Cash Flow Statement' %}
</a>
</div>
@ -227,15 +233,15 @@
<div class="col-12">
<div class="d-flex justify-content-center gap-2 flex-wrap">
<a href="{% url 'django_ledger:ledger-bs-year' entity_slug=view.kwargs.entity_slug ledger_pk=bill.ledger_id year=bill.get_status_action_date.year %}?format=pdf&report_subtitle={{ bill.generate_descriptive_title | safe }}"
class="btn btn-outline-success">
class="btn btn-phoenix-success">
{% trans 'Balance Sheet PDF' %} <i class="fas fa-download ms-1"></i>
</a>
<a href="{% url 'django_ledger:ledger-ic-year' entity_slug=view.kwargs.entity_slug ledger_pk=bill.ledger_id year=bill.get_status_action_date.year %}?format=pdf&report_subtitle={{ bill.generate_descriptive_title | safe }}"
class="btn btn-outline-success">
class="btn btn-phoenix-success">
{% trans 'Income Statement PDF' %} <i class="fas fa-download ms-1"></i>
</a>
<a href="{% url 'django_ledger:ledger-cf-year' entity_slug=view.kwargs.entity_slug ledger_pk=bill.ledger_id year=bill.get_status_action_date.year %}?format=pdf&report_subtitle={{ bill.generate_descriptive_title | safe }}"
class="btn btn-outline-success">
class="btn btn-phoenix-success">
{% trans 'Cash Flow Statement PDF' %} <i class="fas fa-download ms-1"></i>
</a>
</div>
@ -246,7 +252,7 @@
<!-- Bill Transactions Card -->
<div class="card mb-4 shadow-sm">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<div class="d-flex align-items-center mb-1">
<i class="fas fa-exchange-alt me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Transactions' %}</h5>
</div>
@ -257,9 +263,9 @@
</div>
<!-- Bill Notes Card -->
<div class="card shadow-sm">
<div class="card shadow-sm ">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<div class="d-flex align-items-center mb-1">
<i class="fas fa-sticky-note me-3 text-primary"></i>
<h5 class="mb-0">{% trans 'Bill Notes' %}</h5>
</div>

View File

@ -26,17 +26,17 @@
{{ form|crispy }}
</div>
<button type="submit" class="btn btn-primary w-100 mb-2">
<button type="submit" class="btn btn-phoenix-primary w-100 mb-2">
<i class="fas fa-save me-2"></i>{% trans 'Save Bill' %}
</button>
<a href="{% url 'bill-detail' entity_slug=view.kwargs.entity_slug bill_pk=bill_model.uuid %}"
class="btn btn-dark w-100 mb-2">
class="btn btn-phoenix-secondary w-100 mb-2">
<i class="fas fa-arrow-left me-2"></i>{% trans 'Back to Bill Detail' %}
</a>
<a href="{% url 'bill_list' %}"
class="btn btn-info w-100 mb-2">
class="btn btn-phoenix-info w-100 mb-2">
<i class="fas fa-list me-2"></i>{% trans 'Bill List' %}
</a>

View File

@ -1,11 +1,11 @@
{% load django_ledger %}
{% load i18n %}
<div id="djl-bill-card-widget">
<div id="djl-bill-card-widget" class="">
{% if not create_bill %}
{% if style == 'dashboard' %}
<!-- Dashboard Style Card -->
<div class="">
<div class="card-body">
<div class="card-body ">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="text-uppercase text-secondary mb-0">
<i class="fas fa-file-invoice me-2"></i>{% trans 'Bill' %}
@ -60,23 +60,23 @@
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="{% url 'django_ledger:bill-detail' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-outline-primary me-md-2">
class="btn btn-sm btn-phoenix-primary me-md-2">
{% trans 'View' %}
</a>
<a href="{% url 'django_ledger:bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-sm btn-outline-warning me-md-2">
class="btn btn-sm btn-phoenix-warning me-md-2">
{% trans 'Update' %}
</a>
{% if bill.can_pay %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
class="btn btn-sm btn-outline-info">
class="btn btn-sm btn-phoenix-info">
{% trans 'Mark as Paid' %}
</button>
{% endif %}
{% if bill.can_cancel %}
<button onclick="djLedger.toggleModal('{{ bill.get_html_id }}')"
class="btn btn-sm btn-outline-danger">
class="btn btn-sm btn-phoenix-danger">
{% trans 'Cancel' %}
</button>
{% endif %}
@ -199,50 +199,49 @@
{% endif %}
</div>
<div class="card-footer p-0">
<div class="d-flex flex-wrap">
<div class="d-flex flex-wrap gap-2 mt-2">
<!-- Update Button -->
<a href="{% url 'bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}"
class="btn btn-link text-primary w-100 w-md-auto border-end">
{% trans 'Update' %}
</a>
<a href="{% url 'bill-update' entity_slug=entity_slug bill_pk=bill.uuid %}" class="btn btn-phoenix-primary">
<i class="fas fa-edit me-2"></i>{% trans 'Update' %}
</a>
<!-- Mark as Draft -->
{% if bill.can_draft %}
<button class="btn btn-outline-success"
<button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Draft', '{% url 'bill-action-mark-as-draft' entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Draft')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Draft' %}
</button>
{% endif %}
<!-- Mark as Review -->
{% if bill.can_review %}
<button class="btn btn-outline-success"
<button class="btn btn-phoenix-warning"
onclick="showPOModal('Mark as Review', '{% url 'bill-action-mark-as-review' entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Review')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Review' %}
</button>
{% endif %}
<!-- Mark as Approved -->
{% if bill.can_approve %}
<button class="btn btn-outline-success"
<button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Approved', '{% url 'bill-action-mark-as-approved' entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Approved')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Approved' %}
</button>
{% endif %}
<!-- Mark as Paid -->
{% if bill.can_pay %}
<button class="btn btn-outline-success"
<button class="btn btn-phoenix-success"
onclick="showPOModal('Mark as Paid', '{% url 'bill-action-mark-as-paid' entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Paid')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Paid' %}
</button>
{% endif %}
<!-- Void Button -->
{% if bill.can_void %}
<button class="btn btn-outline-success"
<button class="btn btn-phoenix-danger"
onclick="showPOModal('Mark as Void', '{% url 'bill-action-mark-as-void' entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Void')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Void' %}
</button>
{% endif %}
<!-- Cancel Button -->
{% if bill.can_cancel %}
<button class="btn btn-outline-success"
<button class="btn btn-phoenix-danger"
onclick="showPOModal('Mark as Canceled', '{% url 'bill-action-mark-as-canceled' entity_slug=entity_slug bill_pk=bill.pk %}', 'Mark as Canceled')">
<i class="fas fa-check-circle me-2"></i>{% trans 'Mark as Canceled' %}
</button>
@ -292,10 +291,10 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('POModalBody').innerHTML = `
<div class="d-flex justify-content-center gap-3 py-3">
<a class="btn btn-primary px-4" href="${actionUrl}">
<a class="btn btn-phoenix-primary px-4" href="${actionUrl}">
<i class="fas fa-check-circle me-2"></i>${buttonText}
</a>
<button class="btn btn-outline-secondary" data-bs-dismiss="modal">
<button class="btn btn-phoenix-secondary" data-bs-dismiss="modal">
<i class="fas fa-times me-2"></i>Cancel
</button>
</div>

View File

@ -2,7 +2,7 @@
{% load django_ledger %}
{% if style == 'card_1' %}
<div class="card">
<div class="card" style="height:25rem;">
<div class="card-header">
<div class="card-header-title">
<h1 class="is-size-3 has-text-weight-light">{% if title %}{{ title }}{% else %}

View File

@ -1,7 +1,7 @@
{% load i18n %}
{% load django_ledger %}
<div class="card" id="djl-vendor-card-widget">
<div class="card " id="djl-vendor-card-widget" >
<div class="card-header">
<h2 class="card-title d-flex align-items-center text-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" class="bi bi-person-lines-fill me-2" viewBox="0 0 16 16">
@ -24,7 +24,5 @@
</p>
</div>
<div class="card-footer bg-white">
</div>
</div>

View File

@ -71,7 +71,7 @@
<span class="text-xs font-weight-bold">
{% currency_symbol %}{{ f.instance.po_total_amount | currency_format }}
</span>
<a class="btn btn-sm btn-outline-info mt-1"
<a class="btn btn-sm btn-phoenix-info mt-1"
href="{% url 'purchase_order_detail' f.instance.po_model_id %}">
{% trans 'View PO' %}
</a>
@ -143,12 +143,12 @@
<div class="d-flex justify-content-end gap-2">
{% if not item_formset.has_po %}
<a href="{% url 'django_ledger:product-create' entity_slug=entity_slug %}"
class="btn btn-outline-primary">
class="btn btn-phoenix-primary">
<i class="fas fa-plus me-1"></i>
{% trans 'New Item' %}
</a>
{% endif %}
<button type="submit" class="btn btn-primary">
<button type="submit" class="btn btn-phoenix-primary">
<i class="fas fa-save me-1"></i>
{% trans 'Save Changes' %}
</button>

View File

@ -22,7 +22,7 @@
<div class="mb-3 form-group">
<textarea class="form-control" name="notes" id="notes" rows="6"></textarea>
</div>
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>

View File

@ -13,7 +13,7 @@
<form action="{% url 'add_task' content_type slug %}" method="post" class="add_task_form">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>

View File

@ -173,8 +173,8 @@
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="emails-tab" data-bs-toggle="tab" href="#tab-emails" role="tab" aria-controls="tab-emails" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Emails") }}</a></li>
<li class="nav-item text-nowrap me-2" role="presentation"><a class="nav-link" id="tasks-tab" data-bs-toggle="tab" href="#tab-tasks" role="tab" aria-controls="tab-tasks" aria-selected="true"> <span class="fa-solid fa-envelope me-2 tab-icon-color fs-8"></span>{{ _("Tasks") }}</a></li>
<li class="nav-item text-nowrap ml-auto" role="presentation">
<button class="btn btn-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
<button class="btn btn-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
<button class="btn btn-phoenix-primary btn-sm" type="button" data-bs-toggle="modal" data-bs-target="#exampleModal"> <i class="fa-solid fa-user-plus me-2"></i> Reassign Lead</button>
<button class="btn btn-phoenix-primary btn-sm" onclick="openActionModal('{{ lead.id }}', '{{ lead.action }}', '{{ lead.next_action }}', '{{ lead.next_action_date|date:"Y-m-d\TH:i" }}')">
<i class="fa-solid fa-user-plus me-2"></i>
{% trans "Update Actions" %}
</button>
@ -191,8 +191,8 @@
{{transfer_form|crispy}}
</div>
<div class="modal-footer">
<button class="btn btn-primary" type="submit">Save</button>
<button class="btn btn-outline-primary" type="button" data-bs-dismiss="modal">Cancel</button>
<button class="btn btn-phoenix-primary" type="submit">Save</button>
<button class="btn btn-phoenix-primary" type="button" data-bs-dismiss="modal">Cancel</button>
</div>
</form>
</div>
@ -538,7 +538,7 @@
<form action="{% url 'add_task' 'lead' lead.slug %}" method="post" class="add_task_form">
{% csrf_token %}
{{ staff_task_form|crispy }}
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>
@ -558,7 +558,7 @@
<form action="{% url 'add_note' 'lead' lead.slug %}" method="post" class="add_note_form">
{% csrf_token %}
{{ note_form|crispy }}
<button type="submit" class="btn btn-success w-100">{% trans 'Save' %}</button>
<button type="submit" class="btn btn-phoenix-success w-100">{% trans 'Save' %}</button>
</form>
</div>
</div>

View File

@ -33,11 +33,11 @@
{% csrf_token %}
{{ form|crispy }}
<div class="d-flex justify-content-start">
<button class="btn btn-sm btn-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>

View File

@ -58,13 +58,13 @@
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<div class="d-flex align-items-center bg-warning-subtle rounded me-2"><span class="text-warning-dark" data-feather="zap"></span></div>
<span>{{ _("Action")|capfirst }}</span>
</div>
</th>
<th class="align-middle white-space-nowrap text-uppercase" scope="col" style="width: 10%;">
<div class="d-inline-flex flex-center">
<div class="d-flex align-items-center bg-info-subtle rounded me-2"><span class="text-info-dark" data-feather="database"></span></div>
<div class="d-flex align-items-center bg-success-subtle rounded me-2"><span class="text-success-dark" data-feather="user-check"></span></div>
<span>{{ _("Assigned To")|capfirst }}</span>
</div>
</th>
@ -93,7 +93,7 @@
<p>{% trans "Are you sure you want to delete this lead?" %}</p>
</div>
<div class="modal-footer flex justify-content-center border-top-0">
<a type="button" class="btn btn-sm btn-danger w-100" href="{% url 'lead_delete' lead.slug %}">
<a type="button" class="btn btn-sm btn-phoenix-danger w-100" href="{% url 'lead_delete' lead.slug %}">
{% trans "Yes" %}
</a>
</div>

View File

@ -27,8 +27,8 @@
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-link text-body fs-10 text-decoration-none">Discard</a>
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' lead.slug %}?status=draft" class="btn btn-secondary text-white fs-10 text-decoration-none">Save as Draft</a>
<button class="btn btn-primary fs-10" type="submit">Send<span class="fa-solid fa-paper-plane ms-1"></span></button>
<a hx-boost="true" hx-push-url='false' hx-include="#message,#subject,#to" href="{% url 'send_lead_email' lead.slug %}?status=draft" class="btn btn-phoenix-secondary text-white fs-10 text-decoration-none">Save as Draft</a>
<button class="btn btn-phoenix-primary fs-10" type="submit">Send<span class="fa-solid fa-paper-plane ms-1"></span></button>
</div>
</div>
</form>

View File

@ -28,7 +28,7 @@
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0">مرحبًا ismail mosa</h5>
<div>
<button class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">الصفحة الرئيسية لـ ismail mosa</button>
<button class="btn btn-phoenix-secondary dropdown-toggle" data-bs-toggle="dropdown">الصفحة الرئيسية لـ ismail mosa</button>
</div>
</div>

View File

@ -44,8 +44,8 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-primary">{{ _("Save Changes") }}</button>
<button type="button" class="btn btn-phoenix-secondary" data-bs-dismiss="modal">{{ _("Close") }}</button>
<button type="submit" class="btn btn-phoenix-primary">{{ _("Save Changes") }}</button>
</div>
</form>
</div>

View File

@ -10,8 +10,8 @@
{{ form|crispy }}
{% if form.instance.pk %}
<button type="submit" class="btn btn-sm btn-primary w-100">{{ _("Update") }}</button>
<button type="submit" class="btn btn-sm btn-phoenix-primary w-100">{{ _("Update") }}</button>
{% else %}
<button type="submit" class="btn btn-sm btn-success w-100">{{ _("Add") }}</button>
<button type="submit" class="btn btn-sm btn-phoenix-success w-100">{{ _("Add") }}</button>
{% endif %}
</form>

View File

@ -395,7 +395,7 @@
<form action="{% url 'add_note_to_opportunity' opportunity.slug %}" method="post">
{% csrf_token %}
<textarea class="form-control mb-3" id="notes" rows="4" name="notes" required> </textarea>
<button type="submit" class="btn btn-primary mb-3">Add Note</button>
<button type="submit" class="btn btn-phoenix-primary mb-3">Add Note</button>
</form>
<div class="row gy-4 note-list">
<div class="col-12 col-xl-auto flex-1">
@ -428,7 +428,7 @@
<button class="btn btn-link p-0 ms-3 fs-9 text-primary fw-bold text-decoration-none"><span class="fas fa-sort me-1 fw-extra-bold fs-10"></span>Sorting</button>
</div>
<div class="col-auto">
<a href="{% url 'schedule_lead' opportunity.lead.slug %}" class="btn btn-primary"><span class="fa-solid fa-plus me-2"></span>Add Meeting </a>
<a href="{% url 'schedule_lead' opportunity.lead.slug %}" class="btn btn-phoenix-primary"><span class="fa-solid fa-plus me-2"></span>Add Meeting </a>
</div>
</div>
<div class="row g-3">
@ -456,7 +456,7 @@
</div>
<div class="col-auto">
<a href="{% url 'schedule_lead' opportunity.lead.slug %}" class="btn btn-primary"><span class="fa-solid fa-plus me-2"></span>Add Call</a>
<a href="{% url 'schedule_lead' opportunity.lead.slug %}" class="btn btn-phoenix-primary"><span class="fa-solid fa-plus me-2"></span>Add Call</a>
</div>
</div>
<pre>{{opportunity.get_all_notes}}</pre>

View File

@ -8,7 +8,7 @@
<h2 class="mb-5">{{ _("Opportunities") }}</h2>
<div class="d-xl-flex justify-content-between">
<div class="mb-3">
<a class="btn btn-primary me-4" href="{% url 'opportunity_create' %}"><span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}</a>
<a class="btn btn-phoenix-primary me-4" href="{% url 'opportunity_create' slug%}"><span class="fas fa-plus me-2"></span>{{ _("Add Opportunity") }}</a>
</div>
</div>
</div>
@ -24,7 +24,7 @@
<div class="d-flex gap-3">
<button class="btn p-0" type="button" data-bs-toggle="dropdown" data-boundary="window" aria-haspopup="true" aria-expanded="false" data-bs-reference="parent"><span class="fas fa-ellipsis-h fs-10"></span></button>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{% url 'update_opportunity' pk=opportunity.pk %}">{{ _("Edit") }}</a></li>
<li><a class="dropdown-item" href="{% url 'update_opportunity' opportunity.pk %}">{{ _("Edit") }}</a></li>
<li><button class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">{% trans "Delete" %}</button></li>
</ul>
</div>
@ -74,7 +74,7 @@
</tr>
<tr>
<td class="py-1">
<div class="d-flex align-items-center"><span class="me-2 text-body-tertiary" data-feather="dollar-sign"></span>
<div class="d-flex align-items-center"><span class="icon-saudi_riyal me-2 text-body-tertiary"></span>
<p class="fw-semibold fs-9 mb-0 text-body-tertiary">{{ _("Expected Revenue")}}</p>
</div>
</td>
@ -151,10 +151,10 @@
<p class="mb-0 text-danger fw-bold">
{% trans "Are you sure you want to delete this opportunity?" %}
</p>
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
<button type="button" class="btn btn-phoenix-secondary btn-sm" data-bs-dismiss="modal">
{% trans "No" %}
</button>
<a type="button" class="btn btn-danger btn-sm" href="{% url 'delete_opportunity' opportunity.pk %}">
<a type="button" class="btn btn-phoenix-danger btn-sm" href="{% url 'delete_opportunity' opportunity.pk %}">
{% trans "Yes" %}
</a>
</div>
@ -489,7 +489,7 @@
<div class="modal-footer border-0 pt-6 px-0 pb-0">
<button class="btn btn-link text-danger px-3 my-0" data-bs-dismiss="modal" aria-label="Close">Cancel</button>
<button class="btn btn-primary my-0">Create Deal</button>
<button class="btn btn-phoenix-primary my-0">Create Deal</button>
</div>
</div>
</div>

View File

@ -32,11 +32,11 @@
{% csrf_token %}
{{ form|crispy }}
<div class="col-12">
<button class="btn btn-sm btn-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>

View File

@ -3,5 +3,5 @@
<form method="post" action="{% url 'add_note_to_customer' customer.slug %}" enctype="multipart/form-data">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-sm btn-success w-100">{{ _("Add") }}</button>
<button type="submit" class="btn btn-sm btn-phoenix-success w-100">{{ _("Add") }}</button>
</form>

View File

@ -85,7 +85,7 @@
{% endfor %}
</div>
<div class="d-grid gap-2">
<button class="btn btn-primary btn-lg" type="submit"><i class="fa fa-save me-2"></i>{{ _("Save") }}</button>
<button class="btn btn-phoenix-primary btn-lg" type="submit"><i class="fa fa-save me-2"></i>{{ _("Save") }}</button>
</div>
</form>
{% endblock %}

View File

@ -15,10 +15,10 @@
{% csrf_token %}
{{ form|crispy }}
<div class="gap-2 mt-3">
<button type="submit" class="btn btn-success btn-sm">
<button type="submit" class="btn btn-phoenix-success btn-sm">
<i class="fa fa-save"></i> {{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>

View File

@ -3,6 +3,6 @@
<div class="col-12 col-lg-6 text-center order-lg-1"><img class="img-fluid w-lg-100 d-dark-none" src="{% static 'images/spot-illustrations/403-illustration.png' %}" alt="" width="400" /><img class="img-fluid w-md-50 w-lg-100 d-light-none" src="{% static '/images/spot-illustrations/dark_403-illustration.png' %}" alt="" width="540" /></div>
<div class="col-12 col-lg-6 text-center text-lg-start"><img class="img-fluid mb-6 w-50 w-lg-75 d-dark-none" src="{% static 'images/spot-illustrations/403.png' %}" alt="" /><img class="img-fluid mb-6 w-50 w-lg-75 d-light-none" src="{% static '/images/spot-illustrations/dark_403.png' %}" alt="" />
<h2 class="text-body-secondary fw-bolder mb-3">Access Forbidden!</h2>
<p class="text-body mb-5">Halt! Thou art endeavouring to trespass upon a realm not granted unto thee.<br class="d-none d-md-block d-lg-none" />granted unto thee.</p><a class="btn btn-lg btn-primary" href="{% url 'home' %}">Go Home</a>
<p class="text-body mb-5">Halt! Thou art endeavouring to trespass upon a realm not granted unto thee.<br class="d-none d-md-block d-lg-none" />granted unto thee.</p><a class="btn btn-lg btn-phoenix-primary" href="{% url 'home' %}">Go Home</a>
</div>
</div>

View File

@ -70,7 +70,7 @@
<div class="col-12 col-lg-6 text-center order-lg-1"><img class="img-fluid w-lg-100 d-dark-none" src="../../web_assets/img/spot-illustrations/404-illustration.png" alt="" width="400" /><img class="img-fluid w-md-50 w-lg-100 d-light-none" src="../../web_assets/img/spot-illustrations/dark_404-illustration.png" alt="" width="540" /></div>
<div class="col-12 col-lg-6 text-center text-lg-start"><img class="img-fluid mb-6 w-50 w-lg-75 d-dark-none" src="../../web_assets/img/spot-illustrations/404.png" alt="" /><img class="img-fluid mb-6 w-50 w-lg-75 d-light-none" src="../../web_assets/img/spot-illustrations/dark_404.png" alt="" />
<h2 class="text-body-secondary fw-bolder mb-3">Page Missing!</h2>
<p class="text-body mb-5">But no worries! Our ostrich is looking everywhere <br class="d-none d-sm-block" />while you wait safely. </p><a class="btn btn-lg btn-primary" href="../../index.html">Go Home</a>
<p class="text-body mb-5">But no worries! Our ostrich is looking everywhere <br class="d-none d-sm-block" />while you wait safely. </p><a class="btn btn-lg btn-phoenix-primary" href="../../index.html">Go Home</a>
</div>
</div>
</div>
@ -137,6 +137,7 @@
<!-- End of Main Content-->
<!-- ===============================================-->
<div class="offcanvas offcanvas-end settings-panel border-0" id="settings-offcanvas" tabindex="-1" aria-labelledby="settings-offcanvas">
<div class="offcanvas-header align-items-start border-bottom flex-column border-translucent">

View File

@ -70,7 +70,7 @@
<div class="col-12 col-lg-6 text-center order-lg-1"><img class="img-fluid w-lg-100 d-light-none" src="../../web_assets/img/spot-illustrations/500-illustration.png" alt="" width="400" /><img class="img-fluid w-md-50 w-lg-100 d-dark-none" src="../../web_assets/img/spot-illustrations/dark_500-illustration.png" alt="" width="540" /></div>
<div class="col-12 col-lg-6 text-center text-lg-start"><img class="img-fluid mb-6 w-50 w-lg-75 d-dark-none" src="../../web_assets/img/spot-illustrations/500.png" alt="" /><img class="img-fluid mb-6 w-50 w-lg-75 d-light-none" src="../../web_assets/img/spot-illustrations/dark_500.png" alt="" />
<h2 class="text-body-secondary fw-bolder mb-3">Unknow error!</h2>
<p class="text-body mb-5">But relax! Our cat is here to play you some music.</p><a class="btn btn-lg btn-primary" href="../../index.html">Go Home</a>
<p class="text-body mb-5">But relax! Our cat is here to play you some music.</p><a class="btn btn-lg btn-phoenix-primary" href="../../index.html">Go Home</a>
</div>
</div>
</div>

View File

@ -22,12 +22,12 @@
</div>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-phoenix-secondary"
data-bs-dismiss="modal">
{% trans 'No' %}
</button>
<a type="button"
class="btn btn-sm btn-danger"
class="btn btn-sm btn-phoenix-danger"
href="{% url 'group_delete' group.id %}">
{% trans 'Yes' %}
</a>
@ -74,7 +74,7 @@
<div class="card-header ">
</div>
<h4 class="my-4">Permissions</h4>
<a class="btn btn-sm btn-primary mt-2 mb-4" href="{% url 'group_permission' group.id %}"><i class="fa-solid fa-unlock"></i> Manage Permissions</a>
<a class="btn btn-sm btn-phoenix-primary mt-2 mb-4" href="{% url 'group_permission' group.id %}"><i class="fa-solid fa-unlock"></i> Manage Permissions</a>
<table class="table table-hover table-responsive-sm fs-9 mb-0">
<thead>
@ -98,17 +98,17 @@
</table>
</div>
<div class="card-footer d-flex ">
<a class="btn btn-sm btn-primary me-1" href="{% url 'group_update' group.id %}">
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'group_update' group.id %}">
<i class="fa-solid fa-pen-to-square"></i>
{{ _("Edit") }}
</a>
<a class="btn btn-sm btn-danger me-1"
<a class="btn btn-sm btn-phoenix-danger me-1"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
<i class="fa-solid fa-trash"></i>
{{ _("Delete") }}
</a>
<a class="btn btn-sm btn-secondary"
<a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'group_list' %}">
<i class="fa-solid fa-arrow-left"></i>
{% trans "Back to List" %}

View File

@ -39,8 +39,8 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="d-flex mb-3">
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-danger me-2 "><i class="fa-solid fa-ban me-1"></i> {% trans "Cancel"|capfirst %}</a>
<button class="btn btn-success" type="submit">
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-phoenix-danger me-2 "><i class="fa-solid fa-ban me-1"></i> {% trans "Cancel"|capfirst %}</a>
<button class="btn btn-phoenix-success" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
{{ _("Save") }}
</button>

View File

@ -33,7 +33,7 @@
{% endfor %}
<div class="d-flex mb-3">
<a href="{% url 'group_detail' group.pk %}" class="btn btn-phoenix-primary me-2 px-6"><i class="fa-solid fa-ban"></i> {% trans "Cancel"|capfirst %}</a>
<button class="btn btn-primary" type="submit">
<button class="btn btn-phoenix-primary" type="submit">
<i class="fa-solid fa-floppy-disk"></i>
{{ _("Save") }}
</button>

View File

@ -12,7 +12,7 @@
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-robot me-2"></i>{% trans "HaikalBot" %}</h5>
<div>
<button id="export-btn" class="btn btn-sm btn-outline-secondary" style="display:none;">
<button id="export-btn" class="btn btn-sm btn-phoenix-secondary" style="display:none;">
{% trans "Export CSV" %}
</button>
</div>
@ -22,7 +22,7 @@
<form id="chat-form" class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-light" id="mic-btn"><i class="fas fa-microphone"></i></button>
<input type="text" class="form-control" id="chat-input" placeholder="{% trans 'Type your question...' %}" required />
<button type="submit" class="btn btn-primary"><i class="fas fa-paper-plane"></i></button>
<button type="submit" class="btn btn-phoenix-primary"><i class="fas fa-paper-plane"></i></button>
</form>
</div>
<div id="chart-container" style="display:none;" class="p-4 border-top">

View File

@ -79,9 +79,9 @@ AI assistant
<div id="chatMessages" class="overflow-auto p-3" style="height: 60vh;"></div>
<div class="bg-100 border-top p-3">
<div class="d-flex gap-2 flex-wrap mb-3" id="suggestionChips">
<button class="btn btn-sm btn-outline-primary suggestion-chip">{{ _("How many cars are in inventory")}}?</button>
<button class="btn btn-sm btn-outline-primary suggestion-chip">{{ _("Show me sales analysis")}}</button>
<button class="btn btn-sm btn-outline-primary suggestion-chip">{{ _("What are the best-selling cars")}}?</button>
<button class="btn btn-sm btn-phoenix-primary suggestion-chip">{{ _("How many cars are in inventory")}}?</button>
<button class="btn btn-sm btn-phoenix-primary suggestion-chip">{{ _("Show me sales analysis")}}</button>
<button class="btn btn-sm btn-phoenix-primary suggestion-chip">{{ _("What are the best-selling cars")}}?</button>
</div>
<div class="chat-container">
<div class="textarea-container mb-3">

View File

@ -11,7 +11,7 @@
<p class="navbar-vertical-label">Apps</p>
<hr class="navbar-vertical-line" />
<div class="nav-item-wrapper">
<a class="nav-link dropdown-indicator label-1" href="#nv-inventory" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-inventory">
<a id="inventory-nav" class="nav-link dropdown-indicator label-1 inventory-nav" href="#nv-inventory" role="button" data-bs-toggle="collapse" aria-expanded="false" aria-controls="nv-inventory">
<div class="d-flex align-items-center">
<div class="dropdown-indicator-icon-wrapper"><span class="fas fa-caret-right dropdown-indicator-icon"></span></div>
<span class="nav-link-icon"><span class="fas fa-warehouse"></span></span><span class="nav-link-text">{% trans "Inventory"|capfirst %}</span>
@ -22,7 +22,7 @@
<li class="collapsed-nav-item-title d-none">{% trans "Inventory"|capfirst %}</li>
{% if perms.inventory.add_car %}
<li class="nav-item">
<a class="nav-link" href="{% url 'car_add' %}">
<a id="btn-add-car" class="nav-link btn-add-car" href="{% url 'car_add' %}">
<div class="d-flex align-items-center">
<span class="nav-link-icon"><span class="fas fa-plus-circle"></span></span><span class="nav-link-text">{% trans "add car"|capfirst %}</span>
</div>
@ -456,7 +456,7 @@
</ul>
<hr />
<div class="px-3">
<a class="btn btn-sm btn-danger d-flex flex-center w-100" href="{% url 'account_logout' %}"> <span class="me-2" data-feather="log-out"> </span>{% trans 'Sign Out' %}</a>
<a class="btn btn-sm btn-phoenix-danger d-flex flex-center w-100" href="{% url 'account_logout' %}"> <span class="me-2" data-feather="log-out"> </span>{% trans 'Sign Out' %}</a>
</div>
<div class="my-2 text-center fw-bold fs-10 text-body-quaternary">
<a class="text-body-quaternary me-1" href="">Privacy policy</a>&bull;<a class="text-body-quaternary mx-1" href="">Terms</a>&bull;<a class="text-body-quaternary ms-1" href="">Cookies</a>

View File

@ -68,10 +68,10 @@
</div>
</div> {% endcomment %}
<div class="d-flex justify-content-center mt-4">
<button class="btn btn-sm btn-success me-2" type="submit">
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}
</button>
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>

View File

@ -7,13 +7,13 @@
{{ form|crispy }}
<div class="d-flex gap-1">
<button type="button"
class="btn btn-sm btn-danger w-50"
class="btn btn-sm btn-phoenix-danger w-50"
data-bs-dismiss="modal"
aria-label="Cancel and close modal">
<i class="fas fa-times"></i> {% trans 'Cancel' %}
</button>
<button type="submit"
class="btn btn-sm btn-success w-50"
class="btn btn-sm btn-phoenix-success w-50"
aria-label="Save changes">
<i class="fas fa-check"></i> {% trans 'Save' %}
</button>

View File

@ -7,7 +7,7 @@
<p>Are you sure you want to delete the car "{{ car }}"?</p>
<form method="post">
{% csrf_token %}
<button type="submit" class="btn btn-danger">Confirm Delete</button>
<a href="{% url 'car_detail' car.pk %}" class="btn btn-secondary">{% trans 'Cancel' %}</a>
<button type="submit" class="btn btn-phoenix-danger">Confirm Delete</button>
<a href="{% url 'car_detail' car.pk %}" class="btn btn-phoenix-secondary">{% trans 'Cancel' %}</a>
</form>
{% endblock %}

View File

@ -532,13 +532,13 @@
<div class="p-1">
<div class="d-flex gap-1">
<button type="button"
class="btn btn-sm btn-danger w-50"
class="btn btn-sm btn-phoenix-danger w-50"
data-bs-dismiss="modal"
aria-label="Cancel and close modal">
<i class="fas fa-times"></i> {% trans 'No' %}
</button>
<button type="submit"
class="btn btn-sm btn-success w-50"
class="btn btn-sm btn-phoenix-success w-50"
aria-label="Save changes">
<i class="fas fa-check"></i> {% trans 'Yes' %}
</button>
@ -694,8 +694,8 @@
const data = await response.json();
if (data.success) {
this.textContent = "Reserved";
this.classList.remove("btn-success");
this.classList.add("btn-danger");
this.classList.remove("btn-phoenix-success");
this.classList.add("btn-phoenix-danger");
this.disabled = true;
alert("Car reserved successfully.");
} else {

View File

@ -14,8 +14,8 @@
{% csrf_token %} {{ form|crispy }}
<!-- Save and Back Buttons -->
<div class="d-flex justify-content-center mt-4 ms-2">
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-sm btn-danger">{% trans "Back" %}</a>
<button type="submit" class="btn btn-sm btn-success ms-2">{% trans 'Save' %}</button>
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-sm btn-phoenix-danger">{% trans "Back" %}</a>
<button type="submit" class="btn btn-sm btn-phoenix-success ms-2">{% trans 'Save' %}</button>
</div>
</form>
</div>

View File

@ -37,12 +37,12 @@
</div> {% endcomment %}
<div class="d-flex justify-content-center">
<button class="btn btn-sm btn-success me-2" type="submit">
<button class="btn btn-sm btn-phoenix-success me-2" type="submit">
<i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{{ request.META.HTTP_REFERER }}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>

View File

@ -247,7 +247,7 @@
<div id="specificationsContent"></div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-primary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
<button class="btn btn-phoenix-primary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
@ -284,7 +284,7 @@
<div id="optionsContent"></div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-primary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
<button class="btn btn-phoenix-primary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>

View File

@ -189,13 +189,13 @@
<div class="row g-1">
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-danger me-1"
class="btn btn-sm btn-phoenix-danger me-1"
id="specification-btn"
data-bs-toggle="modal"
data-bs-target="#specificationsModal"
disabled>{% trans 'specifications'|capfirst %}</button>
<button type="button"
class="btn btn-sm btn-danger me-1"
class="btn btn-sm btn-phoenix-danger me-1"
id="options-btn"
data-bs-toggle="modal"
data-bs-target="#equipmentOptionsModal"
@ -203,11 +203,11 @@
<button type="submit"
name="add_another"
value="true"
class="btn btn-sm btn-success me-1">{% trans "Save and Add Another" %}</button>
class="btn btn-sm btn-phoenix-success me-1">{% trans "Save and Add Another" %}</button>
<button type="submit"
name="go_to_stats"
value="true"
class="btn btn-sm btn-primary">{% trans "Save and Go to Inventory" %}</button>
class="btn btn-sm btn-phoenix-primary">{% trans "Save and Go to Inventory" %}</button>
</div>
</div>
</main>
@ -267,7 +267,7 @@
<div id="optionsContent"></div>
</div>
<div class="modal-footer">
<button class="btn btn-outline-primary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
<button class="btn btn-phoenix-primary" type="button" data-bs-dismiss="modal">{% trans 'Close' %}</button>
</div>
</div>
</div>
@ -290,7 +290,7 @@
<video id="video" autoplay playsinline>
</video>
<p id="result" class="mt-2">{{ _("VIN will appear here.") }}</p>
<button id="ocr-fallback-btn" class="btn btn-primary mt-3">{{ _("Use OCR Fallback") }}</button>
<button id="ocr-fallback-btn" class="btn btn-phoenix-primary mt-3">{{ _("Use OCR Fallback") }}</button>
</div>
</div>
</div>

View File

@ -154,7 +154,7 @@
<tr>
<td colspan="7" class="d-flex flex-column align-items-center">
<p class="text-muted">{% trans "No cars available." %}</p>
<a href="{% url 'add_car' %}" class="btn btn-primary">{% trans "Add a Car" %}</a>
<a href="{% url 'add_car' %}" class="btn btn-phoenix-primary">{% trans "Add a Car" %}</a>
</td>
</tr>
{% endfor %}

View File

@ -77,7 +77,7 @@
class="form-control form-control-sm"
placeholder="{% trans 'VIN'|capfirst %}"
maxlength="17">
<button type="button" class="btn btn-sm btn-primary" id="decodeVinBtn">{% trans 'search'|capfirst %}</button>
<button type="button" class="btn btn-sm btn-phoenix-primary" id="decodeVinBtn">{% trans 'search'|capfirst %}</button>
</div>
</div>
</div>
@ -222,18 +222,18 @@
<div class="card h-100">
<div class="card-body" id="option-row">
<button type="button"
class="btn btn-sm btn-danger mt-1"
class="btn btn-sm btn-phoenix-danger mt-1"
id="option-btn"
data-bs-toggle="modal"
data-bs-target="#optionsModal"
disabled>{% trans 'options'|capfirst %}</button>
<button type="button"
class="btn btn-sm btn-danger mt-1"
class="btn btn-sm btn-phoenix-anger mt-1"
id="specification-btn"
data-bs-toggle="modal"
data-bs-target="#specificationsModal"
disabled>{% trans 'specifications'|capfirst %}</button>
<button type="submit" class="btn btn-sm btn-primary mt-1" id="saveCarBtn">{% trans 'save'|capfirst %}</button>
<button type="submit" class="btn btn-sm btn-phoenix-primary mt-1" id="saveCarBtn">{% trans 'save'|capfirst %}</button>
</div>
</div>
</div>
@ -302,12 +302,12 @@
equipmentBg.classList.add('bg-danger-subtle');
showOptionsButton.disabled = true;
showOptionsButton.classList.add('btn-danger');
showOptionsButton.classList.remove('btn-success');
showOptionsButton.classList.add('btn-phoenix-danger');
showOptionsButton.classList.remove('btn-phoenix-success');
showSpecificationButton.disabled = true;
showSpecificationButton.classList.add('btn-danger');
showSpecificationButton.classList.remove('btn-success');
showSpecificationButton.classList.add('btn-phoenix-danger');
showSpecificationButton.classList.remove('btn-phoenix-success');
}
function checkFormCompletion() {
@ -497,11 +497,11 @@ checkFormCompletion();
showSpecificationButton.disabled = !this.value;
if (this.value) {
showSpecificationButton.classList.remove('btn-danger');
showSpecificationButton.classList.add('btn-success');
showSpecificationButton.classList.remove('btn-phoenix-danger');
showSpecificationButton.classList.add('btn-phoenix-success');
} else {
showSpecificationButton.classList.add('btn-danger');
showSpecificationButton.classList.remove('btn-success');
showSpecificationButton.classList.add('btn-phoenix-danger');
showSpecificationButton.classList.remove('btn-phoenix-success');
}
checkFormCompletion();
@ -559,11 +559,11 @@ checkFormCompletion();
equipmentSelect.addEventListener('change', function() {
showOptionsButton.disabled = !this.value;
if (this.value) {
showOptionsButton.classList.remove('btn-danger');
showOptionsButton.classList.add('btn-success');
showOptionsButton.classList.remove('btn-phoenix-danger');
showOptionsButton.classList.add('btn-phoenix-success');
} else {
showOptionsButton.classList.add('btn-danger');
showOptionsButton.classList.remove('btn-success');
showOptionsButton.classList.add('btn-phoenix-danger');
showOptionsButton.classList.remove('btn-phoenix-success');
}
});

View File

@ -58,7 +58,7 @@
</li>
<li class="nav-item">
<button hx-on:click="toggle_filter()"
class="btn btn-sm btn-primary px-2 py-1">
class="btn btn-sm btn-phoenix-primary px-2 py-1">
<span><span class="fa fa-filter me-1"></span>{{ _("Filter") }}</span><span class="fas fa-caret-down fs-9 ms-1 filter-icon"></span>
</button>
</li>

View File

@ -8,13 +8,13 @@
{{ form|crispy }}
<div class="d-flex gap-1">
<button type="button"
class="btn btn-sm btn-danger w-50"
class="btn btn-sm btn-phoenix-danger w-50"
data-bs-dismiss="modal"
aria-label="Cancel and close modal">
<i class="fas fa-times"></i> {% trans 'Cancel' %}
</button>
<button type="submit"
class="btn btn-sm btn-success w-50"
class="btn btn-sm btn-phoenix-success w-50"
aria-label="Save changes">
<i class="fas fa-check"></i> {% trans 'Save' %}
</button>

View File

@ -103,9 +103,9 @@
</div>
<!-- Save and Cancel Buttons -->
<div class="btn-group">
<button type="submit" class="btn btn-sm btn-success me-1">{% trans "Save" %}</button>
<button type="submit" class="btn btn-sm btn-phoenix-success me-1">{% trans "Save" %}</button>
<a href="{{ request.META.HTTP_REFERER }}"
class="btn btn-sm btn-danger me-1">{% trans "Cancel" %}</a>
class="btn btn-sm btn-phoenix-danger me-1">{% trans "Cancel" %}</a>
</div>
</main>
</div>

View File

@ -19,7 +19,7 @@
{{ form.reservation_end }}
{% for error in form.reservation_end.errors %}<div class="invalid-feedback">{{ error }}</div>{% endfor %}
</div>
<button type="submit" class="btn btn-primary">{% trans "Reserve" %}</button>
<a href="{% url 'car_detail' car.pk %}" class="btn btn-secondary">{% trans "Cancel" %}</a>
<button type="submit" class="btn btn-phoenix-primary">{% trans "Reserve" %}</button>
<a href="{% url 'car_detail' car.pk %}" class="btn btn-phoenix-secondary">{% trans "Cancel" %}</a>
</form>
{% endblock %}

View File

@ -9,16 +9,16 @@
<input type="text" class="form-control" id="vin_no" name="vin_no" readonly>
</div>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary" id="capture-btn">{{ _("Start Scanning") }}</button>
<button type="submit" class="btn btn-success">{{ _("Search") }}</button>
<button type="button" class="btn btn-phoenix-primary" id="capture-btn">{{ _("Start Scanning") }}</button>
<button type="submit" class="btn btn-phoenix-success">{{ _("Search") }}</button>
</div>
</form>
<div id="camera-container" class="my-3" style="display:none;">
<video id="camera" class="border rounded" autoplay playsinline width="100%">
</video>
<div class="mt-2 d-flex gap-2">
<button class="btn btn-warning" id="toggle-btn">{{ _("Switch Camera") }}</button>
<button class="btn btn-info" id="scan-btn">{{ _("Scan") }}</button>
<button class="btn btn-phoenix-warning" id="toggle-btn">{{ _("Switch Camera") }}</button>
<button class="btn btn-phoenix-info" id="scan-btn">{{ _("Scan") }}</button>
</div>
</div>
<div id="result" class="alert mt-3" style="display:none;"></div>

View File

@ -15,6 +15,6 @@
{% if field.help_text %}<small class="form-text text-muted">{{ field.help_text|safe }}</small>{% endif %}
</div>
{% endfor %}
<button type="submit" class="btn btn-sm btn-primary">{% trans "transfer"|capfirst %}</button>
<button type="submit" class="btn btn-sm btn-phoenix-primary">{% trans "transfer"|capfirst %}</button>
</form>
{% endblock %}

View File

@ -55,11 +55,11 @@
<div class="modal-body">{% trans 'Are you sure' %}?</div>
<div class="p-1">
<div class="d-flex gap-1">
<button type="button" class="btn btn-sm btn-danger" data-bs-dismiss="modal">{% trans 'No' %}</button>
<button type="button" class="btn btn-sm btn-phoenix-danger" data-bs-dismiss="modal">{% trans 'No' %}</button>
<a href="{% url 'transfer_confirm' transfer.car.pk transfer.pk %}?action=cancel"
type="button"
type="submit"
class="btn btn-success btn-sm">{% trans 'Yes' %}</a>
class="btn btn-phoenix-success btn-sm">{% trans 'Yes' %}</a>
</div>
</div>
</div>
@ -125,12 +125,12 @@
<div class="d-flex gap-1">
{% if not action == 'cancel' %}
<button type="button"
class="btn btn-sm btn-success w-100"
class="btn btn-sm btn-phoenix-success w-100"
data-bs-toggle="modal"
data-bs-target="#approveCardModal">{% trans 'Approve' %}</button>
{% endif %}
<button type="button"
class="btn btn-sm btn-warning w-100"
class="btn btn-sm btn-phoenix-warning w-100"
data-bs-toggle="modal"
data-bs-target="#cancelCardModal">{% trans 'Cancel Transfer' %}</button>
</div>

View File

@ -186,17 +186,17 @@
</main>
{% else %}
<div class="button-row">
<button id="download-pdf" class="btn btn-primary">
<button id="download-pdf" class="btn btn-phoenix-primary">
<i class="fas fa-download"></i> {% trans 'Download transfer' %}
</button>
<button id="accept"
class="btn btn-success"
class="btn btn-phoenix-success"
data-bs-toggle="modal"
data-bs-target="#acceptModal">
<i class="fas fa-check-circle"></i> {% trans 'Accept transfer' %}
</button>
<button id="reject"
class="btn btn-danger"
class="btn btn-phoenix-danger"
data-bs-toggle="modal"
data-bs-target="#rejectModal">
<i class="fas fa-times-circle"></i> {% trans 'Reject transfer' %}
@ -219,8 +219,8 @@
</div>
<div class="modal-body">{% trans 'Are you sure you want to accept this transfer?' %}</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Cancel' %}</button>
<a class="btn btn-success"
<button type="button" class="btn btn-phoenix-secondary" data-bs-dismiss="modal">{% trans 'Cancel' %}</button>
<a class="btn btn-phoenix-success"
href="{% url 'transfer_accept_reject' transfer.car.pk transfer.pk %}?status=accepted">Confirm</a>
</div>
</div>
@ -243,8 +243,8 @@
</div>
<div class="modal-body">{% trans 'Are you sure you want to reject this transfer?' %}</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans 'Cancel' %}</button>
<a class="btn btn-success"
<button type="button" class="btn btn-phoenix-secondary" data-bs-dismiss="modal">{% trans 'Cancel' %}</button>
<a class="btn btn-phoenix-success"
href="{% url 'transfer_accept_reject' transfer.car.pk transfer.pk %}?status=rejected">Confirm</a>
</div>
</div>

View File

@ -17,8 +17,8 @@
{% comment %} <button class="btn btn-sm btn-success me-1" type="submit"><i class="fa-solid fa-floppy-disk"></i>{{ _("Save") }}</button> {% endcomment %}
<div class="d-flex justify-content-start">
<button class="btn btn-sm btn-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>{{ _("Save") }}</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>

View File

@ -13,7 +13,7 @@
<form method="post" action="">
{% csrf_token %}
{{ form|crispy }}
<button type="submit" class="btn btn-primary">{% trans 'Save' %}</button>
<button type="submit" class="btn btn-phoenix-primary">{% trans 'Save' %}</button>
</form>
</div>
</div>

View File

@ -21,12 +21,12 @@
</div>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-secondary"
class="btn btn-sm btn-phoenix-secondary"
data-bs-dismiss="modal">
{% trans 'No' %}
</button>
<a type="button"
class="btn btn-sm btn-danger"
class="btn btn-sm btn-phoenix-danger"
href="{% url 'bank_account_delete' bank_account.pk %}">
{% trans 'Yes' %}
</a>
@ -52,17 +52,17 @@
</div>
</div>
<div class="card-footer d-flex ">
<a class="btn btn-sm btn-primary me-1" href="{% url 'bank_account_update' bank_account.pk %}">
<a class="btn btn-sm btn-phoenix-primary me-1" href="{% url 'bank_account_update' bank_account.pk %}">
<!--<i class="bi bi-pencil-square"></i> -->
{{ _("Edit") }}
</a>
<a class="btn btn-sm btn-danger me-1"
<a class="btn btn-sm btn-phoenix-danger me-1"
data-bs-toggle="modal"
data-bs-target="#deleteModal">
<!--<i class="bi bi-trash-fill"></i>-->
{{ _("Delete") }}
</a>
<a class="btn btn-sm btn-secondary"
<a class="btn btn-sm btn-phoenix-secondary"
href="{% url 'bank_account_list' %}">
<!--<i class="bi bi-arrow-left-square-fill"></i>-->
{% trans "Back to List" %}

View File

@ -35,11 +35,11 @@
<div class="text-danger">{{ error }}</div>
{% endfor %}
<div class="d-flex justify-content-start">
<button class="btn btn-sm btn-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<button class="btn btn-sm btn-phoenix-success me-2" type="submit"><i class="fa-solid fa-floppy-disk me-1"></i>
<!--<i class="bi bi-save"></i> -->
{{ _("Save") }}
</button>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
<a href="{{request.META.HTTP_REFERER}}" class="btn btn-sm btn-phoenix-danger"><i class="fa-solid fa-ban me-1"></i>{% trans "Cancel" %}</a>
</div>
</form>
</div>

Some files were not shown because too many files have changed in this diff Show More