diff --git a/backend/.scribe/endpoints.cache/03.yaml b/backend/.scribe/endpoints.cache/03.yaml new file mode 100644 index 00000000..e93cc059 --- /dev/null +++ b/backend/.scribe/endpoints.cache/03.yaml @@ -0,0 +1,681 @@ +## Autogenerated by Scribe. DO NOT MODIFY. + +name: 'Capacity Planning' +description: '' +endpoints: + - + custom: [] + httpMethods: + - GET + uri: api/capacity + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Get Individual Capacity' + description: 'Calculate capacity for a specific team member in a given month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'The month in YYYY-MM format.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + team_member_id: + custom: [] + name: team_member_id + description: 'The team member UUID.' + required: true + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + team_member_id: 550e8400-e29b-41d4-a716-446655440000 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + team_member_id: + custom: [] + name: team_member_id + description: 'The id of an existing record in the team_members table.' + required: true + example: architecto + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + month: 2026-02 + team_member_id: architecto + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "person_days": 18.5, + "hours": 148, + "details": [ + { + "date": "2026-02-02", + "availability": 1, + "is_pto": false + } + ] + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/capacity/team + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Get Team Capacity' + description: 'Summarize the combined capacity for all active team members in a month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'The month in YYYY-MM format.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "month": "2026-02", + "person_days": 180.5, + "hours": 1444, + "members": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Ada Lovelace", + "person_days": 18.5, + "hours": 148 + } + ] + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/capacity/revenue + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Get Possible Revenue' + description: 'Estimate monthly revenue based on capacity hours and hourly rates.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'The month in YYYY-MM format.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "month": "2026-02", + "possible_revenue": 21500.25 + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/holidays + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'List Holidays' + description: 'Retrieve holidays for a specific month or all holidays when no month is provided.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'nullable The month in YYYY-MM format.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Company Holiday", + "description": "Office closed" + } + ] + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - POST + uri: api/holidays + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Create Holiday' + description: 'Add a holiday and clear cached capacity data for the related month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: [] + cleanUrlParameters: [] + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + date: + custom: [] + name: date + description: 'Date of the holiday.' + required: true + example: '2026-02-14' + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + name: + custom: [] + name: name + description: 'Name of the holiday.' + required: true + example: "Presidents' Day" + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + description: + custom: [] + name: description + description: 'nullable Optional description of the holiday.' + required: false + example: 'Eius et animi quos velit et.' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + date: '2026-02-14' + name: "Presidents' Day" + description: 'Eius et animi quos velit et.' + fileParameters: [] + responses: + - + custom: [] + status: 201 + content: |- + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Presidents' Day", + "description": "Office closed" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - DELETE + uri: 'api/holidays/{id}' + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Delete Holiday' + description: 'Remove a holiday and clear affected capacity caches.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + id: + custom: [] + name: id + description: 'The holiday 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": "Holiday deleted" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/ptos + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'List PTO Requests' + description: 'Fetch PTO requests for a team member, optionally constrained to a month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + team_member_id: + custom: [] + name: team_member_id + description: 'The team member UUID.' + required: true + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + month: + custom: [] + name: month + description: 'nullable The month in YYYY-MM format.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + team_member_id: 550e8400-e29b-41d4-a716-446655440000 + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + team_member_id: + custom: [] + name: team_member_id + description: 'The id of an existing record in the team_members table.' + required: true + example: architecto + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + team_member_id: architecto + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + [ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } + ] + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - POST + uri: api/ptos + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Request PTO' + description: 'Create a PTO request for a team member and keep it in pending status.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: [] + cleanUrlParameters: [] + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + team_member_id: + custom: [] + name: team_member_id + description: 'The team member UUID.' + required: true + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + start_date: + custom: [] + name: start_date + description: 'The first day of the PTO.' + required: true + example: '2026-02-10' + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + end_date: + custom: [] + name: end_date + description: 'The final day of the PTO.' + required: true + example: '2026-02-12' + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + reason: + custom: [] + name: reason + description: 'nullable Optional reason for the PTO.' + required: false + example: architecto + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + team_member_id: 550e8400-e29b-41d4-a716-446655440000 + start_date: '2026-02-10' + end_date: '2026-02-12' + reason: architecto + fileParameters: [] + responses: + - + custom: [] + status: 201 + content: |- + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - PUT + uri: 'api/ptos/{id}/approve' + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Approve PTO' + description: 'Approve a pending PTO request and refresh the affected capacity caches.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + id: + custom: [] + name: id + description: 'The PTO UUID that needs approval.' + required: true + example: 550e8400-e29b-41d4-a716-446655440001 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + id: 550e8400-e29b-41d4-a716-446655440001 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: [] + cleanBodyParameters: [] + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "status": "approved" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null diff --git a/backend/.scribe/endpoints/03.yaml b/backend/.scribe/endpoints/03.yaml new file mode 100644 index 00000000..336b8c6d --- /dev/null +++ b/backend/.scribe/endpoints/03.yaml @@ -0,0 +1,679 @@ +name: 'Capacity Planning' +description: '' +endpoints: + - + custom: [] + httpMethods: + - GET + uri: api/capacity + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Get Individual Capacity' + description: 'Calculate capacity for a specific team member in a given month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'The month in YYYY-MM format.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + team_member_id: + custom: [] + name: team_member_id + description: 'The team member UUID.' + required: true + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + team_member_id: 550e8400-e29b-41d4-a716-446655440000 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + team_member_id: + custom: [] + name: team_member_id + description: 'The id of an existing record in the team_members table.' + required: true + example: architecto + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + month: 2026-02 + team_member_id: architecto + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "person_days": 18.5, + "hours": 148, + "details": [ + { + "date": "2026-02-02", + "availability": 1, + "is_pto": false + } + ] + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/capacity/team + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Get Team Capacity' + description: 'Summarize the combined capacity for all active team members in a month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'The month in YYYY-MM format.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "month": "2026-02", + "person_days": 180.5, + "hours": 1444, + "members": [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "name": "Ada Lovelace", + "person_days": 18.5, + "hours": 148 + } + ] + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/capacity/revenue + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Get Possible Revenue' + description: 'Estimate monthly revenue based on capacity hours and hourly rates.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'The month in YYYY-MM format.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: true + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + cleanBodyParameters: + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "month": "2026-02", + "possible_revenue": 21500.25 + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/holidays + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'List Holidays' + description: 'Retrieve holidays for a specific month or all holidays when no month is provided.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + month: + custom: [] + name: month + description: 'nullable The month in YYYY-MM format.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + [ + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Company Holiday", + "description": "Office closed" + } + ] + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - POST + uri: api/holidays + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Create Holiday' + description: 'Add a holiday and clear cached capacity data for the related month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: [] + cleanUrlParameters: [] + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + date: + custom: [] + name: date + description: 'Date of the holiday.' + required: true + example: '2026-02-14' + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + name: + custom: [] + name: name + description: 'Name of the holiday.' + required: true + example: "Presidents' Day" + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + description: + custom: [] + name: description + description: 'nullable Optional description of the holiday.' + required: false + example: 'Eius et animi quos velit et.' + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + date: '2026-02-14' + name: "Presidents' Day" + description: 'Eius et animi quos velit et.' + fileParameters: [] + responses: + - + custom: [] + status: 201 + content: |- + { + "id": "550e8400-e29b-41d4-a716-446655440000", + "date": "2026-02-14", + "name": "Presidents' Day", + "description": "Office closed" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - DELETE + uri: 'api/holidays/{id}' + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Delete Holiday' + description: 'Remove a holiday and clear affected capacity caches.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + id: + custom: [] + name: id + description: 'The holiday 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": "Holiday deleted" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - GET + uri: api/ptos + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'List PTO Requests' + description: 'Fetch PTO requests for a team member, optionally constrained to a month.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + team_member_id: + custom: [] + name: team_member_id + description: 'The team member UUID.' + required: true + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + month: + custom: [] + name: month + description: 'nullable The month in YYYY-MM format.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + team_member_id: 550e8400-e29b-41d4-a716-446655440000 + month: 2026-02 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + team_member_id: + custom: [] + name: team_member_id + description: 'The id of an existing record in the team_members table.' + required: true + example: architecto + type: string + enumValues: [] + exampleWasSpecified: false + nullable: false + deprecated: false + month: + custom: [] + name: month + description: 'Must be a valid date in the format Y-m.' + required: false + example: 2026-02 + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + team_member_id: architecto + month: 2026-02 + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + [ + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } + ] + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - POST + uri: api/ptos + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Request PTO' + description: 'Create a PTO request for a team member and keep it in pending status.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: [] + cleanUrlParameters: [] + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: + team_member_id: + custom: [] + name: team_member_id + description: 'The team member UUID.' + required: true + example: 550e8400-e29b-41d4-a716-446655440000 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + start_date: + custom: [] + name: start_date + description: 'The first day of the PTO.' + required: true + example: '2026-02-10' + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + end_date: + custom: [] + name: end_date + description: 'The final day of the PTO.' + required: true + example: '2026-02-12' + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + reason: + custom: [] + name: reason + description: 'nullable Optional reason for the PTO.' + required: false + example: architecto + type: string + enumValues: [] + exampleWasSpecified: false + nullable: true + deprecated: false + cleanBodyParameters: + team_member_id: 550e8400-e29b-41d4-a716-446655440000 + start_date: '2026-02-10' + end_date: '2026-02-12' + reason: architecto + fileParameters: [] + responses: + - + custom: [] + status: 201 + content: |- + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + "start_date": "2026-02-10", + "end_date": "2026-02-12", + "status": "pending", + "reason": "Family travel" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null + - + custom: [] + httpMethods: + - PUT + uri: 'api/ptos/{id}/approve' + metadata: + custom: [] + groupName: 'Capacity Planning' + groupDescription: '' + subgroup: '' + subgroupDescription: '' + title: 'Approve PTO' + description: 'Approve a pending PTO request and refresh the affected capacity caches.' + authenticated: false + deprecated: false + headers: + Content-Type: application/json + Accept: application/json + urlParameters: + id: + custom: [] + name: id + description: 'The PTO UUID that needs approval.' + required: true + example: 550e8400-e29b-41d4-a716-446655440001 + type: string + enumValues: [] + exampleWasSpecified: true + nullable: false + deprecated: false + cleanUrlParameters: + id: 550e8400-e29b-41d4-a716-446655440001 + queryParameters: [] + cleanQueryParameters: [] + bodyParameters: [] + cleanBodyParameters: [] + fileParameters: [] + responses: + - + custom: [] + status: 200 + content: |- + { + "id": "550e8400-e29b-41d4-a716-446655440001", + "status": "approved" + } + headers: [] + description: '' + responseFields: [] + auth: [] + controller: null + method: null + route: null diff --git a/backend/app/Http/Controllers/Api/CapacityController.php b/backend/app/Http/Controllers/Api/CapacityController.php new file mode 100644 index 00000000..46d483ab --- /dev/null +++ b/backend/app/Http/Controllers/Api/CapacityController.php @@ -0,0 +1,103 @@ +validate([ + 'month' => 'required|date_format:Y-m', + 'team_member_id' => 'required|exists:team_members,id', + ]); + + $capacity = $this->capacityService->calculateIndividualCapacity($data['team_member_id'], $data['month']); + + return response()->json($capacity); + } + + /** + * Get Team Capacity + * + * Summarize the combined capacity for all active team members in a month. + * + * @group Capacity Planning + * @urlParam month string required The month in YYYY-MM format. Example: 2026-02 + * @response { + * "month": "2026-02", + * "person_days": 180.5, + * "hours": 1444, + * "members": [ + * { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "name": "Ada Lovelace", + * "person_days": 18.5, + * "hours": 148 + * } + * ] + * } + */ + public function team(Request $request): JsonResponse + { + $data = $request->validate([ + 'month' => 'required|date_format:Y-m', + ]); + + $payload = $this->capacityService->calculateTeamCapacity($data['month']); + + return response()->json($payload); + } + + /** + * Get Possible Revenue + * + * Estimate monthly revenue based on capacity hours and hourly rates. + * + * @group Capacity Planning + * @urlParam month string required The month in YYYY-MM format. Example: 2026-02 + * @response { + * "month": "2026-02", + * "possible_revenue": 21500.25 + * } + */ + public function revenue(Request $request): JsonResponse + { + $data = $request->validate([ + 'month' => 'required|date_format:Y-m', + ]); + + $revenue = $this->capacityService->calculatePossibleRevenue($data['month']); + + return response()->json([ + 'month' => $data['month'], + 'possible_revenue' => $revenue, + ]); + } +} diff --git a/backend/app/Http/Controllers/Api/HolidayController.php b/backend/app/Http/Controllers/Api/HolidayController.php new file mode 100644 index 00000000..e9085016 --- /dev/null +++ b/backend/app/Http/Controllers/Api/HolidayController.php @@ -0,0 +1,99 @@ +validate([ + 'month' => 'nullable|date_format:Y-m', + ]); + + $holidays = isset($data['month']) + ? $this->capacityService->getHolidaysForMonth($data['month']) + : Holiday::orderBy('date')->get(); + + return response()->json($holidays); + } + + /** + * Create Holiday + * + * Add a holiday and clear cached capacity data for the related month. + * + * @group Capacity Planning + * @bodyParam date string required Date of the holiday. Example: 2026-02-14 + * @bodyParam name string required Name of the holiday. Example: Presidents' Day + * @bodyParam description string nullable Optional description of the holiday. + * @response 201 { + * "id": "550e8400-e29b-41d4-a716-446655440000", + * "date": "2026-02-14", + * "name": "Presidents' Day", + * "description": "Office closed" + * } + */ + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'date' => 'required|date', + 'name' => 'required|string', + 'description' => 'nullable|string', + ]); + + $holiday = Holiday::create($data); + $this->capacityService->forgetCapacityCacheForMonth($holiday->date->format('Y-m')); + + return response()->json($holiday, 201); + } + + /** + * Delete Holiday + * + * Remove a holiday and clear affected capacity caches. + * + * @group Capacity Planning + * @urlParam id string required The holiday UUID. Example: 550e8400-e29b-41d4-a716-446655440000 + * @response { + * "message": "Holiday deleted" + * } + */ + public function destroy(string $id): JsonResponse + { + $holiday = Holiday::find($id); + + if (! $holiday) { + return response()->json(['message' => 'Holiday not found'], 404); + } + + $month = $holiday->date->format('Y-m'); + $holiday->delete(); + $this->capacityService->forgetCapacityCacheForMonth($month); + + return response()->json(['message' => 'Holiday deleted']); + } +} diff --git a/backend/app/Http/Controllers/Api/PtoController.php b/backend/app/Http/Controllers/Api/PtoController.php new file mode 100644 index 00000000..5f018759 --- /dev/null +++ b/backend/app/Http/Controllers/Api/PtoController.php @@ -0,0 +1,135 @@ +validate([ + 'team_member_id' => 'required|exists:team_members,id', + 'month' => 'nullable|date_format:Y-m', + ]); + + $query = Pto::where('team_member_id', $data['team_member_id']); + + if (! empty($data['month'])) { + $start = Carbon::createFromFormat('Y-m', $data['month'])->startOfMonth(); + $end = $start->copy()->endOfMonth(); + + $query->where(function ($statement) use ($start, $end): void { + $statement->whereBetween('start_date', [$start, $end]) + ->orWhereBetween('end_date', [$start, $end]) + ->orWhere(function ($nested) use ($start, $end): void { + $nested->where('start_date', '<=', $start) + ->where('end_date', '>=', $end); + }); + }); + } + + $ptos = $query->orderBy('start_date')->get(); + + return response()->json($ptos); + } + + /** + * Request PTO + * + * Create a PTO request for a team member and keep it in pending status. + * + * @group Capacity Planning + * @bodyParam team_member_id string required The team member UUID. Example: 550e8400-e29b-41d4-a716-446655440000 + * @bodyParam start_date string required The first day of the PTO. Example: 2026-02-10 + * @bodyParam end_date string required The final day of the PTO. Example: 2026-02-12 + * @bodyParam reason string nullable Optional reason for the PTO. + * @response 201 { + * "id": "550e8400-e29b-41d4-a716-446655440001", + * "team_member_id": "550e8400-e29b-41d4-a716-446655440000", + * "start_date": "2026-02-10", + * "end_date": "2026-02-12", + * "status": "pending", + * "reason": "Family travel" + * } + */ + public function store(Request $request): JsonResponse + { + $data = $request->validate([ + 'team_member_id' => 'required|exists:team_members,id', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'reason' => 'nullable|string', + ]); + + $pto = Pto::create(array_merge($data, ['status' => 'pending'])); + + return response()->json($pto, 201); + } + + /** + * Approve PTO + * + * Approve a pending PTO request and refresh the affected capacity caches. + * + * @group Capacity Planning + * @urlParam id string required The PTO UUID that needs approval. Example: 550e8400-e29b-41d4-a716-446655440001 + * @response { + * "id": "550e8400-e29b-41d4-a716-446655440001", + * "status": "approved" + * } + */ + public function approve(string $id): JsonResponse + { + $pto = Pto::findOrFail($id); + + if ($pto->status !== 'approved') { + $pto->status = 'approved'; + $pto->save(); + $months = $this->monthsBetween($pto->start_date, $pto->end_date); + $this->capacityService->forgetCapacityCacheForTeamMember($pto->team_member_id, $months); + } + + return response()->json($pto); + } + + private function monthsBetween(Carbon|string $start, Carbon|string $end): array + { + $startMonth = Carbon::create($start)->copy()->startOfMonth(); + $endMonth = Carbon::create($end)->copy()->startOfMonth(); + $months = []; + + while ($startMonth <= $endMonth) { + $months[] = $startMonth->format('Y-m'); + $startMonth->addMonth(); + } + + return $months; + } +} diff --git a/backend/app/Models/TeamMemberAvailability.php b/backend/app/Models/TeamMemberAvailability.php new file mode 100644 index 00000000..28ceb1dd --- /dev/null +++ b/backend/app/Models/TeamMemberAvailability.php @@ -0,0 +1,37 @@ + 'date', + 'availability' => 'float', + ]; + + public function teamMember(): BelongsTo + { + return $this->belongsTo(TeamMember::class); + } +} diff --git a/backend/app/Services/CapacityService.php b/backend/app/Services/CapacityService.php new file mode 100644 index 00000000..0ea778b6 --- /dev/null +++ b/backend/app/Services/CapacityService.php @@ -0,0 +1,362 @@ +getHolidaysForMonth($month) + ->pluck('date') + ->map(fn (Carbon $date): string => $date->toDateString()) + ->all(); + + return WorkingDaysCalculator::calculate($month, $holidayDates); + } + + /** + * Calculate capacity for a single team member for the requested month. + */ + public function calculateIndividualCapacity(string $teamMemberId, string $month): array + { + $cacheKey = $this->buildCacheKey($month, $teamMemberId); + $tags = $this->getCapacityCacheTags($month, "team_member:{$teamMemberId}"); + + $resolver = function () use ($teamMemberId, $month): array { + $period = $this->createMonthPeriod($month); + $holidayDates = $this->getHolidaysForMonth($month) + ->pluck('date') + ->map(fn (Carbon $date): string => $date->toDateString()) + ->all(); + $holidayLookup = array_flip($holidayDates); + $ptoDates = $this->buildPtoDates($this->getPtoForTeamMember($teamMemberId, $month), $month); + $availabilities = $this->getAvailabilityEntries($teamMemberId, $month); + $personDays = 0.0; + $details = []; + + foreach ($period as $day) { + $date = $day->toDateString(); + + if (! WorkingDaysCalculator::isWorkingDay($date, $holidayLookup)) { + continue; + } + + $availability = $availabilities->get($date, 1.0); + $isPto = in_array($date, $ptoDates, true); + + if ($isPto) { + $availability = 0.0; + } + + $details[] = [ + 'date' => $date, + 'availability' => (float) $availability, + 'is_pto' => $isPto, + ]; + + $personDays += $availability; + } + + $hours = (int) round($personDays * $this->hoursPerDay); + + return [ + 'person_days' => round($personDays, 2), + 'hours' => $hours, + 'details' => $details, + ]; + }; + + /** @var array $capacity */ + /** @var array $capacity */ + $capacity = $this->rememberCapacity($cacheKey, now()->addHour(), $resolver, $tags); + + return $capacity; + } + + /** + * Calculate the combined capacity for all active team members. + */ + public function calculateTeamCapacity(string $month): array + { + $cacheKey = $this->buildCacheKey($month, 'team'); + $tags = $this->getCapacityCacheTags($month, 'team'); + + /** @var array $payload */ + $payload = $this->rememberCapacity($cacheKey, now()->addHour(), function () use ($month): array { + $activeMembers = TeamMember::where('active', true)->get(); + $totalDays = 0.0; + $totalHours = 0; + $members = []; + + foreach ($activeMembers as $member) { + $capacity = $this->calculateIndividualCapacity($member->id, $month); + + $totalDays += $capacity['person_days']; + $totalHours += $capacity['hours']; + + $members[] = [ + 'id' => $member->id, + 'name' => $member->name, + 'person_days' => $capacity['person_days'], + 'hours' => $capacity['hours'], + ]; + } + + return [ + 'month' => $month, + 'person_days' => round($totalDays, 2), + 'hours' => $totalHours, + 'members' => $members, + ]; + }, $tags); + + return $payload; + } + + /** + * Estimate revenue by multiplying capacity hours with hourly rates. + */ + public function calculatePossibleRevenue(string $month): float + { + $cacheKey = $this->buildCacheKey($month, 'revenue'); + $tags = $this->getCapacityCacheTags($month, 'revenue'); + + /** @var float $revenue */ + $revenue = $this->rememberCapacity($cacheKey, now()->addHour(), function () use ($month): float { + $activeMembers = TeamMember::where('active', true)->get(); + $revenue = 0.0; + + foreach ($activeMembers as $member) { + $capacity = $this->calculateIndividualCapacity($member->id, $month); + $revenue += $capacity['hours'] * (float) $member->hourly_rate; + } + + return round($revenue, 2); + }, $tags); + + return $revenue; + } + + /** + * Return all holidays in the requested month. + */ + public function getHolidaysForMonth(string $month): Collection + { + $period = $this->createMonthPeriod($month); + + return Holiday::whereBetween('date', [$period->getStartDate(), $period->getEndDate()]) + ->orderBy('date') + ->get(); + } + + /** + * Return approved PTO records for a team member inside the requested month. + */ + public function getPtoForTeamMember(string $teamMemberId, string $month): Collection + { + $period = $this->createMonthPeriod($month); + + return Pto::where('team_member_id', $teamMemberId) + ->where('status', 'approved') + ->where(function ($query) use ($period): void { + $query->whereBetween('start_date', [$period->getStartDate(), $period->getEndDate()]) + ->orWhereBetween('end_date', [$period->getStartDate(), $period->getEndDate()]) + ->orWhere(function ($nested) use ($period): void { + $nested->where('start_date', '<=', $period->getStartDate()) + ->where('end_date', '>=', $period->getEndDate()); + }); + }) + ->get(); + } + + /** + * Clear redis cache for a specific month and team member. + */ + public function forgetCapacityCacheForTeamMember(string $teamMemberId, array $months): void + { + $useRedis = $this->redisAvailable(); + + foreach ($months as $month) { + $tags = $this->getCapacityCacheTags($month, "team_member:{$teamMemberId}"); + + if ($useRedis) { + $this->flushCapacityTags($tags); + continue; + } + + $this->forgetCapacity($this->buildCacheKey($month, $teamMemberId)); + } + } + + /** + * Clear redis cache for a month across all team members. + */ + public function forgetCapacityCacheForMonth(string $month): void + { + if ($this->redisAvailable()) { + $this->flushCapacityTags($this->getCapacityCacheTags($month)); + return; + } + + foreach (TeamMember::pluck('id') as $teamMemberId) { + $this->forgetCapacity($this->buildCacheKey($month, $teamMemberId)); + } + + $this->forgetCapacity($this->buildCacheKey($month, 'team')); + $this->forgetCapacity($this->buildCacheKey($month, 'revenue')); + } + + /** + * Build the cache key used for storing individual capacity data. + */ + private function buildCacheKey(string $month, string $teamMemberId): string + { + return "capacity:{$month}:{$teamMemberId}"; + } + + private function getCapacityCacheTags(string $month, ?string $context = null): array + { + $tags = ['capacity', "capacity:month:{$month}"]; + + if ($context) { + $tags[] = "capacity:{$context}"; + } + + return $tags; + } + + private function flushCapacityTags(array $tags): void + { + if (! $this->redisAvailable()) { + return; + } + + try { + /** @var CacheRepository $store */ + $store = Cache::store('redis'); + $store->tags($tags)->flush(); + } catch (Throwable) { + // Ignore cache failures when Redis is unavailable. + } + } + + /** + * Load availability entries for the team member within the month, keyed by date. + */ + private function getAvailabilityEntries(string $teamMemberId, string $month): Collection + { + $period = $this->createMonthPeriod($month); + + return TeamMemberAvailability::where('team_member_id', $teamMemberId) + ->whereBetween('date', [$period->getStartDate(), $period->getEndDate()]) + ->get() + ->mapWithKeys(fn (TeamMemberAvailability $entry) => [$entry->date->toDateString() => (float) $entry->availability]); + } + + /** + * Create a CarbonPeriod for the given month. + */ + private function createMonthPeriod(string $month): CarbonPeriod + { + $start = Carbon::createFromFormat('Y-m', $month)->startOfMonth(); + $end = $start->copy()->endOfMonth(); + + return CarbonPeriod::create($start, $end); + } + + /** + * Expand PTO records into a unique list of dates inside the requested month. + */ + private function buildPtoDates(Collection $ptos, string $month): array + { + $period = $this->createMonthPeriod($month); + $dates = []; + + foreach ($ptos as $pto) { + $ptoStart = Carbon::create($pto->start_date)->max($period->getStartDate()); + $ptoEnd = Carbon::create($pto->end_date)->min($period->getEndDate()); + + if ($ptoStart->greaterThan($ptoEnd)) { + continue; + } + + foreach (CarbonPeriod::create($ptoStart, $ptoEnd) as $day) { + $dates[] = $day->toDateString(); + } + } + + return array_unique($dates); + } + + private function rememberCapacity(string $key, DateTimeInterface|int $ttl, callable $callback, array $tags = []): mixed + { + if (! $this->redisAvailable()) { + return Cache::store('array')->remember($key, $ttl, $callback); + } + + try { + /** @var CacheRepository $store */ + $store = Cache::store('redis'); + + if (! empty($tags)) { + $store = $store->tags($tags); + } + + return $store->remember($key, $ttl, $callback); + } catch (Throwable) { + return Cache::store('array')->remember($key, $ttl, $callback); + } + } + + private function forgetCapacity(string $key): void + { + if (! $this->redisAvailable()) { + return; + } + + try { + Cache::store('redis')->forget($key); + } catch (Throwable) { + // Ignore cache failures when Redis is unavailable. + } + } + + private function redisAvailable(): bool + { + if ($this->redisAvailable !== null) { + return $this->redisAvailable; + } + + if (! config('cache.stores.redis')) { + return $this->redisAvailable = false; + } + + $client = config('database.redis.client', 'phpredis'); + + if ($client === 'predis') { + return $this->redisAvailable = class_exists('\Predis\Client'); + } + + return $this->redisAvailable = extension_loaded('redis'); + } +} diff --git a/backend/app/Utilities/WorkingDaysCalculator.php b/backend/app/Utilities/WorkingDaysCalculator.php new file mode 100644 index 00000000..62137aa9 --- /dev/null +++ b/backend/app/Utilities/WorkingDaysCalculator.php @@ -0,0 +1,49 @@ +startOfMonth(); + $end = $start->copy()->endOfMonth(); + + return self::getWorkingDaysInRange($start->toDateString(), $end->toDateString(), $holidays); + } + + public static function getWorkingDaysInRange(string $start, string $end, array $holidays = []): int + { + $period = CarbonPeriod::create(Carbon::create($start), Carbon::create($end)); + $holidayLookup = array_flip($holidays); + $workingDays = 0; + + foreach ($period as $day) { + $date = $day->toDateString(); + + if (self::isWorkingDay($date, $holidayLookup)) { + $workingDays++; + } + } + + return $workingDays; + } + + public static function isWorkingDay(string $date, array $holidays = []): bool + { + $carbonDate = Carbon::create($date); + + if ($carbonDate->isWeekend()) { + return false; + } + + if (isset($holidays[$carbonDate->toDateString()])) { + return false; + } + + return true; + } +} diff --git a/backend/composer.json b/backend/composer.json index fc8891c0..bda01c16 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -18,7 +18,6 @@ "tymon/jwt-auth": "^2.0" }, "require-dev": { - "laravel/boost": "^2.1", "fakerphp/faker": "^1.23", "laravel/pail": "^1.2.2", "laravel/pint": "^1.24", diff --git a/backend/composer.lock b/backend/composer.lock index 972f79af..b623cc3a 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fa711629878d91ad308c94f502ab3af4", + "content-hash": "eb1f270f832bd2bd086e4cccb3a4945d", "packages": [ { "name": "brick/math", @@ -7283,145 +7283,6 @@ }, "time": "2025-03-19T14:43:43+00:00" }, - { - "name": "laravel/boost", - "version": "v2.1.2", - "source": { - "type": "git", - "url": "https://github.com/laravel/boost.git", - "reference": "81ecf79e82c979efd92afaeac012605cc7b2f31f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/81ecf79e82c979efd92afaeac012605cc7b2f31f", - "reference": "81ecf79e82c979efd92afaeac012605cc7b2f31f", - "shasum": "" - }, - "require": { - "guzzlehttp/guzzle": "^7.9", - "illuminate/console": "^11.45.3|^12.41.1", - "illuminate/contracts": "^11.45.3|^12.41.1", - "illuminate/routing": "^11.45.3|^12.41.1", - "illuminate/support": "^11.45.3|^12.41.1", - "laravel/mcp": "^0.5.1", - "laravel/prompts": "^0.3.10", - "laravel/roster": "^0.2.9", - "php": "^8.2" - }, - "require-dev": { - "laravel/pint": "^1.27.0", - "mockery/mockery": "^1.6.12", - "orchestra/testbench": "^9.15.0|^10.6", - "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5", - "phpstan/phpstan": "^2.1.27", - "rector/rector": "^2.1" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Boost\\BoostServiceProvider" - ] - }, - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Laravel\\Boost\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.", - "homepage": "https://github.com/laravel/boost", - "keywords": [ - "ai", - "dev", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel/boost/issues", - "source": "https://github.com/laravel/boost" - }, - "time": "2026-02-10T17:40:45+00:00" - }, - { - "name": "laravel/mcp", - "version": "v0.5.5", - "source": { - "type": "git", - "url": "https://github.com/laravel/mcp.git", - "reference": "b3327bb75fd2327577281e507e2dbc51649513d6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/b3327bb75fd2327577281e507e2dbc51649513d6", - "reference": "b3327bb75fd2327577281e507e2dbc51649513d6", - "shasum": "" - }, - "require": { - "ext-json": "*", - "ext-mbstring": "*", - "illuminate/console": "^11.45.3|^12.41.1|^13.0", - "illuminate/container": "^11.45.3|^12.41.1|^13.0", - "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", - "illuminate/http": "^11.45.3|^12.41.1|^13.0", - "illuminate/json-schema": "^12.41.1|^13.0", - "illuminate/routing": "^11.45.3|^12.41.1|^13.0", - "illuminate/support": "^11.45.3|^12.41.1|^13.0", - "illuminate/validation": "^11.45.3|^12.41.1|^13.0", - "php": "^8.2" - }, - "require-dev": { - "laravel/pint": "^1.20", - "orchestra/testbench": "^9.15|^10.8|^11.0", - "pestphp/pest": "^3.8.5|^4.3.2", - "phpstan/phpstan": "^2.1.27", - "rector/rector": "^2.2.4" - }, - "type": "library", - "extra": { - "laravel": { - "aliases": { - "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp" - }, - "providers": [ - "Laravel\\Mcp\\Server\\McpServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Mcp\\": "src/", - "Laravel\\Mcp\\Server\\": "src/Server/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "Rapidly build MCP servers for your Laravel applications.", - "homepage": "https://github.com/laravel/mcp", - "keywords": [ - "laravel", - "mcp" - ], - "support": { - "issues": "https://github.com/laravel/mcp/issues", - "source": "https://github.com/laravel/mcp" - }, - "time": "2026-02-05T14:05:18+00:00" - }, { "name": "laravel/pail", "version": "v1.2.6", @@ -7569,67 +7430,6 @@ }, "time": "2026-02-10T20:00:20+00:00" }, - { - "name": "laravel/roster", - "version": "v0.2.9", - "source": { - "type": "git", - "url": "https://github.com/laravel/roster.git", - "reference": "82bbd0e2de614906811aebdf16b4305956816fa6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/roster/zipball/82bbd0e2de614906811aebdf16b4305956816fa6", - "reference": "82bbd0e2de614906811aebdf16b4305956816fa6", - "shasum": "" - }, - "require": { - "illuminate/console": "^10.0|^11.0|^12.0", - "illuminate/contracts": "^10.0|^11.0|^12.0", - "illuminate/routing": "^10.0|^11.0|^12.0", - "illuminate/support": "^10.0|^11.0|^12.0", - "php": "^8.1|^8.2", - "symfony/yaml": "^6.4|^7.2" - }, - "require-dev": { - "laravel/pint": "^1.14", - "mockery/mockery": "^1.6", - "orchestra/testbench": "^8.22.0|^9.0|^10.0", - "pestphp/pest": "^2.0|^3.0", - "phpstan/phpstan": "^2.0" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Roster\\RosterServiceProvider" - ] - }, - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Laravel\\Roster\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Detect packages & approaches in use within a Laravel project", - "homepage": "https://github.com/laravel/roster", - "keywords": [ - "dev", - "laravel" - ], - "support": { - "issues": "https://github.com/laravel/roster/issues", - "source": "https://github.com/laravel/roster" - }, - "time": "2025-10-20T09:56:46+00:00" - }, { "name": "laravel/sail", "version": "v1.53.0", diff --git a/backend/database/factories/TeamMemberAvailabilityFactory.php b/backend/database/factories/TeamMemberAvailabilityFactory.php new file mode 100644 index 00000000..a4535e18 --- /dev/null +++ b/backend/database/factories/TeamMemberAvailabilityFactory.php @@ -0,0 +1,41 @@ + + */ +class TeamMemberAvailabilityFactory extends Factory +{ + protected $model = TeamMemberAvailability::class; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'id' => (string) Str::uuid(), + 'team_member_id' => TeamMember::factory(), + 'date' => now()->toDateString(), + 'availability' => 1.0, + ]; + } + + public function forDate(string $date): static + { + return $this->state(fn (array $attributes) => ['date' => $date]); + } + + public function availability(float $value): static + { + return $this->state(fn (array $attributes) => ['availability' => $value]); + } +} diff --git a/backend/database/migrations/2026_02_19_135007_create_team_member_daily_availabilities_table.php b/backend/database/migrations/2026_02_19_135007_create_team_member_daily_availabilities_table.php new file mode 100644 index 00000000..fdddd5be --- /dev/null +++ b/backend/database/migrations/2026_02_19_135007_create_team_member_daily_availabilities_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->foreignUuid('team_member_id')->constrained('team_members'); + $table->date('date'); + $table->decimal('availability', 3, 1)->default(1.0); + $table->timestamps(); + $table->unique(['team_member_id', 'date']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('team_member_daily_availabilities'); + } +}; diff --git a/backend/resources/views/scribe/index.blade.php b/backend/resources/views/scribe/index.blade.php index 18cfc97c..1cf1b2c5 100644 --- a/backend/resources/views/scribe/index.blade.php +++ b/backend/resources/views/scribe/index.blade.php @@ -82,6 +82,40 @@ +