Ga naar inhoud

Statistieken (Backend)

Technisch Ontwerp

De backend van de module Statistieken in Remedice is een aggregatielaag over de overige modules (medicatiebeoordelingen, nieuws, agenda, werkafspraken, Atlas en nazendingen). Per kalenderjaar levert de module een dashboard met tellingen, status- en maandverdelingen, rol- en categorie-uitsplitsingen, en een Excel-export.

De module houdt twee eigen tabellen bij in plaats van alles live te berekenen:

  • YearlyStatistic is een ledger met een teller per (jaar, item_type). Voor gebeurtenissen die zich verspreid over de codebase voordoen (nieuws geplaatst, agendapunt aangemaakt, Atlas-vraag gesteld, nazending toegevoegd) wordt de teller opgehoogd via signalen, zodat het dashboard geen brede live-aggregaties hoeft te draaien.
  • ReviewStatisticsRecord is een gedenormaliseerde feitentabel: per reviewpatient een rij met aanmaakmaand, soort review en huidige status. Hierdoor blijven reviewstatistieken correct ook nadat de bronreview is opgeruimd.

De kernlogica staat in services.py. get_year_statistics(tenant, year) cachet het resultaat 1 uur per tenant en jaar (cache-sleutel stats_{schema_name}_{year}, STATS_CACHE_TTL = 3600) en stelt het dashboard samen uit drie deelfuncties:

  • _compute_review_block(year) leest uitsluitend uit ReviewStatisticsRecord (gefilterd op created_year) en berekent in een doorpas de statusverdeling, de twaalf-maands-reeks, de eerste actieve maand en de jaartotalen (totaal, patient, afdeling). Verwijderde reviews (gemarkeerd via source_deleted_at) tellen bewust mee; statistieken zijn permanent.
  • _compute_users_block(tenant) telt actieve gebruikers en de verdeling per rol.
  • _compute_content_block(year) haalt nieuws, agenda, werkafspraken, Atlas-vragen en nazendingen uit de YearlyStatistic-ledger (jaartelling of cumulatief) en vult de categorie-uitsplitsingen aan met live queries.

De synchronisatie tussen de bronmodules en deze twee tabellen verloopt via signals.py en review_statistics.py. Een nieuw NewsItem, AgendaItem, WorkAgreementItem, ChatMessage (alleen role="user") of Nazending hoogt de bijbehorende YearlyStatistic-teller op en invalideert de cache. Een nieuwe of gewijzigde ReviewPatient roept ensure_review_statistics_record aan; een verwijderde ReviewPatient roept mark_review_statistics_record_deleted aan, die source_deleted_at zet zonder de rij te verwijderen.

De export genereert via openpyxl een Excel-bestand (.xlsx) in geheugen en streamt het direct terug als HTTP-response.

flowchart TD
    A[GET /api/statistieken/] --> B[YearStatisticsView.get]
    B --> C{cache hit? stats_schema_year}
    C -- ja --> Z[JSON Response]
    C -- nee --> D[_compute_year_statistics]
    D --> E[_compute_review_block: ReviewStatisticsRecord]
    D --> F[_compute_users_block: member_users_qs + Role]
    D --> G[_compute_content_block: YearlyStatistic + live queries]
    E --> H[cache.set TTL 3600]
    F --> H
    G --> H
    H --> Z

    I[ReviewPatient/NewsItem/AgendaItem/... post_save] --> J[signals]
    J --> K[YearlyStatistic.increment of ensure_review_statistics_record]
    J --> L[invalidate cache]

    M[GET /api/statistieken/export/] --> N[ExportXlsxView.get]
    N --> O[get_all_patients_for_export]
    N --> P[get_afdeling_history_for_export]
    O --> Q[build_statistieken_xlsx: openpyxl, 2 sheets]
    P --> Q
    Q --> R[xlsx HttpResponse]

Datamodel (ERD)

De module beheert twee persistente tabellen en aggregeert daarnaast data uit andere modules.

erDiagram
    YearlyStatistic {
        bigint id PK
        smallint year
        varchar item_type
        int count
    }

    ReviewStatisticsRecord {
        bigint id PK
        uuid source_review_patient_id UK
        uuid source_review_id FK
        smallint created_year
        smallint created_month
        varchar review_kind
        varchar current_status
        datetime review_created_at
        datetime source_deleted_at "nullable, soft-delete marker"
        datetime last_status_updated_at
    }

    ReviewStatisticsRecord }o--|| ReviewPatient : "denormaliseert"
    ReviewStatisticsRecord }o--|| MedicatieReview : "denormaliseert"
    YearlyStatistic ||--o{ NewsItem : "telt"
    YearlyStatistic ||--o{ AgendaItem : "telt"
    YearlyStatistic ||--o{ WorkAgreementItem : "telt cumulatief"
    YearlyStatistic ||--o{ ChatMessage : "telt (atlas_question)"
    YearlyStatistic ||--o{ Nazending : "telt"
    Statistieken ||--o{ Role : "rolverdeling"
    Statistieken ||--o{ User : "actieve gebruikers"

YearlyStatistic heeft een unieke constraint op (year, item_type). Ophogen verloopt via de classmethod YearlyStatistic.increment(year=..., item_type=...), die atomisch werkt via get_or_create gevolgd door een update met F("count") + 1.

Ondersteunde item_type-waarden:

  • news
  • agenda
  • work_agreement
  • atlas_question
  • nazending

ReviewStatisticsRecord heeft een unieke source_review_patient_id (een rij per reviewpatient), een geindexeerde source_review_id, een CheckConstraint op created_month tussen 1 en 12, en indexen op (created_year, created_month), (created_year, review_kind) en (created_year, current_status). review_kind is patient of afdeling; current_status is in_voorbereiding, voorbereid of uitgevoerd.

API & Communicatie

  • GET /api/statistieken/: Geeft de geaggregeerde statistieken voor een jaar (query-parameter year, standaard het huidige jaar). Retourneert een YearStatisticsSerializer-response.
  • GET /api/statistieken/years/: Geeft een aflopend gesorteerde lijst van jaren waarvoor data bestaat, op basis van de unie van jaren in MedicatieReview, ReviewStatisticsRecord en YearlyStatistic. Het huidige jaar wordt altijd toegevoegd als het ontbreekt.
  • GET /api/statistieken/export/: Genereert een Excel-bestand (.xlsx) voor het opgegeven jaar (query-parameter year) met twee tabbladen. Patiënten heeft de kolommen Patiënt ID, Naam, Geboortedatum, Leeftijd, Status, Review ID, Review aangemaakt op, Reviewer, Afdeling en AIS bron. Afdelingen heeft de kolommen Afdeling, Review ID, Aangemaakt op, Reviewer, Status, AIS bron en Aantal patiënten. De kopregel is vetgedrukt op een donkere achtergrond met bevroren bovenste rij. Celwaarden die met een formuletoken beginnen worden via _safe voorzien van een aanhalingsteken (bescherming tegen CSV-injectie). De response heeft Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet en een Content-Disposition-header met bestandsnaam statistieken_{year}.xlsx.

Response-structuur GET /api/statistieken/

month in reviewsPerMonth is nul-geindexeerd (0 = januari, 11 = december) en bevat altijd twaalf punten; firstActiveMonth is een-geindexeerd (1 = januari) of null als er geen reviews zijn.

{
  "year": 2025,
  "firstActiveMonth": 2,
  "reviewStatusCounts": {
    "inPreparation": 12,
    "prepared": 34,
    "performed": 78
  },
  "reviewsPerMonth": [
    { "month": 0, "label": "Jan", "count": 5, "countPatient": 3, "countAfdeling": 2 }
  ],
  "users": {
    "total": 8,
    "byRole": [{ "label": "Apotheker", "value": 4 }]
  },
  "news": {
    "total": 14,
    "byCategory": [{ "label": "Farmacotherapeutisch", "value": 9 }]
  },
  "agendaItems": 22,
  "workAgreements": {
    "total": 47,
    "byCategory": [{ "label": "Interacties", "value": 15 }]
  },
  "atlasQuestions": 103,
  "nazendingen": {
    "currentYear": 31,
    "total": 96
  },
  "reviews": 124,
  "reviewsPatient": 56,
  "reviewsAfdeling": 68
}

Foutafhandeling & Statuscodes

  • 400 Bad Request: Niet van toepassing. Een ongeldig of ontbrekend year-formaat valt stilzwijgend terug op het huidige jaar; de waarde wordt geklemd op het bereik 2020 tot en met het volgende jaar.
  • 403 Forbidden: Als de gebruiker niet over de permissie statistieken.view beschikt.

Autorisatie & Beveiliging

  • Permissie: statistieken.view is vereist voor alle drie de endpoints, afgedwongen via HasPermissionCode in de permission_classes van elke view. Er is geen step-up vereist.
  • Authenticatie: Alle endpoints vereisen IsAuthenticated.
  • Tenant-isolatie: Statistieken worden berekend binnen het database-schema van de actieve tenant (django-tenants). Het User-model heeft geen directe tenant-FK; actieve gebruikers worden daarom geteld via member_users_qs(tenant) (op basis van lidmaatschap) en de rolverdeling via Role.objects.filter(tenant=tenant).

Bestandsstructuur & Verantwoordelijkheden

  • backend/features/statistieken/models.py: Definieert YearlyStatistic (met de ItemType-enum en de increment-classmethod) en ReviewStatisticsRecord.
  • backend/features/statistieken/services.py: Bevat get_year_statistics (met caching), de deelfuncties _compute_review_block, _compute_users_block en _compute_content_block, get_available_years, de exportbronnen get_all_patients_for_export en get_afdeling_history_for_export, en de Excel-builder build_statistieken_xlsx.
  • backend/features/statistieken/review_statistics.py: Synchroniseert reviewpatienten naar ReviewStatisticsRecord (ensure_review_statistics_record, sync_patient_status, sync_bulk_review_status, mark_review_statistics_record_deleted).
  • backend/features/statistieken/signals.py: Hoogt YearlyStatistic op en invalideert de cache bij wijzigingen in nieuws, agenda, werkafspraken, Atlas-berichten, nazendingen en reviewpatienten.
  • backend/features/statistieken/views.py: Bevat YearStatisticsView, AvailableYearsView en ExportXlsxView.
  • backend/features/statistieken/serializers.py: YearStatisticsSerializer met de geneste serializers voor maandpunten, rol- en categorie-uitsplitsingen.
  • backend/features/statistieken/apps.py: Importeert de signalen in de ready-hook.
  • backend/features/statistieken/urls.py: Definieert de routes "", "years/" en "export/" onder /api/statistieken/.

Belangrijke bestanden

  • backend/features/statistieken/services.py
  • backend/features/statistieken/review_statistics.py
  • backend/features/statistieken/signals.py
  • backend/features/statistieken/models.py
  • backend/features/statistieken/views.py

API & Communicatie (Swagger)