openapi: 3.1.0
info:
  title: Pointeur Backend – Frontend API
  description: |
    REST API for the Pointeur scheduling application.
    This specification covers the **frontend-facing** surface.
    All endpoints require JWT Bearer authentication unless stated otherwise.
  version: 2.0.0
  contact:
    name: 2RK-dev
servers:
  - url: /api/v1
    description: Frontend API base path
security:
  - bearerAuth: []
tags:
  - name: Authentication
    description: Login, logout, token refresh, and password management
  - name: Users
    description: User account management (SuperAdmin only)
  - name: API Keys
    description: API key management for the integration surface (SuperAdmin only)
  - name: Levels
    description: Academic levels
  - name: Groups
    description: Student groups within levels
  - name: Teachers
    description: Teacher registry
  - name: Teaching Units
    description: Teaching unit (subject) registry
  - name: Rooms
    description: Room registry and availability
  - name: Schedule
    description: Schedule item management
  - name: Import
    description: Bulk data import
  - name: Export
    description: Data export
paths:
  /auth/login:
    post:
      operationId: login
      tags:
        - Authentication
      summary: Authenticate a user
      description: |
        Validates credentials and returns a JWT access token together with user
        information.  Refresh-token and device-id cookies are set automatically.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginRequestDTO'
      responses:
        '200':
          description: Authentication successful
          headers:
            Set-Cookie:
              description: '`refresh_token` and `device_id` HttpOnly cookies'
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponseDTO'
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /auth/me:
    get:
      operationId: getCurrentUser
      tags:
        - Authentication
      summary: Get the current authenticated user
      responses:
        '200':
          description: Current user information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserInfoDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/refresh:
    post:
      operationId: refreshToken
      tags:
        - Authentication
      summary: Refresh the access token
      description: |
        Uses the `refresh_token` and `device_id` cookies to issue a new access
        token.  The refresh-token cookie is rotated.
      security: []
      parameters:
        - name: device_id
          in: cookie
          schema:
            type: string
        - name: refresh_token
          in: cookie
          schema:
            type: string
      responses:
        '200':
          description: Token refreshed
          headers:
            Set-Cookie:
              description: Updated `refresh_token` HttpOnly cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LoginResponseDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /auth/logout:
    post:
      operationId: logout
      tags:
        - Authentication
      summary: Log out the current session
      description: Clears the refresh-token cookie.
      security: []
      parameters:
        - name: device_id
          in: cookie
          schema:
            type: string
        - name: refresh_token
          in: cookie
          schema:
            type: string
      responses:
        '204':
          description: Logged out successfully
  /auth/password:
    put:
      operationId: changePassword
      tags:
        - Authentication
      summary: Change the current user's password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChangePasswordDTO'
      responses:
        '200':
          description: Password changed successfully
          content:
            application/json:
              schema:
                type: string
        '400':
          description: Validation error or wrong old password
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /users:
    get:
      operationId: listUsers
      tags:
        - Users
      summary: List all users
      description: Returns all users except the SuperAdmin account. Requires SuperAdmin role.
      responses:
        '200':
          description: List of users
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/UserDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      operationId: createUser
      tags:
        - Users
      summary: Create a new admin user
      description: Creates a new user with a randomly generated password. Requires SuperAdmin role.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserDTO'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserCreatedDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /users/{userId}:
    get:
      operationId: getUser
      tags:
        - Users
      summary: Get a user by ID
      parameters:
        - $ref: '#/components/parameters/userId'
      responses:
        '200':
          description: User details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteUser
      tags:
        - Users
      summary: Delete a user
      parameters:
        - $ref: '#/components/parameters/userId'
      responses:
        '204':
          description: User deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /api-keys:
    get:
      operationId: listApiKeys
      tags:
        - API Keys
      summary: List all API keys
      responses:
        '200':
          description: List of API keys (tokens are masked)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ApiKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
    post:
      operationId: createApiKey
      tags:
        - API Keys
      summary: Create a new API key
      description: |
        The raw token is returned **only once** in this response.  Store it
        securely — it cannot be retrieved again.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiKeyCreateRequest'
      responses:
        '200':
          description: API key created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiKeyWithRawToken'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /api-keys/{apiKeyId}:
    delete:
      operationId: deleteApiKey
      tags:
        - API Keys
      summary: Revoke an API key
      parameters:
        - $ref: '#/components/parameters/apiKeyId'
      responses:
        '204':
          description: API key revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
  /levels:
    get:
      operationId: listLevels
      tags:
        - Levels
      summary: List all levels with their groups
      responses:
        '200':
          description: List of levels with nested groups
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/LevelDetailsDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createLevel
      tags:
        - Levels
      summary: Create a new level
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateLevelDTO'
      responses:
        '201':
          description: Level created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LevelDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '409':
          description: Level name already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /levels/{levelId}:
    get:
      operationId: getLevel
      tags:
        - Levels
      summary: Get a level with its groups
      parameters:
        - $ref: '#/components/parameters/levelId'
      responses:
        '200':
          description: Level details with groups
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LevelDetailsDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    put:
      operationId: updateLevel
      tags:
        - Levels
      summary: Update a level
      parameters:
        - $ref: '#/components/parameters/levelId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateLevelDTO'
      responses:
        '200':
          description: Level updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LevelDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '409':
          description: Level name already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteLevel
      tags:
        - Levels
      summary: Delete a level
      parameters:
        - $ref: '#/components/parameters/levelId'
      responses:
        '204':
          description: Level deleted
  /levels/{levelId}/groups:
    get:
      operationId: listGroupsByLevel
      tags:
        - Groups
      summary: List groups in a level
      parameters:
        - $ref: '#/components/parameters/levelId'
      responses:
        '200':
          description: List of groups
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/GroupDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    post:
      operationId: createGroup
      tags:
        - Groups
      summary: Create a group in a level
      parameters:
        - $ref: '#/components/parameters/levelId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGroupDTO'
      responses:
        '201':
          description: Group created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /levels/{levelId}/groups/{groupId}:
    get:
      operationId: getGroup
      tags:
        - Groups
      summary: Get a group
      parameters:
        - $ref: '#/components/parameters/levelId'
        - $ref: '#/components/parameters/groupId'
      responses:
        '200':
          description: Group details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Group or level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    put:
      operationId: updateGroup
      tags:
        - Groups
      summary: Update a group
      parameters:
        - $ref: '#/components/parameters/levelId'
        - $ref: '#/components/parameters/groupId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateGroupDTO'
      responses:
        '200':
          description: Group updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Group or level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteGroup
      tags:
        - Groups
      summary: Delete a group
      parameters:
        - $ref: '#/components/parameters/levelId'
        - $ref: '#/components/parameters/groupId'
      responses:
        '204':
          description: Group deleted
  /teachers:
    get:
      operationId: listTeachers
      tags:
        - Teachers
      summary: List all teachers
      responses:
        '200':
          description: List of teachers
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TeacherDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createTeacher
      tags:
        - Teachers
      summary: Register a new teacher
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTeacherDTO'
      responses:
        '201':
          description: Teacher registered
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeacherDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '409':
          description: Abbreviation already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /teachers/{teacherId}:
    get:
      operationId: getTeacher
      tags:
        - Teachers
      summary: Get a teacher
      parameters:
        - $ref: '#/components/parameters/teacherId'
      responses:
        '200':
          description: Teacher details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeacherDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Teacher not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    put:
      operationId: updateTeacher
      tags:
        - Teachers
      summary: Update a teacher
      parameters:
        - $ref: '#/components/parameters/teacherId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTeacherDTO'
      responses:
        '200':
          description: Teacher updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeacherDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Teacher not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '409':
          description: Abbreviation already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteTeacher
      tags:
        - Teachers
      summary: Delete a teacher
      parameters:
        - $ref: '#/components/parameters/teacherId'
      responses:
        '204':
          description: Teacher deleted
  /teachingUnits:
    get:
      operationId: listTeachingUnits
      tags:
        - Teaching Units
      summary: List all teaching units
      responses:
        '200':
          description: List of teaching units
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TeachingUnitDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createTeachingUnit
      tags:
        - Teaching Units
      summary: Create a teaching unit
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTeachingUnitDTO'
      responses:
        '201':
          description: Teaching unit created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeachingUnitDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '409':
          description: Abbreviation already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /teachingUnits/{unitId}:
    get:
      operationId: getTeachingUnit
      tags:
        - Teaching Units
      summary: Get a teaching unit
      parameters:
        - $ref: '#/components/parameters/unitId'
      responses:
        '200':
          description: Teaching unit details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeachingUnitDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Teaching unit not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    put:
      operationId: updateTeachingUnit
      tags:
        - Teaching Units
      summary: Update a teaching unit
      parameters:
        - $ref: '#/components/parameters/unitId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTeachingUnitDTO'
      responses:
        '200':
          description: Teaching unit updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TeachingUnitDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Teaching unit not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '409':
          description: Abbreviation already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteTeachingUnit
      tags:
        - Teaching Units
      summary: Delete a teaching unit
      parameters:
        - $ref: '#/components/parameters/unitId'
      responses:
        '204':
          description: Teaching unit deleted
  /levels/{levelId}/teachingUnits:
    get:
      operationId: listTeachingUnitsByLevel
      tags:
        - Teaching Units
      summary: List teaching units for a level
      parameters:
        - $ref: '#/components/parameters/levelId'
      responses:
        '200':
          description: Teaching units taught in the level
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/TeachingUnitDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Level not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /rooms:
    get:
      operationId: listRooms
      tags:
        - Rooms
      summary: List all rooms
      responses:
        '200':
          description: List of rooms
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RoomDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createRoom
      tags:
        - Rooms
      summary: Create a room
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateRoomDTO'
      responses:
        '201':
          description: Room created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '409':
          description: Room name already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /rooms/{roomId}:
    get:
      operationId: getRoom
      tags:
        - Rooms
      summary: Get a room
      parameters:
        - $ref: '#/components/parameters/roomId'
      responses:
        '200':
          description: Room details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Room not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    put:
      operationId: updateRoom
      tags:
        - Rooms
      summary: Update a room
      parameters:
        - $ref: '#/components/parameters/roomId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateRoomDTO'
      responses:
        '200':
          description: Room updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RoomDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Room not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '409':
          description: Room name already taken
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteRoom
      tags:
        - Rooms
      summary: Delete a room
      parameters:
        - $ref: '#/components/parameters/roomId'
      responses:
        '204':
          description: Room deleted
  /rooms/available:
    get:
      operationId: listAvailableRooms
      tags:
        - Rooms
      summary: List available rooms in a time range
      description: Returns rooms that are unoccupied between the given start and end times.
      parameters:
        - name: startTime
          in: query
          required: true
          description: Start date-time (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: endTime
          in: query
          required: true
          description: End date-time (ISO 8601)
          schema:
            type: string
            format: date-time
        - name: size
          in: query
          required: false
          description: Minimum room capacity (defaults to 1)
          schema:
            type: integer
            minimum: 1
            default: 1
      responses:
        '200':
          description: Available rooms
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/RoomDTO'
        '400':
          description: Invalid or missing parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /schedule:
    get:
      operationId: getSchedule
      tags:
        - Schedule
      summary: Query the schedule
      description: |
        Returns schedule items in the given date range.  Optionally filter by
        level and/or group.  If `groupId` is supplied without `levelId`, only
        that group's timetable is returned.
      parameters:
        - name: startDate
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: endDate
          in: query
          required: true
          schema:
            type: string
            format: date
        - name: levelId
          in: query
          required: false
          schema:
            type: integer
            format: int64
        - name: groupId
          in: query
          required: false
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: Matching schedule items
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/ScheduleItemDTO'
        '400':
          description: Invalid date range or parameters
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Group not found in the specified level
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    post:
      operationId: createScheduleItem
      tags:
        - Schedule
      summary: Create a schedule item
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateScheduleItemDTO'
      responses:
        '201':
          description: Schedule item created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleItemDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Referenced resource not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '409':
          description: Schedule conflict (room, teacher, or group overlap)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /schedule/batch:
    post:
      operationId: createScheduleItemsBatch
      tags:
        - Schedule
      summary: Create multiple schedule items
      description: |
        Processes each item independently.  Successfully created items and
        failed items (with reasons) are returned together.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/CreateScheduleItemDTO'
      responses:
        '200':
          description: Batch result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchCreateResponseScheduleItem'
  /schedule/{scheduleItemId}:
    get:
      operationId: getScheduleItem
      tags:
        - Schedule
      summary: Get a schedule item
      parameters:
        - $ref: '#/components/parameters/scheduleItemId'
      responses:
        '200':
          description: Schedule item details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleItemDTO'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Schedule item not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    put:
      operationId: updateScheduleItem
      tags:
        - Schedule
      summary: Update a schedule item
      parameters:
        - $ref: '#/components/parameters/scheduleItemId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateScheduleItemDTO'
      responses:
        '200':
          description: Schedule item updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScheduleItemDTO'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationErrorDetails'
        '404':
          description: Schedule item or referenced resource not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
        '409':
          description: Schedule conflict
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
    delete:
      operationId: deleteScheduleItem
      tags:
        - Schedule
      summary: Delete a schedule item
      parameters:
        - $ref: '#/components/parameters/scheduleItemId'
      responses:
        '204':
          description: Schedule item deleted
  /export:
    get:
      operationId: exportData
      tags:
        - Export
      summary: Export entities
      description: |
        Exports the requested entities in the specified format.  The response
        is a downloadable file (Excel, ZIP of CSVs, or JSON).
      parameters:
        - name: entitiesList
          in: query
          required: true
          description: Entity types to export (e.g. room, teacher, level, group, teaching_unit)
          schema:
            type: array
            items:
              type: string
              enum:
                - room
                - teacher
                - teaching_unit
                - group
                - level
        - name: format
          in: query
          required: true
          description: Output format
          schema:
            type: string
            enum:
              - excel
              - zip_csv
              - json
      responses:
        '200':
          description: Exported file
          headers:
            Content-Disposition:
              description: attachment; filename=export_<entities>_<date>.<ext>
              schema:
                type: string
          content:
            application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
              schema:
                type: string
                format: binary
            application/zip:
              schema:
                type: string
                format: binary
            application/json:
              schema:
                type: string
                format: binary
        '400':
          description: Unknown entity or unsupported format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
  /import/upload:
    post:
      operationId: importData
      tags:
        - Import
      summary: Import data from files
      description: |
        Accepts one or more files together with a metadata mapping that
        describes how columns map to entity fields.
      parameters:
        - name: ignoreConflicts
          in: query
          required: false
          description: Whether to skip rows that conflict with existing data
          schema:
            type: boolean
            default: true
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - metadata
                - files
              properties:
                metadata:
                  $ref: '#/components/schemas/ImportMapping'
                files:
                  type: array
                  items:
                    type: string
                    format: binary
      responses:
        '200':
          description: Import summary
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSummary'
        '400':
          description: Invalid file format or unsupported codec
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorDetails'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: JWT access token obtained via /auth/login
  schemas:
    LoginRequestDTO:
      type: object
      required:
        - username
        - password
      properties:
        username:
          type: string
          minLength: 1
        password:
          type: string
          minLength: 1
    UserInfoDTO:
      type: object
      required:
        - username
        - role
      properties:
        username:
          type: string
        role:
          type: string
          description: User role (e.g. ADMIN, SUPERADMIN)
    LoginResponseDTO:
      type: object
      required:
        - access_token
        - user
      properties:
        access_token:
          type: string
          description: JWT access token
        user:
          $ref: '#/components/schemas/UserInfoDTO'
    ErrorDetails:
      type: object
      required:
        - timestamp
        - message
        - errorCode
      properties:
        timestamp:
          type: string
          format: date-time
        message:
          type: string
        details:
          type: string
        errorCode:
          type: string
          description: Machine-readable error code
    ChangePasswordDTO:
      type: object
      required:
        - old
        - new
        - confirm
      properties:
        old:
          type: string
          minLength: 1
        new:
          type: string
          minLength: 1
        confirm:
          type: string
          minLength: 1
          description: Must match `new`
    UserDTO:
      type: object
      required:
        - id
        - info
      properties:
        id:
          type: integer
          format: int64
        info:
          $ref: '#/components/schemas/UserInfoDTO'
    CreateUserDTO:
      type: object
      required:
        - username
      properties:
        username:
          type: string
          pattern: ^[a-zA-Z0-9_\-]{1,50}$
    UserCreatedDTO:
      type: object
      required:
        - id
        - password
        - info
      properties:
        id:
          type: integer
          format: int64
        password:
          type: string
          description: Auto-generated password (shown only once)
        info:
          $ref: '#/components/schemas/UserInfoDTO'
    ValidationError:
      type: object
      required:
        - field
        - error
      properties:
        field:
          type: string
        error:
          type: string
    ValidationErrorDetails:
      type: object
      required:
        - timestamp
        - errors
        - errorCode
      properties:
        timestamp:
          type: string
          format: date-time
        errors:
          type: array
          items:
            $ref: '#/components/schemas/ValidationError'
        errorCode:
          type: string
    ApiKeyResponse:
      type: object
      required:
        - id
        - name
        - prefix
        - createdAt
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        prefix:
          type: string
        createdAt:
          type: string
          format: date-time
    ApiKeyCreateRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 50
    ApiKeyWithRawToken:
      type: object
      required:
        - id
        - name
        - prefix
        - createdAt
        - rawToken
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        prefix:
          type: string
        createdAt:
          type: string
          format: date-time
        rawToken:
          type: string
          description: Full API key value (shown only once)
    LevelDTO:
      type: object
      required:
        - id
        - name
        - abbreviation
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        abbreviation:
          type: string
    GroupDTO:
      type: object
      required:
        - id
        - name
        - type
        - classe
        - size
        - level
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        type:
          type: string
        classe:
          type: string
        size:
          type: integer
        level:
          $ref: '#/components/schemas/LevelDTO'
    LevelDetailsDTO:
      type: object
      required:
        - level
        - groups
      properties:
        level:
          $ref: '#/components/schemas/LevelDTO'
        groups:
          type: array
          items:
            $ref: '#/components/schemas/GroupDTO'
    CreateLevelDTO:
      type: object
      required:
        - name
        - abbreviation
      properties:
        name:
          type: string
          minLength: 1
        abbreviation:
          type: string
    UpdateLevelDTO:
      type: object
      required:
        - name
        - abbreviation
      properties:
        name:
          type: string
          minLength: 1
        abbreviation:
          type: string
    CreateGroupDTO:
      type: object
      required:
        - name
        - type
        - classe
        - size
      properties:
        name:
          type: string
        type:
          type: string
        classe:
          type: string
        size:
          type: integer
          minimum: 1
    UpdateGroupDTO:
      type: object
      required:
        - name
        - type
        - classe
        - size
      properties:
        name:
          type: string
        type:
          type: string
        classe:
          type: string
        size:
          type: integer
          minimum: 1
    TeacherDTO:
      type: object
      required:
        - id
        - name
        - abbreviation
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        abbreviation:
          type: string
    CreateTeacherDTO:
      type: object
      required:
        - name
        - abbreviation
      properties:
        name:
          type: string
        abbreviation:
          type: string
    UpdateTeacherDTO:
      type: object
      required:
        - name
        - abbreviation
      properties:
        name:
          type: string
        abbreviation:
          type: string
    TeachingUnitDTO:
      type: object
      required:
        - id
        - abbreviation
        - name
      properties:
        id:
          type: integer
          format: int64
        abbreviation:
          type: string
        name:
          type: string
        level:
          description: Associated level (may be null)
          allOf:
            - $ref: '#/components/schemas/LevelDTO'
    CreateTeachingUnitDTO:
      type: object
      required:
        - abbreviation
        - name
      properties:
        abbreviation:
          type: string
        name:
          type: string
        levelId:
          type: integer
          format: int64
          description: Optional level association
    UpdateTeachingUnitDTO:
      type: object
      properties:
        abbreviation:
          type: string
        name:
          type: string
        levelId:
          type: integer
          format: int64
          description: Optional level association
    RoomDTO:
      type: object
      required:
        - id
        - name
        - abbreviation
        - size
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        abbreviation:
          type: string
        size:
          type: integer
    CreateRoomDTO:
      type: object
      required:
        - name
        - abbreviation
        - size
      properties:
        name:
          type: string
        abbreviation:
          type: string
        size:
          type: integer
          minimum: 1
    UpdateRoomDTO:
      type: object
      required:
        - name
        - abbreviation
        - size
      properties:
        name:
          type: string
        abbreviation:
          type: string
        size:
          type: integer
          minimum: 1
    ScheduleItemDTO:
      type: object
      required:
        - id
        - groups
        - teacher
        - teachingUnit
        - room
        - startTime
        - endTime
      properties:
        id:
          type: integer
          format: int64
        groups:
          type: array
          items:
            $ref: '#/components/schemas/GroupDTO'
        teacher:
          $ref: '#/components/schemas/TeacherDTO'
        teachingUnit:
          $ref: '#/components/schemas/TeachingUnitDTO'
        room:
          $ref: '#/components/schemas/RoomDTO'
        startTime:
          type: string
          format: date-time
        endTime:
          type: string
          format: date-time
    CreateScheduleItemDTO:
      type: object
      required:
        - groupIds
        - teacherId
        - teachingUnitId
        - roomId
        - startTime
        - endTime
      properties:
        groupIds:
          type: array
          items:
            type: integer
            format: int64
        teacherId:
          type: integer
          format: int64
        teachingUnitId:
          type: integer
          format: int64
        roomId:
          type: integer
          format: int64
        startTime:
          type: string
          format: date-time
        endTime:
          type: string
          format: date-time
    FailedScheduleItem:
      type: object
      required:
        - item
        - reason
      properties:
        item:
          $ref: '#/components/schemas/CreateScheduleItemDTO'
        reason:
          type: string
    BatchCreateResponseScheduleItem:
      type: object
      required:
        - successItems
        - failedItems
      properties:
        successItems:
          type: array
          items:
            $ref: '#/components/schemas/ScheduleItemDTO'
        failedItems:
          type: array
          items:
            $ref: '#/components/schemas/FailedScheduleItem'
    UpdateScheduleItemDTO:
      type: object
      properties:
        groupIds:
          type: array
          items:
            type: integer
            format: int64
        teacherId:
          type: integer
          format: int64
        teachingUnitId:
          type: integer
          format: int64
        roomId:
          type: integer
          format: int64
        startTime:
          type: string
          format: date-time
        endTime:
          type: string
          format: date-time
    TableMapping:
      type: object
      required:
        - entityType
        - headersMapping
      properties:
        entityType:
          type: string
          description: Target entity type (room, teacher, teaching_unit, group, level)
        headersMapping:
          type: object
          additionalProperties:
            type: string
          description: Source column name → entity property name
    ImportMapping:
      type: object
      description: |
        Maps file names to their table mappings.  Structure:
        `{ "<filename>": { "<sheetOrTable>": TableMapping } }`
      additionalProperties:
        type: object
        additionalProperties:
          $ref: '#/components/schemas/TableMapping'
    SyncError:
      type: object
      properties:
        entityType:
          type: string
        rowIndex:
          type: integer
        errorMessage:
          type: string
        invalidValue:
          type: string
    ImportSummary:
      type: object
      required:
        - totalRows
        - successfulRows
        - failedRows
        - errors
        - skippedFiles
        - entitySummary
      properties:
        totalRows:
          type: integer
        successfulRows:
          type: integer
        failedRows:
          type: integer
        errors:
          type: array
          items:
            $ref: '#/components/schemas/SyncError'
        skippedFiles:
          type: array
          items:
            type: string
        entitySummary:
          type: object
          additionalProperties:
            type: integer
          description: Count of successfully imported rows per entity type
  responses:
    Unauthorized:
      description: Authentication required
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorDetails'
    Forbidden:
      description: Insufficient permissions
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorDetails'
  parameters:
    userId:
      name: userId
      in: path
      required: true
      description: User identifier
      schema:
        type: integer
        format: int64
    apiKeyId:
      name: apiKeyId
      in: path
      required: true
      description: API key identifier
      schema:
        type: integer
        format: int64
    levelId:
      name: levelId
      in: path
      required: true
      description: Level identifier
      schema:
        type: integer
        format: int64
    groupId:
      name: groupId
      in: path
      required: true
      description: Group identifier
      schema:
        type: integer
        format: int64
    teacherId:
      name: teacherId
      in: path
      required: true
      description: Teacher identifier
      schema:
        type: integer
        format: int64
    unitId:
      name: unitId
      in: path
      required: true
      description: Teaching unit identifier
      schema:
        type: integer
        format: int64
    roomId:
      name: roomId
      in: path
      required: true
      description: Room identifier
      schema:
        type: integer
        format: int64
    scheduleItemId:
      name: scheduleItemId
      in: path
      required: true
      description: Schedule item identifier
      schema:
        type: integer
        format: int64
