package api import ( "encoding/json" "fmt" "io" "log" "net/http" "net/url" "strconv" "time" ) // SearchJobsParams defines parameters for searching jobs type SearchJobsParams struct { Query string Status string StartDate time.Time EndDate time.Time Page int Limit int IncludeArchived bool } // SearchJobs searches for jobs based on the provided parameters func (s *Session) SearchJobs(params SearchJobsParams) ([]map[string]interface{}, error) { queryValues := url.Values{} if params.Query != "" { queryValues.Add("q", params.Query) } if params.Status != "" { queryValues.Add("status", params.Status) } if !params.StartDate.IsZero() { queryValues.Add("start", params.StartDate.Format("2006-01-02")) } if !params.EndDate.IsZero() { queryValues.Add("end", params.EndDate.Format("2006-01-02")) } if params.Page > 0 { queryValues.Add("page", strconv.Itoa(params.Page)) } else { queryValues.Add("page", "1") } if params.Limit > 0 { queryValues.Add("limit", strconv.Itoa(params.Limit)) } else { queryValues.Add("limit", "50") } if params.IncludeArchived { queryValues.Add("include", "archived") } endpoint := fmt.Sprintf("/job?%s", queryValues.Encode()) resp, err := s.DoRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } if resp.StatusCode != 200 { return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body)) } var result struct { Data struct { Jobs []map[string]interface{} `json:"records"` } `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("error unmarshalling response: %v", err) } log.Printf("Found %d jobs", len(result.Data.Jobs)) log.Printf("Parsed Data: %+v", result.Data) return result.Data.Jobs, nil } // GetJobDetails retrieves detailed information about a specific job func (s *Session) GetJobDetails(jobID string) (map[string]interface{}, error) { endpoint := fmt.Sprintf("/job/%s", jobID) resp, err := s.DoRequest(http.MethodGet, endpoint, nil) if err != nil { return nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("error reading response body: %v", err) } if resp.StatusCode != 200 { return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body)) } var result struct { Data map[string]interface{} `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("error unmarshalling response: %v", err) } return result.Data, nil } // GetAttachmentsForJob retrieves attachments for a specific job func (s *Session) GetAttachmentsForJob(jobID string) (map[string]interface{}, error) { log.Printf("GetAttachmentsForJob: Fetching attachments for job %s", jobID) // Check if we have a valid cookie if s.Cookie == "" { log.Printf("GetAttachmentsForJob: No cookie found in session for job %s", jobID) return nil, fmt.Errorf("no authentication cookie found") } // Use the new API endpoint that returns attachments directly url := fmt.Sprintf("%s/job/%s", BaseURL, jobID) log.Printf("GetAttachmentsForJob: Using URL: %s", url) req, err := http.NewRequest("GET", url, nil) if err != nil { log.Printf("GetAttachmentsForJob: Error creating request: %v", err) return nil, fmt.Errorf("error creating request: %v", err) } // Add authorization header req.Header.Set("Cookie", s.Cookie) log.Printf("GetAttachmentsForJob: Authorization cookie length: %d", len(s.Cookie)) // Set Accept header to get all data in one call req.Header.Set("Accept", "application/json") // Add query parameter to include attachments q := req.URL.Query() q.Add("include", "attachments") q.Add("include", "paperwork") req.URL.RawQuery = q.Encode() log.Printf("GetAttachmentsForJob: Full URL with params: %s", req.URL.String()) // Debug headers log.Printf("GetAttachmentsForJob: Request headers: %+v", req.Header) // Send the request log.Printf("GetAttachmentsForJob: Sending request for job %s", jobID) resp, err := s.Client.Do(req) if err != nil { log.Printf("GetAttachmentsForJob: Error sending request: %v", err) return nil, fmt.Errorf("error sending request: %v", err) } defer resp.Body.Close() // Read the full response body for complete debugging body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("GetAttachmentsForJob: Error reading response body: %v", err) return nil, fmt.Errorf("error reading response body: %v", err) } log.Printf("GetAttachmentsForJob: Response status for job %s: %s", jobID, resp.Status) // Check for errors if resp.StatusCode != http.StatusOK { log.Printf("GetAttachmentsForJob: API error for job %s: %s - %s", jobID, resp.Status, string(body)) return nil, fmt.Errorf("API error: %s - %s", resp.Status, string(body)) } // Log a truncated version of the response for debugging responsePreview := string(body) if len(responsePreview) > 500 { responsePreview = responsePreview[:500] + "... [truncated]" } log.Printf("GetAttachmentsForJob: Response preview for job %s: %s", jobID, responsePreview) // Parse the response var result map[string]interface{} if err := json.Unmarshal(body, &result); err != nil { log.Printf("GetAttachmentsForJob: Error parsing response: %v", err) return nil, fmt.Errorf("error parsing response: %v", err) } // Log the structure of the response log.Printf("GetAttachmentsForJob: Root keys for job %s: %v", jobID, mapKeys(result)) // Check for data object structure if data, ok := result["data"].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: Data keys for job %s: %v", jobID, mapKeys(data)) // Look for job information if jobData, ok := data["job"].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: Job data keys: %v", mapKeys(jobData)) } // First try to find attachments directly if attachments, ok := data["attachments"]; ok { switch att := attachments.(type) { case []interface{}: log.Printf("GetAttachmentsForJob: Found %d attachments in data.attachments array for job %s", len(att), jobID) if len(att) > 0 { log.Printf("GetAttachmentsForJob: First attachment type: %T", att[0]) if attMap, ok := att[0].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: First attachment keys: %v", mapKeys(attMap)) } } case map[string]interface{}: log.Printf("GetAttachmentsForJob: Attachments is a map with keys: %v", mapKeys(att)) // Check for attachments in items or objects for _, key := range []string{"items", "objects", "components", "records"} { if items, ok := att[key].([]interface{}); ok { log.Printf("GetAttachmentsForJob: Found %d items in attachments.%s for job %s", len(items), key, jobID) if len(items) > 0 { if itemMap, ok := items[0].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: First item in %s keys: %v", key, mapKeys(itemMap)) } } } } default: log.Printf("GetAttachmentsForJob: Attachments has unexpected type: %T", attachments) } } else { log.Printf("GetAttachmentsForJob: No 'attachments' field in data for job %s", jobID) // Try to find paperwork directly if paperwork, ok := data["paperwork"].([]interface{}); ok { log.Printf("GetAttachmentsForJob: Found %d items in data.paperwork for job %s", len(paperwork), jobID) if len(paperwork) > 0 { if papMap, ok := paperwork[0].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: First paperwork keys: %v", mapKeys(papMap)) } } } // Check for other potential containers for _, key := range []string{"objects", "components", "items", "records"} { if items, ok := data[key].([]interface{}); ok { log.Printf("GetAttachmentsForJob: Found %d items in data.%s for job %s", len(items), key, jobID) if len(items) > 0 { if itemMap, ok := items[0].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: First item in %s keys: %v", key, mapKeys(itemMap)) } } } } } } else { log.Printf("GetAttachmentsForJob: No 'data' field or it's not a map for job %s", jobID) // Try to find objects at root level if objects, ok := result["objects"].([]interface{}); ok { log.Printf("GetAttachmentsForJob: Found %d objects at root level for job %s", len(objects), jobID) if len(objects) > 0 { if objMap, ok := objects[0].(map[string]interface{}); ok { log.Printf("GetAttachmentsForJob: First root object keys: %v", mapKeys(objMap)) } } } } // If response has an empty data structure, try a direct paperwork endpoint if emptyAttachments := isEmptyResponse(result); emptyAttachments { log.Printf("GetAttachmentsForJob: No attachments found in primary response, trying paperwork endpoint for job %s", jobID) paperworkURL := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID) paperworkReq, err := http.NewRequest("GET", paperworkURL, nil) if err == nil { paperworkReq.Header.Set("Cookie", s.Cookie) paperworkResp, err := s.Client.Do(paperworkReq) if err == nil && paperworkResp.StatusCode == http.StatusOK { defer paperworkResp.Body.Close() paperworkBody, err := io.ReadAll(paperworkResp.Body) if err == nil { var paperworkResult map[string]interface{} if err := json.Unmarshal(paperworkBody, &paperworkResult); err == nil { log.Printf("GetAttachmentsForJob: Paperwork endpoint response keys: %v", mapKeys(paperworkResult)) // Merge the paperwork result with our original result result["paperwork_data"] = paperworkResult // If the original result had a data field, add paperwork to it if data, ok := result["data"].(map[string]interface{}); ok { if objects, ok := paperworkResult["objects"].([]interface{}); ok && len(objects) > 0 { log.Printf("GetAttachmentsForJob: Found %d objects in paperwork response", len(objects)) data["paperwork_objects"] = objects } } } } } } } return result, nil } // Helper function to check if the response has no attachments func isEmptyResponse(result map[string]interface{}) bool { if data, ok := result["data"].(map[string]interface{}); ok { // Check if attachments exist and have content if attachments, ok := data["attachments"]; ok { switch att := attachments.(type) { case []interface{}: return len(att) == 0 case map[string]interface{}: // Check items/objects inside the attachments map hasItems := false for _, key := range []string{"items", "objects", "components", "records"} { if items, ok := att[key].([]interface{}); ok && len(items) > 0 { hasItems = true break } } return !hasItems default: return true // Unknown type, consider empty } } // Check for paperwork if paperwork, ok := data["paperwork"].([]interface{}); ok && len(paperwork) > 0 { return false } // Check for paperwork in job if job, ok := data["job"].(map[string]interface{}); ok { if paperwork, ok := job["paperwork"].([]interface{}); ok && len(paperwork) > 0 { return false } } // Check other containers for _, key := range []string{"objects", "components", "items", "records"} { if items, ok := data[key].([]interface{}); ok && len(items) > 0 { return false } } } // Check root level objects if objects, ok := result["objects"].([]interface{}); ok && len(objects) > 0 { return false } return true } // Helper function to get map keys for logging func mapKeys(m map[string]interface{}) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) } return keys } // GetDeficiencyInfoForJob retrieves deficiency information for a specific job func (s *Session) GetDeficiencyInfoForJob(jobID string) ([]map[string]interface{}, error) { resp, err := s.DoRequest("GET", fmt.Sprintf("/deficiency/%s", jobID), nil) if err != nil { return nil, err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) if resp.StatusCode != 200 { return nil, fmt.Errorf("failed to get deficiency info: %s, response: %s", resp.Status, string(body)) } var result struct { Data []map[string]interface{} `json:"data"` } if err := json.Unmarshal(body, &result); err != nil { return nil, fmt.Errorf("error unmarshalling response: %v", err) } return result.Data, nil }