Compare commits
8 Commits
13ca8d315c
...
5880de54cd
| Author | SHA1 | Date | |
|---|---|---|---|
| 5880de54cd | |||
| ceb6ddf902 | |||
| f1277a2ed4 | |||
|
|
85793dfba7 | ||
|
|
008ada38ae | ||
|
|
c212a65185 | ||
| 7d2b70e950 | |||
| cba8a39d1b |
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal 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>
|
||||
@ -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"
|
||||
|
||||
@ -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
0
database.sqlite
Normal file
2950
haikal_kb.yaml
Normal file
2950
haikal_kb.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
801
haikalbot/haikal_agent.py
Normal 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
@ -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)
|
||||
|
||||
|
||||
50
haikalbot/migrations/0001_initial.py
Normal file
50
haikalbot/migrations/0001_initial.py
Normal 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')],
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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)
|
||||
77
haikalbot/utils/ask_haikalbot.py
Normal file
77
haikalbot/utils/ask_haikalbot.py
Normal 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)
|
||||
@ -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
@ -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
77
run_haikal_qa.py
Normal 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
159
sql_agent.py
Normal 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())
|
||||
61
static/js/tours/add-new-car_tour.json
Normal file
61
static/js/tours/add-new-car_tour.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
52
static/js/tours/create-new-invoice_tour.json
Normal file
52
static/js/tours/create-new-invoice_tour.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
163
static/js/tours/help-button.js
Normal file
163
static/js/tours/help-button.js
Normal 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 };
|
||||
134
static/js/tours/tour-manager.js
Normal file
134
static/js/tours/tour-manager.js
Normal 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 };
|
||||
2811
static/js/tours/ui_element_map.json
Normal file
2811
static/js/tours/ui_element_map.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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="">{{ _("Didn’t receive the code") }}</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>•<a class="text-body-quaternary mx-1" href="">Terms</a>•<a class="text-body-quaternary ms-1" href="">Cookies</a>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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" %}
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user