diff --git a/internal/handlers/web/documents.go b/internal/handlers/web/documents.go index 9867fa2..6cfb7c6 100644 --- a/internal/handlers/web/documents.go +++ b/internal/handlers/web/documents.go @@ -10,8 +10,10 @@ import ( "net/http" "path/filepath" "regexp" + "sort" "strconv" "strings" + "sync" "time" root "marmic/servicetrade-toolbox" @@ -131,62 +133,57 @@ func ProcessCSVHandler(w http.ResponseWriter, r *http.Request) { jobID := strings.TrimSpace(row[idColumnIndex]) if jobID != "" { jobNumbers = append(jobNumbers, jobID) - log.Printf("Added job ID: %s", jobID) } } } - log.Printf("Extracted %d job IDs from CSV", len(jobNumbers)) + totalJobs := len(jobNumbers) + log.Printf("Extracted %d job IDs from CSV", totalJobs) - // Create a list of valid job numbers - var validJobNumbers []string - validJobNumbers = append(validJobNumbers, jobNumbers...) + if totalJobs == 0 { + http.Error(w, "No valid job IDs found in the CSV file", http.StatusBadRequest) + return + } - // Generate HTML for job list - var jobListHTML string - if len(validJobNumbers) > 0 { - // Create a hidden input with the job IDs - jobsValue := strings.Join(validJobNumbers, ",") + // Create a hidden input with the job IDs + jobsValue := strings.Join(jobNumbers, ",") - // Insert a hidden input for job numbers and show the job list - jobListHTML = fmt.Sprintf(` + // Generate HTML for job preview - don't show all IDs for large datasets + var jobPreviewHTML string + if totalJobs > 0 { + jobPreviewHTML = fmt.Sprintf(` - - `, jobsValue, buildJobListJS(validJobNumbers)) +
Found %d job(s) in the CSV file
+Sample job IDs: %s
+No valid job numbers found in the CSV file.
` } w.Header().Set("Content-Type", "text/html") - w.Write([]byte(jobListHTML)) + w.Write([]byte(jobPreviewHTML)) } -// Helper function to build JavaScript for job list -func buildJobListJS(jobIDs []string) string { - var js strings.Builder - for _, id := range jobIDs { - js.WriteString(fmt.Sprintf(` - var li = document.createElement("li"); - li.setAttribute("data-job-id", "%s"); - li.textContent = "Job #%s"; - ul.appendChild(li); - `, id, id)) - } - return js.String() +// Helper function to show sample job IDs with a limit +func getJobSampleDisplay(jobIDs []string) string { + const maxSamples = 5 + if len(jobIDs) <= maxSamples { + return strings.Join(jobIDs, ", ") + } + + sample := append([]string{}, jobIDs[:maxSamples]...) + return strings.Join(sample, ", ") + fmt.Sprintf(" and %d more...", len(jobIDs)-maxSamples) } // UploadDocumentsHandler handles document uploads to jobs @@ -308,79 +305,230 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { }) } - // Process each file and upload to each job - results := make(map[string][]map[string]interface{}) - for _, doc := range documents { - defer doc.File.Close() + if len(documents) == 0 { + http.Error(w, "No valid documents selected for upload", http.StatusBadRequest) + return + } + // Read all file contents first to avoid keeping files open during concurrent uploads + type DocumentWithContent struct { + Name string + Type string + FileContent []byte + } + + var docsWithContent []DocumentWithContent + for _, doc := range documents { // Read file content fileContent, err := io.ReadAll(doc.File) if err != nil { - http.Error(w, fmt.Sprintf("Error reading file %s: %v", doc.Header.Filename, err), http.StatusInternalServerError) - return + log.Printf("Error reading file %s: %v", doc.Header.Filename, err) + continue } + doc.File.Close() // Close the file as soon as we're done with it + + docsWithContent = append(docsWithContent, DocumentWithContent{ + Name: doc.Name, + Type: doc.Type, + FileContent: fileContent, + }) + } - // Upload to each job - for _, jobID := range jobs { - // Call the ServiceTrade API - result, err := session.UploadAttachment(jobID, doc.Name, doc.Type, fileContent) - if err != nil { - log.Printf("Error uploading %s to job %s: %v", doc.Name, jobID, err) - if _, exists := results[jobID]; !exists { - results[jobID] = []map[string]interface{}{} + // Concurrent upload with throttling + // ServiceTrade API allows 30s of availability per minute (approximately 15 requests at 2s each) + const maxConcurrent = 5 // A conservative limit to avoid rate limiting + const requestDelay = 300 * time.Millisecond // Delay between requests + + // Channel for collecting results + type UploadResult struct { + JobID string + DocName string + Success bool + Error string + Data map[string]interface{} + } + + totalUploads := len(jobs) * len(docsWithContent) + resultsChan := make(chan UploadResult, totalUploads) + + // Create a wait group to track when all uploads are done + var wg sync.WaitGroup + + // Create a semaphore channel to limit concurrent uploads + semaphore := make(chan struct{}, maxConcurrent) + + // Start the upload workers + for _, jobID := range jobs { + for _, doc := range docsWithContent { + wg.Add(1) + + // Launch a goroutine for each job+document combination + go func(jobID string, doc DocumentWithContent) { + defer wg.Done() + + // Acquire a semaphore slot + semaphore <- struct{}{} + defer func() { <-semaphore }() // Release when done + + // Add a small delay to avoid overwhelming the API + time.Sleep(requestDelay) + + // Call the ServiceTrade API + result, err := session.UploadAttachment(jobID, doc.Name, doc.Type, doc.FileContent) + + if err != nil { + log.Printf("Error uploading %s to job %s: %v", doc.Name, jobID, err) + resultsChan <- UploadResult{ + JobID: jobID, + DocName: doc.Name, + Success: false, + Error: err.Error(), + } + } else { + log.Printf("Successfully uploaded %s to job %s", doc.Name, jobID) + resultsChan <- UploadResult{ + JobID: jobID, + DocName: doc.Name, + Success: true, + Data: result, + } } - results[jobID] = append(results[jobID], map[string]interface{}{ - "filename": doc.Name, - "success": false, - "error": err.Error(), - }) - continue - } + }(jobID, doc) + } + } - // Record the success - if _, exists := results[jobID]; !exists { - results[jobID] = []map[string]interface{}{} - } - results[jobID] = append(results[jobID], map[string]interface{}{ - "filename": doc.Name, - "success": true, - "data": result, - }) - log.Printf("Successfully uploaded %s to job %s", doc.Name, jobID) + // Close the results channel when all uploads are done + go func() { + wg.Wait() + close(resultsChan) + }() + + // Collect results + results := make(map[string][]UploadResult) + for result := range resultsChan { + if _, exists := results[result.JobID]; !exists { + results[result.JobID] = []UploadResult{} } + results[result.JobID] = append(results[result.JobID], result) } // Generate HTML for results var resultHTML bytes.Buffer - resultHTML.WriteString("No documents were uploaded. Please check that you have selected files and document types.
") + // Count successes and failures + var totalSuccess, totalFailure int + for _, jobResults := range results { + for _, result := range jobResults { + if result.Success { + totalSuccess++ + } else { + totalFailure++ + } + } + } + + // Add summary section + resultHTML.WriteString("All documents were successfully uploaded to ServiceTrade!
") } else { - for jobID, jobResults := range results { - resultHTML.WriteString(fmt.Sprintf("Some documents failed to upload. See details below.
") + } - for _, result := range jobResults { - filename := result["filename"].(string) - success := result["success"].(bool) + resultHTML.WriteString("No files processed for this job.
") } - } - resultHTML.WriteString("