diff --git a/backend/.scribe/.filehashes b/backend/.scribe/.filehashes
index 9cf1036f..12844723 100644
--- a/backend/.scribe/.filehashes
+++ b/backend/.scribe/.filehashes
@@ -1,4 +1,4 @@
# GENERATED. YOU SHOULDN'T MODIFY OR DELETE THIS FILE.
# Scribe uses this file to know when you change something manually in your docs.
-.scribe/intro.md=63d14186b9cbbb0a80ee87cd913db091
-.scribe/auth.md=5c5a140c89034600ae349aede2a22ec8
\ No newline at end of file
+.scribe/intro.md=4bf90470e636417926ae5d9227747d45
+.scribe/auth.md=9bee2b1ef8a238b2e58613fa636d5f39
\ No newline at end of file
diff --git a/backend/.scribe/auth.md b/backend/.scribe/auth.md
index bf1aa6a7..82903629 100644
--- a/backend/.scribe/auth.md
+++ b/backend/.scribe/auth.md
@@ -1,7 +1,3 @@
# Authenticating requests
-To authenticate requests, include an **`Authorization`** header with the value **`"Bearer Bearer {token}"`**.
-
-All authenticated endpoints are marked with a `requires authentication` badge in the documentation below.
-
-Get tokens from `POST /api/auth/login`, send access token as `Bearer {token}`, and renew with `POST /api/auth/refresh` before access token expiry.
+This API is not authenticated.
diff --git a/backend/.scribe/endpoints.cache/00.yaml b/backend/.scribe/endpoints.cache/00.yaml
new file mode 100644
index 00000000..261eb5e4
--- /dev/null
+++ b/backend/.scribe/endpoints.cache/00.yaml
@@ -0,0 +1,214 @@
+## Autogenerated by Scribe. DO NOT MODIFY.
+
+name: Authentication
+description: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+endpoints:
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/auth/login
+ metadata:
+ custom: []
+ groupName: Authentication
+ groupDescription: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Login and get tokens'
+ description: 'Authenticate with email and password to receive an access token and refresh token.'
+ authenticated: false
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ email:
+ custom: []
+ name: email
+ description: 'User email address.'
+ required: true
+ example: user@example.com
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ password:
+ custom: []
+ name: password
+ description: 'User password.'
+ required: true
+ example: secret123
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ email: user@example.com
+ password: secret123
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "refresh_token": "abc123def456",
+ "token_type": "bearer",
+ "expires_in": 3600,
+ "user": {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "Alice Johnson",
+ "email": "user@example.com",
+ "role": "manager"
+ }
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 401
+ content: '{"message":"Invalid credentials"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 403
+ content: '{"message":"Account is inactive"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"errors":{"email":["The email field is required."],"password":["The password field is required."]}}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/auth/refresh
+ metadata:
+ custom: []
+ groupName: Authentication
+ groupDescription: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Refresh access token'
+ description: 'Exchange a valid refresh token for a new access token and refresh token pair.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ refresh_token:
+ custom: []
+ name: refresh_token
+ description: 'Refresh token returned by login.'
+ required: true
+ example: abc123def456
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ refresh_token: abc123def456
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "refresh_token": "newtoken123",
+ "token_type": "bearer",
+ "expires_in": 3600
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 401
+ content: '{"message":"Invalid or expired refresh token"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/auth/logout
+ metadata:
+ custom: []
+ groupName: Authentication
+ groupDescription: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Logout current session'
+ description: 'Invalidate a refresh token and end the active authenticated session.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ refresh_token:
+ custom: []
+ name: refresh_token
+ description: 'Optional refresh token to invalidate immediately.'
+ required: false
+ example: abc123def456
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ refresh_token: abc123def456
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: '{"message":"Logged out successfully"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
diff --git a/backend/.scribe/endpoints.cache/01.yaml b/backend/.scribe/endpoints.cache/01.yaml
new file mode 100644
index 00000000..4eaae247
--- /dev/null
+++ b/backend/.scribe/endpoints.cache/01.yaml
@@ -0,0 +1,443 @@
+## Autogenerated by Scribe. DO NOT MODIFY.
+
+name: 'Team Members'
+description: |-
+
+ Endpoints for managing team members.
+endpoints:
+ -
+ custom: []
+ httpMethods:
+ - GET
+ uri: api/team-members
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'List all team members'
+ description: 'Get a list of all team members with optional filtering by active status.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters:
+ active:
+ custom: []
+ name: active
+ description: 'Filter by active status.'
+ required: false
+ example: true
+ type: boolean
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanQueryParameters:
+ active: true
+ bodyParameters: []
+ cleanBodyParameters: []
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ [
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "150.00",
+ "active": true,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T10:00:00.000000Z"
+ }
+ ]
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/team-members
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Create a new team member'
+ description: 'Create a new team member with name, role, and hourly rate.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ name:
+ custom: []
+ name: name
+ description: 'Team member name.'
+ required: true
+ example: 'John Doe'
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ role_id:
+ custom: []
+ name: role_id
+ description: 'Role ID.'
+ required: true
+ example: 1
+ type: integer
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ hourly_rate:
+ custom: []
+ name: hourly_rate
+ description: 'Hourly rate (must be > 0).'
+ required: true
+ example: '150.00'
+ type: numeric
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ active:
+ custom: []
+ name: active
+ description: 'Active status (defaults to true).'
+ required: false
+ example: true
+ type: boolean
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ name: 'John Doe'
+ role_id: 1
+ hourly_rate: '150.00'
+ active: true
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 201
+ content: |-
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "150.00",
+ "active": true,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T10:00:00.000000Z"
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Validation failed","errors":{"name":["The name field is required."],"hourly_rate":["Hourly rate must be greater than 0"]}}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - GET
+ uri: 'api/team-members/{id}'
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Get a single team member'
+ description: 'Get details of a specific team member by ID.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters:
+ id:
+ custom: []
+ name: id
+ description: 'Team member UUID.'
+ required: true
+ example: 550e8400-e29b-41d4-a716-446655440000
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanUrlParameters:
+ id: 550e8400-e29b-41d4-a716-446655440000
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters: []
+ cleanBodyParameters: []
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "150.00",
+ "active": true,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T10:00:00.000000Z"
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 404
+ content: '{"message":"Team member not found"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - PUT
+ - PATCH
+ uri: 'api/team-members/{id}'
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Update a team member'
+ description: 'Update details of an existing team member.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters:
+ id:
+ custom: []
+ name: id
+ description: 'Team member UUID.'
+ required: true
+ example: 550e8400-e29b-41d4-a716-446655440000
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanUrlParameters:
+ id: 550e8400-e29b-41d4-a716-446655440000
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ name:
+ custom: []
+ name: name
+ description: 'Team member name.'
+ required: false
+ example: 'John Doe'
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ role_id:
+ custom: []
+ name: role_id
+ description: 'Role ID.'
+ required: false
+ example: 1
+ type: integer
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ hourly_rate:
+ custom: []
+ name: hourly_rate
+ description: 'Hourly rate (must be > 0).'
+ required: false
+ example: '175.00'
+ type: numeric
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ active:
+ custom: []
+ name: active
+ description: 'Active status.'
+ required: false
+ example: false
+ type: boolean
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ name: 'John Doe'
+ role_id: 1
+ hourly_rate: '175.00'
+ active: false
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "175.00",
+ "active": false,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T11:00:00.000000Z"
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 404
+ content: '{"message":"Team member not found"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Validation failed","errors":{"hourly_rate":["Hourly rate must be greater than 0"]}}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - DELETE
+ uri: 'api/team-members/{id}'
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Delete a team member'
+ description: 'Delete a team member. Cannot delete if member has allocations or actuals.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters:
+ id:
+ custom: []
+ name: id
+ description: 'Team member UUID.'
+ required: true
+ example: 550e8400-e29b-41d4-a716-446655440000
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanUrlParameters:
+ id: 550e8400-e29b-41d4-a716-446655440000
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters: []
+ cleanBodyParameters: []
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: '{"message":"Team member deleted successfully"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 404
+ content: '{"message":"Team member not found"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Cannot delete team member with active allocations","suggestion":"Consider deactivating the team member instead"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Cannot delete team member with historical data","suggestion":"Consider deactivating the team member instead"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
diff --git a/backend/.scribe/endpoints/00.yaml b/backend/.scribe/endpoints/00.yaml
new file mode 100644
index 00000000..f7772fb6
--- /dev/null
+++ b/backend/.scribe/endpoints/00.yaml
@@ -0,0 +1,212 @@
+name: Authentication
+description: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+endpoints:
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/auth/login
+ metadata:
+ custom: []
+ groupName: Authentication
+ groupDescription: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Login and get tokens'
+ description: 'Authenticate with email and password to receive an access token and refresh token.'
+ authenticated: false
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ email:
+ custom: []
+ name: email
+ description: 'User email address.'
+ required: true
+ example: user@example.com
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ password:
+ custom: []
+ name: password
+ description: 'User password.'
+ required: true
+ example: secret123
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ email: user@example.com
+ password: secret123
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "refresh_token": "abc123def456",
+ "token_type": "bearer",
+ "expires_in": 3600,
+ "user": {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "Alice Johnson",
+ "email": "user@example.com",
+ "role": "manager"
+ }
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 401
+ content: '{"message":"Invalid credentials"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 403
+ content: '{"message":"Account is inactive"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"errors":{"email":["The email field is required."],"password":["The password field is required."]}}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/auth/refresh
+ metadata:
+ custom: []
+ groupName: Authentication
+ groupDescription: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Refresh access token'
+ description: 'Exchange a valid refresh token for a new access token and refresh token pair.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ refresh_token:
+ custom: []
+ name: refresh_token
+ description: 'Refresh token returned by login.'
+ required: true
+ example: abc123def456
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ refresh_token: abc123def456
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...",
+ "refresh_token": "newtoken123",
+ "token_type": "bearer",
+ "expires_in": 3600
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 401
+ content: '{"message":"Invalid or expired refresh token"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/auth/logout
+ metadata:
+ custom: []
+ groupName: Authentication
+ groupDescription: |-
+
+ Endpoints for JWT authentication and session lifecycle.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Logout current session'
+ description: 'Invalidate a refresh token and end the active authenticated session.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ refresh_token:
+ custom: []
+ name: refresh_token
+ description: 'Optional refresh token to invalidate immediately.'
+ required: false
+ example: abc123def456
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ refresh_token: abc123def456
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: '{"message":"Logged out successfully"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
diff --git a/backend/.scribe/endpoints/01.yaml b/backend/.scribe/endpoints/01.yaml
new file mode 100644
index 00000000..0d3fb6b3
--- /dev/null
+++ b/backend/.scribe/endpoints/01.yaml
@@ -0,0 +1,441 @@
+name: 'Team Members'
+description: |-
+
+ Endpoints for managing team members.
+endpoints:
+ -
+ custom: []
+ httpMethods:
+ - GET
+ uri: api/team-members
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'List all team members'
+ description: 'Get a list of all team members with optional filtering by active status.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters:
+ active:
+ custom: []
+ name: active
+ description: 'Filter by active status.'
+ required: false
+ example: true
+ type: boolean
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanQueryParameters:
+ active: true
+ bodyParameters: []
+ cleanBodyParameters: []
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ [
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "150.00",
+ "active": true,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T10:00:00.000000Z"
+ }
+ ]
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - POST
+ uri: api/team-members
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Create a new team member'
+ description: 'Create a new team member with name, role, and hourly rate.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters: []
+ cleanUrlParameters: []
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ name:
+ custom: []
+ name: name
+ description: 'Team member name.'
+ required: true
+ example: 'John Doe'
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ role_id:
+ custom: []
+ name: role_id
+ description: 'Role ID.'
+ required: true
+ example: 1
+ type: integer
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ hourly_rate:
+ custom: []
+ name: hourly_rate
+ description: 'Hourly rate (must be > 0).'
+ required: true
+ example: '150.00'
+ type: numeric
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ active:
+ custom: []
+ name: active
+ description: 'Active status (defaults to true).'
+ required: false
+ example: true
+ type: boolean
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ name: 'John Doe'
+ role_id: 1
+ hourly_rate: '150.00'
+ active: true
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 201
+ content: |-
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "150.00",
+ "active": true,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T10:00:00.000000Z"
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Validation failed","errors":{"name":["The name field is required."],"hourly_rate":["Hourly rate must be greater than 0"]}}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - GET
+ uri: 'api/team-members/{id}'
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Get a single team member'
+ description: 'Get details of a specific team member by ID.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters:
+ id:
+ custom: []
+ name: id
+ description: 'Team member UUID.'
+ required: true
+ example: 550e8400-e29b-41d4-a716-446655440000
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanUrlParameters:
+ id: 550e8400-e29b-41d4-a716-446655440000
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters: []
+ cleanBodyParameters: []
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "150.00",
+ "active": true,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T10:00:00.000000Z"
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 404
+ content: '{"message":"Team member not found"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - PUT
+ - PATCH
+ uri: 'api/team-members/{id}'
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Update a team member'
+ description: 'Update details of an existing team member.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters:
+ id:
+ custom: []
+ name: id
+ description: 'Team member UUID.'
+ required: true
+ example: 550e8400-e29b-41d4-a716-446655440000
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanUrlParameters:
+ id: 550e8400-e29b-41d4-a716-446655440000
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters:
+ name:
+ custom: []
+ name: name
+ description: 'Team member name.'
+ required: false
+ example: 'John Doe'
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ role_id:
+ custom: []
+ name: role_id
+ description: 'Role ID.'
+ required: false
+ example: 1
+ type: integer
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ hourly_rate:
+ custom: []
+ name: hourly_rate
+ description: 'Hourly rate (must be > 0).'
+ required: false
+ example: '175.00'
+ type: numeric
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ active:
+ custom: []
+ name: active
+ description: 'Active status.'
+ required: false
+ example: false
+ type: boolean
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanBodyParameters:
+ name: 'John Doe'
+ role_id: 1
+ hourly_rate: '175.00'
+ active: false
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: |-
+ {
+ "id": "550e8400-e29b-41d4-a716-446655440000",
+ "name": "John Doe",
+ "role_id": 1,
+ "role": {
+ "id": 1,
+ "name": "Backend Developer"
+ },
+ "hourly_rate": "175.00",
+ "active": false,
+ "created_at": "2024-01-15T10:00:00.000000Z",
+ "updated_at": "2024-01-15T11:00:00.000000Z"
+ }
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 404
+ content: '{"message":"Team member not found"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Validation failed","errors":{"hourly_rate":["Hourly rate must be greater than 0"]}}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
+ -
+ custom: []
+ httpMethods:
+ - DELETE
+ uri: 'api/team-members/{id}'
+ metadata:
+ custom: []
+ groupName: 'Team Members'
+ groupDescription: |-
+
+ Endpoints for managing team members.
+ subgroup: ''
+ subgroupDescription: ''
+ title: 'Delete a team member'
+ description: 'Delete a team member. Cannot delete if member has allocations or actuals.'
+ authenticated: true
+ deprecated: false
+ headers:
+ Content-Type: application/json
+ Accept: application/json
+ urlParameters:
+ id:
+ custom: []
+ name: id
+ description: 'Team member UUID.'
+ required: true
+ example: 550e8400-e29b-41d4-a716-446655440000
+ type: string
+ enumValues: []
+ exampleWasSpecified: true
+ nullable: false
+ deprecated: false
+ cleanUrlParameters:
+ id: 550e8400-e29b-41d4-a716-446655440000
+ queryParameters: []
+ cleanQueryParameters: []
+ bodyParameters: []
+ cleanBodyParameters: []
+ fileParameters: []
+ responses:
+ -
+ custom: []
+ status: 200
+ content: '{"message":"Team member deleted successfully"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 404
+ content: '{"message":"Team member not found"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Cannot delete team member with active allocations","suggestion":"Consider deactivating the team member instead"}'
+ headers: []
+ description: ''
+ -
+ custom: []
+ status: 422
+ content: '{"message":"Cannot delete team member with historical data","suggestion":"Consider deactivating the team member instead"}'
+ headers: []
+ description: ''
+ responseFields: []
+ auth: []
+ controller: null
+ method: null
+ route: null
diff --git a/backend/.scribe/intro.md b/backend/.scribe/intro.md
index bc297e8a..95f6bdff 100644
--- a/backend/.scribe/intro.md
+++ b/backend/.scribe/intro.md
@@ -1,12 +1,13 @@
# Introduction
-Resource planning and capacity management API
+
- Base URL : http://localhost/api
+ Base URL : http://localhost
- Authenticate by sending `Authorization: Bearer {access_token}` on protected endpoints.
+ This documentation aims to provide all the information you need to work with our API.
- Access tokens are valid for 60 minutes. Use `/api/auth/refresh` with your refresh token to obtain a new access token and refresh token pair.
+ As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
+ You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).
diff --git a/backend/config/scribe.php b/backend/config/scribe.php
index f747ed20..8fdbe8a8 100644
--- a/backend/config/scribe.php
+++ b/backend/config/scribe.php
@@ -1,24 +1,32 @@
for the generated documentation.
- 'title' => 'Headroom API',
+ 'title' => config('app.name').' API Documentation',
// A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec.
- 'description' => 'Resource planning and capacity management API',
+ 'description' => '',
// Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported.
'intro_text' => <<<'INTRO'
- Authenticate by sending `Authorization: Bearer {access_token}` on protected endpoints.
+ This documentation aims to provide all the information you need to work with our API.
- Access tokens are valid for 60 minutes. Use `/api/auth/refresh` with your refresh token to obtain a new access token and refresh token pair.
+ As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile).
+ You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).
INTRO,
// The base URL displayed in the docs.
// If you're using `laravel` type, you can set this to a dynamic string, like '{{ config("app.tenant_url") }}' to get a dynamic base URL.
- 'base_url' => rtrim(config('app.url'), '/').'/api',
+ 'base_url' => config('app.url'),
// Routes to include in the docs
'routes' => [
@@ -38,7 +46,7 @@ return [
// Exclude these routes even if they matched the rules above.
'exclude' => [
- 'api/user',
+ // 'GET /health', 'admin.*'
],
],
],
@@ -64,7 +72,7 @@ return [
// URL path to use for the docs endpoint (if `add_routes` is true).
// By default, `/docs` opens the HTML page, `/docs.postman` opens the Postman collection, and `/docs.openapi` the OpenAPI spec.
- 'docs_url' => '/api/documentation',
+ 'docs_url' => '/docs',
// Directory within `public` in which to store CSS and JS assets.
// By default, assets are stored in `public/vendor/scribe`.
@@ -97,28 +105,28 @@ return [
// How is your API authenticated? This information will be used in the displayed docs, generated examples and response calls.
'auth' => [
// Set this to true if ANY endpoints in your API use authentication.
- 'enabled' => true,
+ 'enabled' => false,
// Set this to true if your API should be authenticated by default. If so, you must also set `enabled` (above) to true.
// You can then use @unauthenticated or @authenticated on individual endpoints to change their status from the default.
- 'default' => true,
+ 'default' => false,
// Where is the auth value meant to be sent in a request?
- 'in' => 'bearer',
+ 'in' => AuthIn::BEARER->value,
// The name of the auth parameter (e.g. token, key, apiKey) or header (e.g. Authorization, Api-Key).
- 'name' => 'Authorization',
+ 'name' => 'key',
// The value of the parameter to be used by Scribe to authenticate response calls.
// This will NOT be included in the generated documentation. If empty, Scribe will use a random value.
- 'use_value' => 'Bearer {token}',
+ 'use_value' => env('SCRIBE_AUTH_KEY'),
// Placeholder your users will see for the auth parameter in the example requests.
// Set this to null if you want Scribe to use a random value as placeholder instead.
- 'placeholder' => 'Bearer {token}',
+ 'placeholder' => '{YOUR_AUTH_KEY}',
// Any extra authentication-related info for your users. Markdown and HTML are supported.
- 'extra_info' => 'Get tokens from `POST /api/auth/login`, send access token as `Bearer {token}`, and renew with `POST /api/auth/refresh` before access token expiry.',
+ 'extra_info' => 'You can retrieve your token by visiting your dashboard and clicking Generate API token .',
],
// Example requests for each endpoint will be shown in each of these languages.
@@ -205,18 +213,38 @@ return [
// Use configureStrategy() to specify settings for a strategy in the list.
// Use removeStrategies() to remove an included strategy.
'strategies' => [
- 'metadata' => [],
+ 'metadata' => [
+ ...Defaults::METADATA_STRATEGIES,
+ ],
'headers' => [
- [
+ ...Defaults::HEADERS_STRATEGIES,
+ Strategies\StaticData::withSettings(data: [
'Content-Type' => 'application/json',
'Accept' => 'application/json',
- ],
+ ]),
+ ],
+ 'urlParameters' => [
+ ...Defaults::URL_PARAMETERS_STRATEGIES,
+ ],
+ 'queryParameters' => [
+ ...Defaults::QUERY_PARAMETERS_STRATEGIES,
+ ],
+ 'bodyParameters' => [
+ ...Defaults::BODY_PARAMETERS_STRATEGIES,
+ ],
+ 'responses' => configureStrategy(
+ Defaults::RESPONSES_STRATEGIES,
+ Strategies\Responses\ResponseCalls::withSettings(
+ only: ['GET *'],
+ // Recommended: disable debug mode in response calls to avoid error stack traces in responses
+ config: [
+ 'app.debug' => false,
+ ]
+ )
+ ),
+ 'responseFields' => [
+ ...Defaults::RESPONSE_FIELDS_STRATEGIES,
],
- 'urlParameters' => [],
- 'queryParameters' => [],
- 'bodyParameters' => [],
- 'responses' => [],
- 'responseFields' => [],
],
// For response calls, API resource responses and transformer responses,
diff --git a/backend/resources/views/scribe/index.blade.php b/backend/resources/views/scribe/index.blade.php
index 49c5df9a..a95f5128 100644
--- a/backend/resources/views/scribe/index.blade.php
+++ b/backend/resources/views/scribe/index.blade.php
@@ -4,7 +4,7 @@
-
Headroom API
+ Laravel API Documentation
@@ -26,7 +26,7 @@
@@ -66,6 +66,44 @@
Authenticating requests
+
+