diff --git a/apps/customer-portal/backend/modules/entity/entity.bal b/apps/customer-portal/backend/modules/entity/entity.bal index 943f0c54d5..0df85e3f16 100644 --- a/apps/customer-portal/backend/modules/entity/entity.bal +++ b/apps/customer-portal/backend/modules/entity/entity.bal @@ -104,7 +104,7 @@ public isolated function createCase(string idToken, CaseCreatePayload payload) r } # Update an existing case. -# +# # + idToken - ID token for authorization # + caseId - Unique ID of the case to be updated # + payload - Case update payload @@ -187,7 +187,7 @@ public isolated function createDeployedProduct(string idToken, DeployedProductCr } # Update a deployed product. -# +# # + idToken - ID token for authorization # + deployedProductId - ID of the deployed product to update # + payload - Deployed product update payload @@ -325,3 +325,14 @@ public isolated function searchProductVersions(string idToken, string productId, return csEntityClient->/products/[productId]/versions/search.post(payload, generateHeaders(idToken)); } + +# Search time cards. +# +# + idToken - ID token for authorization +# + payload - Payload containing search criteria for time cards +# + return - Response containing matching time cards or error +public isolated function searchTimeCards(string idToken, TimeCardSearchPayload payload) + returns TimeCardsResponse|error { + + return csEntityClient->/time\-cards/search.post(payload, generateHeaders(idToken)); +} diff --git a/apps/customer-portal/backend/modules/entity/types.bal b/apps/customer-portal/backend/modules/entity/types.bal index c3fb45ae47..1e1069af66 100644 --- a/apps/customer-portal/backend/modules/entity/types.bal +++ b/apps/customer-portal/backend/modules/entity/types.bal @@ -918,14 +918,14 @@ public type CallRequestsResponse record {| json...; |}; -# Date Constraint. +# Date-time (UTC). @constraint:String { pattern: { value: re `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d{1,9})?)?(Z|[+-]([01]\d|2[0-3]):?[0-5]\d)$`, message: "Invalid date provided. Please provide a valid date value." } } -public type Date string; +public type DateTime string; # Request payload for creating a call request. public type CallRequestCreatePayload record {| @@ -935,7 +935,7 @@ public type CallRequestCreatePayload record {| string reason; # Preferred UTC times for the call @constraint:Array {minLength: 1} - Date[] utcTimes; + DateTime[] utcTimes; # Duration in minutes @constraint:Int {minValue: 1} int durationInMinutes; @@ -968,9 +968,9 @@ public type CallRequestUpdatePayload record {| # State key int stateKey; # Reason for the update - string? reason; + string reason?; # New preferred UTC times for the call (mandatory when stateKey is 2) - Date[] utcTimes?; + DateTime[] utcTimes?; |}; # Updated call request details. @@ -1077,3 +1077,64 @@ public type ProductVersionsResponse record {| ProductVersion[] versions; json...; // TODO: Add pagination |}; + +# Date. +@constraint:String { + pattern: { + value: re `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$`, + message: "Invalid date format." + } +} +public type Date string; + +# Request payload for searching time cards. +public type TimeCardSearchPayload record {| + # Filter criteria + record { + # List of project IDs to filter + string[] projectIds?; + # Start date for filtering time cards (ISO 8601 format) + Date startDate?; + # End date for filtering time cards (ISO 8601 format) + Date endDate?; + } filters?; + # Pagination details + Pagination pagination?; +|}; + +# Time card data. +public type TimeCard record {| + # ID + string id; + # Total time logged + decimal totalTime; + # Created date and time + string createdOn; + # Indicates if the time card has billable hours + boolean hasBillable; + # State information (e.g., "Approved", "Submitted") + string state; + # User who approved the time card + ReferenceTableItem? approvedBy; + # Associated project + ReferenceTableItem? project; + # Associated case + TimeCardCase? case; + json...; +|}; + +# Time card information associated with a case. +public type TimeCardCase record {| + *ReferenceTableItem; + # Case number + string number; +|}; + +# Time cards response. +public type TimeCardsResponse record {| + # List of time cards + TimeCard[] timeCards; + # Total records count + int totalRecords; + *Pagination; +|}; diff --git a/apps/customer-portal/backend/modules/entity/utils.bal b/apps/customer-portal/backend/modules/entity/utils.bal index f2d9043fe2..c9879dbbd7 100644 --- a/apps/customer-portal/backend/modules/entity/utils.bal +++ b/apps/customer-portal/backend/modules/entity/utils.bal @@ -92,7 +92,7 @@ public isolated function validateCallRequestUpdatePayload(CallRequestUpdatePaylo # # + utcTimes - Array of UTC time strings to validate # + return - Error message if any UTC time is invalid or in the past, nil otherwise -public isolated function validateUtcTimes(Date[]? utcTimes) returns string|error? { +public isolated function validateUtcTimes(DateTime[]? utcTimes) returns string|error? { if utcTimes != () { foreach string utcTime in utcTimes { time:Utc input = check time:utcFromString(utcTime); diff --git a/apps/customer-portal/backend/modules/types/types.bal b/apps/customer-portal/backend/modules/types/types.bal index cea2460cd9..1cb9f8c37c 100644 --- a/apps/customer-portal/backend/modules/types/types.bal +++ b/apps/customer-portal/backend/modules/types/types.bal @@ -674,22 +674,13 @@ public type CallRequestsResponse record {| // TODO: Remove after adding pagination |}; -# Date Constraint. -@constraint:String { - pattern: { - value: re `^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d{1,9})?)?(Z|[+-]([01]\d|2[0-3]):?[0-5]\d)$`, - message: "Invalid date provided. Please provide a valid date value." - } -} -public type Date string; - # Request payload for creating a call request. public type CallRequestCreatePayload record {| # Reason for the call request string reason; # Preferred UTC times for the call @constraint:Array {minLength: 1} - Date[] utcTimes; + entity:DateTime[] utcTimes; # Duration in minutes @constraint:Int {minValue: 1} int durationInMinutes; @@ -700,9 +691,9 @@ public type CallRequestUpdatePayload record {| # State key int stateKey; # Reason for the update - string? reason; + string reason?; # New preferred UTC times for the call (mandatory when stateKey is 2) - Date[] utcTimes?; + entity:DateTime[] utcTimes?; |}; # Product version data. @@ -730,3 +721,50 @@ public type ProductVersionsResponse record {| ProductVersion[] versions; json...; // TODO: Add pagination |}; + +# Time card data. +public type TimeCard record {| + # ID + string id; + # Total time logged + decimal totalTime; + # Created date and time + string createdOn; + # Indicates if the time card has billable hours + boolean hasBillable; + # State information (e.g., "Approved", "Submitted") + string state; + # User who approved the time card + ReferenceItem? approvedBy; + # Associated project + ReferenceItem? project; + # Associated case + record {| + *ReferenceItem; + # Case number + string number; + |}? case; + json...; +|}; + +# Time cards response. +public type TimeCardsResponse record {| + # List of time cards + TimeCard[] timeCards; + # Total records count + int totalRecords; + *entity:Pagination; +|}; + +# Request payload for searching time cards. +public type TimeCardSearchPayload record {| + # Filter criteria + record { + # Start date for filtering time cards (ISO 8601 format) + entity:Date startDate?; + # End date for filtering time cards (ISO 8601 format) + entity:Date endDate?; + } filters?; + # Pagination details + entity:Pagination pagination?; +|}; diff --git a/apps/customer-portal/backend/service.bal b/apps/customer-portal/backend/service.bal index f2b6bf8306..3ff9f2b809 100644 --- a/apps/customer-portal/backend/service.bal +++ b/apps/customer-portal/backend/service.bal @@ -2116,4 +2116,71 @@ service http:InterceptableService / on new http:Listener(9090, listenerConf) { body: mapProductVersionsResponse(response) }; } + + # Search time cards based on provided filters. + # + # + id - ID of the project + # + payload - Time card search payload containing filters and pagination info + # + return - List of time cards matching the criteria or an error + resource function post projects/[string id]/time\-cards/search(http:RequestContext ctx, + types:TimeCardSearchPayload payload) + returns http:Ok|http:BadRequest|http:Unauthorized|http:Forbidden|http:InternalServerError { + + authorization:UserInfoPayload|error userInfo = ctx.getWithType(authorization:HEADER_USER_INFO); + if userInfo is error { + return { + body: { + message: ERR_MSG_USER_INFO_HEADER_NOT_FOUND + } + }; + } + + entity:TimeCardsResponse|error response = entity:searchTimeCards(userInfo.idToken, + { + filters: { + projectIds: [id], + startDate: payload.filters?.startDate, + endDate: payload.filters?.endDate + }, + pagination: payload.pagination + }); + if response is error { + if getStatusCode(response) == http:STATUS_BAD_REQUEST { + return { + body: { + message: "Invalid request parameters for searching time cards." + } + }; + } + + if getStatusCode(response) == http:STATUS_UNAUTHORIZED { + log:printWarn(string `User: ${userInfo.userId} is not authorized to access time card information!`); + return { + body: { + message: ERR_MSG_UNAUTHORIZED_ACCESS + } + }; + } + + if getStatusCode(response) == http:STATUS_FORBIDDEN { + log:printWarn(string `Access to time card information is forbidden for user: ${userInfo.userId}`); + return { + body: { + message: "Access to time card information is forbidden for the user!" + } + }; + } + + string customError = "Failed to search time cards."; + log:printError(customError, response); + return { + body: { + message: customError + } + }; + } + return { + body: mapTimeCardSearchResponse(response) + }; + } } diff --git a/apps/customer-portal/backend/utils.bal b/apps/customer-portal/backend/utils.bal index 13233b246f..8cff44da04 100644 --- a/apps/customer-portal/backend/utils.bal +++ b/apps/customer-portal/backend/utils.bal @@ -416,3 +416,26 @@ public isolated function mapProductVersionsResponse(entity:ProductVersionsRespon }; return {versions}; } + +# Map time cards search response to the desired structure. +# +# + response - Time cards search response from the entity service +# + return - Mapped time cards search response +public isolated function mapTimeCardSearchResponse(entity:TimeCardsResponse response) returns types:TimeCardsResponse { + types:TimeCard[] timeCards = from entity:TimeCard timeCard in response.timeCards + let entity:ReferenceTableItem? approvedBy = timeCard.approvedBy + let entity:ReferenceTableItem? project = timeCard.project + let entity:TimeCardCase? case = timeCard.case + select { + id: timeCard.id, + totalTime: timeCard.totalTime, + createdOn: timeCard.createdOn, + hasBillable: timeCard.hasBillable, + state: timeCard.state, + approvedBy: approvedBy != () ? {id: approvedBy.id, label: approvedBy.name} : (), + project: project != () ? {id: project.id, label: project.name} : (), + case: case != () ? {id: case.id, label: case.name, number: case.number} : () + }; + + return {timeCards, totalRecords: response.totalRecords, 'limit: response.'limit, offset: response.offset}; +}