an updated and hopefully faster version of the ST Toolbox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1010 lines
28 KiB

package web
import (
"bytes"
"encoding/csv"
"fmt"
"log"
"net/http"
"regexp"
"strconv"
"strings"
"sync"
"time"
root "marmic/servicetrade-toolbox"
"marmic/servicetrade-toolbox/internal/api"
"marmic/servicetrade-toolbox/internal/middleware"
"marmic/servicetrade-toolbox/internal/utils"
"github.com/gorilla/mux"
)
// RemovalResult represents the result of a single job removal
type RemovalResult struct {
JobID string
FilesFound int
FilesRemoved int
Success bool
ErrorMsg string
Files []struct {
Name string
Success bool
Error string
}
}
// RemovalSession stores removal results for pagination
type RemovalSession struct {
Results []RemovalResult
TotalJobs int
SuccessCount int
ErrorCount int
TotalFiles int
TotalTime time.Duration
CreatedAt time.Time
}
// Global map to store removal sessions (in production, use Redis or database)
var removalSessions = make(map[string]RemovalSession)
// DocumentRemoveHandler handles the document removal page
func DocumentRemoveHandler(w http.ResponseWriter, r *http.Request) {
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
tmpl := root.WebTemplates
data := map[string]interface{}{
"Title": "Document Removal",
"Session": session,
"DocumentTypes": []map[string]string{
{"value": "1", "label": "Job Paperwork"},
{"value": "2", "label": "Job Vendor Bill"},
{"value": "7", "label": "Generic Attachment"},
{"value": "10", "label": "Blank Paperwork"},
{"value": "14", "label": "Job Invoice"},
},
}
if r.Header.Get("HX-Request") == "true" {
// For HTMX requests, just send the document_remove partial
if err := tmpl.ExecuteTemplate(w, "document_remove", data); err != nil {
log.Printf("Template execution error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
} else {
// For full page requests, first render document_remove into a buffer
var contentBuf bytes.Buffer
if err := tmpl.ExecuteTemplate(&contentBuf, "document_remove", data); err != nil {
log.Printf("Template execution error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
// Add the rendered content to the data for the layout
data["BodyContent"] = contentBuf.String()
// Now render the layout with our content
if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil {
log.Printf("Template execution error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
}
}
// ProcessRemoveCSVHandler processes a CSV file containing job IDs for document removal
func ProcessRemoveCSVHandler(w http.ResponseWriter, r *http.Request) {
// We don't use the session in the body but check it for auth
_, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Check if the request method is POST
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// Parse the multipart form data with a 10MB limit
if err := r.ParseMultipartForm(10 << 20); err != nil {
http.Error(w, "Unable to parse form: "+err.Error(), http.StatusBadRequest)
return
}
// Get the file from the form
file, _, err := r.FormFile("csv")
if err != nil {
http.Error(w, "Error retrieving file: "+err.Error(), http.StatusBadRequest)
return
}
defer file.Close()
// Read the CSV data
csvData, err := csv.NewReader(file).ReadAll()
if err != nil {
http.Error(w, "Error reading CSV file: "+err.Error(), http.StatusBadRequest)
return
}
// Extract job IDs from CSV - first column only for simplicity
var jobIDs []string
for rowIndex, row := range csvData {
// Skip header row if present
if rowIndex == 0 && len(csvData) > 1 {
// Check if first row looks like a header (non-numeric content)
_, err := strconv.Atoi(strings.TrimSpace(row[0]))
if err != nil {
continue // Skip this row as it's likely a header
}
}
if len(row) > 0 && row[0] != "" {
jobID := strings.TrimSpace(row[0])
if jobID != "" {
jobIDs = append(jobIDs, jobID)
}
}
}
totalJobs := len(jobIDs)
log.Printf("Extracted %d job IDs from CSV", totalJobs)
if totalJobs == 0 {
http.Error(w, "No valid job IDs found in the CSV file", http.StatusBadRequest)
return
}
// Create a hidden input with the job IDs
jobsValue := strings.Join(jobIDs, ",")
// Generate HTML for the main response (hidden input for job-ids-removal-container)
var responseHTML bytes.Buffer
responseHTML.WriteString(fmt.Sprintf(`<input type="hidden" name="jobIDs" value="%s">`, jobsValue))
responseHTML.WriteString(fmt.Sprintf(`<p>Found <strong>%d</strong> job(s) in the CSV file</p>`, totalJobs))
// Generate out-of-band swap for the preview section - simplified version
responseHTML.WriteString(fmt.Sprintf(`
<div id="csv-preview-removal" class="fade-me-out csv-preview-active" hx-swap-oob="true">
<h4>✓ Jobs Detected</h4>
<p>Remove from <strong>%d</strong> job(s)</p>
</div>
`, totalJobs))
// 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(middleware.SessionKey).(*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("<div class=\"job-list-container\">")
resultHTML.WriteString("<h4>Jobs from CSV</h4>")
resultHTML.WriteString("<p>Click a job to view and manage its documents.</p>")
resultHTML.WriteString("<div class=\"job-list\">")
for _, jobID := range jobs {
resultHTML.WriteString(fmt.Sprintf(`
<div class="job-item">
<a href="#job-%s" hx-get="/documents/remove/job/%s" hx-target="#job-%s-attachments"
hx-swap="outerHTML" hx-trigger="click">
Job ID: %s
</a>
<div id="job-%s-attachments"></div>
</div>
`, jobID, jobID, jobID, jobID, jobID))
}
resultHTML.WriteString("</div>") // End of job-list
resultHTML.WriteString("</div>") // 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(middleware.SessionKey).(*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(middleware.SessionKey).(*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
}
}
// For all attachment types, we'll use the attachment endpoint for deletion
// The API endpoint is /attachment/{id} as defined in DeleteAttachment method
log.Printf("Deleting attachment %s (ID: %s) using attachment endpoint", fileResult.Name, id)
deleteErr := session.DeleteAttachment(id)
mu.Lock()
defer mu.Unlock()
if deleteErr != nil {
fileResult.Success = false
fileResult.Error = deleteErr.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(middleware.SessionKey).(*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)
}
}
// BulkRemoveDocumentsHandler handles bulk removal of documents from multiple jobs
func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
if !ok {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse the form
if err := r.ParseForm(); err != nil {
http.Error(w, fmt.Sprintf("Error parsing form: %v", err), http.StatusBadRequest)
return
}
// Get job IDs from the form
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 provided", http.StatusBadRequest)
return
}
// Get document types to remove (optional)
var docTypes []string
if types := r.Form["documentTypes"]; len(types) > 0 {
docTypes = types
log.Printf("Filtering by document types: %v", docTypes)
}
// Get filename patterns to match (optional)
var filenamePatterns []string
if patterns := r.FormValue("filenamePatterns"); patterns != "" {
filenamePatterns = strings.Split(patterns, ",")
for i, p := range filenamePatterns {
filenamePatterns[i] = strings.TrimSpace(p)
}
log.Printf("Filtering by filename patterns: %v", filenamePatterns)
}
// Get age filter (optional)
var ageFilterDays int
if ageStr := r.FormValue("age-filter"); ageStr != "" {
if days, err := strconv.Atoi(ageStr); err == nil && days > 0 {
ageFilterDays = days
log.Printf("Using age filter: older than %d days", ageFilterDays)
}
}
// Calculate cutoff date if using age filter
var cutoffDate time.Time
if ageFilterDays > 0 {
cutoffDate = time.Now().AddDate(0, 0, -ageFilterDays)
log.Printf("Cutoff date for age filter: %s", cutoffDate.Format("2006-01-02"))
}
// Structure to track results
type BulkRemovalResult struct {
JobsProcessed int
JobsWithErrors int
TotalFiles int
SuccessCount int
ErrorCount int
Duration time.Duration
JobResults []struct {
JobID string
FilesFound int
FilesRemoved int
Success bool
ErrorMsg string
Files []struct {
Name string
Success bool
Error string
}
}
}
results := BulkRemovalResult{}
var wg sync.WaitGroup
var mu sync.Mutex
semaphore := make(chan struct{}, 5) // Increased back to 5 since we've optimized the overhead
// Process each job
for _, jobID := range jobs {
wg.Add(1)
go func(jobID string) {
defer wg.Done()
log.Printf("Processing job ID: %s for document removal", jobID)
jobResult := struct {
JobID string
FilesFound int
FilesRemoved int
Success bool
ErrorMsg string
Files []struct {
Name string
Success bool
Error string
}
}{
JobID: jobID,
Success: true,
Files: []struct {
Name string
Success bool
Error string
}{},
}
// Check job permissions first
hasAccess, reason, err := session.CheckJobPermissions(jobID)
if err != nil {
log.Printf("Error checking permissions for job %s: %v", jobID, err)
} else if !hasAccess {
log.Printf("No access to job %s: %s", jobID, reason)
mu.Lock()
jobResult.Success = false
jobResult.ErrorMsg = fmt.Sprintf("Cannot access job: %s", reason)
results.JobResults = append(results.JobResults, jobResult)
results.JobsWithErrors++
mu.Unlock()
return
}
// Get attachments using the most reliable method first
var attachments []map[string]interface{}
// Try the specialized GetJobAttachments method first
attachments, err = session.GetJobAttachments(jobID)
if err != nil {
log.Printf("Error getting attachments for job %s: %v", jobID, err)
attachments = []map[string]interface{}{} // Ensure it's initialized
}
// If no attachments found, try the paperwork endpoint as fallback
if len(attachments) == 0 {
log.Printf("No attachments found via GetJobAttachments for job %s, trying paperwork endpoint", jobID)
paperworkItems, err := session.GetJobPaperwork(jobID)
if err != nil {
log.Printf("Error getting paperwork for job %s: %v", jobID, err)
} else {
attachments = paperworkItems
}
}
log.Printf("Found %d attachments for job %s", len(attachments), jobID)
// Filter attachments based on criteria
var attachmentsToDelete []map[string]interface{}
for _, attachment := range attachments {
// Get the attachment ID
attachmentIDRaw, idOk := attachment["id"].(float64)
if !idOk {
continue
}
attachmentIDStr := fmt.Sprintf("%.0f", attachmentIDRaw)
// Get filename from any available field
var filename string
if desc, ok := attachment["description"].(string); ok && desc != "" {
filename = desc
} else if name, ok := attachment["name"].(string); ok && name != "" {
filename = name
} else if fname, ok := attachment["fileName"].(string); ok && fname != "" {
filename = fname
} else {
filename = fmt.Sprintf("Attachment ID: %s", attachmentIDStr)
}
// Check document type filter
if len(docTypes) > 0 {
typeMatches := false
if purposeId, ok := attachment["purposeId"].(float64); ok {
for _, docType := range docTypes {
docTypeClean := strings.TrimLeft(docType, "0")
if docTypeInt, err := strconv.Atoi(docTypeClean); err == nil {
if float64(docTypeInt) == purposeId {
typeMatches = true
break
}
}
}
}
if !typeMatches {
continue
}
}
// Check filename pattern
if len(filenamePatterns) > 0 && !matchesAnyPattern(filename, filenamePatterns) {
continue
}
// Check age filter if applicable
if ageFilterDays > 0 {
var createdAt time.Time
var hasDate bool
// Try to get the creation date
if created, ok := attachment["createdOn"].(string); ok {
if parsedTime, err := time.Parse(time.RFC3339, created); err == nil {
createdAt = parsedTime
hasDate = true
}
} else if created, ok := attachment["created"].(string); ok {
if parsedTime, err := time.Parse(time.RFC3339, created); err == nil {
createdAt = parsedTime
hasDate = true
}
} else if createdVal, ok := attachment["created"].(float64); ok {
createdAt = time.Unix(int64(createdVal), 0)
hasDate = true
}
if hasDate && createdAt.After(cutoffDate) {
continue // Skip if not old enough
}
}
// This attachment matches all criteria
attachmentsToDelete = append(attachmentsToDelete, attachment)
}
jobResult.FilesFound = len(attachmentsToDelete)
// Process deletions with rate limiting
var deletionWg sync.WaitGroup
for _, attachment := range attachmentsToDelete {
deletionWg.Add(1)
attachmentCopy := attachment
go func(att map[string]interface{}) {
defer deletionWg.Done()
semaphore <- struct{}{}
defer func() { <-semaphore }()
attachmentIDFloat := att["id"].(float64)
attachmentIDStr := fmt.Sprintf("%.0f", attachmentIDFloat)
var filename string
if desc, ok := att["description"].(string); ok && desc != "" {
filename = desc
} else if name, ok := att["name"].(string); ok && name != "" {
filename = name
} else if fname, ok := att["fileName"].(string); ok && fname != "" {
filename = fname
} else {
filename = fmt.Sprintf("Attachment ID: %s", attachmentIDStr)
}
fileResult := struct {
Name string
Success bool
Error string
}{
Name: filename,
}
deleteErr := session.DeleteAttachment(attachmentIDStr)
mu.Lock()
defer mu.Unlock()
if deleteErr != nil {
fileResult.Success = false
fileResult.Error = deleteErr.Error()
jobResult.Success = false
} else {
fileResult.Success = true
jobResult.FilesRemoved++
}
jobResult.Files = append(jobResult.Files, fileResult)
// Reduced delay
time.Sleep(200 * time.Millisecond)
}(attachmentCopy)
}
deletionWg.Wait()
mu.Lock()
results.JobsProcessed++
if jobResult.Success {
results.SuccessCount += jobResult.FilesRemoved
} else {
results.ErrorCount += (jobResult.FilesFound - jobResult.FilesRemoved)
}
results.TotalFiles += jobResult.FilesFound
results.JobResults = append(results.JobResults, jobResult)
mu.Unlock()
}(jobID)
}
// Wait for all jobs to complete
wg.Wait()
// Calculate total duration
totalDuration := time.Since(startTime)
results.Duration = totalDuration
log.Printf("Document removal completed in %v", totalDuration)
// Convert results to RemovalResult format for session storage
var removalResults []RemovalResult
for _, jobResult := range results.JobResults {
removalResult := RemovalResult{
JobID: jobResult.JobID,
FilesFound: jobResult.FilesFound,
FilesRemoved: jobResult.FilesRemoved,
Success: jobResult.Success,
ErrorMsg: jobResult.ErrorMsg,
Files: jobResult.Files,
}
removalResults = append(removalResults, removalResult)
}
// Store results in session for pagination
sessionID := fmt.Sprintf("removal_%d", time.Now().UnixNano())
removalSession := RemovalSession{
Results: removalResults,
TotalJobs: results.JobsProcessed,
SuccessCount: results.SuccessCount,
ErrorCount: results.ErrorCount,
TotalFiles: results.TotalFiles,
TotalTime: totalDuration,
CreatedAt: time.Now(),
}
// Store in global map (in production, use Redis or database)
removalSessions[sessionID] = removalSession
// Get configurable page size from form, with fallback to default
limitStr := r.FormValue("limit")
limit := utils.DefaultPageSize
if limitStr != "" {
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
if parsedLimit > utils.MaxPageSize {
limit = utils.MaxPageSize
} else {
limit = parsedLimit
}
}
}
// Return first page of results with configurable page size
renderRemovalResultsPage(w, sessionID, utils.DefaultPage, limit)
}
// RemovalResultsHandler handles pagination for removal results
func RemovalResultsHandler(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
http.Error(w, "Session ID required", http.StatusBadRequest)
return
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = utils.DefaultPage
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit < 1 {
limit = utils.DefaultPageSize
}
renderRemovalResultsPage(w, sessionID, page, limit)
}
// renderRemovalResultsPage renders a paginated page of removal results
func renderRemovalResultsPage(w http.ResponseWriter, sessionID string, page, limit int) {
removalSession, exists := removalSessions[sessionID]
if !exists {
http.Error(w, "Removal session not found", http.StatusNotFound)
return
}
totalResults := len(removalSession.Results)
pagination := utils.CalculatePagination(totalResults, page, limit)
// Get results for this page
startIndex := (pagination.CurrentPage - 1) * pagination.Limit
endIndex := startIndex + pagination.Limit
if endIndex > totalResults {
endIndex = totalResults
}
pageResults := utils.GetPageResults(removalSession.Results, startIndex, endIndex)
// Add pagination info to each job result for the template
var resultsWithPagination []map[string]interface{}
for _, jobResult := range pageResults {
resultMap := map[string]interface{}{
"JobID": jobResult.JobID,
"FilesFound": jobResult.FilesFound,
"FilesRemoved": jobResult.FilesRemoved,
"Success": jobResult.Success,
"ErrorMsg": jobResult.ErrorMsg,
"Files": jobResult.Files,
"FilePage": 1, // Default to first file
"TotalFiles": len(jobResult.Files),
"SessionID": sessionID,
}
resultsWithPagination = append(resultsWithPagination, resultMap)
}
data := map[string]interface{}{
"Results": resultsWithPagination,
"TotalJobs": removalSession.TotalJobs,
"SuccessCount": removalSession.SuccessCount,
"ErrorCount": removalSession.ErrorCount,
"TotalFiles": removalSession.TotalFiles,
"TotalTime": removalSession.TotalTime,
"TotalResults": pagination.TotalResults,
"TotalPages": pagination.TotalPages,
"CurrentPage": pagination.CurrentPage,
"Limit": pagination.Limit,
"StartIndex": pagination.StartIndex,
"EndIndex": pagination.EndIndex,
"StartPage": pagination.StartPage,
"EndPage": pagination.EndPage,
"SessionID": sessionID,
}
tmpl := root.WebTemplates
if err := tmpl.ExecuteTemplate(w, "removal_results", data); err != nil {
log.Printf("Template execution error: %v", err)
// Don't call http.Error here as the response may have already started
// Just log the error and return
return
}
}
// Helper function to check if a string matches any pattern in a slice
func matchesAnyPattern(s string, patterns []string) bool {
// Convert the string to lowercase for case-insensitive comparison
sLower := strings.ToLower(s)
for _, pattern := range patterns {
// Check if the pattern is a wildcard pattern (contains *)
if strings.Contains(pattern, "*") {
// Convert wildcard pattern to regex
regexPattern := strings.ReplaceAll(pattern, "*", ".*")
match, _ := regexp.MatchString("(?i)^"+regexPattern+"$", s)
if match {
return true
}
} else {
// For non-wildcard patterns, check for exact match (case-insensitive)
if sLower == strings.ToLower(pattern) {
return true
}
}
}
return false
}
// New handler: Serve a single job card with only one file (per-job file pagination)
func RemovalJobFileHandler(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
jobID := r.URL.Query().Get("job_id")
filePageStr := r.URL.Query().Get("file_page")
filePage := 1
if filePageStr != "" {
if parsed, err := strconv.Atoi(filePageStr); err == nil && parsed > 0 {
filePage = parsed
}
}
removalSession, exists := removalSessions[sessionID]
if !exists {
http.Error(w, "Removal session not found", http.StatusNotFound)
return
}
// Find the job result
var jobResult *RemovalResult
for i := range removalSession.Results {
if removalSession.Results[i].JobID == jobID {
jobResult = &removalSession.Results[i]
break
}
}
if jobResult == nil {
http.Error(w, "Job not found in session", http.StatusNotFound)
return
}
totalFiles := len(jobResult.Files)
if totalFiles == 0 {
// No files to show
data := map[string]interface{}{
"JobID": jobResult.JobID,
"FilesFound": jobResult.FilesFound,
"FilesRemoved": jobResult.FilesRemoved,
"Success": jobResult.Success,
"ErrorMsg": jobResult.ErrorMsg,
"Files": nil,
"FilePage": 1,
"TotalFiles": 0,
"SessionID": sessionID,
}
tmpl := root.WebTemplates
if err := tmpl.ExecuteTemplate(w, "removal_result_card", data); err != nil {
log.Printf("Template execution error: %v", err)
return
}
return
}
// Ensure filePage is within bounds
if filePage > totalFiles {
filePage = totalFiles
}
if filePage < 1 {
filePage = 1
}
// Create a copy of the job result with only the requested file
jobResultCopy := RemovalResult{
JobID: jobResult.JobID,
FilesFound: jobResult.FilesFound,
FilesRemoved: jobResult.FilesRemoved,
Success: jobResult.Success,
ErrorMsg: jobResult.ErrorMsg,
Files: []struct {
Name string
Success bool
Error string
}{jobResult.Files[filePage-1]},
}
// Add pagination info for the template
data := map[string]interface{}{
"JobID": jobResultCopy.JobID,
"FilesFound": jobResultCopy.FilesFound,
"FilesRemoved": jobResultCopy.FilesRemoved,
"Success": jobResultCopy.Success,
"ErrorMsg": jobResultCopy.ErrorMsg,
"Files": jobResultCopy.Files,
"FilePage": filePage,
"TotalFiles": totalFiles,
"SessionID": sessionID,
}
tmpl := root.WebTemplates
if err := tmpl.ExecuteTemplate(w, "removal_result_card", data); err != nil {
log.Printf("Template execution error: %v", err)
return
}
}