{
  "openapi": "3.1.0",
  "info": {
    "title": "Nexus Ops API",
    "version": "0.1.1",
    "description": "Absensi Divisi Operation — OpenAPI di-generate otomatis dari Zod schemas (@hono/zod-openapi)."
  },
  "servers": [
    {
      "url": "http://localhost:3000",
      "description": "Local"
    },
    {
      "url": "https://dev-nexus-ops-api.dev-akmal69.workers.dev",
      "description": "Development"
    },
    {
      "url": "https://nexus-ops-api.dev-akmal69.workers.dev",
      "description": "Production"
    }
  ],
  "components": {
    "securitySchemes": {
      "Bearer": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT"
      }
    },
    "schemas": {
      "HealthResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "service": {
            "type": "string"
          },
          "env": {
            "type": "string"
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "status",
          "service",
          "env",
          "time"
        ]
      },
      "SupervisorRef": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "fullName": {
            "type": "string",
            "example": "Andi Pratama"
          }
        },
        "required": [
          "id",
          "fullName"
        ]
      },
      "SiteRef": {
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Site A"
          }
        },
        "required": [
          "id",
          "name"
        ]
      },
      "PublicUser": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "fullName": {
            "type": "string"
          },
          "role": {
            "type": "string",
            "enum": [
              "employee",
              "supervisor",
              "hrd"
            ]
          },
          "isActive": {
            "type": "boolean"
          },
          "nip": {
            "type": [
              "string",
              "null"
            ],
            "example": "104892"
          },
          "supervisor": {
            "$ref": "#/components/schemas/SupervisorRef"
          },
          "defaultSite": {
            "$ref": "#/components/schemas/SiteRef"
          }
        },
        "required": [
          "id",
          "email",
          "fullName",
          "role",
          "isActive",
          "nip",
          "supervisor",
          "defaultSite"
        ]
      },
      "LoginResponse": {
        "type": "object",
        "properties": {
          "accessToken": {
            "type": "string"
          },
          "tokenType": {
            "type": "string",
            "enum": [
              "Bearer"
            ]
          },
          "expiresIn": {
            "type": "string"
          },
          "user": {
            "$ref": "#/components/schemas/PublicUser"
          }
        },
        "required": [
          "accessToken",
          "tokenType",
          "expiresIn",
          "user"
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "example": "UNAUTHORIZED"
              },
              "message": {
                "type": "string",
                "example": "Invalid email or password"
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "error"
        ]
      },
      "LoginRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "example": "budi@operation.local"
          },
          "password": {
            "type": "string",
            "minLength": 8,
            "example": "password123"
          }
        },
        "required": [
          "email",
          "password"
        ]
      },
      "MeResponse": {
        "type": "object",
        "properties": {
          "user": {
            "$ref": "#/components/schemas/PublicUser"
          }
        },
        "required": [
          "user"
        ]
      },
      "ClockStatus": {
        "type": "string",
        "enum": [
          "not_clocked_in",
          "clocked_in",
          "clocked_out"
        ]
      },
      "MeTodayShiftBrief": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Shift Pagi"
          },
          "startTime": {
            "type": "string",
            "description": "Local time HH:mm",
            "example": "07:30"
          },
          "endTime": {
            "type": "string",
            "description": "Local time HH:mm",
            "example": "16:30"
          }
        },
        "required": [
          "id",
          "name",
          "startTime",
          "endTime"
        ]
      },
      "MeTodaySiteBrief": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SiteRef"
          },
          {
            "type": "object",
            "properties": {
              "geofenceActive": {
                "type": "boolean",
                "example": true
              }
            },
            "required": [
              "geofenceActive"
            ]
          }
        ]
      },
      "PendingRequestType": {
        "type": "string",
        "enum": [
          "leave",
          "overtime"
        ]
      },
      "PendingRequestStatus": {
        "type": "string",
        "enum": [
          "pending",
          "approved",
          "rejected"
        ]
      },
      "PendingRequestSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "type": {
            "$ref": "#/components/schemas/PendingRequestType"
          },
          "title": {
            "type": "string",
            "example": "Izin sakit"
          },
          "summary": {
            "type": "string",
            "example": "22–23 Sep"
          },
          "status": {
            "$ref": "#/components/schemas/PendingRequestStatus"
          }
        },
        "required": [
          "id",
          "type",
          "title",
          "summary",
          "status"
        ]
      },
      "MeTodayResponse": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-09-22"
          },
          "clockStatus": {
            "$ref": "#/components/schemas/ClockStatus"
          },
          "clockInAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "clockOutAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "targetClockIn": {
            "type": "string",
            "example": "07:30"
          },
          "shift": {
            "$ref": "#/components/schemas/MeTodayShiftBrief"
          },
          "site": {
            "$ref": "#/components/schemas/MeTodaySiteBrief"
          },
          "pendingRequests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PendingRequestSummary"
            }
          }
        },
        "required": [
          "date",
          "clockStatus",
          "clockInAt",
          "clockOutAt",
          "targetClockIn",
          "shift",
          "site",
          "pendingRequests"
        ]
      },
      "MeShiftDetail": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string",
            "example": "Shift Pagi"
          },
          "daysLabel": {
            "type": "string",
            "example": "Senin–Jumat"
          },
          "startTime": {
            "type": "string",
            "example": "07:30"
          },
          "endTime": {
            "type": "string",
            "example": "16:30"
          }
        },
        "required": [
          "id",
          "name",
          "daysLabel",
          "startTime",
          "endTime"
        ]
      },
      "MeShiftSite": {
        "allOf": [
          {
            "$ref": "#/components/schemas/SiteRef"
          },
          {
            "type": "object",
            "properties": {
              "detailName": {
                "type": "string",
                "example": "Site A — Gudang"
              },
              "radiusMeters": {
                "type": "integer",
                "exclusiveMinimum": 0,
                "example": 150
              }
            },
            "required": [
              "detailName",
              "radiusMeters"
            ]
          }
        ]
      },
      "MeShiftResponse": {
        "type": "object",
        "properties": {
          "shift": {
            "$ref": "#/components/schemas/MeShiftDetail"
          },
          "defaultSite": {
            "$ref": "#/components/schemas/MeShiftSite"
          },
          "supervisor": {
            "$ref": "#/components/schemas/SupervisorRef"
          }
        },
        "required": [
          "shift",
          "defaultSite",
          "supervisor"
        ]
      },
      "DashboardMetrics": {
        "type": "object",
        "properties": {
          "present": {
            "type": "integer",
            "minimum": 0,
            "description": "Hadir",
            "example": 42
          },
          "late": {
            "type": "integer",
            "minimum": 0,
            "description": "Terlambat",
            "example": 3
          },
          "notYet": {
            "type": "integer",
            "minimum": 0,
            "description": "Belum absen",
            "example": 5
          },
          "onLeave": {
            "type": "integer",
            "minimum": 0,
            "description": "Izin",
            "example": 2
          }
        },
        "required": [
          "present",
          "late",
          "notYet",
          "onLeave"
        ]
      },
      "LiveAttendanceStatus": {
        "type": "string",
        "enum": [
          "present",
          "late",
          "not_yet",
          "on_leave"
        ]
      },
      "DashboardActivityItem": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "fullName": {
            "type": "string",
            "example": "Budi Santoso"
          },
          "status": {
            "$ref": "#/components/schemas/LiveAttendanceStatus"
          },
          "detail": {
            "type": "string",
            "example": "Clock-in 07:28 · Site A"
          },
          "occurredAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        },
        "required": [
          "userId",
          "fullName",
          "status",
          "detail",
          "occurredAt"
        ]
      },
      "DashboardSummaryResponse": {
        "type": "object",
        "properties": {
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-09-22"
          },
          "siteId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "shiftId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "shiftLabel": {
            "type": [
              "string",
              "null"
            ],
            "example": "Shift pagi"
          },
          "sitesLabel": {
            "type": [
              "string",
              "null"
            ],
            "example": "Site A & B"
          },
          "metrics": {
            "$ref": "#/components/schemas/DashboardMetrics"
          },
          "recentActivity": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DashboardActivityItem"
            }
          }
        },
        "required": [
          "date",
          "siteId",
          "shiftId",
          "shiftLabel",
          "sitesLabel",
          "metrics",
          "recentActivity"
        ]
      },
      "LiveNotYetItem": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid"
          },
          "fullName": {
            "type": "string",
            "example": "Citra Lestari"
          },
          "siteName": {
            "type": "string",
            "example": "Site A"
          },
          "targetClockIn": {
            "type": "string",
            "example": "07:30"
          }
        },
        "required": [
          "userId",
          "fullName",
          "siteName",
          "targetClockIn"
        ]
      },
      "DashboardLiveResponse": {
        "type": "object",
        "properties": {
          "refreshedAt": {
            "type": "string",
            "format": "date-time"
          },
          "date": {
            "type": "string",
            "format": "date",
            "example": "2026-09-22"
          },
          "siteId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "shiftId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "shift": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string",
                "example": "Shift Pagi"
              }
            },
            "required": [
              "id",
              "name"
            ]
          },
          "notYetCount": {
            "type": "integer",
            "minimum": 0,
            "example": 5
          },
          "notYet": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LiveNotYetItem"
            }
          }
        },
        "required": [
          "refreshedAt",
          "date",
          "siteId",
          "shiftId",
          "shift",
          "notYetCount",
          "notYet"
        ]
      }
    },
    "parameters": {}
  },
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "System"
        ],
        "summary": "Health check",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/login": {
      "post": {
        "tags": [
          "Auth"
        ],
        "summary": "Login karyawan / supervisor / HRD",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LoginRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Login sukses",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LoginResponse"
                }
              }
            }
          },
          "401": {
            "description": "Kredensial salah",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/me": {
      "get": {
        "tags": [
          "Auth"
        ],
        "summary": "Profil user terautentikasi",
        "description": "Includes NIP, supervisor, and default site refs for M-P01 when employee profile exists.",
        "security": [
          {
            "Bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Profil user",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeResponse"
                }
              }
            }
          },
          "401": {
            "description": "Tidak terautentikasi",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/today": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Status absensi & ringkas pengajuan hari ini (M-H01)",
        "description": "AuthZ: any authenticated role (`employee` | `supervisor` | `hrd`). Single payload for home — clock status, target, site/geofence flag, pending leave/OT. Work date uses org timezone (Asia/Jakarta).",
        "security": [
          {
            "Bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Konteks hari ini untuk beranda",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeTodayResponse"
                }
              }
            }
          },
          "401": {
            "description": "Tidak terautentikasi",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Profil karyawan / shift / site belum lengkap",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me/shift": {
      "get": {
        "tags": [
          "Me"
        ],
        "summary": "Detail shift & lokasi default (M-H02)",
        "description": "AuthZ: any authenticated role (`employee` | `supervisor` | `hrd`). Jam kerja, lokasi default + radius geofence, supervisor (nullable).",
        "security": [
          {
            "Bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Detail shift karyawan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeShiftResponse"
                }
              }
            }
          },
          "401": {
            "description": "Tidak terautentikasi",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Profil karyawan / shift / site belum lengkap",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/summary": {
      "get": {
        "tags": [
          "Dashboard"
        ],
        "summary": "Ringkasan operasional harian (W-D01)",
        "description": "AuthZ: `supervisor` | `hrd`. Metrics: Hadir / Terlambat / Belum absen / Izin + recent activity. Optional filters: siteId, shiftId. Supervisor scope = reportees only.",
        "security": [
          {
            "Bearer": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
            },
            "required": false,
            "name": "siteId",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
            },
            "required": false,
            "name": "shiftId",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Ringkasan dashboard",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DashboardSummaryResponse"
                }
              }
            }
          },
          "401": {
            "description": "Tidak terautentikasi",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Role tidak diizinkan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Profil viewer tidak ditemukan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    },
    "/v1/dashboard/live": {
      "get": {
        "tags": [
          "Dashboard"
        ],
        "summary": "Kehadiran live — belum absen (W-D02)",
        "description": "AuthZ: `supervisor` | `hrd`. Lists employees who have not clocked in for the selected shift/site. Reuse attendance projections — not a separate live table. Supervisor scope = reportees only.",
        "security": [
          {
            "Bearer": []
          }
        ],
        "parameters": [
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
            },
            "required": false,
            "name": "siteId",
            "in": "query"
          },
          {
            "schema": {
              "type": "string",
              "format": "uuid",
              "example": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
            },
            "required": false,
            "name": "shiftId",
            "in": "query"
          }
        ],
        "responses": {
          "200": {
            "description": "Daftar belum absen",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DashboardLiveResponse"
                }
              }
            }
          },
          "401": {
            "description": "Tidak terautentikasi",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "403": {
            "description": "Role tidak diizinkan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Profil viewer tidak ditemukan",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {}
}