diff --git a/backend/igny8_core/admin/alerts.py b/backend/igny8_core/admin/alerts.py new file mode 100644 index 00000000..f497040c --- /dev/null +++ b/backend/igny8_core/admin/alerts.py @@ -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 diff --git a/backend/igny8_core/admin/apps.py b/backend/igny8_core/admin/apps.py index aa4b19d3..0840715b 100644 --- a/backend/igny8_core/admin/apps.py +++ b/backend/igny8_core/admin/apps.py @@ -39,5 +39,28 @@ class Igny8AdminConfig(AdminConfig): _safe_register(Group, admin.ModelAdmin) _safe_register(ContentType, 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}") diff --git a/backend/igny8_core/admin/celery_admin.py b/backend/igny8_core/admin/celery_admin.py new file mode 100644 index 00000000..3bcb3e56 --- /dev/null +++ b/backend/igny8_core/admin/celery_admin.py @@ -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( + '{}', + 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('{:.2f}ms', seconds * 1000) + elif seconds < 60: + return format_html('{:.2f}s', seconds) + else: + minutes = seconds / 60 + return format_html('{:.1f}m', 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 diff --git a/backend/igny8_core/admin/dashboard.py b/backend/igny8_core/admin/dashboard.py new file mode 100644 index 00000000..9f9aff30 --- /dev/null +++ b/backend/igny8_core/admin/dashboard.py @@ -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) diff --git a/backend/igny8_core/admin/site.py b/backend/igny8_core/admin/site.py index 7bd162c5..4985acd6 100644 --- a/backend/igny8_core/admin/site.py +++ b/backend/igny8_core/admin/site.py @@ -4,6 +4,8 @@ Custom AdminSite for IGNY8 to organize models into proper groups from django.contrib import admin from django.contrib.admin.apps import AdminConfig from django.apps import apps +from django.urls import path +from django.shortcuts import redirect class Igny8AdminSite(admin.AdminSite): @@ -20,6 +22,19 @@ class Igny8AdminSite(admin.AdminSite): site_header = 'IGNY8 Administration' site_title = 'IGNY8 Admin' 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): """ @@ -113,7 +128,13 @@ class Igny8AdminSite(admin.AdminSite): ('system', 'SystemStatus'), ], }, - '🔧 Django System': { + 'īŋŊ Monitoring & Tasks': { + 'models': [ + ('django_celery_results', 'TaskResult'), + ('django_celery_results', 'GroupResult'), + ], + }, + 'īŋŊ🔧 Django System': { 'models': [ ('admin', 'LogEntry'), ('auth', 'Group'), diff --git a/backend/igny8_core/auth/admin.py b/backend/igny8_core/auth/admin.py index f8082a97..197bf488 100644 --- a/backend/igny8_core/auth/admin.py +++ b/backend/igny8_core/auth/admin.py @@ -145,10 +145,10 @@ class PlanAdmin(admin.ModelAdmin): @admin.register(Account) class AccountAdmin(AccountAdminMixin, admin.ModelAdmin): 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'] 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): """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 pass 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( + '{} {}', + 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'🔴 Critical: Only {obj.credits} credits remaining') + elif obj.credits < 100: + details.append(f'âš ī¸ Warning: Only {obj.credits} credits remaining') + else: + details.append(f'✅ Credits: {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'📚 Activity: {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'🔴 Automations: {failed_runs} failures this week') + else: + details.append(f'✅ Automations: 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'âš ī¸ Syncs: {failed_syncs} failures today') + else: + details.append(f'✅ Syncs: No failures today') + except: + pass + + # Account status + if obj.status == 'active': + details.append(f'✅ Status: Active') + else: + details.append(f'🔴 Status: {obj.status.title()}') + + return format_html('
'.join(details)) + health_details.short_description = 'Health Details' def has_delete_permission(self, request, obj=None): if obj and getattr(obj, 'slug', '') == 'aws-admin': diff --git a/backend/igny8_core/modules/planner/admin.py b/backend/igny8_core/modules/planner/admin.py index 6ee79aa3..b4b05857 100644 --- a/backend/igny8_core/modules/planner/admin.py +++ b/backend/igny8_core/modules/planner/admin.py @@ -1,6 +1,18 @@ from django.contrib import admin +from django.contrib import messages from igny8_core.admin.base import SiteSectorAdminMixin 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) @@ -9,6 +21,7 @@ class ClustersAdmin(SiteSectorAdminMixin, admin.ModelAdmin): list_filter = ['status', 'site', 'sector'] search_fields = ['name'] ordering = ['name'] + autocomplete_fields = ['site', 'sector'] def get_site_display(self, obj): """Safely get site name""" @@ -27,11 +40,18 @@ class ClustersAdmin(SiteSectorAdminMixin, admin.ModelAdmin): @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_filter = ['status', 'seed_keyword__intent', 'site', 'sector', 'seed_keyword__industry', 'seed_keyword__sector'] search_fields = ['seed_keyword__keyword'] 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): """Safely get site name""" @@ -55,6 +75,58 @@ class KeywordsAdmin(SiteSectorAdminMixin, admin.ModelAdmin): except: return '-' 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) diff --git a/backend/igny8_core/modules/writer/admin.py b/backend/igny8_core/modules/writer/admin.py index d1228b86..d168e074 100644 --- a/backend/igny8_core/modules/writer/admin.py +++ b/backend/igny8_core/modules/writer/admin.py @@ -33,7 +33,13 @@ class TasksAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin): search_fields = ['title', 'description'] ordering = ['-created_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 = ( ('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) 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): """Safely get site name""" try: @@ -143,8 +190,13 @@ class ContentAdmin(ExportMixin, SiteSectorAdminMixin, admin.ModelAdmin): search_fields = ['title', 'content_html', 'external_url'] ordering = ['-created_at'] readonly_fields = ['created_at', 'updated_at', 'word_count', 'get_tags_display', 'get_categories_display'] + autocomplete_fields = ['cluster', 'site', 'sector'] inlines = [ContentTaxonomyInline] - actions = ['bulk_set_status_published', 'bulk_set_status_draft'] + actions = [ + 'bulk_set_status_published', + 'bulk_set_status_draft', + 'bulk_add_taxonomy', + ] fieldsets = ( ('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) 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): """Safely get site name""" try: diff --git a/backend/igny8_core/static/admin/css/igny8_admin.css b/backend/igny8_core/static/admin/css/igny8_admin.css index 98c10a42..00b55e29 100644 --- a/backend/igny8_core/static/admin/css/igny8_admin.css +++ b/backend/igny8_core/static/admin/css/igny8_admin.css @@ -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 { + /* Primary Colors */ --igny8-primary: #0693e3; /* Primary brand blue */ --igny8-primary-dark: #0472b8; /* Primary dark */ + --igny8-primary-light: #3da9e8; /* Primary light */ + + /* Accent Colors */ --igny8-success: #0bbf87; /* Success teal-green */ --igny8-success-dark: #08966b; /* Success dark */ --igny8-warning: #ff7a00; /* Warning orange */ @@ -16,34 +20,560 @@ --igny8-danger-dark: #d13333; /* Danger dark */ --igny8-purple: #5d4ae3; /* Purple accent */ --igny8-purple-dark: #3a2f94; /* Purple dark */ + + /* Neutral Colors */ --igny8-navy: #0d1b2a; /* Dark navy background */ - --igny8-navy-light: #142b3f; /* Navy light */ + --igny8-navy-light: #1a2e44; /* Navy light */ --igny8-surface: #f8fafc; /* Page background */ --igny8-panel: #ffffff; /* Panel background */ - --igny8-text: #555a68; /* Main text */ - --igny8-text-dim: #64748b; /* Dimmed text */ + --igny8-text: #1e293b; /* Main text */ + --igny8-text-light: #64748b; /* Light text */ + --igny8-text-dim: #94a3b8; /* Dimmed text */ --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 { background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !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; } -#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; + 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; } +/* =================================================================== + 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 =================================================================== */ @@ -517,3 +1258,46 @@ fieldset.module h2 { border-left-color: var(--igny8-success); 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; +} diff --git a/backend/igny8_core/templates/admin/base_site.html b/backend/igny8_core/templates/admin/base_site.html index 1446add4..c46fa9ef 100644 --- a/backend/igny8_core/templates/admin/base_site.html +++ b/backend/igny8_core/templates/admin/base_site.html @@ -3,17 +3,68 @@ {% block title %}{{ title }} | IGNY8 Admin{% endblock %} +{% block extrahead %} +{{ block.super }} + + +{% endblock %} + {% block branding %}

- 🚀 IGNY8 Administration + IGNY8 Administration

{% endblock %} +{% block userlinks %} + + + Dashboard + +{{ block.super }} +{% endblock %} + {% block extrastyle %} {{ block.super }} + {% endblock %} {% block nav-global %}{% endblock %} diff --git a/backend/igny8_core/templates/admin/bulk_action_form.html b/backend/igny8_core/templates/admin/bulk_action_form.html new file mode 100644 index 00000000..67f6d35c --- /dev/null +++ b/backend/igny8_core/templates/admin/bulk_action_form.html @@ -0,0 +1,75 @@ +{% extends "admin/base_site.html" %} +{% load static %} + +{% block content %} +

{{ title }}

+ +
+ {% csrf_token %} + +
+ {{ form.as_p }} +
+ +
+ + + + Cancel +
+ +
+

Selected Items ({{ queryset.count }})

+ +
+
+ + +{% endblock %} diff --git a/backend/igny8_core/templates/admin/dashboard.html b/backend/igny8_core/templates/admin/dashboard.html new file mode 100644 index 00000000..08a7b127 --- /dev/null +++ b/backend/igny8_core/templates/admin/dashboard.html @@ -0,0 +1,419 @@ +{% extends "admin/base_site.html" %} +{% load static %} + +{% block title %}IGNY8 Dashboard{% endblock %} + +{% block extrahead %} +{{ block.super }} + +{% endblock %} + +{% block content %} +
+
+
+

🚀 IGNY8 Admin Dashboard

+

Real-time operational metrics and system health monitoring

+
+ +
+ + {% if alerts %} +
+

đŸ“ĸ Active Alerts

+ {% for alert in alerts %} +
+
+ {{ alert.icon }} + {{ alert.message }} +
+ {{ alert.action }} +
+ {% endfor %} +
+ {% else %} +
+
+
✅
+

All Systems Operational

+

No active alerts or issues detected

+
+
+ {% endif %} + +
+ +
+

đŸ‘Ĩ Accounts

+
+ Total Accounts + {{ accounts.total }} +
+
+ Active Accounts + {{ accounts.active }} +
+
+ Low Credit Accounts + {{ accounts.low_credit }} +
+
+ + +
+

📚 Content

+
+ Created This Week + {{ content.this_week }} +
+
+ Created This Month + {{ content.this_month }} +
+
+ Pending Tasks + {{ content.tasks_pending }} +
+
+ In Progress + {{ content.tasks_in_progress }} +
+
+ + +
+

💰 Billing

+
+ Pending Payments + {{ billing.pending_payments }} +
+
+ Revenue This Month + ${{ billing.payments_this_month|floatformat:2 }} +
+
+ Credits Used This Month + {{ billing.credit_usage_this_month }} +
+
+ + +
+

🤖 Automation & Sync

+
+ Automations Running + {{ automation.running }} +
+
+ Failed This Week + {{ automation.failed_this_week }} +
+
+ Failed Syncs Today + {{ integration.sync_failed_today }} +
+
+ + +
+

âš™ī¸ Celery Tasks

+
+ Failed Today + {{ celery.failed_today }} +
+
+ Pending Tasks + {{ celery.pending }} +
+
+
+ +
+

⚡ Quick Actions

+ +
+
+{% endblock %} diff --git a/backend/staticfiles/admin/css/igny8_admin.css b/backend/staticfiles/admin/css/igny8_admin.css index 98c10a42..00b55e29 100644 --- a/backend/staticfiles/admin/css/igny8_admin.css +++ b/backend/staticfiles/admin/css/igny8_admin.css @@ -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 { + /* Primary Colors */ --igny8-primary: #0693e3; /* Primary brand blue */ --igny8-primary-dark: #0472b8; /* Primary dark */ + --igny8-primary-light: #3da9e8; /* Primary light */ + + /* Accent Colors */ --igny8-success: #0bbf87; /* Success teal-green */ --igny8-success-dark: #08966b; /* Success dark */ --igny8-warning: #ff7a00; /* Warning orange */ @@ -16,34 +20,560 @@ --igny8-danger-dark: #d13333; /* Danger dark */ --igny8-purple: #5d4ae3; /* Purple accent */ --igny8-purple-dark: #3a2f94; /* Purple dark */ + + /* Neutral Colors */ --igny8-navy: #0d1b2a; /* Dark navy background */ - --igny8-navy-light: #142b3f; /* Navy light */ + --igny8-navy-light: #1a2e44; /* Navy light */ --igny8-surface: #f8fafc; /* Page background */ --igny8-panel: #ffffff; /* Panel background */ - --igny8-text: #555a68; /* Main text */ - --igny8-text-dim: #64748b; /* Dimmed text */ + --igny8-text: #1e293b; /* Main text */ + --igny8-text-light: #64748b; /* Light text */ + --igny8-text-dim: #94a3b8; /* Dimmed text */ --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 { background: linear-gradient(135deg, var(--igny8-primary) 0%, var(--igny8-primary-dark) 100%) !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; } -#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; + 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; } +/* =================================================================== + 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 =================================================================== */ @@ -517,3 +1258,46 @@ fieldset.module h2 { border-left-color: var(--igny8-success); 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; +}