+
Interactive Guides
+
Learn how to use the car inventory system with these interactive step-by-step guides.
+
+
+ {% for tour in tours %}
+
+
+
+
{{ tour.name }}
+
{{ tour.description }}
+
+
+
+
+ {% empty %}
+
+
+ No interactive guides available at this time.
+
+
+ {% endfor %}
+
+
+{% endblock %}
diff --git a/tours/__init__.py b/tours/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tours/admin.py b/tours/admin.py
new file mode 100644
index 00000000..0bdb93a8
--- /dev/null
+++ b/tours/admin.py
@@ -0,0 +1,6 @@
+from django.contrib import admin
+from . import models
+
+# Register your models here.
+admin.site.register(models.Tour)
+admin.site.register(models.TourCompletion)
\ No newline at end of file
diff --git a/tours/apps.py b/tours/apps.py
new file mode 100644
index 00000000..57f8071a
--- /dev/null
+++ b/tours/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class ToursConfig(AppConfig):
+ default_auto_field = 'django.db.models.BigAutoField'
+ name = 'tours'
diff --git a/tours/management/__init__.py b/tours/management/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tours/management/commands/__init__.py b/tours/management/commands/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tours/management/commands/generate_tours.py b/tours/management/commands/generate_tours.py
new file mode 100644
index 00000000..b7fa82e7
--- /dev/null
+++ b/tours/management/commands/generate_tours.py
@@ -0,0 +1,84 @@
+from django.core.management.base import BaseCommand
+import yaml
+import os
+import json
+from django.conf import settings
+
+
+class Command(BaseCommand):
+ help = "Generate IntroJS tour definitions from workflow documentation"
+
+ def handle(self, *args, **kwargs):
+ input_file = "haikal_kb.yaml"
+ output_dir = os.path.join(settings.BASE_DIR, 'static', 'js', 'tours')
+
+ # Create output directory if it doesn't exist
+ os.makedirs(output_dir, exist_ok=True)
+
+ try:
+ with open(input_file, 'r', encoding='utf-8') as f:
+ kb = yaml.safe_load(f)
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f"Error reading knowledge base file: {e}"))
+ return
+
+ workflows = kb.get("user_workflows", {})
+
+ tours_created = 0
+
+ # Map of common UI elements to their likely selectors
+ element_selectors = {
+ "inventory": "#inventory-menu, .inventory-nav, nav .inventory",
+ "add car": "#add-car-button, .btn-add-car, button:contains('Add Car')",
+ "save": "button[type='submit'], .btn-save, #save-button",
+ "cancel": ".btn-cancel, #cancel-button, button:contains('Cancel')",
+ "vin": "#vin-input, input[name='vin'], .vin-field",
+ "make": "#make-select, select[name='make'], .make-field",
+ "model": "#model-select, select[name='model'], .model-field",
+ "series": "#series-select, select[name='series'], .series-field",
+ "trim": "#trim-select, select[name='trim'], .trim-field",
+ "price": "#price-input, input[name='price'], .price-field",
+ "color": "#color-select, select[name='color'], .color-field",
+ "invoice": "#invoice-section, .invoice-tab, #create-invoice",
+ "customer": "#customer-select, select[name='customer'], .customer-field",
+ "finance": "#finance-menu, .finance-nav, nav .finance",
+ }
+
+ for workflow_name, workflow in workflows.items():
+ steps = workflow.get("steps", [])
+ if not steps:
+ continue
+
+ tour_steps = []
+
+ for i, step in enumerate(steps):
+ # Try to identify UI element from step description
+ element = None
+ for key, selector in element_selectors.items():
+ if key.lower() in step.lower():
+ element = selector
+ break
+
+ tour_step = {
+ "title": f"Step {i + 1}",
+ "intro": step,
+ "position": "bottom"
+ }
+
+ if element:
+ tour_step["element"] = element
+
+ tour_steps.append(tour_step)
+
+ # Save the tour definition as JSON
+ tour_filename = workflow_name.lower().replace(' ', '_') + '_tour.json'
+ with open(os.path.join(output_dir, tour_filename), 'w', encoding='utf-8') as f:
+ json.dump({
+ "name": workflow_name,
+ "description": workflow.get("description", ""),
+ "steps": tour_steps
+ }, f, indent=2)
+
+ tours_created += 1
+
+ self.stdout.write(self.style.SUCCESS(f"✅ Created {tours_created} IntroJS tour definitions in {output_dir}"))
diff --git a/tours/management/commands/generate_ui_map.py b/tours/management/commands/generate_ui_map.py
new file mode 100644
index 00000000..30258ec3
--- /dev/null
+++ b/tours/management/commands/generate_ui_map.py
@@ -0,0 +1,88 @@
+from django.core.management.base import BaseCommand
+from django.urls import get_resolver
+import os
+import json
+from django.template.loaders.app_directories import get_app_template_dirs
+
+
+class Command(BaseCommand):
+ help = "Generate UI element map for IntroJS tours"
+
+ def handle(self, *args, **kwargs):
+ output_file = os.path.join('static', 'js', 'tours', 'ui_element_map.json')
+
+ ui_map = {
+ "pages": {},
+ "common_elements": {
+ "navigation": {
+ "inventory": "#inventory-menu, .inventory-nav, nav .inventory",
+ "finance": "#finance-menu, .finance-nav, nav .finance",
+ "customers": "#customers-menu, .customers-nav, nav .customers"
+ },
+ "actions": {
+ "add": ".btn-add, .add-button, button:contains('Add')",
+ "edit": ".btn-edit, .edit-button, button:contains('Edit')",
+ "save": ".btn-save, button[type='submit'], #save-button",
+ "cancel": ".btn-cancel, #cancel-button, button:contains('Cancel')",
+ "delete": ".btn-delete, .delete-button, button:contains('Delete')"
+ },
+ "forms": {
+ "search": "#search-form, .search-input, input[name='q']",
+ "date_range": ".date-range, input[type='date']",
+ "dropdown": "select, .dropdown, .select-field"
+ }
+ }
+ }
+
+ # Extract URL patterns to identify pages
+ resolver = get_resolver()
+ for url_pattern in resolver.url_patterns:
+ if hasattr(url_pattern, 'name') and url_pattern.name:
+ pattern_name = url_pattern.name
+ # Skip admin and API URLs
+ if pattern_name.startswith(('admin:', 'api:')):
+ continue
+
+ ui_map["pages"][pattern_name] = {
+ "url_pattern": str(url_pattern.pattern),
+ "elements": {}
+ }
+
+ # Scan templates for UI elements with IDs
+ template_dirs = get_app_template_dirs('templates')
+ for template_dir in template_dirs:
+ for root, dirs, files in os.walk(template_dir):
+ for file in files:
+ if not file.endswith(('.html', '.htm')):
+ continue
+
+ try:
+ with open(os.path.join(root, file), 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Try to identify the page/view this template is for
+ template_path = os.path.relpath(os.path.join(root, file), template_dir)
+ page_key = template_path.replace('/', '_').replace('.html', '')
+
+ # Create page entry if it doesn't exist
+ if page_key not in ui_map["pages"]:
+ ui_map["pages"][page_key] = {
+ "template": template_path,
+ "elements": {}
+ }
+
+ # Extract elements with IDs
+ import re
+ id_matches = re.findall(r'id=["\']([^"\']+)["\']', content)
+ for id_match in id_matches:
+ ui_map["pages"][page_key]["elements"][id_match] = f"#{id_match}"
+
+ except Exception as e:
+ self.stdout.write(self.style.WARNING(f"Error processing template {file}: {e}"))
+
+ # Save UI map as JSON
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
+ with open(output_file, 'w', encoding='utf-8') as f:
+ json.dump(ui_map, f, indent=2)
+
+ self.stdout.write(self.style.SUCCESS(f"✅ UI element map saved to {output_file}"))
diff --git a/tours/management/commands/import_tours.py b/tours/management/commands/import_tours.py
new file mode 100644
index 00000000..16474a30
--- /dev/null
+++ b/tours/management/commands/import_tours.py
@@ -0,0 +1,45 @@
+from django.core.management.base import BaseCommand
+import yaml
+import os
+from django.utils.text import slugify
+from tours.models import Tour
+
+
+class Command(BaseCommand):
+ help = "Import tours from knowledge base"
+
+ def handle(self, *args, **kwargs):
+ input_file = "haikal_kb.yaml"
+
+ try:
+ with open(input_file, 'r', encoding='utf-8') as f:
+ kb = yaml.safe_load(f)
+ except Exception as e:
+ self.stdout.write(self.style.ERROR(f"Error reading knowledge base file: {e}"))
+ return
+
+ workflows = kb.get("user_workflows", {})
+
+ tours_created = 0
+
+ for workflow_name, workflow in workflows.items():
+ slug = slugify(workflow_name)
+ tour_file = f"{slug}_tour.json"
+
+ tour, created = Tour.objects.update_or_create(
+ slug=slug,
+ defaults={
+ 'name': workflow_name,
+ 'description': workflow.get('description', ''),
+ 'tour_file': tour_file,
+ 'is_active': True
+ }
+ )
+
+ if created:
+ tours_created += 1
+ self.stdout.write(self.style.SUCCESS(f"Created tour: {workflow_name}"))
+ else:
+ self.stdout.write(self.style.SUCCESS(f"Updated tour: {workflow_name}"))
+
+ self.stdout.write(self.style.SUCCESS(f"✅ Imported {tours_created} tours from knowledge base"))
diff --git a/tours/migrations/0001_initial.py b/tours/migrations/0001_initial.py
new file mode 100644
index 00000000..d0939f6e
--- /dev/null
+++ b/tours/migrations/0001_initial.py
@@ -0,0 +1,40 @@
+# Generated by Django 5.2.1 on 2025-06-06 14:05
+
+import django.db.models.deletion
+from django.conf import settings
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ migrations.swappable_dependency(settings.AUTH_USER_MODEL),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Tour',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=100)),
+ ('description', models.TextField(blank=True)),
+ ('slug', models.SlugField(unique=True)),
+ ('tour_file', models.CharField(max_length=255)),
+ ('is_active', models.BooleanField(default=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='TourCompletion',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('completed_on', models.DateTimeField(auto_now_add=True)),
+ ('tour', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tours.tour')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
+ ],
+ options={
+ 'unique_together': {('tour', 'user')},
+ },
+ ),
+ ]
diff --git a/tours/migrations/__init__.py b/tours/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tours/models.py b/tours/models.py
new file mode 100644
index 00000000..5179072f
--- /dev/null
+++ b/tours/models.py
@@ -0,0 +1,22 @@
+from django.db import models
+
+
+class Tour(models.Model):
+ name = models.CharField(max_length=100)
+ description = models.TextField(blank=True)
+ slug = models.SlugField(unique=True)
+ tour_file = models.CharField(max_length=255)
+ is_active = models.BooleanField(default=True)
+
+ def __str__(self):
+ return self.name
+
+
+class TourCompletion(models.Model):
+ tour = models.ForeignKey(Tour, on_delete=models.CASCADE)
+ user = models.ForeignKey('auth.User', on_delete=models.CASCADE)
+ completed_on = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ unique_together = ('tour', 'user')
+
diff --git a/tours/tests.py b/tours/tests.py
new file mode 100644
index 00000000..7ce503c2
--- /dev/null
+++ b/tours/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/tours/urls.py b/tours/urls.py
new file mode 100644
index 00000000..ffa32531
--- /dev/null
+++ b/tours/urls.py
@@ -0,0 +1,9 @@
+from django.urls import path
+from . import views
+
+urlpatterns = [
+ path('', views.tour_list, name='tour_list'),
+ path('data/