`, jobSampleDisplay))
+
+ // Generate out-of-band swap for the preview section
+ responseHTML.WriteString(fmt.Sprintf(`
+
+
Detected Jobs
+
+
+
Sample job IDs: %s
+
+
+
+ `, jobSampleDisplay))
+
+ // Send the response with the hidden input and preview
+ w.Header().Set("Content-Type", "text/html")
+ w.Write(responseHTML.Bytes())
+}
+
+// After the CSV is processed, a separate request should load job attachments
+// This handler is for Step 2
+func JobSelectionHandler(w http.ResponseWriter, r *http.Request) {
+ // We don't use the session directly but check it for auth
+ _, ok := r.Context().Value("session").(*api.Session)
+ if !ok {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ jobIDs := r.FormValue("jobIDs")
+ if jobIDs == "" {
+ http.Error(w, "No job IDs provided", http.StatusBadRequest)
+ return
+ }
+
+ jobs := strings.Split(jobIDs, ",")
+ if len(jobs) == 0 {
+ http.Error(w, "No valid job IDs found", http.StatusBadRequest)
+ return
+ }
+
+ var resultHTML bytes.Buffer
+ resultHTML.WriteString("
")
+ resultHTML.WriteString("
Jobs from CSV
")
+ resultHTML.WriteString("
Click a job to view and manage its documents.
")
+ resultHTML.WriteString("
")
+
+ for _, jobID := range jobs {
+ resultHTML.WriteString(fmt.Sprintf(`
+
") // End of job-list-container
+
+ w.Header().Set("Content-Type", "text/html")
+ w.Write(resultHTML.Bytes())
+}
+
+// GetJobAttachmentsHandler retrieves attachments for a specific job
+func GetJobAttachmentsHandler(w http.ResponseWriter, r *http.Request) {
+ session, ok := r.Context().Value("session").(*api.Session)
+ if !ok {
+ vars := mux.Vars(r)
+ jobID := vars["jobID"]
+ renderErrorTemplate(w, "job_attachments", "You must be logged in to use this feature", jobID)
+ return
+ }
+
+ vars := mux.Vars(r)
+ jobID := vars["jobID"]
+ if jobID == "" {
+ renderErrorTemplate(w, "job_attachments", "Job ID is required", jobID)
+ return
+ }
+
+ // Get attachments for the job
+ attachments, err := session.GetJobAttachments(jobID)
+ if err != nil {
+ renderErrorTemplate(w, "job_attachments", fmt.Sprintf("Failed to get attachments: %v", err), jobID)
+ return
+ }
+
+ tmpl := root.WebTemplates
+ data := map[string]interface{}{
+ "JobID": jobID,
+ "Attachments": attachments,
+ "Session": session,
+ }
+
+ if err := tmpl.ExecuteTemplate(w, "job_attachments", data); err != nil {
+ log.Printf("Template execution error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+}
+
+// RemoveJobAttachmentsHandler handles the removal of attachments from a job
+func RemoveJobAttachmentsHandler(w http.ResponseWriter, r *http.Request) {
+ session, ok := r.Context().Value("session").(*api.Session)
+ if !ok {
+ vars := mux.Vars(r)
+ jobID := vars["jobID"]
+ renderErrorTemplate(w, "job_attachments", "You must be logged in to use this feature", jobID)
+ return
+ }
+
+ vars := mux.Vars(r)
+ jobID := vars["jobID"]
+ if jobID == "" {
+ renderErrorTemplate(w, "job_attachments", "Job ID is required", jobID)
+ return
+ }
+
+ // Parse the form
+ if err := r.ParseForm(); err != nil {
+ renderErrorTemplate(w, "job_attachments", fmt.Sprintf("Error parsing form: %v", err), jobID)
+ return
+ }
+
+ // Get the attachment IDs to remove
+ attachmentIDs := r.PostForm["attachment_ids"]
+ if len(attachmentIDs) == 0 {
+ renderErrorTemplate(w, "job_attachments", "No attachments selected for deletion", jobID)
+ return
+ }
+
+ // Process deletion with rate limiting (max 5 concurrent requests)
+ results := struct {
+ Success bool
+ SuccessCount int
+ ErrorCount int
+ Files []struct {
+ Name string
+ Success bool
+ Error string
+ }
+ }{
+ Success: true,
+ Files: make([]struct {
+ Name string
+ Success bool
+ Error string
+ }, 0, len(attachmentIDs)),
+ }
+
+ // Set up rate limiting
+ semaphore := make(chan struct{}, 5) // Allow 5 concurrent requests
+ var wg sync.WaitGroup
+ var mu sync.Mutex // Mutex for updating results
+
+ for _, attachmentID := range attachmentIDs {
+ wg.Add(1)
+ semaphore <- struct{}{} // Acquire semaphore
+
+ go func(id string) {
+ defer wg.Done()
+ defer func() { <-semaphore }() // Release semaphore
+
+ // Get attachment info first to get the name
+ attachmentInfo, err := session.GetAttachmentInfo(id)
+
+ fileResult := struct {
+ Name string
+ Success bool
+ Error string
+ }{
+ Name: fmt.Sprintf("Attachment ID: %s", id),
+ }
+
+ if err == nil {
+ // Get description if available
+ if description, ok := attachmentInfo["description"].(string); ok {
+ fileResult.Name = description
+ }
+ }
+
+ // Delete the attachment
+ err = session.DeleteAttachment(id)
+
+ mu.Lock()
+ defer mu.Unlock()
+
+ if err != nil {
+ fileResult.Success = false
+ fileResult.Error = err.Error()
+ results.ErrorCount++
+ results.Success = false
+ } else {
+ fileResult.Success = true
+ results.SuccessCount++
+ }
+
+ results.Files = append(results.Files, fileResult)
+
+ // Add a slight delay to avoid overwhelming the API
+ time.Sleep(100 * time.Millisecond)
+ }(attachmentID)
+ }
+
+ wg.Wait() // Wait for all deletions to complete
+
+ tmpl := root.WebTemplates
+ data := map[string]interface{}{
+ "JobID": jobID,
+ "Session": session,
+ "SuccessCount": results.SuccessCount,
+ "ErrorCount": results.ErrorCount,
+ "JobsProcessed": 1,
+ "Results": []map[string]interface{}{
+ {
+ "JobID": jobID,
+ "Success": results.Success,
+ "Files": results.Files,
+ },
+ },
+ }
+
+ // Generate HTML for results that will go to the removal_results div
+ if err := tmpl.ExecuteTemplate(w, "removal_results", data); err != nil {
+ log.Printf("Template execution error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+}
+
+// JobListHandler renders the job list for document removal
+func JobListHandler(w http.ResponseWriter, r *http.Request) {
+ session, ok := r.Context().Value("session").(*api.Session)
+ if !ok {
+ renderErrorTemplate(w, "job_list", "You must be logged in to use this feature")
+ return
+ }
+
+ if err := r.ParseForm(); err != nil {
+ renderErrorTemplate(w, "job_list", fmt.Sprintf("Error parsing form: %v", err))
+ return
+ }
+
+ tmpl := root.WebTemplates
+ data := map[string]interface{}{
+ "JobIDs": r.PostForm["job_ids"],
+ "Session": session,
+ }
+
+ if err := tmpl.ExecuteTemplate(w, "job_list", data); err != nil {
+ log.Printf("Template execution error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+}
+
+// Helper function to render error templates
+func renderErrorTemplate(w http.ResponseWriter, templateName, errorMsg string, jobID ...string) {
+ tmpl := root.WebTemplates
+ data := map[string]interface{}{
+ "Error": errorMsg,
+ }
+
+ if len(jobID) > 0 && jobID[0] != "" {
+ data["JobID"] = jobID[0]
+ }
+
+ if err := tmpl.ExecuteTemplate(w, templateName, data); err != nil {
+ log.Printf("Template execution error: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ }
+}
diff --git a/internal/handlers/web/documents.go b/internal/handlers/web/documents.go
index 6cfb7c6..3f30469 100644
--- a/internal/handlers/web/documents.go
+++ b/internal/handlers/web/documents.go
@@ -147,32 +147,29 @@ func ProcessCSVHandler(w http.ResponseWriter, r *http.Request) {
// Create a hidden input with the job IDs
jobsValue := strings.Join(jobNumbers, ",")
-
- // Generate HTML for job preview - don't show all IDs for large datasets
- var jobPreviewHTML string
- if totalJobs > 0 {
- jobPreviewHTML = fmt.Sprintf(`
-
-
-
-
-
+ jobSampleDisplay := getJobSampleDisplay(jobNumbers)
+
+ // Generate HTML for the main response (hidden input for job-ids-container)
+ var responseHTML bytes.Buffer
+ responseHTML.WriteString(fmt.Sprintf(``, jobsValue))
+ responseHTML.WriteString(fmt.Sprintf(`
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/document_remove_csv.html b/templates/partials/document_remove_csv.html
new file mode 100644
index 0000000..5a5ceed
--- /dev/null
+++ b/templates/partials/document_remove_csv.html
@@ -0,0 +1,25 @@
+{{define "document_remove_csv"}}
+
+
+
+
+
+
+
+ Processing CSV...
+
+
+
+
+
+
Detected Jobs
+
+
+
No jobs loaded yet
+
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/document_remove_form.html b/templates/partials/document_remove_form.html
new file mode 100644
index 0000000..41c8762
--- /dev/null
+++ b/templates/partials/document_remove_form.html
@@ -0,0 +1,28 @@
+{{define "document_remove_form"}}
+
+
+
+
+
+
After uploading a CSV and clicking "Load Jobs", you'll see jobs here.
+
+
+
+
+
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/document_upload.html b/templates/partials/document_upload.html
index 478759c..19ab302 100644
--- a/templates/partials/document_upload.html
+++ b/templates/partials/document_upload.html
@@ -5,7 +5,7 @@
hx-indicator="#upload-loading-indicator">
-
No jobs found. Please upload a CSV file with job IDs.
+ {{end}}
+ {{end}}
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/removal_results.html b/templates/partials/removal_results.html
new file mode 100644
index 0000000..e43ab1f
--- /dev/null
+++ b/templates/partials/removal_results.html
@@ -0,0 +1,51 @@
+{{define "removal_results"}}
+