openapi: 3.0.3
info:
  title: Prickar API
  version: 0.1.0
  description: |
    API för kontorets musikregel-prickar. Bygg valfritt gränssnitt
    (webb, väggskärm, fysisk knapp) mot dessa endpoints.

    Prickar räknas per kalenderår — § 1.5.0 (nollställning 1 januari)
    sker automatiskt. Alla list-endpoints tar `?year=` för historik.

    Realtid: lyssna på `GET /api/events` (Server-Sent Events) för att få
    push när något ändras, istället för att polla.
# Ingen autentisering (medvetet – kontorsnätet)
security: []

servers:
  - url: http://localhost:3210
    description: Lokal utveckling (docker compose)

tags:
  - name: personer
  - name: prickar
  - name: regler
  - name: förslag
  - name: överklaganden
  - name: händelser

paths:
  /api/persons:
    get:
      tags: [personer]
      summary: Lista personer med pricksaldo
      parameters:
        - $ref: '#/components/parameters/year'
      responses:
        '200':
          description: Personer sorterade på flest prickar
          content:
            application/json:
              schema:
                type: object
                properties:
                  year: { type: integer, example: 2026 }
                  persons:
                    type: array
                    items: { $ref: '#/components/schemas/Person' }
    post:
      tags: [personer]
      summary: Lägg till person
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, example: Erik }
      responses:
        '201':
          description: Person skapad
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  name: { type: string }
        '400': { $ref: '#/components/responses/Error' }
        '409': { description: Namnet finns redan }

  /api/scoreboard:
    get:
      tags: [personer]
      summary: Slimmad ställning för displayer
      description: Endast namn + antal prickar, sorterat på flest först.
      parameters:
        - $ref: '#/components/parameters/year'
      responses:
        '200':
          description: Ställningen
          content:
            application/json:
              schema:
                type: object
                properties:
                  year: { type: integer }
                  scoreboard:
                    type: array
                    items:
                      type: object
                      properties:
                        name: { type: string }
                        pricks: { type: integer }

  /api/pricks:
    get:
      tags: [prickar]
      summary: Lista prickar
      parameters:
        - $ref: '#/components/parameters/year'
        - name: person_id
          in: query
          schema: { type: integer }
          description: Filtrera på en person
      responses:
        '200':
          description: Prickar, senaste först
          content:
            application/json:
              schema:
                type: object
                properties:
                  year: { type: integer }
                  pricks:
                    type: array
                    items: { $ref: '#/components/schemas/Prick' }
    post:
      tags: [prickar]
      summary: Dela ut prick
      description: |
        Ange personen med `personId` **eller** `personName`
        (skiftlägesokänsligt — tänkt för enkla klienter som fysiska
        knappar som bara känner till namnet).

        `ruleParagraph` är valfri men måste, om den anges, peka på en
        aktiv regel som ger prick (`gives_prick = 1`). Informationsregler
        och upphävda regler avvisas med 400.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                personId: { type: integer, example: 1 }
                personName: { type: string, example: Erik }
                ruleParagraph: { type: string, example: 1.10.0 }
                comment: { type: string, example: shufflade fredagslistan }
                givenBy: { type: string, example: Alexander }
            examples:
              medNamn:
                summary: Från en fysisk knapp
                value: { personName: Erik, ruleParagraph: 1.10.0 }
              medId:
                summary: Med person-id
                value: { personId: 1, ruleParagraph: 1.6.0, givenBy: Alexander }
      responses:
        '201':
          description: Prick registrerad (broadcastas som `prick.added`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  person: { type: string }
                  ruleParagraph: { type: string, nullable: true }
        '400': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }

  /api/pricks/{id}:
    delete:
      tags: [prickar]
      summary: Ta bort prick
      parameters:
        - $ref: '#/components/parameters/id'
      responses:
        '200':
          description: Borttagen (broadcastas som `prick.deleted`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted: { type: integer }
        '404': { $ref: '#/components/responses/Error' }

  /api/rules:
    get:
      tags: [regler]
      summary: Lista musikreglerna
      parameters:
        - name: givesPrick
          in: query
          schema: { type: string, enum: ['1'] }
          description: Endast prickgivande regler (för prick-gränssnitt)
        - name: includeRepealed
          in: query
          schema: { type: string, enum: ['1'] }
          description: Inkludera upphävda regler
      responses:
        '200':
          description: Regler sorterade på paragraf
          content:
            application/json:
              schema:
                type: object
                properties:
                  rules:
                    type: array
                    items: { $ref: '#/components/schemas/Rule' }

  /api/rule-suggestions:
    get:
      tags: [förslag]
      summary: Lista regelförslag
      responses:
        '200':
          description: Förslag, senaste först
          content:
            application/json:
              schema:
                type: object
                properties:
                  suggestions:
                    type: array
                    items: { $ref: '#/components/schemas/Suggestion' }
    post:
      tags: [förslag]
      summary: Skicka in regelförslag
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, example: Max en Eurovision-låt per dag }
                suggestedBy: { type: string, example: Erik }
      responses:
        '201':
          description: Förslag registrerat med status pending
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  status: { type: string, example: pending }
        '400': { $ref: '#/components/responses/Error' }

  /api/rule-suggestions/{id}/decision:
    post:
      tags: [förslag]
      summary: 'Kommittébeslut: godkänn eller avslå förslag'
      description: |
        Godkännande kräver `paragraph` (t.ex. `1.14.0`) och skriver in
        regeln i regelverket. `givesPrick: false` gör den till
        informationsregel (standard är att den ger prick).
      parameters:
        - $ref: '#/components/parameters/id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [approved, rejected] }
                paragraph: { type: string, example: 1.14.0 }
                givesPrick: { type: boolean, default: true }
      responses:
        '200':
          description: Beslut registrerat (godkännande broadcastas som `rules.updated`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  status: { type: string }
        '400': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }
        '409': { description: Paragrafnumret finns redan }

  /api/appeals:
    get:
      tags: [överklaganden]
      summary: Lista överklaganden
      responses:
        '200':
          description: Överklaganden, senaste först
          content:
            application/json:
              schema:
                type: object
                properties:
                  appeals:
                    type: array
                    items: { $ref: '#/components/schemas/Appeal' }
    post:
      tags: [överklaganden]
      summary: Överklaga en regel
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ruleParagraph, reason]
              properties:
                ruleParagraph: { type: string, example: 1.6.0 }
                reason: { type: string, example: Orimlig regel }
                appealedBy: { type: string, example: Alexander }
      responses:
        '201':
          description: Överklagande registrerat med status pending
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  status: { type: string, example: pending }
        '400': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }

  /api/appeals/{id}/decision:
    post:
      tags: [överklaganden]
      summary: 'Kommittébeslut: godkänn eller avslå överklagande'
      description: |
        Ett godkänt överklagande upphäver regeln (soft delete —
        prick-historiken behålls).
      parameters:
        - $ref: '#/components/parameters/id'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [status]
              properties:
                status: { type: string, enum: [approved, rejected] }
      responses:
        '200':
          description: Beslut registrerat (godkännande broadcastas som `rules.updated`)
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: integer }
                  status: { type: string }
        '400': { $ref: '#/components/responses/Error' }
        '404': { $ref: '#/components/responses/Error' }

  /api/events:
    get:
      tags: [händelser]
      summary: Händelseström (Server-Sent Events)
      description: |
        Håller anslutningen öppen och pushar `text/event-stream`-händelser:

        | event | data |
        |---|---|
        | `prick.added` | `{ id, personId, person, ruleParagraph }` |
        | `prick.deleted` | `{ id }` |
        | `person.added` | `{ id, name }` |
        | `rules.updated` | ny/upphävd regel |

        Kommentarsrader (`: ping`) skickas var 25:e sekund som heartbeat.
        Använd `EventSource` i webbläsare; på mikrokontroller räcker en
        vanlig HTTP-klient som läser raderna löpande.
      responses:
        '200':
          description: Öppen händelseström
          content:
            text/event-stream:
              schema: { type: string }

components:
  parameters:
    year:
      name: year
      in: query
      schema: { type: string, pattern: '^\d{4}$' }
      description: Kalenderår (standard är innevarande år)
    id:
      name: id
      in: path
      required: true
      schema: { type: integer }
  responses:
    Error:
      description: Fel
      content:
        application/json:
          schema:
            type: object
            properties:
              error: { type: string, example: person hittades inte }
  schemas:
    Person:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        active: { type: integer, description: 1 = aktiv }
        pricks: { type: integer, description: Antal prickar valt år }
    Prick:
      type: object
      properties:
        id: { type: integer }
        person_id: { type: integer }
        person: { type: string }
        paragraph: { type: string, nullable: true }
        rule: { type: string, nullable: true }
        comment: { type: string, nullable: true }
        given_by: { type: string, nullable: true }
        created_at: { type: string, example: '2026-08-21 11:41:43' }
    Rule:
      type: object
      properties:
        id: { type: integer }
        paragraph: { type: string, example: 1.10.0 }
        text: { type: string, example: Fredagslistan får inte shufflas }
        revision: { type: string, example: 1.0.5 - 2024-06-26 }
        repealed: { type: integer, description: 1 = upphävd }
        gives_prick: { type: integer, description: 1 = kan ge prick }
    Suggestion:
      type: object
      properties:
        id: { type: integer }
        text: { type: string }
        suggested_by: { type: string, nullable: true }
        status: { type: string, enum: [pending, approved, rejected] }
        decided_at: { type: string, nullable: true }
        created_at: { type: string }
    Appeal:
      type: object
      properties:
        id: { type: integer }
        paragraph: { type: string }
        rule: { type: string }
        reason: { type: string }
        appealed_by: { type: string, nullable: true }
        status: { type: string, enum: [pending, approved, rejected] }
        decided_at: { type: string, nullable: true }
        created_at: { type: string }
