from __future__ import annotations from typing import Any from django.contrib.auth import get_user_model from django.db.models import ( CharField, Count, DateTimeField, DecimalField, F, JSONField, OuterRef, Q, QuerySet, Subquery, ) from django.utils import timezone from apps.profiles.models import SearchProfile from apps.sources.models import Source, SourceRun from ..models import Application, ApplicationTimelineEvent, JobPosting, ScoreRun from .relevance import it_relevance_query User = get_user_model() def annotate_latest_profile_score( queryset: QuerySet[JobPosting], profile: SearchProfile | None ) -> QuerySet[JobPosting]: """Annotate jobs with one deterministic latest score for the active profile.""" if profile is None: return queryset latest = ScoreRun.objects.filter(job=OuterRef("pk"), profile=profile).order_by("-created_at") return queryset.annotate( match_score=Subquery( latest.values("score")[:1], output_field=DecimalField(max_digits=5, decimal_places=2), ), match_confidence=Subquery( latest.values("confidence")[:1], output_field=DecimalField(max_digits=4, decimal_places=3), ), match_recommendation=Subquery( latest.values("recommendation")[:1], output_field=CharField() ), match_positives=Subquery(latest.values("positives")[:1], output_field=JSONField()), match_concerns=Subquery(latest.values("concerns")[:1], output_field=JSONField()), match_hard_exclusions=Subquery( latest.values("hard_exclusions")[:1], output_field=JSONField() ), match_scored_at=Subquery(latest.values("created_at")[:1], output_field=DateTimeField()), ) def _recent_dashboard_activity(user: User) -> list[dict[str, Any]]: activity: list[dict[str, Any]] = [] source_runs = SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL) for run in source_runs.select_related("source").order_by("-started_at")[:4]: activity.append( { "occurred_at": run.started_at, "kind": "source", "title": f"Bronrun {run.get_status_display().lower()}", "detail": run.source.name, "tone": "positive" if run.status == SourceRun.Status.SUCCESS else "attention", } ) for event in ( ApplicationTimelineEvent.objects.filter(user=user) .select_related("application__job") .order_by("-created_at")[:4] ): activity.append( { "occurred_at": event.created_at, "kind": "application", "title": event.get_event_type_display(), "detail": event.application.job.original_title, "tone": "neutral", } ) return sorted(activity, key=lambda item: item["occurred_at"], reverse=True)[:5] def build_dashboard_cockpit(user: User, profile: SearchProfile | None) -> dict[str, Any]: """Build truthful dashboard metrics and bounded read lists.""" active_jobs = JobPosting.objects.filter(status=JobPosting.Status.ACTIVE) active_it_jobs = active_jobs.filter(it_relevance_query()) today = timezone.localdate() latest_scores = ScoreRun.objects.none() if profile is not None: latest_score_id = ( ScoreRun.objects.filter(job_id=OuterRef("job_id"), profile=profile) .order_by("-created_at") .values("pk")[:1] ) latest_scores = ( ScoreRun.objects.select_related("job", "job__employer") .filter( profile=profile, pk=Subquery(latest_score_id), job__status=JobPosting.Status.ACTIVE, recommendation__in=[ ScoreRun.Recommendation.STRONG, ScoreRun.Recommendation.POSSIBLE, ], hard_exclusions=[], ) .filter(it_relevance_query("job__")) ) sources = Source.objects.exclude(domain="jobs.example.org", status=Source.Status.DISABLED) source_counts = sources.aggregate( total=Count("id"), active=Count("id", filter=Q(status=Source.Status.ACTIVE)), attention=Count("id", filter=~Q(status=Source.Status.ACTIVE)), ) open_applications = Application.objects.filter(user=user).exclude( status__in=[Application.Status.REJECTED, Application.Status.WITHDRAWN] ) new_today = active_it_jobs.filter(first_seen__date=today).count() strong_today = latest_scores.filter( recommendation=ScoreRun.Recommendation.STRONG, job__first_seen__date=today, ).count() filtered_count = active_jobs.exclude(it_relevance_query()).count() duplicate_count = JobPosting.objects.filter(status=JobPosting.Status.DUPLICATE).count() if strong_today: day_conclusion = f"{strong_today} sterke match(es) verdienen vandaag eerst je aandacht." elif new_today: day_conclusion = f"De radar vond vandaag {new_today} nieuwe relevante vacature(s)." else: day_conclusion = "Geen nieuw sterk signaal; je bestaande opvolging blijft prioritair." return { # Het dagoverzicht blijft een beslissingsscherm; de volledige rangschikking # hoort in de verkenner. Vijf kaarten houden de primaire taak scanbaar. "score_cards": list(latest_scores.order_by("-score", "-created_at")[:5]), "priority_applications": list( open_applications.select_related("job", "job__employer").order_by( F("follow_up_date").asc(nulls_last=True), "-updated_at" )[:4] ), "active_jobs": active_it_jobs.count(), "filtered_non_it_jobs": filtered_count, "duplicate_jobs": duplicate_count, "new_today": new_today, "strong_today": strong_today, "applications_open": open_applications.count(), "source_counts": source_counts, "recent_activity": _recent_dashboard_activity(user), "day_conclusion": day_conclusion, "latest_scan": SourceRun.objects.exclude(source__source_type=Source.Type.EMAIL) .order_by("-started_at") .first(), } def build_job_intelligence( *, job: JobPosting, profile: SearchProfile | None, user: User ) -> dict[str, Any]: """Return the bounded, user-scoped read model for explorer and detail.""" score = ( ScoreRun.objects.filter(job=job, profile=profile).order_by("-created_at").first() if profile else None ) return { "score": score, "application": Application.objects.filter(user=user, job=job).first(), "duplicate_count": job.duplicates.count(), "source_count": job.source_aliases.count(), "version_count": job.versions.count(), "latest_versions": list(job.versions.all()[:5]), "provenance_rows": list(job.provenance.select_related("source_alias__source")[:12]), }