File size: 6,931 Bytes
b84ea83
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
"""
Analytics Service β€” Visit Data Statistics & Aggregation

Provides:
- Overall summary (total visits, status breakdown, completion rates)
- Per-visit analytics
- Trend data (visits per day/week/month)
- Checklist completion statistics

Uses raw SQLAlchemy async queries for efficient aggregation.
"""

import logging
from datetime import datetime, timedelta
from typing import Optional

from sqlalchemy import func, select, case
from sqlalchemy.ext.asyncio import AsyncSession

from models.visit_model import Visit
from models.checklist_model import Checklist
from models.photo_model import Photo

logger = logging.getLogger(__name__)


class AnalyticsService:
    """Computes visit and checklist statistics from the database."""

    # ── Overall Summary ───────────────────────────────────────────────────

    async def get_summary(self, db: AsyncSession) -> dict:
        """
        Overall analytics summary across all visits.

        Returns:
            {
              "visits": { total, by_status: {pending, in_progress, completed, cancelled} },
              "checklists": { total_items, checked_items, completion_rate_pct },
              "photos": { total },
              "generated_at": ISO timestamp
            }
        """
        logger.info("Computing analytics summary")

        # ── Visit counts by status ──
        visit_q = await db.execute(
            select(
                func.count(Visit.id).label("total"),
                func.sum(case((Visit.status == "pending", 1), else_=0)).label("pending"),
                func.sum(case((Visit.status == "in_progress", 1), else_=0)).label("in_progress"),
                func.sum(case((Visit.status == "completed", 1), else_=0)).label("completed"),
                func.sum(case((Visit.status == "cancelled", 1), else_=0)).label("cancelled"),
            )
        )
        visit_row = visit_q.one()

        # ── Checklist stats ──
        checklist_q = await db.execute(
            select(
                func.count(Checklist.id).label("total"),
                func.sum(case((Checklist.is_checked == True, 1), else_=0)).label("checked"),  # noqa: E712
            )
        )
        checklist_row = checklist_q.one()

        total_items = checklist_row.total or 0
        checked_items = checklist_row.checked or 0
        completion_rate = round((checked_items / total_items * 100), 1) if total_items > 0 else 0.0

        # ── Photo count ──
        photo_q = await db.execute(select(func.count(Photo.id)))
        total_photos = photo_q.scalar() or 0

        return {
            "status": "success",
            "data": {
                "visits": {
                    "total": visit_row.total or 0,
                    "by_status": {
                        "pending": visit_row.pending or 0,
                        "in_progress": visit_row.in_progress or 0,
                        "completed": visit_row.completed or 0,
                        "cancelled": visit_row.cancelled or 0,
                    },
                },
                "checklists": {
                    "total_items": total_items,
                    "checked_items": checked_items,
                    "completion_rate_pct": completion_rate,
                },
                "photos": {
                    "total": total_photos,
                },
            },
            "meta": {
                "generated_at": datetime.utcnow().isoformat(),
            },
        }

    # ── Per-Visit Analytics ───────────────────────────────────────────────

    async def get_visit_analytics(self, db: AsyncSession, visit_id) -> dict:
        """
        Analytics for a single visit.

        Returns checklist completion rate, item counts, photo count,
        and a breakdown of checked vs unchecked items.
        """
        # Visit exists check
        visit_q = await db.execute(select(Visit).where(Visit.id == visit_id))
        visit = visit_q.scalar_one_or_none()
        if not visit:
            return None

        # Checklist breakdown
        cl_q = await db.execute(
            select(
                func.count(Checklist.id).label("total"),
                func.sum(case((Checklist.is_checked == True, 1), else_=0)).label("checked"),  # noqa: E712
            ).where(Checklist.visit_id == visit_id)
        )
        cl_row = cl_q.one()
        total = cl_row.total or 0
        checked = cl_row.checked or 0
        rate = round((checked / total * 100), 1) if total > 0 else 0.0

        # Photo count
        photo_q = await db.execute(
            select(func.count(Photo.id)).where(Photo.visit_id == visit_id)
        )
        photo_count = photo_q.scalar() or 0

        return {
            "status": "success",
            "data": {
                "visit_id": str(visit_id),
                "title": visit.title,
                "status": visit.status,
                "checklists": {
                    "total_items": total,
                    "checked_items": checked,
                    "unchecked_items": total - checked,
                    "completion_rate_pct": rate,
                },
                "photos": {"total": photo_count},
            },
            "meta": {
                "generated_at": datetime.utcnow().isoformat(),
            },
        }

    # ── Trend Data ────────────────────────────────────────────────────────

    async def get_visit_trends(
        self, db: AsyncSession, days: int = 30
    ) -> dict:
        """
        Number of visits created per day over the last [days] days.

        Useful for charts in the Flutter analytics dashboard.
        """
        since = datetime.utcnow() - timedelta(days=days)

        # Use a literal column reference for GROUP BY to satisfy PostgreSQL
        day_col = func.date_trunc("day", Visit.created_at)
        trend_q = await db.execute(
            select(
                day_col.label("day"),
                func.count(Visit.id).label("count"),
            )
            .where(Visit.created_at >= since)
            .group_by(day_col)
            .order_by(day_col)
        )
        rows = trend_q.all()

        return {
            "status": "success",
            "data": {
                "period_days": days,
                "trends": [
                    {"date": str(r.day.date()), "count": r.count}
                    for r in rows
                ],
            },
            "meta": {
                "generated_at": datetime.utcnow().isoformat(),
                "since": since.date().isoformat(),
            },
        }


# Singleton
analytics_service = AnalyticsService()