Files
boc/landvex/reporting-module/DESIGN.md
T
Bernt 05ed037fe8 pilot.landvex.com: HTTPS + Full Stack Verified
- DNS: pilot.landvex.com -> 16.170.83.169
- TLS: Let's Encrypt certificate (expires 2026-09-30)
- Nginx: reverse proxy with SSL termination
- API: https://pilot.landvex.com/api/v1/missions
- UI: https://pilot.landvex.com/
- Upload: POST /api/v1/missions/import (multipart/form-data)

Verified:
 https://pilot.landvex.com/health
 https://pilot.landvex.com/version
 https://pilot.landvex.com/api/v1/missions (list)
 https://pilot.landvex.com/api/v1/missions/:id (get)
 POST /api/v1/missions/import (video upload)
 UI loads with title 'LandveX Intelligence Lab'

Next: Pilot 001 — Break the system!
2026-07-02 17:34:19 +00:00

25 KiB

LandveX Enterprise Platform — Rapporterings- och Analysmodul

Översikt

Denna modul ger kommuner och andra kunder self-service rapportering med schemaläggning, delning och export. Den integreras med LandveX befintliga datakällor och exponeras via ett REST-API.


1. Datamodell

1.1 Entiteter (sammansatt vy)

┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│   Report        │     │  ReportSchedule │     │  ReportShare    │
├─────────────────┤     ├─────────────────┤     ├─────────────────┤
│ id (PK)         │     │ id (PK)         │     │ id (PK)         │
│ tenantId (FK)   │◄────┤ reportId (FK)   │     │ reportId (FK)   │
│ name            │     │ cronExpression  │     │ sharedWithUserId│
│ description     │     │ nextRunAt       │     │ sharedWithEmail │
│ type            │     │ lastRunAt       │     │ permission      │
│ config (JSON)   │     │ status          │     │ expiresAt       │
│ dataSourceId    │     │ createdBy       │     │ createdAt       │
│ createdBy       │     └─────────────────┘     └─────────────────┘
│ createdAt       │
│ updatedAt       │     ┌─────────────────┐     ┌─────────────────┐
└─────────────────┘     │  ReportRun      │     │  DataSource     │
                        ├─────────────────┤     ├─────────────────┤
                        │ id (PK)         │     │ id (PK)         │
                        │ reportId (FK)   │     │ tenantId (FK)   │
                        │ status          │     │ name            │
                        │ startedAt       │     │ type            │
                        │ completedAt     │     │ connection      │
                        │ resultUrl       │     │ config (JSON)   │
                        │ rowCount        │     │ status          │
                        │ errorMessage    │     └─────────────────┘
                        └─────────────────┘

1.2 PostgreSQL DDL

-- Rapporter
CREATE TABLE reports (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL REFERENCES tenants(id),
    name            VARCHAR(255) NOT NULL,
    description     TEXT,
    type            VARCHAR(50) NOT NULL CHECK (type IN ('table','chart','pivot','map','dashboard')),
    config          JSONB NOT NULL DEFAULT '{}',
    data_source_id  UUID REFERENCES data_sources(id),
    created_by      UUID NOT NULL REFERENCES users(id),
    created_at      TIMESTAMPTZ DEFAULT now(),
    updated_at      TIMESTAMPTZ DEFAULT now()
);

-- Körningshistorik
CREATE TABLE report_runs (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    report_id       UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
    status          VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','running','completed','failed')),
    started_at      TIMESTAMPTZ,
    completed_at    TIMESTAMPTZ,
    result_url      TEXT,
    row_count       INTEGER,
    error_message   TEXT,
    triggered_by    UUID REFERENCES users(id),
    created_at      TIMESTAMPTZ DEFAULT now()
);

-- Scheman
CREATE TABLE report_schedules (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    report_id       UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
    cron_expression VARCHAR(100) NOT NULL,
    timezone        VARCHAR(50) DEFAULT 'Europe/Stockholm',
    next_run_at     TIMESTAMPTZ,
    last_run_at     TIMESTAMPTZ,
    status          VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active','paused','disabled')),
    created_by      UUID NOT NULL REFERENCES users(id),
    created_at      TIMESTAMPTZ DEFAULT now()
);

-- Delning
CREATE TABLE report_shares (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    report_id           UUID NOT NULL REFERENCES reports(id) ON DELETE CASCADE,
    shared_with_user_id UUID REFERENCES users(id),
    shared_with_email   VARCHAR(255),
    permission          VARCHAR(20) NOT NULL DEFAULT 'read' CHECK (permission IN ('read','write','admin')),
    expires_at          TIMESTAMPTZ,
    created_by          UUID NOT NULL REFERENCES users(id),
    created_at          TIMESTAMPTZ DEFAULT now()
);

-- Datakällor (befintlig tabell, utökas)
CREATE TABLE data_sources (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id   UUID NOT NULL REFERENCES tenants(id),
    name        VARCHAR(255) NOT NULL,
    type        VARCHAR(50) NOT NULL CHECK (type IN ('postgresql','mysql','api','s3','bigquery','snowflake')),
    connection  JSONB NOT NULL,
    config      JSONB DEFAULT '{}',
    status      VARCHAR(20) DEFAULT 'active',
    created_at  TIMESTAMPTZ DEFAULT now()
);

-- Index
CREATE INDEX idx_reports_tenant ON reports(tenant_id);
CREATE INDEX idx_report_runs_report ON report_runs(report_id, created_at DESC);
CREATE INDEX idx_schedules_next_run ON report_schedules(next_run_at) WHERE status = 'active';
CREATE INDEX idx_shares_user ON report_shares(shared_with_user_id);

2. Rapport-API

2.1 REST Endpoints

Metod Endpoint Beskrivning
GET /api/v1/reports Lista rapporter (tenant-scoped)
POST /api/v1/reports Skapa ny rapport
GET /api/v1/reports/:id Hämta rapport med config
PUT /api/v1/reports/:id Uppdatera rapport
DELETE /api/v1/reports/:id Ta bort rapport
POST /api/v1/reports/:id/run Kör rapport on-demand
GET /api/v1/reports/:id/runs Körningshistorik
GET /api/v1/reports/:id/runs/:runId/result Hämta resultat (JSON/CSV)
POST /api/v1/reports/:id/export Exportera rapport
POST /api/v1/reports/:id/schedule Skapa schema
GET /api/v1/reports/:id/schedule Visa schema
PUT /api/v1/reports/:id/schedule Uppdatera schema
DELETE /api/v1/reports/:id/schedule Ta bort schema
POST /api/v1/reports/:id/share Dela rapport
GET /api/v1/reports/:id/shares Lista delningar
DELETE /api/v1/reports/:id/shares/:shareId Återkalla delning

2.2 OpenAPI-spec (kärn-endpoints)

openapi: 3.0.3
info:
  title: LandveX Reporting API
  version: 1.0.0
paths:
  /api/v1/reports:
    get:
      summary: Lista rapporter
      parameters:
        - name: tenantId
          in: query
          required: true
          schema: { type: string, format: uuid }
        - name: type
          in: query
          schema: { type: string, enum: [table, chart, pivot, map, dashboard] }
        - name: q
          in: query
          schema: { type: string }
      responses:
        200:
          description: Lista med rapporter
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Report'
                  meta:
                    type: object
                    properties:
                      total: { type: integer }
                      page: { type: integer }
                      pageSize: { type: integer }
    post:
      summary: Skapa rapport
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReportInput'
      responses:
        201:
          description: Skapad
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Report'

  /api/v1/reports/{id}/run:
    post:
      summary: Kör rapport
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                parameters:
                  type: object
                  description: Körningsparametrar (ersätter default)
                format:
                  type: string
                  enum: [json, csv, xlsx, pdf]
                  default: json
      responses:
        202:
          description: Körs
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReportRun'

  /api/v1/reports/{id}/export:
    post:
      summary: Exportera rapport
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                format:
                  type: string
                  enum: [csv, xlsx, pdf, json]
                  default: csv
                filters:
                  type: object
                email:
                  type: string
                  description: Skicka via e-post istället för nedladdning
      responses:
        200:
          description: Fil eller bekräftelse
        202:
          description: E-post kommer skickas

  /api/v1/reports/{id}/schedule:
    post:
      summary: Skapa schema
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ScheduleInput'
      responses:
        201:
          description: Schema skapat

components:
  schemas:
    Report:
      type: object
      properties:
        id: { type: string, format: uuid }
        tenantId: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        type: { type: string, enum: [table, chart, pivot, map, dashboard] }
        config: { type: object }
        dataSourceId: { type: string, format: uuid }
        createdBy: { type: string, format: uuid }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    ReportInput:
      type: object
      required: [name, type, config, dataSourceId]
      properties:
        name: { type: string, minLength: 1, maxLength: 255 }
        description: { type: string }
        type: { type: string, enum: [table, chart, pivot, map, dashboard] }
        config: { type: object }
        dataSourceId: { type: string, format: uuid }

    ReportRun:
      type: object
      properties:
        id: { type: string, format: uuid }
        reportId: { type: string, format: uuid }
        status: { type: string, enum: [pending, running, completed, failed] }
        startedAt: { type: string, format: date-time }
        completedAt: { type: string, format: date-time }
        resultUrl: { type: string }
        rowCount: { type: integer }
        errorMessage: { type: string }

    ScheduleInput:
      type: object
      required: [cronExpression]
      properties:
        cronExpression: { type: string, example: "0 6 * * 1" }
        timezone: { type: string, default: "Europe/Stockholm" }
        status: { type: string, enum: [active, paused, disabled], default: active }

3. Tre Standardrapporter

3.1 Rapport A: "Fastighetsöversikt per Kommun"

Typ: Tabell + Karta
Beskrivning: Översikt över alla fastigheter inom kommunens gränser med nyckeltal.

{
  "name": "Fastighetsöversikt per Kommun",
  "type": "dashboard",
  "description": "Sammanställning av fastigheter, areal och värden per kommun",
  "dataSourceId": "ds-landvex-gis",
  "config": {
    "widgets": [
      {
        "type": "metric",
        "title": "Totalt antal fastigheter",
        "query": "SELECT COUNT(*) FROM properties WHERE municipality_id = :municipalityId"
      },
      {
        "type": "metric",
        "title": "Total areal (ha)",
        "query": "SELECT SUM(area_hectares) FROM properties WHERE municipality_id = :municipalityId"
      },
      {
        "type": "table",
        "title": "Fastigheter per typ",
        "query": "SELECT type, COUNT(*) as count, SUM(area_hectares) as total_area FROM properties WHERE municipality_id = :municipalityId GROUP BY type",
        "columns": [
          { "field": "type", "header": "Fastighetstyp" },
          { "field": "count", "header": "Antal" },
          { "field": "total_area", "header": "Total areal (ha)" }
        ]
      },
      {
        "type": "map",
        "title": "Fastighetskarta",
        "layer": "properties",
        "filter": { "municipality_id": ":municipalityId" }
      }
    ],
    "parameters": [
      {
        "name": "municipalityId",
        "type": "string",
        "required": true,
        "label": "Kommun",
        "source": "tenant.config.municipality_id"
      }
    ],
    "defaultFormat": "pdf"
  }
}

3.2 Rapport B: "Transaktionshistorik med Trender"

Typ: Diagram + Tabell
Beskrivning: Historik över fastighetstransaktioner med prisutveckling och trender.

{
  "name": "Transaktionshistorik med Trender",
  "type": "chart",
  "description": "Transaktioner över tid med prisutveckling",
  "dataSourceId": "ds-landvex-transactions",
  "config": {
    "charts": [
      {
        "type": "line",
        "title": "Prisutveckling över tid",
        "xAxis": { "field": "transaction_month", "label": "Månad" },
        "yAxis": { "field": "avg_price_per_hectare", "label": "Kr/ha (genomsnitt)" },
        "query": "SELECT DATE_TRUNC('month', transaction_date) as transaction_month, AVG(price_per_hectare) as avg_price_per_hectare FROM transactions WHERE municipality_id = :municipalityId AND transaction_date >= :fromDate GROUP BY 1 ORDER BY 1"
      },
      {
        "type": "bar",
        "title": "Antal transaktioner per månad",
        "xAxis": { "field": "transaction_month" },
        "yAxis": { "field": "transaction_count" },
        "query": "SELECT DATE_TRUNC('month', transaction_date) as transaction_month, COUNT(*) as transaction_count FROM transactions WHERE municipality_id = :municipalityId AND transaction_date >= :fromDate GROUP BY 1 ORDER BY 1"
      }
    ],
    "detailTable": {
      "title": "Senaste transaktioner",
      "query": "SELECT t.id, t.property_id, p.address, t.transaction_date, t.price, t.area_hectares, t.price_per_hectare FROM transactions t JOIN properties p ON t.property_id = p.id WHERE t.municipality_id = :municipalityId ORDER BY t.transaction_date DESC LIMIT 100",
      "columns": [
        { "field": "id", "header": "Transaktions-ID" },
        { "field": "address", "header": "Fastighet" },
        { "field": "transaction_date", "header": "Datum", "format": "date" },
        { "field": "price", "header": "Pris (kr)", "format": "currency" },
        { "field": "area_hectares", "header": "Areal (ha)", "format": "number" },
        { "field": "price_per_hectare", "header": "Kr/ha", "format": "currency" }
      ]
    },
    "parameters": [
      {
        "name": "municipalityId",
        "type": "string",
        "required": true,
        "source": "tenant.config.municipality_id"
      },
      {
        "name": "fromDate",
        "type": "date",
        "required": false,
        "default": "-1 year",
        "label": "Från datum"
      }
    ],
    "defaultFormat": "xlsx"
  }
}

3.3 Rapport C: "Skogsbruksplan — Åtgärdsförslag"

Typ: Tabell + Pivot
Beskrivning: Åtgärdsförslag från skogsbruksplaner med tidsplan och volymuppskattning.

{
  "name": "Skogsbruksplan — Åtgärdsförslag",
  "type": "pivot",
  "description": "Åtgärdsförslag från skogsbruksplaner sorterade på prioritet och tidsplan",
  "dataSourceId": "ds-landvex-forestry",
  "config": {
    "pivot": {
      "rows": ["action_type", "forest_stand_id"],
      "columns": ["planned_year"],
      "values": [
        { "field": "estimated_volume", "aggregation": "sum", "format": "number" },
        { "field": "estimated_cost", "aggregation": "sum", "format": "currency" }
      ]
    },
    "sourceQuery": "SELECT fa.action_type, fa.forest_stand_id, fa.planned_year, fa.estimated_volume_m3 as estimated_volume, fa.estimated_cost, fa.priority, fa.status FROM forestry_actions fa JOIN forest_stands fs ON fa.forest_stand_id = fs.id WHERE fs.municipality_id = :municipalityId AND fa.planned_year BETWEEN :fromYear AND :toYear",
    "detailTable": {
      "title": "Åtgärdslista",
      "query": "SELECT fa.id, fa.action_type, fs.stand_name, fa.planned_year, fa.estimated_volume_m3, fa.estimated_cost, fa.priority, fa.status, fa.notes FROM forestry_actions fa JOIN forest_stands fs ON fa.forest_stand_id = fs.id WHERE fs.municipality_id = :municipalityId AND fa.planned_year BETWEEN :fromYear AND :toYear ORDER BY fa.priority, fa.planned_year",
      "columns": [
        { "field": "action_type", "header": "Åtgärd" },
        { "field": "stand_name", "header": "Bestånd" },
        { "field": "planned_year", "header": "Planerat år" },
        { "field": "estimated_volume_m3", "header": "Volym (m³)", "format": "number" },
        { "field": "estimated_cost", "header": "Kostnad (kr)", "format": "currency" },
        { "field": "priority", "header": "Prioritet" },
        { "field": "status", "header": "Status" }
      ]
    },
    "parameters": [
      {
        "name": "municipalityId",
        "type": "string",
        "required": true,
        "source": "tenant.config.municipality_id"
      },
      {
        "name": "fromYear",
        "type": "integer",
        "required": false,
        "default": "current_year",
        "label": "Från år"
      },
      {
        "name": "toYear",
        "type": "integer",
        "required": false,
        "default": "current_year + 5",
        "label": "Till år"
      }
    ],
    "defaultFormat": "xlsx"
  }
}

4. Arkitektur & Integration

4.1 Komponentdiagram

┌─────────────────────────────────────────────────────────────┐
│                     LandveX Enterprise Platform              │
│                                                              │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐     │
│  │   Web UI    │    │  Mobile App │    │  API-klient │     │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘     │
│         │                  │                  │              │
│         └──────────────────┼──────────────────┘              │
│                            ▼                                 │
│              ┌─────────────────────────┐                     │
│              │    Reporting API        │                     │
│              │  (REST + Auth/tenant)   │                     │
│              └───────────┬─────────────┘                     │
│                          │                                   │
│         ┌────────────────┼────────────────┐                  │
│         ▼                ▼                ▼                  │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │  Scheduler  │  │  Query Eng. │  │  Export Svc │          │
│  │  (BullMQ)   │  │  (PostgREST)│  │ (CSV/XLSX/  │          │
│  │             │  │  (Direct)   │  │  PDF/JSON)  │          │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘          │
│         │                │                │                  │
│         ▼                ▼                ▼                  │
│  ┌─────────────────────────────────────────────────┐        │
│  │              LandveX Datakällor                  │        │
│  │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌────────┐│        │
│  │  │GIS/PostGIS│ │Transakt.│ │Skogsbr. │ │ Extern ││        │
│  │  │         │ │  DB     │ │  DB     │ │  API   ││        │
│  │  └─────────┘ └─────────┘ └─────────┘ └────────┘│        │
│  └─────────────────────────────────────────────────┘        │
│                                                              │
│  ┌─────────────────────────────────────────────────┐        │
│  │           Delning & Notifikationer               │        │
│  │  • E-post (SendGrid/AWS SES)                     │        │
│  │  • Länk-baserad delning (token)                  │        │
│  │  • Inbäddad rapport (iframe)                     │        │
│  └─────────────────────────────────────────────────┘        │
└─────────────────────────────────────────────────────────────┘

4.2 Scheduler-flöde (BullMQ)

┌──────────────┐     ┌──────────────┐     ┌──────────────┐
│  Cron-jobb   │────►│  Query Engine │────►│  Store result│
│  (var 1 min) │     │  (timeout 5m) │     │  (S3/DB)     │
└──────────────┘     └──────────────┘     └──────────────┘
                            │
                            ▼
                     ┌──────────────┐
                     │  Notifiera   │
                     │  (e-post/    │
                     │   webhook)   │
                     └──────────────┘

4.3 Exempel: Köra rapport via API

# 1. Skapa rapport
curl -X POST https://api.landvex.se/v1/reports \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d @fastighetsöversikt.json

# 2. Kör rapport
curl -X POST https://api.landvex.se/v1/reports/$REPORT_ID/run \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"format": "pdf", "parameters": {"municipalityId": "0180"}}'
# → { "id": "run-uuid", "status": "pending" }

# 3. Polling tills klar
curl https://api.landvex.se/v1/reports/$REPORT_ID/runs/$RUN_ID \
  -H "Authorization: Bearer $TOKEN"
# → { "status": "completed", "resultUrl": "https://cdn.landvex.se/..." }

# 4. Ladda ner resultat
curl -O "$RESULT_URL"

5. Säkerhet & Rättigheter

Nivå Behörighet
read Se rapport och resultat
write Redigera config, köra manuellt
admin Ändra schema, dela, ta bort
  • Tenant-isolering: Alla queries filtreras automatiskt på tenant_id
  • Parametriserade queries: Inga strängkonkateneringar — använd prepared statements
  • Resultat-URL:er: Tidsbegränsade signerade URL:er (S3 presigned, 1h default)
  • Audit-logg: Alla körningar och exporter loggas med user_id och timestamp

6. Leverabler

Fil Innehåll
DESIGN.md Denna fil — översikt och design
schema.sql PostgreSQL DDL
openapi.yaml Fullständig OpenAPI-spec
examples/ 3 standardrapporter (JSON)
architecture.md Djupare arkitekturbeskrivning