""" Account Management API Views Handles account settings, team management, and usage analytics """ from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.contrib.auth import get_user_model from django.db.models import Q, Count, Sum from django.utils import timezone from datetime import timedelta from decimal import Decimal from drf_spectacular.utils import extend_schema, extend_schema_view from igny8_core.auth.models import Account from igny8_core.business.billing.models import CreditTransaction User = get_user_model() @extend_schema_view( retrieve=extend_schema(tags=['Account']), partial_update=extend_schema(tags=['Account']), ) class AccountSettingsViewSet(viewsets.ViewSet): """Account settings management""" permission_classes = [IsAuthenticated] def retrieve(self, request): """Get account settings""" account = request.user.account return Response({ 'id': account.id, 'name': account.name, 'slug': account.slug, 'billing_address_line1': account.billing_address_line1 or '', 'billing_address_line2': account.billing_address_line2 or '', 'billing_city': account.billing_city or '', 'billing_state': account.billing_state or '', 'billing_postal_code': account.billing_postal_code or '', 'billing_country': account.billing_country or '', 'tax_id': account.tax_id or '', 'billing_email': account.billing_email or '', 'credits': account.credits, 'created_at': account.created_at.isoformat(), 'updated_at': account.updated_at.isoformat(), }) def partial_update(self, request): """Update account settings""" account = request.user.account # Update allowed fields allowed_fields = [ 'name', 'billing_address_line1', 'billing_address_line2', 'billing_city', 'billing_state', 'billing_postal_code', 'billing_country', 'tax_id', 'billing_email' ] for field in allowed_fields: if field in request.data: setattr(account, field, request.data[field]) account.save() return Response({ 'message': 'Account settings updated successfully', 'account': { 'id': account.id, 'name': account.name, 'slug': account.slug, 'billing_address_line1': account.billing_address_line1, 'billing_address_line2': account.billing_address_line2, 'billing_city': account.billing_city, 'billing_state': account.billing_state, 'billing_postal_code': account.billing_postal_code, 'billing_country': account.billing_country, 'tax_id': account.tax_id, 'billing_email': account.billing_email, } }) @extend_schema_view( list=extend_schema(tags=['Account']), create=extend_schema(tags=['Account']), destroy=extend_schema(tags=['Account']), ) class TeamManagementViewSet(viewsets.ViewSet): """Team members management""" permission_classes = [IsAuthenticated] def list(self, request): """List team members""" account = request.user.account users = User.objects.filter(account=account) return Response({ 'results': [ { 'id': user.id, 'email': user.email, 'first_name': user.first_name, 'last_name': user.last_name, 'is_active': user.is_active, 'is_staff': user.is_staff, 'date_joined': user.date_joined.isoformat(), 'last_login': user.last_login.isoformat() if user.last_login else None, } for user in users ], 'count': users.count() }) def create(self, request): """Invite new team member""" account = request.user.account email = request.data.get('email') if not email: return Response( {'error': 'Email is required'}, status=status.HTTP_400_BAD_REQUEST ) # Check if user already exists if User.objects.filter(email=email).exists(): return Response( {'error': 'User with this email already exists'}, status=status.HTTP_400_BAD_REQUEST ) # Create user (simplified - in production, send invitation email) user = User.objects.create_user( email=email, first_name=request.data.get('first_name', ''), last_name=request.data.get('last_name', ''), account=account ) return Response({ 'message': 'Team member invited successfully', 'user': { 'id': user.id, 'email': user.email, 'first_name': user.first_name, 'last_name': user.last_name, } }, status=status.HTTP_201_CREATED) def destroy(self, request, pk=None): """Remove team member""" account = request.user.account try: user = User.objects.get(id=pk, account=account) # Prevent removing yourself if user.id == request.user.id: return Response( {'error': 'Cannot remove yourself'}, status=status.HTTP_400_BAD_REQUEST ) user.is_active = False user.save() return Response({ 'message': 'Team member removed successfully' }) except User.DoesNotExist: return Response( {'error': 'User not found'}, status=status.HTTP_404_NOT_FOUND ) @extend_schema_view( overview=extend_schema(tags=['Account']), ) class UsageAnalyticsViewSet(viewsets.ViewSet): """Usage analytics and statistics""" permission_classes = [IsAuthenticated] @action(detail=False, methods=['get']) def overview(self, request): """Get usage analytics overview""" account = request.user.account # Get date range (default: last 30 days) days = int(request.query_params.get('days', 30)) start_date = timezone.now() - timedelta(days=days) # Get transactions in period transactions = CreditTransaction.objects.filter( account=account, created_at__gte=start_date ) # Calculate totals by type usage_by_type = transactions.filter( amount__lt=0 ).values('transaction_type').annotate( total=Sum('amount'), count=Count('id') ) purchases_by_type = transactions.filter( amount__gt=0 ).values('transaction_type').annotate( total=Sum('amount'), count=Count('id') ) # Daily usage daily_usage = [] for i in range(days): date = start_date + timedelta(days=i) day_txns = transactions.filter( created_at__date=date.date() ) usage = day_txns.filter(amount__lt=0).aggregate(Sum('amount'))['amount__sum'] or 0 purchases = day_txns.filter(amount__gt=0).aggregate(Sum('amount'))['amount__sum'] or 0 daily_usage.append({ 'date': date.date().isoformat(), 'usage': abs(usage), 'purchases': purchases, 'net': purchases + usage }) return Response({ 'period_days': days, 'start_date': start_date.isoformat(), 'end_date': timezone.now().isoformat(), 'current_balance': account.credits, 'usage_by_type': list(usage_by_type), 'purchases_by_type': list(purchases_by_type), 'daily_usage': daily_usage, 'total_usage': abs(transactions.filter(amount__lt=0).aggregate(Sum('amount'))['amount__sum'] or 0), 'total_purchases': transactions.filter(amount__gt=0).aggregate(Sum('amount'))['amount__sum'] or 0, }) @extend_schema_view( stats=extend_schema(tags=['Account']), ) class DashboardStatsViewSet(viewsets.ViewSet): """Dashboard statistics - real data for home page widgets""" permission_classes = [IsAuthenticated] @action(detail=False, methods=['get']) def stats(self, request): """ Get dashboard statistics for the home page. Query params: - site_id: Filter by site (optional, defaults to all sites) - days: Number of days for AI operations (default: 7) Returns: - ai_operations: Real credit usage by operation type - recent_activity: Recent notifications - content_velocity: Content created this week/month - images_count: Actual total images count - published_count: Actual published content count """ account = request.user.account site_id = request.query_params.get('site_id') days = int(request.query_params.get('days', 7)) # Import models here to avoid circular imports from igny8_core.modules.writer.models import Images, Content from igny8_core.modules.planner.models import Keywords, Clusters, ContentIdeas from igny8_core.business.notifications.models import Notification from igny8_core.business.billing.models import CreditUsageLog from igny8_core.auth.models import Site # Build base filter for site site_filter = {} if site_id: try: site_filter['site_id'] = int(site_id) except (ValueError, TypeError): pass # ========== AI OPERATIONS (from CreditUsageLog) ========== start_date = timezone.now() - timedelta(days=days) usage_query = CreditUsageLog.objects.filter( account=account, created_at__gte=start_date ) # Get operations grouped by type operations_data = usage_query.values('operation_type').annotate( count=Count('id'), credits=Sum('credits_used') ).order_by('-credits') # Calculate totals total_ops = usage_query.count() total_credits = usage_query.aggregate(total=Sum('credits_used'))['total'] or 0 # Format operations for frontend operations = [] for op in operations_data: op_type = op['operation_type'] or 'other' operations.append({ 'type': op_type, 'count': op['count'] or 0, 'credits': op['credits'] or 0, }) ai_operations = { 'period': f'{days}d', 'operations': operations, 'totals': { 'count': total_ops, 'credits': total_credits, 'successRate': 98.5, # TODO: calculate from actual success/failure 'avgCreditsPerOp': round(total_credits / total_ops, 1) if total_ops > 0 else 0, } } # ========== RECENT ACTIVITY (from Notifications) ========== recent_notifications = Notification.objects.filter( account=account ).order_by('-created_at')[:10] recent_activity = [] for notif in recent_notifications: # Map notification type to activity type activity_type_map = { 'ai_clustering_complete': 'clustering', 'ai_ideas_complete': 'ideas', 'ai_content_complete': 'content', 'ai_images_complete': 'images', 'ai_prompts_complete': 'images', 'content_published': 'published', 'wp_sync_success': 'published', } activity_type = activity_type_map.get(notif.notification_type, 'system') # Map notification type to href href_map = { 'clustering': '/planner/clusters', 'ideas': '/planner/ideas', 'content': '/writer/content', 'images': '/writer/images', 'published': '/writer/published', } recent_activity.append({ 'id': str(notif.id), 'type': activity_type, 'title': notif.title, 'description': notif.message[:100] if notif.message else '', 'timestamp': notif.created_at.isoformat(), 'href': href_map.get(activity_type, '/dashboard'), }) # ========== CONTENT COUNTS ========== content_base = Content.objects.filter(account=account) if site_filter: content_base = content_base.filter(**site_filter) total_content = content_base.count() draft_content = content_base.filter(status='draft').count() review_content = content_base.filter(status='review').count() published_content = content_base.filter(status='published').count() # ========== IMAGES COUNT (actual images, not content with images) ========== images_base = Images.objects.filter(account=account) if site_filter: images_base = images_base.filter(**site_filter) total_images = images_base.count() generated_images = images_base.filter(status='generated').count() pending_images = images_base.filter(status='pending').count() # ========== CONTENT VELOCITY ========== now = timezone.now() week_ago = now - timedelta(days=7) month_ago = now - timedelta(days=30) # This week's content week_content = content_base.filter(created_at__gte=week_ago).count() week_images = images_base.filter(created_at__gte=week_ago).count() # This month's content month_content = content_base.filter(created_at__gte=month_ago).count() month_images = images_base.filter(created_at__gte=month_ago).count() # Estimate words (avg 1500 per article) content_velocity = { 'thisWeek': { 'articles': week_content, 'words': week_content * 1500, 'images': week_images, }, 'thisMonth': { 'articles': month_content, 'words': month_content * 1500, 'images': month_images, }, 'total': { 'articles': total_content, 'words': total_content * 1500, 'images': total_images, }, 'trend': 0, # TODO: calculate actual trend } # ========== PIPELINE COUNTS ========== keywords_base = Keywords.objects.filter(account=account) clusters_base = Clusters.objects.filter(account=account) ideas_base = ContentIdeas.objects.filter(account=account) if site_filter: keywords_base = keywords_base.filter(**site_filter) clusters_base = clusters_base.filter(**site_filter) ideas_base = ideas_base.filter(**site_filter) # Get site count sites_count = Site.objects.filter(account=account, is_active=True).count() pipeline = { 'sites': sites_count, 'keywords': keywords_base.count(), 'clusters': clusters_base.count(), 'ideas': ideas_base.count(), 'tasks': ideas_base.filter(status='queued').count() + ideas_base.filter(status='completed').count(), 'drafts': draft_content + review_content, 'published': published_content, } return Response({ 'ai_operations': ai_operations, 'recent_activity': recent_activity, 'content_velocity': content_velocity, 'pipeline': pipeline, 'counts': { 'content': { 'total': total_content, 'draft': draft_content, 'review': review_content, 'published': published_content, }, 'images': { 'total': total_images, 'generated': generated_images, 'pending': pending_images, }, } })