django phase2.3.4.

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-14 15:10:41 +00:00
parent d161378bd9
commit eb88a0e12d
13 changed files with 2880 additions and 31 deletions

View File

@@ -0,0 +1,122 @@
"""
Admin Alert System
"""
from django.utils import timezone
from datetime import timedelta
class AdminAlerts:
"""System for admin alerts and notifications"""
@staticmethod
def get_alerts():
"""Get all active alerts"""
alerts = []
today = timezone.now().date()
# Check for pending payments
from igny8_core.business.billing.models import Payment
pending_payments = Payment.objects.filter(status='pending_approval').count()
if pending_payments > 0:
alerts.append({
'level': 'warning',
'icon': '⚠️',
'message': f'{pending_payments} payment(s) awaiting approval',
'url': '/admin/billing/payment/?status=pending_approval',
'action': 'Review Payments'
})
# Check for low credit accounts
from igny8_core.auth.models import Account
low_credit_accounts = Account.objects.filter(
status='active',
credits__lt=100
).count()
if low_credit_accounts > 0:
alerts.append({
'level': 'info',
'icon': '',
'message': f'{low_credit_accounts} account(s) with low credits',
'url': '/admin/igny8_core_auth/account/?credits__lt=100',
'action': 'View Accounts'
})
# Check for very low credits (critical)
critical_credit_accounts = Account.objects.filter(
status='active',
credits__lt=10
).count()
if critical_credit_accounts > 0:
alerts.append({
'level': 'error',
'icon': '🔴',
'message': f'{critical_credit_accounts} account(s) with critical low credits (< 10)',
'url': '/admin/igny8_core_auth/account/?credits__lt=10',
'action': 'Urgent Review'
})
# Check for failed automations
from igny8_core.business.automation.models import AutomationRun
failed_today = AutomationRun.objects.filter(
status='failed',
started_at__date=today
).count()
if failed_today > 0:
alerts.append({
'level': 'error',
'icon': '🔴',
'message': f'{failed_today} automation(s) failed today',
'url': '/admin/automation/automationrun/?status=failed',
'action': 'Review Failures'
})
# Check for failed syncs
from igny8_core.business.integration.models import SyncEvent
failed_syncs = SyncEvent.objects.filter(
success=False,
created_at__date=today
).count()
if failed_syncs > 5: # Only alert if more than 5
alerts.append({
'level': 'warning',
'icon': '⚠️',
'message': f'{failed_syncs} WordPress sync failures today',
'url': '/admin/integration/syncevent/?success=False',
'action': 'Review Syncs'
})
# Check for failed Celery tasks
try:
from django_celery_results.models import TaskResult
celery_failed = TaskResult.objects.filter(
status='FAILURE',
date_created__date=today
).count()
if celery_failed > 0:
alerts.append({
'level': 'error',
'icon': '🔴',
'message': f'{celery_failed} Celery task(s) failed today',
'url': '/admin/django_celery_results/taskresult/?status=FAILURE',
'action': 'Review Tasks'
})
except:
pass
# Check for stale pending tasks (older than 24 hours)
from igny8_core.modules.writer.models import Tasks
yesterday = today - timedelta(days=1)
stale_tasks = Tasks.objects.filter(
status='pending',
created_at__date__lte=yesterday
).count()
if stale_tasks > 10:
alerts.append({
'level': 'info',
'icon': '',
'message': f'{stale_tasks} tasks pending for more than 24 hours',
'url': '/admin/writer/tasks/?status=pending',
'action': 'Review Tasks'
})
return alerts

View File

@@ -39,5 +39,28 @@ class Igny8AdminConfig(AdminConfig):
_safe_register(Group, admin.ModelAdmin) _safe_register(Group, admin.ModelAdmin)
_safe_register(ContentType, ReadOnlyAdmin) _safe_register(ContentType, ReadOnlyAdmin)
_safe_register(Session, ReadOnlyAdmin) _safe_register(Session, ReadOnlyAdmin)
# Import and setup enhanced Celery task monitoring
self._setup_celery_admin()
def _setup_celery_admin(self):
"""Setup enhanced Celery admin with proper unregister/register"""
try:
from django_celery_results.models import TaskResult
from igny8_core.admin.celery_admin import CeleryTaskResultAdmin
# Unregister the default TaskResult admin
try:
admin.site.unregister(TaskResult)
except admin.sites.NotRegistered:
pass
# Register our enhanced version
admin.site.register(TaskResult, CeleryTaskResultAdmin)
except Exception as e:
# Log the error but don't crash the app
import logging
logger = logging.getLogger(__name__)
logger.warning(f"Could not setup enhanced Celery admin: {e}")

View File

@@ -0,0 +1,154 @@
"""
Celery Task Monitoring Admin
"""
from django.contrib import admin
from django.utils.html import format_html
from django.contrib import messages
from django_celery_results.models import TaskResult
from rangefilter.filters import DateRangeFilter
class CeleryTaskResultAdmin(admin.ModelAdmin):
"""Admin interface for monitoring Celery tasks"""
list_display = [
'task_id',
'task_name',
'colored_status',
'date_created',
'date_done',
'execution_time',
]
list_filter = [
'status',
'task_name',
('date_created', DateRangeFilter),
('date_done', DateRangeFilter),
]
search_fields = ['task_id', 'task_name', 'task_args']
readonly_fields = [
'task_id', 'task_name', 'task_args', 'task_kwargs',
'result', 'traceback', 'date_created', 'date_done',
'colored_status', 'execution_time'
]
date_hierarchy = 'date_created'
ordering = ['-date_created']
actions = ['retry_failed_tasks', 'clear_old_tasks']
fieldsets = (
('Task Information', {
'fields': ('task_id', 'task_name', 'colored_status')
}),
('Execution Details', {
'fields': ('date_created', 'date_done', 'execution_time')
}),
('Task Arguments', {
'fields': ('task_args', 'task_kwargs'),
'classes': ('collapse',)
}),
('Result & Errors', {
'fields': ('result', 'traceback'),
'classes': ('collapse',)
}),
)
def colored_status(self, obj):
"""Display status with color coding"""
colors = {
'SUCCESS': '#0bbf87', # IGNY8 success green
'FAILURE': '#ef4444', # IGNY8 danger red
'PENDING': '#ff7a00', # IGNY8 warning orange
'STARTED': '#0693e3', # IGNY8 primary blue
'RETRY': '#5d4ae3', # IGNY8 purple
}
color = colors.get(obj.status, '#64748b') # Default gray
return format_html(
'<span style="color: {}; font-weight: bold; font-size: 14px;">{}</span>',
color,
obj.status
)
colored_status.short_description = 'Status'
def execution_time(self, obj):
"""Calculate and display execution time"""
if obj.date_done and obj.date_created:
duration = obj.date_done - obj.date_created
seconds = duration.total_seconds()
if seconds < 1:
return format_html('<span style="color: #0bbf87;">{:.2f}ms</span>', seconds * 1000)
elif seconds < 60:
return format_html('<span style="color: #0693e3;">{:.2f}s</span>', seconds)
else:
minutes = seconds / 60
return format_html('<span style="color: #ff7a00;">{:.1f}m</span>', minutes)
return '-'
execution_time.short_description = 'Duration'
def retry_failed_tasks(self, request, queryset):
"""Retry failed celery tasks"""
from celery import current_app
failed_tasks = queryset.filter(status='FAILURE')
count = 0
errors = []
for task in failed_tasks:
try:
# Get task function
task_func = current_app.tasks.get(task.task_name)
if task_func:
# Parse task args and kwargs
import ast
try:
args = ast.literal_eval(task.task_args) if task.task_args else []
kwargs = ast.literal_eval(task.task_kwargs) if task.task_kwargs else {}
except:
args = []
kwargs = {}
# Retry the task
task_func.apply_async(args=args, kwargs=kwargs)
count += 1
else:
errors.append(f'Task function not found: {task.task_name}')
except Exception as e:
errors.append(f'Error retrying {task.task_id}: {str(e)}')
if count > 0:
self.message_user(request, f'✅ Retried {count} failed task(s)', messages.SUCCESS)
if errors:
for error in errors[:5]: # Show max 5 errors
self.message_user(request, f'⚠️ {error}', messages.WARNING)
retry_failed_tasks.short_description = '🔄 Retry Failed Tasks'
def clear_old_tasks(self, request, queryset):
"""Clear old completed tasks"""
from datetime import timedelta
from django.utils import timezone
# Delete tasks older than 30 days
cutoff_date = timezone.now() - timedelta(days=30)
old_tasks = queryset.filter(
date_created__lt=cutoff_date,
status__in=['SUCCESS', 'FAILURE']
)
count = old_tasks.count()
old_tasks.delete()
self.message_user(request, f'🗑️ Cleared {count} old task(s)', messages.SUCCESS)
clear_old_tasks.short_description = '🗑️ Clear Old Tasks (30+ days)'
def has_add_permission(self, request):
"""Disable manual task creation"""
return False
def has_change_permission(self, request, obj=None):
"""Make read-only"""
return False

View File

@@ -0,0 +1,112 @@
"""
Custom Admin Dashboard with Key Metrics
"""
from django.contrib.admin.views.decorators import staff_member_required
from django.shortcuts import render
from django.db.models import Count, Sum, Q
from django.utils import timezone
from datetime import timedelta
@staff_member_required
def admin_dashboard(request):
"""Custom admin dashboard with operational metrics"""
# Date ranges
today = timezone.now().date()
week_ago = today - timedelta(days=7)
month_ago = today - timedelta(days=30)
# Account metrics
from igny8_core.auth.models import Account
total_accounts = Account.objects.count()
active_accounts = Account.objects.filter(status='active').count()
low_credit_accounts = Account.objects.filter(
status='active',
credits__lt=100
).count()
# Content metrics
from igny8_core.modules.writer.models import Content, Tasks
content_this_week = Content.objects.filter(created_at__gte=week_ago).count()
content_this_month = Content.objects.filter(created_at__gte=month_ago).count()
tasks_pending = Tasks.objects.filter(status='pending').count()
tasks_in_progress = Tasks.objects.filter(status='in_progress').count()
# Billing metrics
from igny8_core.business.billing.models import Payment, CreditTransaction
pending_payments = Payment.objects.filter(status='pending_approval').count()
payments_this_month = Payment.objects.filter(
created_at__gte=month_ago,
status='succeeded'
).aggregate(total=Sum('amount'))['total'] or 0
credit_usage_this_month = CreditTransaction.objects.filter(
created_at__gte=month_ago,
transaction_type='deduction'
).aggregate(total=Sum('amount'))['total'] or 0
# Automation metrics
from igny8_core.business.automation.models import AutomationRun
automation_running = AutomationRun.objects.filter(status='running').count()
automation_failed = AutomationRun.objects.filter(
status='failed',
started_at__gte=week_ago
).count()
# WordPress sync metrics
from igny8_core.business.integration.models import SyncEvent
sync_failed_today = SyncEvent.objects.filter(
success=False,
created_at__date=today
).count()
# Celery task metrics
try:
from django_celery_results.models import TaskResult
celery_failed = TaskResult.objects.filter(
status='FAILURE',
date_created__date=today
).count()
celery_pending = TaskResult.objects.filter(status='PENDING').count()
except:
celery_failed = 0
celery_pending = 0
# Get alerts
from .alerts import AdminAlerts
alerts = AdminAlerts.get_alerts()
context = {
'title': 'IGNY8 Dashboard',
'accounts': {
'total': total_accounts,
'active': active_accounts,
'low_credit': low_credit_accounts,
},
'content': {
'this_week': content_this_week,
'this_month': content_this_month,
'tasks_pending': tasks_pending,
'tasks_in_progress': tasks_in_progress,
},
'billing': {
'pending_payments': pending_payments,
'payments_this_month': float(payments_this_month),
'credit_usage_this_month': abs(credit_usage_this_month),
},
'automation': {
'running': automation_running,
'failed_this_week': automation_failed,
},
'integration': {
'sync_failed_today': sync_failed_today,
},
'celery': {
'failed_today': celery_failed,
'pending': celery_pending,
},
'alerts': alerts,
}
return render(request, 'admin/dashboard.html', context)

View File

@@ -4,6 +4,8 @@ Custom AdminSite for IGNY8 to organize models into proper groups
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.apps import AdminConfig from django.contrib.admin.apps import AdminConfig
from django.apps import apps from django.apps import apps
from django.urls import path
from django.shortcuts import redirect
class Igny8AdminSite(admin.AdminSite): class Igny8AdminSite(admin.AdminSite):
@@ -20,6 +22,19 @@ class Igny8AdminSite(admin.AdminSite):
site_header = 'IGNY8 Administration' site_header = 'IGNY8 Administration'
site_title = 'IGNY8 Admin' site_title = 'IGNY8 Admin'
index_title = 'IGNY8 Administration' index_title = 'IGNY8 Administration'
def get_urls(self):
"""Add dashboard URL"""
urls = super().get_urls()
custom_urls = [
path('dashboard/', self.dashboard_view, name='dashboard'),
]
return custom_urls + urls
def dashboard_view(self, request):
"""Dashboard view wrapper"""
from igny8_core.admin.dashboard import admin_dashboard
return admin_dashboard(request)
def get_app_list(self, request): def get_app_list(self, request):
""" """
@@ -113,7 +128,13 @@ class Igny8AdminSite(admin.AdminSite):
('system', 'SystemStatus'), ('system', 'SystemStatus'),
], ],
}, },
'🔧 Django System': { '<EFBFBD> Monitoring & Tasks': {
'models': [
('django_celery_results', 'TaskResult'),
('django_celery_results', 'GroupResult'),
],
},
'<EFBFBD>🔧 Django System': {
'models': [ 'models': [
('admin', 'LogEntry'), ('admin', 'LogEntry'),
('auth', 'Group'), ('auth', 'Group'),

View File

@@ -145,10 +145,10 @@ class PlanAdmin(admin.ModelAdmin):
@admin.register(Account) @admin.register(Account)
class AccountAdmin(AccountAdminMixin, admin.ModelAdmin): class AccountAdmin(AccountAdminMixin, admin.ModelAdmin):
form = AccountAdminForm form = AccountAdminForm
list_display = ['name', 'slug', 'owner', 'plan', 'status', 'credits', 'created_at'] list_display = ['name', 'slug', 'owner', 'plan', 'status', 'health_indicator', 'credits', 'created_at']
list_filter = ['status', 'plan'] list_filter = ['status', 'plan']
search_fields = ['name', 'slug'] search_fields = ['name', 'slug']
readonly_fields = ['created_at', 'updated_at'] readonly_fields = ['created_at', 'updated_at', 'health_indicator', 'health_details']
def get_queryset(self, request): def get_queryset(self, request):
"""Override to filter by account for non-superusers""" """Override to filter by account for non-superusers"""
@@ -167,6 +167,137 @@ class AccountAdmin(AccountAdminMixin, admin.ModelAdmin):
# If account access fails (e.g., column mismatch), return empty # If account access fails (e.g., column mismatch), return empty
pass pass
return qs.none() return qs.none()
def health_indicator(self, obj):
"""Display health status with visual indicator"""
from django.utils.html import format_html
from django.utils import timezone
from datetime import timedelta
# Check credits
if obj.credits < 10:
status = 'critical'
icon = '🔴'
message = 'Critical: Very low credits'
elif obj.credits < 100:
status = 'warning'
icon = '⚠️'
message = 'Warning: Low credits'
else:
status = 'good'
icon = ''
message = 'Good'
# Check for recent failed automations
try:
from igny8_core.business.automation.models import AutomationRun
week_ago = timezone.now() - timedelta(days=7)
failed_runs = AutomationRun.objects.filter(
account=obj,
status='failed',
created_at__gte=week_ago
).count()
if failed_runs > 5:
status = 'critical'
icon = '🔴'
message = f'Critical: {failed_runs} automation failures'
elif failed_runs > 0:
if status == 'good':
status = 'warning'
icon = '⚠️'
message = f'Warning: {failed_runs} automation failures'
except:
pass
# Check account status
if obj.status != 'active':
status = 'critical'
icon = '🔴'
message = f'Critical: Account {obj.status}'
colors = {
'good': '#0bbf87',
'warning': '#ff7a00',
'critical': '#ef4444'
}
return format_html(
'<span style="font-size: 16px;">{}</span> <span style="color: {}; font-weight: 600;">{}</span>',
icon, colors[status], message
)
health_indicator.short_description = 'Health'
def health_details(self, obj):
"""Detailed health information"""
from django.utils.html import format_html
from django.utils import timezone
from datetime import timedelta
details = []
# Credits status
if obj.credits < 10:
details.append(f'🔴 <b>Critical:</b> Only {obj.credits} credits remaining')
elif obj.credits < 100:
details.append(f'⚠️ <b>Warning:</b> Only {obj.credits} credits remaining')
else:
details.append(f'✅ <b>Credits:</b> {obj.credits} available')
# Recent activity
try:
from igny8_core.modules.writer.models import Content
week_ago = timezone.now() - timedelta(days=7)
recent_content = Content.objects.filter(
site__account=obj,
created_at__gte=week_ago
).count()
details.append(f'📚 <b>Activity:</b> {recent_content} content pieces created this week')
except:
pass
# Failed automations
try:
from igny8_core.business.automation.models import AutomationRun
week_ago = timezone.now() - timedelta(days=7)
failed_runs = AutomationRun.objects.filter(
account=obj,
status='failed',
created_at__gte=week_ago
).count()
if failed_runs > 0:
details.append(f'🔴 <b>Automations:</b> {failed_runs} failures this week')
else:
details.append(f'✅ <b>Automations:</b> No failures this week')
except:
pass
# Failed syncs
try:
from igny8_core.business.integration.models import SyncEvent
today = timezone.now().date()
failed_syncs = SyncEvent.objects.filter(
site__account=obj,
success=False,
created_at__date=today
).count()
if failed_syncs > 0:
details.append(f'⚠️ <b>Syncs:</b> {failed_syncs} failures today')
else:
details.append(f'✅ <b>Syncs:</b> No failures today')
except:
pass
# Account status
if obj.status == 'active':
details.append(f'✅ <b>Status:</b> Active')
else:
details.append(f'🔴 <b>Status:</b> {obj.status.title()}')
return format_html('<br>'.join(details))
health_details.short_description = 'Health Details'
def has_delete_permission(self, request, obj=None): def has_delete_permission(self, request, obj=None):
if obj and getattr(obj, 'slug', '') == 'aws-admin': if obj and getattr(obj, 'slug', '') == 'aws-admin':

View File

@@ -1,6 +1,18 @@
from django.contrib import admin from django.contrib import admin
from django.contrib import messages
from igny8_core.admin.base import SiteSectorAdminMixin from igny8_core.admin.base import SiteSectorAdminMixin
from .models import Keywords, Clusters, ContentIdeas from .models import Keywords, Clusters, ContentIdeas
from import_export.admin import ExportMixin
from import_export import resources
class KeywordsResource(resources.ModelResource):
"""Resource class for exporting Keywords"""
class Meta:
model = Keywords
fields = ('id', 'keyword', 'seed_keyword__keyword', 'site__name', 'sector__name',
'cluster__name', 'volume', 'difficulty', 'intent', 'status', 'created_at')
export_order = fields
@admin.register(Clusters) @admin.register(Clusters)
@@ -9,6 +21,7 @@ class ClustersAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
list_filter = ['status', 'site', 'sector'] list_filter = ['status', 'site', 'sector']
search_fields = ['name'] search_fields = ['name']
ordering = ['name'] ordering = ['name']
autocomplete_fields = ['site', 'sector']
def get_site_display(self, obj): def get_site_display(self, obj):
"""Safely get site name""" """Safely get site name"""
@@ -27,11 +40,18 @@ class ClustersAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
@admin.register(Keywords) @admin.register(Keywords)
class KeywordsAdmin(SiteSectorAdminMixin, admin.ModelAdmin): class KeywordsAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
resource_class = KeywordsResource
list_display = ['keyword', 'seed_keyword', 'site', 'sector', 'cluster', 'volume', 'difficulty', 'intent', 'status', 'created_at'] list_display = ['keyword', 'seed_keyword', 'site', 'sector', 'cluster', 'volume', 'difficulty', 'intent', 'status', 'created_at']
list_filter = ['status', 'seed_keyword__intent', 'site', 'sector', 'seed_keyword__industry', 'seed_keyword__sector'] list_filter = ['status', 'seed_keyword__intent', 'site', 'sector', 'seed_keyword__industry', 'seed_keyword__sector']
search_fields = ['seed_keyword__keyword'] search_fields = ['seed_keyword__keyword']
ordering = ['-created_at'] ordering = ['-created_at']
autocomplete_fields = ['cluster', 'site', 'sector', 'seed_keyword']
actions = [
'bulk_assign_cluster',
'bulk_set_status_active',
'bulk_set_status_inactive',
]
def get_site_display(self, obj): def get_site_display(self, obj):
"""Safely get site name""" """Safely get site name"""
@@ -55,6 +75,58 @@ class KeywordsAdmin(SiteSectorAdminMixin, admin.ModelAdmin):
except: except:
return '-' return '-'
get_cluster_display.short_description = 'Cluster' get_cluster_display.short_description = 'Cluster'
def bulk_assign_cluster(self, request, queryset):
"""Assign selected keywords to a cluster"""
from django import forms
# If this is the POST request with cluster selection
if 'apply' in request.POST:
cluster_id = request.POST.get('cluster')
if cluster_id:
cluster = Clusters.objects.get(pk=cluster_id)
updated = queryset.update(cluster=cluster)
self.message_user(request, f'{updated} keyword(s) assigned to cluster: {cluster.name}', messages.SUCCESS)
return
# Get first keyword's site/sector for filtering clusters
first_keyword = queryset.first()
if first_keyword:
clusters = Clusters.objects.filter(site=first_keyword.site, sector=first_keyword.sector)
else:
clusters = Clusters.objects.all()
# Create form for cluster selection
class ClusterForm(forms.Form):
cluster = forms.ModelChoiceField(
queryset=clusters,
label="Select Cluster",
help_text=f"Assign {queryset.count()} selected keyword(s) to:"
)
if clusters.exists():
from django.shortcuts import render
return render(request, 'admin/bulk_action_form.html', {
'title': 'Assign Keywords to Cluster',
'queryset': queryset,
'form': ClusterForm(),
'action': 'bulk_assign_cluster',
})
else:
self.message_user(request, 'No clusters available for the selected keywords.', messages.WARNING)
bulk_assign_cluster.short_description = 'Assign to Cluster'
def bulk_set_status_active(self, request, queryset):
"""Set selected keywords to active status"""
updated = queryset.update(status='active')
self.message_user(request, f'{updated} keyword(s) set to active.', messages.SUCCESS)
bulk_set_status_active.short_description = 'Set status to Active'
def bulk_set_status_inactive(self, request, queryset):
"""Set selected keywords to inactive status"""
updated = queryset.update(status='inactive')
self.message_user(request, f'{updated} keyword(s) set to inactive.', messages.SUCCESS)
bulk_set_status_inactive.short_description = 'Set status to Inactive'
@admin.register(ContentIdeas) @admin.register(ContentIdeas)

View File

@@ -33,7 +33,13 @@ class TasksAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
search_fields = ['title', 'description'] search_fields = ['title', 'description']
ordering = ['-created_at'] ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at'] readonly_fields = ['created_at', 'updated_at']
actions = ['bulk_set_status_draft', 'bulk_set_status_in_progress', 'bulk_set_status_completed'] autocomplete_fields = ['cluster', 'site', 'sector']
actions = [
'bulk_set_status_draft',
'bulk_set_status_in_progress',
'bulk_set_status_completed',
'bulk_assign_cluster',
]
fieldsets = ( fieldsets = (
('Basic Info', { ('Basic Info', {
@@ -69,6 +75,47 @@ class TasksAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
self.message_user(request, f'{updated} task(s) set to completed.', messages.SUCCESS) self.message_user(request, f'{updated} task(s) set to completed.', messages.SUCCESS)
bulk_set_status_completed.short_description = 'Set status to Completed' bulk_set_status_completed.short_description = 'Set status to Completed'
def bulk_assign_cluster(self, request, queryset):
"""Assign selected tasks to a cluster - requires form input"""
from django import forms
from igny8_core.modules.planner.models import Clusters
# If this is the POST request with cluster selection
if 'apply' in request.POST:
cluster_id = request.POST.get('cluster')
if cluster_id:
cluster = Clusters.objects.get(pk=cluster_id)
updated = queryset.update(cluster=cluster)
self.message_user(request, f'{updated} task(s) assigned to cluster: {cluster.name}', messages.SUCCESS)
return
# Get first task's site/sector for filtering clusters
first_task = queryset.first()
if first_task:
clusters = Clusters.objects.filter(site=first_task.site, sector=first_task.sector)
else:
clusters = Clusters.objects.all()
# Create form for cluster selection
class ClusterForm(forms.Form):
cluster = forms.ModelChoiceField(
queryset=clusters,
label="Select Cluster",
help_text=f"Assign {queryset.count()} selected task(s) to:"
)
if clusters.exists():
from django.shortcuts import render
return render(request, 'admin/bulk_action_form.html', {
'title': 'Assign Tasks to Cluster',
'queryset': queryset,
'form': ClusterForm(),
'action': 'bulk_assign_cluster',
})
else:
self.message_user(request, 'No clusters available for the selected tasks.', messages.WARNING)
bulk_assign_cluster.short_description = 'Assign to Cluster'
def get_site_display(self, obj): def get_site_display(self, obj):
"""Safely get site name""" """Safely get site name"""
try: try:
@@ -143,8 +190,13 @@ class ContentAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
search_fields = ['title', 'content_html', 'external_url'] search_fields = ['title', 'content_html', 'external_url']
ordering = ['-created_at'] ordering = ['-created_at']
readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display'] readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display']
autocomplete_fields = ['cluster', 'site', 'sector']
inlines = [ContentTaxonomyInline] inlines = [ContentTaxonomyInline]
actions = ['bulk_set_status_published', 'bulk_set_status_draft'] actions = [
'bulk_set_status_published',
'bulk_set_status_draft',
'bulk_add_taxonomy',
]
fieldsets = ( fieldsets = (
('Basic Info', { ('Basic Info', {
@@ -208,6 +260,55 @@ class ContentAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin):
self.message_user(request, f'{updated} content item(s) set to draft.', messages.SUCCESS) self.message_user(request, f'{updated} content item(s) set to draft.', messages.SUCCESS)
bulk_set_status_draft.short_description = 'Set status to Draft' bulk_set_status_draft.short_description = 'Set status to Draft'
def bulk_add_taxonomy(self, request, queryset):
"""Add taxonomy terms to selected content"""
from django import forms
from igny8_core.business.content.models import ContentTaxonomy, ContentTaxonomyRelation
# If this is the POST request with taxonomy selection
if 'apply' in request.POST:
taxonomy_ids = request.POST.getlist('taxonomies')
if taxonomy_ids:
count = 0
for content in queryset:
for tax_id in taxonomy_ids:
taxonomy = ContentTaxonomy.objects.get(pk=tax_id)
ContentTaxonomyRelation.objects.get_or_create(
content=content,
taxonomy=taxonomy
)
count += 1
self.message_user(request, f'Added {count} taxonomy relation(s) to {queryset.count()} content item(s).', messages.SUCCESS)
return
# Get first content's site/sector for filtering taxonomies
first_content = queryset.first()
if first_content:
taxonomies = ContentTaxonomy.objects.filter(site=first_content.site, sector=first_content.sector)
else:
taxonomies = ContentTaxonomy.objects.all()
# Create form for taxonomy selection
class TaxonomyForm(forms.Form):
taxonomies = forms.ModelMultipleChoiceField(
queryset=taxonomies,
label="Select Taxonomies",
help_text=f"Add taxonomy terms to {queryset.count()} selected content item(s)",
widget=forms.CheckboxSelectMultiple
)
if taxonomies.exists():
from django.shortcuts import render
return render(request, 'admin/bulk_action_form.html', {
'title': 'Add Taxonomies to Content',
'queryset': queryset,
'form': TaxonomyForm(),
'action': 'bulk_add_taxonomy',
})
else:
self.message_user(request, 'No taxonomies available for the selected content.', messages.WARNING)
bulk_add_taxonomy.short_description = 'Add Taxonomy Terms'
def get_site_display(self, obj): def get_site_display(self, obj):
"""Safely get site name""" """Safely get site name"""
try: try:

View File

@@ -1,13 +1,17 @@
/* =================================================================== /* ===================================================================
IGNY8 CUSTOM ADMIN STYLES IGNY8 CUSTOM ADMIN STYLES - COMPLETE REDESIGN
=================================================================== ===================================================================
Using IGNY8 brand colors from frontend design system Using exact IGNY8 brand colors from frontend design system
=================================================================== */ =================================================================== */
/* IGNY8 Brand Color Variables */ /* IGNY8 Brand Color Variables - Matching Frontend App */
:root { :root {
/* Primary Colors */
--igny8-primary: #0693e3; /* Primary brand blue */ --igny8-primary: #0693e3; /* Primary brand blue */
--igny8-primary-dark: #0472b8; /* Primary dark */ --igny8-primary-dark: #0472b8; /* Primary dark */
--igny8-primary-light: #3da9e8; /* Primary light */
/* Accent Colors */
--igny8-success: #0bbf87; /* Success teal-green */ --igny8-success: #0bbf87; /* Success teal-green */
--igny8-success-dark: #08966b; /* Success dark */ --igny8-success-dark: #08966b; /* Success dark */
--igny8-warning: #ff7a00; /* Warning orange */ --igny8-warning: #ff7a00; /* Warning orange */
@@ -16,34 +20,560 @@
--igny8-danger-dark: #d13333; /* Danger dark */ --igny8-danger-dark: #d13333; /* Danger dark */
--igny8-purple: #5d4ae3; /* Purple accent */ --igny8-purple: #5d4ae3; /* Purple accent */
--igny8-purple-dark: #3a2f94; /* Purple dark */ --igny8-purple-dark: #3a2f94; /* Purple dark */
/* Neutral Colors */
--igny8-navy: #0d1b2a; /* Dark navy background */ --igny8-navy: #0d1b2a; /* Dark navy background */
--igny8-navy-light: #142b3f; /* Navy light */ --igny8-navy-light: #1a2e44; /* Navy light */
--igny8-surface: #f8fafc; /* Page background */ --igny8-surface: #f8fafc; /* Page background */
--igny8-panel: #ffffff; /* Panel background */ --igny8-panel: #ffffff; /* Panel background */
--igny8-text: #555a68; /* Main text */ --igny8-text: #1e293b; /* Main text */
--igny8-text-dim: #64748b; /* Dimmed text */ --igny8-text-light: #64748b; /* Light text */
--igny8-text-dim: #94a3b8; /* Dimmed text */
--igny8-stroke: #e2e8f0; /* Borders */ --igny8-stroke: #e2e8f0; /* Borders */
--igny8-stroke-dark: #cbd5e1; /* Dark borders */
} }
/* =================================================================== /* ===================================================================
HEADER & BRANDING - IGNY8 Primary Blue with Gradient GLOBAL RESETS
=================================================================== */
body {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
/* ===================================================================
HEADER - Clean Professional Design
=================================================================== */ =================================================================== */
#header { #header {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important; background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important; color: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
padding: 0 !important;
} }
#header a:link, #header a:visited { #branding {
padding: 16px 30px !important;
}
#branding h1 {
margin: 0 !important;
font-size: 20px !important;
font-weight: 600 !important;
}
#branding h1 a:link,
#branding h1 a:visited {
color: white !important;
text-decoration: none !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
#header a:link,
#header a:visited {
color: white !important; color: white !important;
} }
#branding h1, #branding h1 a:link, #branding h1 a:visited { /* ===================================================================
color: white !important; ADD RECORD BUTTON - Modern Accent Color
=================================================================== */
.object-tools {
margin-bottom: 24px !important;
float: right !important;
} }
.header-user-tools a { .object-tools li {
margin: 0 !important;
list-style: none !important;
}
.object-tools a.addlink {
background: linear-gradient(135deg, var(--igny8-success) 0%, var(--igny8-success-dark) 100%) !important;
color: white !important; color: white !important;
padding: 12px 28px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
gap: 10px !important;
box-shadow: 0 2px 6px rgba(11, 191, 135, 0.3) !important;
transition: all 0.2s ease !important;
border: none !important;
text-transform: none !important;
}
.object-tools a.addlink:before {
content: "\f067" !important;
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
font-size: 14px !important;
margin-right: 0 !important;
background: none !important;
border: none !important;
width: auto !important;
height: auto !important;
}
.object-tools a.addlink:hover {
background: linear-gradient(135deg, var(--igny8-success-dark) 0%, #067354 100%) !important;
box-shadow: 0 4px 12px rgba(11, 191, 135, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
EXPORT BUTTON - Purple Accent
=================================================================== */
a[href*="export"],
.export-button,
input[name="_export"] {
background: linear-gradient(135deg, var(--igny8-purple) 0%, var(--igny8-purple-dark) 100%) !important;
color: white !important;
padding: 10px 24px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 13px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
box-shadow: 0 2px 6px rgba(93, 74, 227, 0.3) !important;
transition: all 0.2s ease !important;
border: none !important;
cursor: pointer !important;
}
a[href*="export"]:hover,
.export-button:hover,
input[name="_export"]:hover {
background: linear-gradient(135deg, var(--igny8-purple-dark) 0%, #2a1f6b 100%) !important;
box-shadow: 0 4px 12px rgba(93, 74, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
SEARCH BAR & TOOLBAR - Professional Layout
=================================================================== */
#toolbar {
padding: 20px 24px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
margin-bottom: 24px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#toolbar form {
display: flex !important;
gap: 12px !important;
align-items: center !important;
}
#toolbar input[type="text"],
#searchbar {
flex: 1 !important;
min-width: 320px !important;
max-width: 500px !important;
padding: 12px 18px !important;
border: 1.5px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
background: var(--igny8-panel) !important;
color: var(--igny8-text) !important;
}
#toolbar input[type="text"]:focus,
#searchbar:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 4px rgba(6, 147, 227, 0.1) !important;
}
#toolbar input[type="submit"],
#toolbar button[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 12px 28px !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 6px rgba(6, 147, 227, 0.3) !important;
}
#toolbar input[type="submit"]:hover,
#toolbar button[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 12px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
ACTIONS BAR - Better Styling
=================================================================== */
#changelist-form .actions {
padding: 20px 24px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
margin-bottom: 24px !important;
display: flex !important;
align-items: center !important;
gap: 16px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#changelist-form .actions label {
font-weight: 600 !important;
color: var(--igny8-text) !important;
font-size: 14px !important;
margin-right: 0 !important;
}
#changelist-form .actions select {
min-width: 240px !important;
padding: 12px 18px !important;
border: 1.5px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
background: var(--igny8-panel) !important;
color: var(--igny8-text) !important;
}
#changelist-form .actions select:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 4px rgba(6, 147, 227, 0.1) !important;
}
#changelist-form .actions button,
#changelist-form .actions input[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 12px 28px !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 6px rgba(6, 147, 227, 0.3) !important;
}
#changelist-form .actions button:hover,
#changelist-form .actions input[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 12px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
FILTERS PANEL - Clear Organization
=================================================================== */
#changelist-filter {
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
padding: 0 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#changelist-filter h2 {
background: linear-gradient(135deg, var(--igny8-navy) 0%, var(--igny8-navy-light) 100%) !important;
color: white !important;
padding: 18px 24px !important;
margin: 0 !important;
font-size: 16px !important;
font-weight: 600 !important;
border-radius: 12px 12px 0 0 !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
#changelist-filter h2:before {
content: "\f0b0" !important;
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
}
#changelist-filter h3 {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
padding: 14px 24px !important;
margin: 0 !important;
font-size: 14px !important;
font-weight: 600 !important;
border-top: 1px solid var(--igny8-stroke) !important;
border-bottom: 1px solid var(--igny8-stroke) !important;
}
#changelist-filter ul {
padding: 16px 24px !important;
margin: 0 !important;
}
#changelist-filter li {
padding: 0 !important;
margin: 0 0 8px 0 !important;
list-style: none !important;
}
#changelist-filter a {
color: var(--igny8-text) !important;
text-decoration: none !important;
display: block !important;
padding: 10px 16px !important;
border-radius: 6px !important;
transition: all 0.2s ease !important;
font-size: 14px !important;
}
#changelist-filter a:hover {
background: var(--igny8-surface) !important;
color: var(--igny8-primary) !important;
}
#changelist-filter a.selected {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
font-weight: 600 !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2) !important;
}
#changelist-filter .quiet {
color: var(--igny8-text-dim) !important;
font-size: 13px !important;
}
/* ===================================================================
ADD RECORD BUTTON - Clean Professional Style
=================================================================== */
.object-tools {
margin-bottom: 20px !important;
}
.object-tools li {
margin: 0 !important;
}
.object-tools a.addlink {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 12px 24px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.3) !important;
transition: all 0.2s ease !important;
border: none !important;
}
.object-tools a.addlink:before {
content: "+" !important;
font-size: 18px !important;
font-weight: bold !important;
margin-right: 0 !important;
background: none !important;
border: none !important;
width: auto !important;
height: auto !important;
}
.object-tools a.addlink:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-1px) !important;
}
/* Remove the background from the + icon */
.object-tools a.addlink:before {
background: none !important;
}
/* ===================================================================
SEARCH & ACTION BAR - Better Sizing and Layout
=================================================================== */
#toolbar {
padding: 16px 20px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
display: flex !important;
gap: 16px !important;
align-items: center !important;
flex-wrap: wrap !important;
}
#toolbar form {
display: flex !important;
gap: 12px !important;
align-items: center !important;
flex: 1 !important;
max-width: 600px !important;
}
#toolbar input[type="text"] {
flex: 1 !important;
min-width: 250px !important;
padding: 10px 16px !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 6px !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
}
#toolbar input[type="text"]:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(6, 147, 227, 0.1) !important;
}
#toolbar input[type="submit"],
#toolbar button[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 10px 24px !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2) !important;
}
#toolbar input[type="submit"]:hover,
#toolbar button[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.3) !important;
transform: translateY(-1px) !important;
}
/* Action dropdown */
#changelist-form .actions {
padding: 16px 20px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
}
#changelist-form .actions select {
min-width: 220px !important;
padding: 10px 16px !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 6px !important;
font-size: 14px !important;
margin-right: 12px !important;
transition: all 0.2s ease !important;
}
#changelist-form .actions select:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(6, 147, 227, 0.1) !important;
}
#changelist-form .actions button,
#changelist-form .actions input[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 10px 24px !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2) !important;
}
#changelist-form .actions button:hover,
#changelist-form .actions input[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.3) !important;
transform: translateY(-1px) !important;
}
/* ===================================================================
FILTERS - Clear Labels and Better Organization
=================================================================== */
#changelist-filter {
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
padding: 0 !important;
}
#changelist-filter h2 {
background: linear-gradient(135deg, var(--igny8-navy) 0%, var(--igny8-navy-light) 100%) !important;
color: white !important;
padding: 16px 20px !important;
margin: 0 !important;
font-size: 16px !important;
font-weight: 600 !important;
border-radius: 8px 8px 0 0 !important;
}
#changelist-filter h3 {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
padding: 12px 20px !important;
margin: 0 !important;
font-size: 14px !important;
font-weight: 600 !important;
border-top: 1px solid var(--igny8-stroke) !important;
border-bottom: 1px solid var(--igny8-stroke) !important;
}
#changelist-filter ul {
padding: 12px 20px !important;
margin: 0 0 16px 0 !important;
}
#changelist-filter li {
padding: 8px 0 !important;
margin: 0 !important;
list-style: none !important;
}
#changelist-filter a {
color: var(--igny8-text) !important;
text-decoration: none !important;
display: block !important;
padding: 6px 12px !important;
border-radius: 4px !important;
transition: all 0.2s ease !important;
font-size: 14px !important;
}
#changelist-filter a:hover {
background: var(--igny8-surface) !important;
color: var(--igny8-primary) !important;
}
#changelist-filter a.selected {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
font-weight: 600 !important;
}
#changelist-filter .quiet {
color: var(--igny8-text-dim) !important;
font-size: 13px !important;
} }
/* =================================================================== /* ===================================================================
@@ -271,6 +801,217 @@
background: var(--igny8-danger-dark) !important; background: var(--igny8-danger-dark) !important;
} }
/* ===================================================================
SIDEBAR MODULE "ADD" LINKS - Icon Only with Theme Colors
=================================================================== */
.module .addlink,
.module a.addlink,
#content-main .module .addlink {
color: var(--igny8-text) !important;
text-decoration: none !important;
font-size: 0 !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.2s ease !important;
}
.module .addlink:before,
.module a.addlink:before {
content: "\f067" !important;
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
font-size: 12px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 24px !important;
height: 24px !important;
border-radius: 6px !important;
background: linear-gradient(135deg, var(--igny8-success) 0%, var(--igny8-success-dark) 100%) !important;
color: white !important;
margin: 0 !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(11, 191, 135, 0.2) !important;
}
.module .addlink:hover:before,
.module a.addlink:hover:before {
background: linear-gradient(135deg, var(--igny8-success-dark) 0%, #067354 100%) !important;
box-shadow: 0 4px 8px rgba(11, 191, 135, 0.3) !important;
transform: translateY(-1px) scale(1.05) !important;
}
/* Module navigation styling */
.module {
margin-bottom: 20px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
overflow: hidden !important;
}
.module h2 {
background: linear-gradient(135deg, var(--igny8-navy) 0%, var(--igny8-navy-light) 100%) !important;
color: white !important;
padding: 14px 16px !important;
font-size: 14px !important;
font-weight: 600 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
.module table {
width: 100% !important;
}
.module tr {
border-bottom: 1px solid var(--igny8-stroke) !important;
}
.module tr:last-child {
border-bottom: none !important;
}
.module th,
.module td {
padding: 12px 16px !important;
text-align: left !important;
}
.module th {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
font-weight: 600 !important;
font-size: 13px !important;
}
.module a {
color: var(--igny8-primary) !important;
text-decoration: none !important;
transition: color 0.2s ease !important;
}
.module a:hover {
color: var(--igny8-primary-dark) !important;
}
/* ===================================================================
ALL BUTTONS - Consistent Theme Colors
=================================================================== */
.button,
input[type=submit],
input[type=button],
.submit-row input,
button:not(.close) {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
border: none !important;
padding: 12px 24px !important;
border-radius: 8px !important;
cursor: pointer !important;
font-weight: 600 !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 6px rgba(6, 147, 227, 0.3) !important;
}
.button:hover,
input[type=submit]:hover,
input[type=button]:hover,
button:not(.close):hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 12px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* Save buttons - Success color */
input[name="_save"],
input[name="_continue"],
.submit-row input[type="submit"]:first-child {
background: linear-gradient(135deg, var(--igny8-success) 0%, var(--igny8-success-dark) 100%) !important;
box-shadow: 0 2px 6px rgba(11, 191, 135, 0.3) !important;
}
input[name="_save"]:hover,
input[name="_continue"]:hover {
background: linear-gradient(135deg, var(--igny8-success-dark) 0%, #067354 100%) !important;
box-shadow: 0 4px 12px rgba(11, 191, 135, 0.4) !important;
}
/* Delete buttons - Danger color */
.deletelink,
.deletelink-box a,
a.deletelink:link,
a.deletelink:visited,
input[name="_delete"],
.delete-confirmation input[type="submit"] {
background: linear-gradient(135deg, var(--igny8-danger) 0%, var(--igny8-danger-dark) 100%) !important;
color: white !important;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3) !important;
}
.deletelink:hover,
.deletelink-box a:hover,
input[name="_delete"]:hover {
background: linear-gradient(135deg, var(--igny8-danger-dark) 0%, #b82222 100%) !important;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important;
}
/* ===================================================================
TABLE IMPROVEMENTS
=================================================================== */
#result_list {
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
overflow: hidden !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#result_list thead th {
background: linear-gradient(135deg, var(--igny8-surface) 0%, #f1f5f9 100%) !important;
color: var(--igny8-text) !important;
padding: 16px 12px !important;
font-weight: 600 !important;
font-size: 13px !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
border-bottom: 2px solid var(--igny8-stroke-dark) !important;
}
#result_list thead th a {
color: var(--igny8-text) !important;
text-decoration: none !important;
}
#result_list tbody tr {
transition: background-color 0.2s ease !important;
}
#result_list tbody tr:nth-child(odd) {
background-color: white !important;
}
#result_list tbody tr:nth-child(even) {
background-color: var(--igny8-surface) !important;
}
#result_list tbody tr:hover {
background-color: rgba(6, 147, 227, 0.05) !important;
}
#result_list td {
padding: 14px 12px !important;
color: var(--igny8-text) !important;
font-size: 14px !important;
border-bottom: 1px solid var(--igny8-stroke) !important;
}
background: var(--igny8-danger-dark) !important;
}
/* =================================================================== /* ===================================================================
LINKS - IGNY8 Primary Blue LINKS - IGNY8 Primary Blue
=================================================================== */ =================================================================== */
@@ -517,3 +1258,46 @@ fieldset.module h2 {
border-left-color: var(--igny8-success); border-left-color: var(--igny8-success);
color: var(--igny8-success-dark); color: var(--igny8-success-dark);
} }
/* ===================================================================
SIDEBAR MODULE ADD LINKS - Clean and Professional
=================================================================== */
.module .addlink,
.module a.addlink,
#content-main .module .addlink {
color: var(--igny8-primary) !important;
text-decoration: none !important;
font-size: 13px !important;
font-weight: 500 !important;
padding: 6px 10px !important;
display: inline-block !important;
border-radius: 4px !important;
transition: all 0.2s ease !important;
}
.module .addlink:before,
.module a.addlink:before {
content: "+" !important;
display: inline-block !important;
width: 18px !important;
height: 18px !important;
line-height: 18px !important;
text-align: center !important;
border-radius: 3px !important;
background: var(--igny8-primary) !important;
color: white !important;
margin-right: 6px !important;
font-size: 14px !important;
font-weight: bold !important;
}
.module .addlink:hover,
.module a.addlink:hover {
background: rgba(6, 147, 227, 0.1) !important;
color: var(--igny8-primary-dark) !important;
}
.module .addlink:hover:before,
.module a.addlink:hover:before {
background: var(--igny8-primary-dark) !important;
}

View File

@@ -3,17 +3,68 @@
{% block title %}{{ title }} | IGNY8 Admin{% endblock %} {% block title %}{{ title }} | IGNY8 Admin{% endblock %}
{% block extrahead %}
{{ block.super }}
<!-- FontAwesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
{% endblock %}
{% block branding %} {% block branding %}
<h1 id="site-name"> <h1 id="site-name">
<a href="{% url 'admin:index' %}"> <a href="{% url 'admin:index' %}">
🚀 IGNY8 Administration <i class="fas fa-rocket"></i> IGNY8 Administration
</a> </a>
</h1> </h1>
{% endblock %} {% endblock %}
{% block userlinks %}
<a href="{% url 'admin:dashboard' %}" class="dashboard-link">
<i class="fas fa-chart-line"></i>
<span>Dashboard</span>
</a>
{{ block.super }}
{% endblock %}
{% block extrastyle %} {% block extrastyle %}
{{ block.super }} {{ block.super }}
<link rel="stylesheet" href="{% static 'admin/css/igny8_admin.css' %}"> <link rel="stylesheet" href="{% static 'admin/css/igny8_admin.css' %}">
<style>
/* Dashboard link in header */
.dashboard-link {
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
padding: 10px 20px !important;
margin-right: 20px !important;
background: rgba(255, 255, 255, 0.15) !important;
color: white !important;
text-decoration: none !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
backdrop-filter: blur(10px) !important;
border: 1px solid rgba(255, 255, 255, 0.2) !important;
}
.dashboard-link:hover {
background: rgba(255, 255, 255, 0.25) !important;
transform: translateY(-1px) !important;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15) !important;
}
.dashboard-link i {
font-size: 16px !important;
}
/* User tools spacing */
#user-tools {
display: flex !important;
align-items: center !important;
gap: 15px !important;
padding: 10px 20px !important;
}
</style>
{% endblock %} {% endblock %}
{% block nav-global %}{% endblock %} {% block nav-global %}{% endblock %}

View File

@@ -0,0 +1,75 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block content %}
<h1>{{ title }}</h1>
<form method="post">
{% csrf_token %}
<div class="form-group" style="margin: 20px 0;">
{{ form.as_p }}
</div>
<div class="submit-row" style="margin-top: 20px;">
<input type="hidden" name="action" value="{{ action }}">
<input type="hidden" name="_selected_action" value="{{ queryset|join:',' }}">
<input type="submit" name="apply" value="Apply" class="button" style="margin-right: 10px;">
<a href="javascript:history.back()" class="button">Cancel</a>
</div>
<fieldset class="module aligned" style="margin-top: 20px;">
<h2>Selected Items ({{ queryset.count }})</h2>
<ul style="list-style: none; padding: 10px;">
{% for item in queryset|slice:":10" %}
<li style="padding: 5px 0; border-bottom: 1px solid var(--igny8-stroke);">
{{ item }}
</li>
{% endfor %}
{% if queryset.count > 10 %}
<li style="padding: 10px 0; font-style: italic; color: var(--igny8-text-dim);">
...and {{ queryset.count|add:"-10" }} more
</li>
{% endif %}
</ul>
</fieldset>
</form>
<style>
.form-group label {
font-weight: bold;
display: block;
margin-bottom: 5px;
color: var(--igny8-text);
}
.form-group select,
.form-group input {
padding: 8px;
border: 1px solid var(--igny8-stroke);
border-radius: 4px;
min-width: 300px;
}
.form-group .helptext {
display: block;
margin-top: 5px;
color: var(--igny8-text-dim);
font-size: 12px;
}
.form-group ul {
list-style: none;
padding: 0;
}
.form-group ul li {
padding: 5px 0;
}
.form-group ul li label {
font-weight: normal;
margin-left: 5px;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,419 @@
{% extends "admin/base_site.html" %}
{% load static %}
{% block title %}IGNY8 Dashboard{% endblock %}
{% block extrahead %}
{{ block.super }}
<style>
.dashboard-container {
padding: 20px;
max-width: 1400px;
margin: 0 auto;
}
.dashboard-header {
background: linear-gradient(135deg, #0d1b2a 0%, #1a2e44 100%);
color: white;
padding: 30px;
border-radius: 12px;
margin-bottom: 30px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.dashboard-header-content h1 {
margin: 0 0 10px 0;
font-size: 32px;
font-weight: 600;
}
.dashboard-header-content p {
margin: 0;
opacity: 0.9;
font-size: 14px;
}
.dashboard-nav {
display: flex;
gap: 12px;
}
.dashboard-nav a {
padding: 10px 20px;
background: rgba(255, 255, 255, 0.15);
color: white !important;
text-decoration: none;
border-radius: 6px;
font-weight: 500;
font-size: 14px;
transition: background 0.2s;
backdrop-filter: blur(10px);
}
.dashboard-nav a:hover {
background: rgba(255, 255, 255, 0.25);
}
.alerts-section {
margin-bottom: 30px;
}
.alert-card {
padding: 16px 20px;
border-radius: 8px;
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.alert-card.error {
background: #fef2f2;
border-left: 4px solid #ef4444;
}
.alert-card.warning {
background: #fff7ed;
border-left: 4px solid #ff7a00;
}
.alert-card.info {
background: #eff6ff;
border-left: 4px solid #0693e3;
}
.alert-content {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.alert-icon {
font-size: 24px;
}
.alert-message {
font-size: 14px;
font-weight: 500;
color: #1f2937;
}
.alert-action {
padding: 8px 16px;
background: white;
border: 1px solid #d1d5db;
border-radius: 6px;
text-decoration: none;
color: #374151;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.alert-action:hover {
background: #f9fafb;
border-color: #9ca3af;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.metric-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-top: 4px solid #0693e3;
}
.metric-card h3 {
margin: 0 0 20px 0;
font-size: 16px;
font-weight: 600;
color: #0d1b2a;
display: flex;
align-items: center;
gap: 8px;
}
.metric-card .icon {
font-size: 20px;
}
.metric-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
}
.metric-row:last-child {
border-bottom: none;
padding-bottom: 0;
}
.metric-label {
font-size: 14px;
color: #6b7280;
}
.metric-value {
font-size: 18px;
font-weight: 600;
color: #1f2937;
}
.metric-value.success {
color: #0bbf87;
}
.metric-value.warning {
color: #ff7a00;
}
.metric-value.error {
color: #ef4444;
}
.metric-value.info {
color: #0693e3;
}
.quick-actions {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.quick-actions h3 {
margin: 0 0 20px 0;
font-size: 18px;
font-weight: 600;
color: #0d1b2a;
}
.action-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
}
.action-button {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
padding: 20px 16px;
background: linear-gradient(135deg, #0693e3 0%, #0d82c8 100%);
color: white !important;
text-decoration: none;
border-radius: 8px;
text-align: center;
font-weight: 600;
font-size: 14px;
transition: transform 0.2s, box-shadow 0.2s;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2);
min-height: 80px;
}
.action-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.3);
color: white !important;
background: linear-gradient(135deg, #0780cb 0%, #0c75b5 100%);
}
.action-button .icon {
font-size: 24px;
}
.action-button .label {
color: white !important;
font-weight: 600;
}
.no-alerts {
text-align: center;
padding: 40px;
color: #6b7280;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.no-alerts .icon {
font-size: 48px;
margin-bottom: 16px;
}
</style>
{% endblock %}
{% block content %}
<div class="dashboard-container">
<div class="dashboard-header">
<div class="dashboard-header-content">
<h1>🚀 IGNY8 Admin Dashboard</h1>
<p>Real-time operational metrics and system health monitoring</p>
</div>
<div class="dashboard-nav">
<a href="/admin/">← Back to Admin</a>
</div>
</div>
{% if alerts %}
<div class="alerts-section">
<h2 style="margin: 0 0 16px 0; font-size: 20px; font-weight: 600;">📢 Active Alerts</h2>
{% for alert in alerts %}
<div class="alert-card {{ alert.level }}">
<div class="alert-content">
<span class="alert-icon">{{ alert.icon }}</span>
<span class="alert-message">{{ alert.message }}</span>
</div>
<a href="{{ alert.url }}" class="alert-action">{{ alert.action }}</a>
</div>
{% endfor %}
</div>
{% else %}
<div class="alerts-section">
<div class="no-alerts">
<div class="icon"></div>
<h3 style="margin: 0 0 8px 0; color: #0bbf87;">All Systems Operational</h3>
<p style="margin: 0;">No active alerts or issues detected</p>
</div>
</div>
{% endif %}
<div class="metrics-grid">
<!-- Accounts Card -->
<div class="metric-card">
<h3><span class="icon">👥</span> Accounts</h3>
<div class="metric-row">
<span class="metric-label">Total Accounts</span>
<span class="metric-value info">{{ accounts.total }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Active Accounts</span>
<span class="metric-value success">{{ accounts.active }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Low Credit Accounts</span>
<span class="metric-value {% if accounts.low_credit > 0 %}warning{% else %}success{% endif %}">{{ accounts.low_credit }}</span>
</div>
</div>
<!-- Content Card -->
<div class="metric-card">
<h3><span class="icon">📚</span> Content</h3>
<div class="metric-row">
<span class="metric-label">Created This Week</span>
<span class="metric-value info">{{ content.this_week }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Created This Month</span>
<span class="metric-value info">{{ content.this_month }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Pending Tasks</span>
<span class="metric-value {% if content.tasks_pending > 50 %}warning{% else %}info{% endif %}">{{ content.tasks_pending }}</span>
</div>
<div class="metric-row">
<span class="metric-label">In Progress</span>
<span class="metric-value info">{{ content.tasks_in_progress }}</span>
</div>
</div>
<!-- Billing Card -->
<div class="metric-card">
<h3><span class="icon">💰</span> Billing</h3>
<div class="metric-row">
<span class="metric-label">Pending Payments</span>
<span class="metric-value {% if billing.pending_payments > 0 %}warning{% else %}success{% endif %}">{{ billing.pending_payments }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Revenue This Month</span>
<span class="metric-value success">${{ billing.payments_this_month|floatformat:2 }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Credits Used This Month</span>
<span class="metric-value info">{{ billing.credit_usage_this_month }}</span>
</div>
</div>
<!-- Automation & Integration Card -->
<div class="metric-card">
<h3><span class="icon">🤖</span> Automation & Sync</h3>
<div class="metric-row">
<span class="metric-label">Automations Running</span>
<span class="metric-value info">{{ automation.running }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Failed This Week</span>
<span class="metric-value {% if automation.failed_this_week > 0 %}error{% else %}success{% endif %}">{{ automation.failed_this_week }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Failed Syncs Today</span>
<span class="metric-value {% if integration.sync_failed_today > 5 %}warning{% elif integration.sync_failed_today > 0 %}info{% else %}success{% endif %}">{{ integration.sync_failed_today }}</span>
</div>
</div>
<!-- Celery Tasks Card -->
<div class="metric-card">
<h3><span class="icon">⚙️</span> Celery Tasks</h3>
<div class="metric-row">
<span class="metric-label">Failed Today</span>
<span class="metric-value {% if celery.failed_today > 0 %}error{% else %}success{% endif %}">{{ celery.failed_today }}</span>
</div>
<div class="metric-row">
<span class="metric-label">Pending Tasks</span>
<span class="metric-value info">{{ celery.pending }}</span>
</div>
</div>
</div>
<div class="quick-actions">
<h3>⚡ Quick Actions</h3>
<div class="action-grid">
<a href="/admin/" class="action-button" style="background: linear-gradient(135deg, #0d1b2a 0%, #1a2e44 100%);">
<span class="icon">🏠</span>
<span class="label">Admin Home</span>
</a>
<a href="/admin/igny8_core_auth/account/" class="action-button">
<span class="icon">👥</span>
<span class="label">Manage Accounts</span>
</a>
<a href="/admin/writer/content/" class="action-button">
<span class="icon">📝</span>
<span class="label">View Content</span>
</a>
<a href="/admin/writer/tasks/" class="action-button">
<span class="icon"></span>
<span class="label">Manage Tasks</span>
</a>
<a href="/admin/billing/payment/" class="action-button">
<span class="icon">💳</span>
<span class="label">Review Payments</span>
</a>
<a href="/admin/automation/automationrun/" class="action-button">
<span class="icon">🤖</span>
<span class="label">View Automations</span>
</a>
<a href="/admin/django_celery_results/taskresult/" class="action-button">
<span class="icon">⚙️</span>
<span class="label">Celery Monitor</span>
</a>
</div>
</div>
</div>
{% endblock %}

View File

@@ -1,13 +1,17 @@
/* =================================================================== /* ===================================================================
IGNY8 CUSTOM ADMIN STYLES IGNY8 CUSTOM ADMIN STYLES - COMPLETE REDESIGN
=================================================================== ===================================================================
Using IGNY8 brand colors from frontend design system Using exact IGNY8 brand colors from frontend design system
=================================================================== */ =================================================================== */
/* IGNY8 Brand Color Variables */ /* IGNY8 Brand Color Variables - Matching Frontend App */
:root { :root {
/* Primary Colors */
--igny8-primary: #0693e3; /* Primary brand blue */ --igny8-primary: #0693e3; /* Primary brand blue */
--igny8-primary-dark: #0472b8; /* Primary dark */ --igny8-primary-dark: #0472b8; /* Primary dark */
--igny8-primary-light: #3da9e8; /* Primary light */
/* Accent Colors */
--igny8-success: #0bbf87; /* Success teal-green */ --igny8-success: #0bbf87; /* Success teal-green */
--igny8-success-dark: #08966b; /* Success dark */ --igny8-success-dark: #08966b; /* Success dark */
--igny8-warning: #ff7a00; /* Warning orange */ --igny8-warning: #ff7a00; /* Warning orange */
@@ -16,34 +20,560 @@
--igny8-danger-dark: #d13333; /* Danger dark */ --igny8-danger-dark: #d13333; /* Danger dark */
--igny8-purple: #5d4ae3; /* Purple accent */ --igny8-purple: #5d4ae3; /* Purple accent */
--igny8-purple-dark: #3a2f94; /* Purple dark */ --igny8-purple-dark: #3a2f94; /* Purple dark */
/* Neutral Colors */
--igny8-navy: #0d1b2a; /* Dark navy background */ --igny8-navy: #0d1b2a; /* Dark navy background */
--igny8-navy-light: #142b3f; /* Navy light */ --igny8-navy-light: #1a2e44; /* Navy light */
--igny8-surface: #f8fafc; /* Page background */ --igny8-surface: #f8fafc; /* Page background */
--igny8-panel: #ffffff; /* Panel background */ --igny8-panel: #ffffff; /* Panel background */
--igny8-text: #555a68; /* Main text */ --igny8-text: #1e293b; /* Main text */
--igny8-text-dim: #64748b; /* Dimmed text */ --igny8-text-light: #64748b; /* Light text */
--igny8-text-dim: #94a3b8; /* Dimmed text */
--igny8-stroke: #e2e8f0; /* Borders */ --igny8-stroke: #e2e8f0; /* Borders */
--igny8-stroke-dark: #cbd5e1; /* Dark borders */
} }
/* =================================================================== /* ===================================================================
HEADER & BRANDING - IGNY8 Primary Blue with Gradient GLOBAL RESETS
=================================================================== */
body {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
}
/* ===================================================================
HEADER - Clean Professional Design
=================================================================== */ =================================================================== */
#header { #header {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important; background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important; color: white !important;
box-shadow: 0 2px 4px rgba(0,0,0,0.1) !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
padding: 0 !important;
} }
#header a:link, #header a:visited { #branding {
padding: 16px 30px !important;
}
#branding h1 {
margin: 0 !important;
font-size: 20px !important;
font-weight: 600 !important;
}
#branding h1 a:link,
#branding h1 a:visited {
color: white !important;
text-decoration: none !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
#header a:link,
#header a:visited {
color: white !important; color: white !important;
} }
#branding h1, #branding h1 a:link, #branding h1 a:visited { /* ===================================================================
color: white !important; ADD RECORD BUTTON - Modern Accent Color
=================================================================== */
.object-tools {
margin-bottom: 24px !important;
float: right !important;
} }
.header-user-tools a { .object-tools li {
margin: 0 !important;
list-style: none !important;
}
.object-tools a.addlink {
background: linear-gradient(135deg, var(--igny8-success) 0%, var(--igny8-success-dark) 100%) !important;
color: white !important; color: white !important;
padding: 12px 28px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
gap: 10px !important;
box-shadow: 0 2px 6px rgba(11, 191, 135, 0.3) !important;
transition: all 0.2s ease !important;
border: none !important;
text-transform: none !important;
}
.object-tools a.addlink:before {
content: "\f067" !important;
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
font-size: 14px !important;
margin-right: 0 !important;
background: none !important;
border: none !important;
width: auto !important;
height: auto !important;
}
.object-tools a.addlink:hover {
background: linear-gradient(135deg, var(--igny8-success-dark) 0%, #067354 100%) !important;
box-shadow: 0 4px 12px rgba(11, 191, 135, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
EXPORT BUTTON - Purple Accent
=================================================================== */
a[href*="export"],
.export-button,
input[name="_export"] {
background: linear-gradient(135deg, var(--igny8-purple) 0%, var(--igny8-purple-dark) 100%) !important;
color: white !important;
padding: 10px 24px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 13px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
box-shadow: 0 2px 6px rgba(93, 74, 227, 0.3) !important;
transition: all 0.2s ease !important;
border: none !important;
cursor: pointer !important;
}
a[href*="export"]:hover,
.export-button:hover,
input[name="_export"]:hover {
background: linear-gradient(135deg, var(--igny8-purple-dark) 0%, #2a1f6b 100%) !important;
box-shadow: 0 4px 12px rgba(93, 74, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
SEARCH BAR & TOOLBAR - Professional Layout
=================================================================== */
#toolbar {
padding: 20px 24px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
margin-bottom: 24px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#toolbar form {
display: flex !important;
gap: 12px !important;
align-items: center !important;
}
#toolbar input[type="text"],
#searchbar {
flex: 1 !important;
min-width: 320px !important;
max-width: 500px !important;
padding: 12px 18px !important;
border: 1.5px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
background: var(--igny8-panel) !important;
color: var(--igny8-text) !important;
}
#toolbar input[type="text"]:focus,
#searchbar:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 4px rgba(6, 147, 227, 0.1) !important;
}
#toolbar input[type="submit"],
#toolbar button[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 12px 28px !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 6px rgba(6, 147, 227, 0.3) !important;
}
#toolbar input[type="submit"]:hover,
#toolbar button[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 12px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
ACTIONS BAR - Better Styling
=================================================================== */
#changelist-form .actions {
padding: 20px 24px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
margin-bottom: 24px !important;
display: flex !important;
align-items: center !important;
gap: 16px !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#changelist-form .actions label {
font-weight: 600 !important;
color: var(--igny8-text) !important;
font-size: 14px !important;
margin-right: 0 !important;
}
#changelist-form .actions select {
min-width: 240px !important;
padding: 12px 18px !important;
border: 1.5px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
background: var(--igny8-panel) !important;
color: var(--igny8-text) !important;
}
#changelist-form .actions select:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 4px rgba(6, 147, 227, 0.1) !important;
}
#changelist-form .actions button,
#changelist-form .actions input[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 12px 28px !important;
border: none !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 6px rgba(6, 147, 227, 0.3) !important;
}
#changelist-form .actions button:hover,
#changelist-form .actions input[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 12px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* ===================================================================
FILTERS PANEL - Clear Organization
=================================================================== */
#changelist-filter {
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
padding: 0 !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#changelist-filter h2 {
background: linear-gradient(135deg, var(--igny8-navy) 0%, var(--igny8-navy-light) 100%) !important;
color: white !important;
padding: 18px 24px !important;
margin: 0 !important;
font-size: 16px !important;
font-weight: 600 !important;
border-radius: 12px 12px 0 0 !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
#changelist-filter h2:before {
content: "\f0b0" !important;
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
}
#changelist-filter h3 {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
padding: 14px 24px !important;
margin: 0 !important;
font-size: 14px !important;
font-weight: 600 !important;
border-top: 1px solid var(--igny8-stroke) !important;
border-bottom: 1px solid var(--igny8-stroke) !important;
}
#changelist-filter ul {
padding: 16px 24px !important;
margin: 0 !important;
}
#changelist-filter li {
padding: 0 !important;
margin: 0 0 8px 0 !important;
list-style: none !important;
}
#changelist-filter a {
color: var(--igny8-text) !important;
text-decoration: none !important;
display: block !important;
padding: 10px 16px !important;
border-radius: 6px !important;
transition: all 0.2s ease !important;
font-size: 14px !important;
}
#changelist-filter a:hover {
background: var(--igny8-surface) !important;
color: var(--igny8-primary) !important;
}
#changelist-filter a.selected {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
font-weight: 600 !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2) !important;
}
#changelist-filter .quiet {
color: var(--igny8-text-dim) !important;
font-size: 13px !important;
}
/* ===================================================================
ADD RECORD BUTTON - Clean Professional Style
=================================================================== */
.object-tools {
margin-bottom: 20px !important;
}
.object-tools li {
margin: 0 !important;
}
.object-tools a.addlink {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 12px 24px !important;
border-radius: 8px !important;
font-weight: 600 !important;
font-size: 14px !important;
text-decoration: none !important;
display: inline-flex !important;
align-items: center !important;
gap: 8px !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.3) !important;
transition: all 0.2s ease !important;
border: none !important;
}
.object-tools a.addlink:before {
content: "+" !important;
font-size: 18px !important;
font-weight: bold !important;
margin-right: 0 !important;
background: none !important;
border: none !important;
width: auto !important;
height: auto !important;
}
.object-tools a.addlink:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-1px) !important;
}
/* Remove the background from the + icon */
.object-tools a.addlink:before {
background: none !important;
}
/* ===================================================================
SEARCH & ACTION BAR - Better Sizing and Layout
=================================================================== */
#toolbar {
padding: 16px 20px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
display: flex !important;
gap: 16px !important;
align-items: center !important;
flex-wrap: wrap !important;
}
#toolbar form {
display: flex !important;
gap: 12px !important;
align-items: center !important;
flex: 1 !important;
max-width: 600px !important;
}
#toolbar input[type="text"] {
flex: 1 !important;
min-width: 250px !important;
padding: 10px 16px !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 6px !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
}
#toolbar input[type="text"]:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(6, 147, 227, 0.1) !important;
}
#toolbar input[type="submit"],
#toolbar button[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 10px 24px !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2) !important;
}
#toolbar input[type="submit"]:hover,
#toolbar button[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.3) !important;
transform: translateY(-1px) !important;
}
/* Action dropdown */
#changelist-form .actions {
padding: 16px 20px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
margin-bottom: 20px !important;
}
#changelist-form .actions select {
min-width: 220px !important;
padding: 10px 16px !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 6px !important;
font-size: 14px !important;
margin-right: 12px !important;
transition: all 0.2s ease !important;
}
#changelist-form .actions select:focus {
border-color: var(--igny8-primary) !important;
outline: none !important;
box-shadow: 0 0 0 3px rgba(6, 147, 227, 0.1) !important;
}
#changelist-form .actions button,
#changelist-form .actions input[type="submit"] {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
padding: 10px 24px !important;
border: none !important;
border-radius: 6px !important;
font-weight: 600 !important;
font-size: 14px !important;
cursor: pointer !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(6, 147, 227, 0.2) !important;
}
#changelist-form .actions button:hover,
#changelist-form .actions input[type="submit"]:hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 8px rgba(6, 147, 227, 0.3) !important;
transform: translateY(-1px) !important;
}
/* ===================================================================
FILTERS - Clear Labels and Better Organization
=================================================================== */
#changelist-filter {
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
padding: 0 !important;
}
#changelist-filter h2 {
background: linear-gradient(135deg, var(--igny8-navy) 0%, var(--igny8-navy-light) 100%) !important;
color: white !important;
padding: 16px 20px !important;
margin: 0 !important;
font-size: 16px !important;
font-weight: 600 !important;
border-radius: 8px 8px 0 0 !important;
}
#changelist-filter h3 {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
padding: 12px 20px !important;
margin: 0 !important;
font-size: 14px !important;
font-weight: 600 !important;
border-top: 1px solid var(--igny8-stroke) !important;
border-bottom: 1px solid var(--igny8-stroke) !important;
}
#changelist-filter ul {
padding: 12px 20px !important;
margin: 0 0 16px 0 !important;
}
#changelist-filter li {
padding: 8px 0 !important;
margin: 0 !important;
list-style: none !important;
}
#changelist-filter a {
color: var(--igny8-text) !important;
text-decoration: none !important;
display: block !important;
padding: 6px 12px !important;
border-radius: 4px !important;
transition: all 0.2s ease !important;
font-size: 14px !important;
}
#changelist-filter a:hover {
background: var(--igny8-surface) !important;
color: var(--igny8-primary) !important;
}
#changelist-filter a.selected {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
font-weight: 600 !important;
}
#changelist-filter .quiet {
color: var(--igny8-text-dim) !important;
font-size: 13px !important;
} }
/* =================================================================== /* ===================================================================
@@ -271,6 +801,217 @@
background: var(--igny8-danger-dark) !important; background: var(--igny8-danger-dark) !important;
} }
/* ===================================================================
SIDEBAR MODULE "ADD" LINKS - Icon Only with Theme Colors
=================================================================== */
.module .addlink,
.module a.addlink,
#content-main .module .addlink {
color: var(--igny8-text) !important;
text-decoration: none !important;
font-size: 0 !important;
padding: 0 !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
transition: all 0.2s ease !important;
}
.module .addlink:before,
.module a.addlink:before {
content: "\f067" !important;
font-family: "Font Awesome 6 Free" !important;
font-weight: 900 !important;
font-size: 12px !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
width: 24px !important;
height: 24px !important;
border-radius: 6px !important;
background: linear-gradient(135deg, var(--igny8-success) 0%, var(--igny8-success-dark) 100%) !important;
color: white !important;
margin: 0 !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 4px rgba(11, 191, 135, 0.2) !important;
}
.module .addlink:hover:before,
.module a.addlink:hover:before {
background: linear-gradient(135deg, var(--igny8-success-dark) 0%, #067354 100%) !important;
box-shadow: 0 4px 8px rgba(11, 191, 135, 0.3) !important;
transform: translateY(-1px) scale(1.05) !important;
}
/* Module navigation styling */
.module {
margin-bottom: 20px !important;
background: var(--igny8-panel) !important;
border: 1px solid var(--igny8-stroke) !important;
border-radius: 8px !important;
overflow: hidden !important;
}
.module h2 {
background: linear-gradient(135deg, var(--igny8-navy) 0%, var(--igny8-navy-light) 100%) !important;
color: white !important;
padding: 14px 16px !important;
font-size: 14px !important;
font-weight: 600 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
}
.module table {
width: 100% !important;
}
.module tr {
border-bottom: 1px solid var(--igny8-stroke) !important;
}
.module tr:last-child {
border-bottom: none !important;
}
.module th,
.module td {
padding: 12px 16px !important;
text-align: left !important;
}
.module th {
background: var(--igny8-surface) !important;
color: var(--igny8-text) !important;
font-weight: 600 !important;
font-size: 13px !important;
}
.module a {
color: var(--igny8-primary) !important;
text-decoration: none !important;
transition: color 0.2s ease !important;
}
.module a:hover {
color: var(--igny8-primary-dark) !important;
}
/* ===================================================================
ALL BUTTONS - Consistent Theme Colors
=================================================================== */
.button,
input[type=submit],
input[type=button],
.submit-row input,
button:not(.close) {
background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !important;
color: white !important;
border: none !important;
padding: 12px 24px !important;
border-radius: 8px !important;
cursor: pointer !important;
font-weight: 600 !important;
font-size: 14px !important;
transition: all 0.2s ease !important;
box-shadow: 0 2px 6px rgba(6, 147, 227, 0.3) !important;
}
.button:hover,
input[type=submit]:hover,
input[type=button]:hover,
button:not(.close):hover {
background: linear-gradient(135deg, var(--igny8-primary-dark) 0%, #035a8f 100%) !important;
box-shadow: 0 4px 12px rgba(6, 147, 227, 0.4) !important;
transform: translateY(-2px) !important;
}
/* Save buttons - Success color */
input[name="_save"],
input[name="_continue"],
.submit-row input[type="submit"]:first-child {
background: linear-gradient(135deg, var(--igny8-success) 0%, var(--igny8-success-dark) 100%) !important;
box-shadow: 0 2px 6px rgba(11, 191, 135, 0.3) !important;
}
input[name="_save"]:hover,
input[name="_continue"]:hover {
background: linear-gradient(135deg, var(--igny8-success-dark) 0%, #067354 100%) !important;
box-shadow: 0 4px 12px rgba(11, 191, 135, 0.4) !important;
}
/* Delete buttons - Danger color */
.deletelink,
.deletelink-box a,
a.deletelink:link,
a.deletelink:visited,
input[name="_delete"],
.delete-confirmation input[type="submit"] {
background: linear-gradient(135deg, var(--igny8-danger) 0%, var(--igny8-danger-dark) 100%) !important;
color: white !important;
box-shadow: 0 2px 6px rgba(239, 68, 68, 0.3) !important;
}
.deletelink:hover,
.deletelink-box a:hover,
input[name="_delete"]:hover {
background: linear-gradient(135deg, var(--igny8-danger-dark) 0%, #b82222 100%) !important;
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.4) !important;
}
/* ===================================================================
TABLE IMPROVEMENTS
=================================================================== */
#result_list {
border: 1px solid var(--igny8-stroke) !important;
border-radius: 12px !important;
overflow: hidden !important;
box-shadow: 0 1px 3px rgba(0,0,0,0.05) !important;
}
#result_list thead th {
background: linear-gradient(135deg, var(--igny8-surface) 0%, #f1f5f9 100%) !important;
color: var(--igny8-text) !important;
padding: 16px 12px !important;
font-weight: 600 !important;
font-size: 13px !important;
text-transform: uppercase !important;
letter-spacing: 0.5px !important;
border-bottom: 2px solid var(--igny8-stroke-dark) !important;
}
#result_list thead th a {
color: var(--igny8-text) !important;
text-decoration: none !important;
}
#result_list tbody tr {
transition: background-color 0.2s ease !important;
}
#result_list tbody tr:nth-child(odd) {
background-color: white !important;
}
#result_list tbody tr:nth-child(even) {
background-color: var(--igny8-surface) !important;
}
#result_list tbody tr:hover {
background-color: rgba(6, 147, 227, 0.05) !important;
}
#result_list td {
padding: 14px 12px !important;
color: var(--igny8-text) !important;
font-size: 14px !important;
border-bottom: 1px solid var(--igny8-stroke) !important;
}
background: var(--igny8-danger-dark) !important;
}
/* =================================================================== /* ===================================================================
LINKS - IGNY8 Primary Blue LINKS - IGNY8 Primary Blue
=================================================================== */ =================================================================== */
@@ -517,3 +1258,46 @@ fieldset.module h2 {
border-left-color: var(--igny8-success); border-left-color: var(--igny8-success);
color: var(--igny8-success-dark); color: var(--igny8-success-dark);
} }
/* ===================================================================
SIDEBAR MODULE ADD LINKS - Clean and Professional
=================================================================== */
.module .addlink,
.module a.addlink,
#content-main .module .addlink {
color: var(--igny8-primary) !important;
text-decoration: none !important;
font-size: 13px !important;
font-weight: 500 !important;
padding: 6px 10px !important;
display: inline-block !important;
border-radius: 4px !important;
transition: all 0.2s ease !important;
}
.module .addlink:before,
.module a.addlink:before {
content: "+" !important;
display: inline-block !important;
width: 18px !important;
height: 18px !important;
line-height: 18px !important;
text-align: center !important;
border-radius: 3px !important;
background: var(--igny8-primary) !important;
color: white !important;
margin-right: 6px !important;
font-size: 14px !important;
font-weight: bold !important;
}
.module .addlink:hover,
.module a.addlink:hover {
background: rgba(6, 147, 227, 0.1) !important;
color: var(--igny8-primary-dark) !important;
}
.module .addlink:hover:before,
.module a.addlink:hover:before {
background: var(--igny8-primary-dark) !important;
}