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.
851 lines
24 KiB
851 lines
24 KiB
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
root "marmic/servicetrade-toolbox"
|
|
"marmic/servicetrade-toolbox/internal/api"
|
|
"marmic/servicetrade-toolbox/internal/middleware"
|
|
|
|
"github.com/gorilla/mux"
|
|
)
|
|
|
|
// 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) {
|
|
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
|
|
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()
|
|
|
|
// Generate HTML for results
|
|
var resultHTML bytes.Buffer
|
|
|
|
// Add summary section
|
|
resultHTML.WriteString("<div class=\"upload-summary\">")
|
|
resultHTML.WriteString("<h3>Document Removal Results</h3>")
|
|
resultHTML.WriteString("<div class=\"upload-stats\">")
|
|
|
|
// Total jobs stat
|
|
resultHTML.WriteString("<div class=\"stat-box\">")
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", results.JobsProcessed))
|
|
resultHTML.WriteString("<div class=\"stat-label\">Total Jobs</div>")
|
|
resultHTML.WriteString("</div>")
|
|
|
|
// Success stat
|
|
resultHTML.WriteString("<div class=\"stat-box success-stat\">")
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", results.SuccessCount))
|
|
resultHTML.WriteString("<div class=\"stat-label\">Successful Removals</div>")
|
|
resultHTML.WriteString("</div>")
|
|
|
|
// Failure stat
|
|
resultHTML.WriteString("<div class=\"stat-box error-stat\">")
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", results.ErrorCount))
|
|
resultHTML.WriteString("<div class=\"stat-label\">Failed Removals</div>")
|
|
resultHTML.WriteString("</div>")
|
|
|
|
// File count stat
|
|
resultHTML.WriteString("<div class=\"stat-box\">")
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", results.TotalFiles))
|
|
resultHTML.WriteString("<div class=\"stat-label\">Files Processed</div>")
|
|
resultHTML.WriteString("</div>")
|
|
|
|
resultHTML.WriteString("</div>") // End of upload-stats
|
|
|
|
// Add completion message
|
|
if results.ErrorCount == 0 {
|
|
resultHTML.WriteString("<p>All documents were successfully removed from ServiceTrade!</p>")
|
|
} else {
|
|
resultHTML.WriteString("<p>Some documents failed to be removed. See details below.</p>")
|
|
}
|
|
|
|
resultHTML.WriteString("</div>") // End of upload-summary
|
|
|
|
// Add detailed job results
|
|
resultHTML.WriteString("<div class=\"job-results\">")
|
|
|
|
// Sort job IDs for consistent display
|
|
sort.Slice(results.JobResults, func(i, j int) bool {
|
|
return results.JobResults[i].JobID < results.JobResults[j].JobID
|
|
})
|
|
|
|
for _, jobResult := range results.JobResults {
|
|
// Job result row
|
|
jobClass := "success"
|
|
if !jobResult.Success {
|
|
jobClass = "error"
|
|
}
|
|
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"job-result %s\">", jobClass))
|
|
resultHTML.WriteString(fmt.Sprintf("<span class=\"job-id\">Job ID: %s</span>", jobResult.JobID))
|
|
|
|
if jobResult.ErrorMsg != "" {
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"error-message\">%s</div>", jobResult.ErrorMsg))
|
|
} else {
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"job-summary\">Found %d document(s), removed %d</div>",
|
|
jobResult.FilesFound, jobResult.FilesRemoved))
|
|
}
|
|
|
|
// File results
|
|
if len(jobResult.Files) > 0 {
|
|
resultHTML.WriteString("<div class=\"file-results\">")
|
|
|
|
for _, file := range jobResult.Files {
|
|
fileClass := "success"
|
|
icon := "✓"
|
|
message := "Successfully removed"
|
|
|
|
if !file.Success {
|
|
fileClass = "error"
|
|
icon = "✗"
|
|
message = file.Error
|
|
}
|
|
|
|
resultHTML.WriteString(fmt.Sprintf("<div class=\"file-result %s\">", fileClass))
|
|
resultHTML.WriteString(fmt.Sprintf("<span class=\"status-icon\">%s</span>", icon))
|
|
resultHTML.WriteString(fmt.Sprintf("<span class=\"file-name\">%s:</span>", file.Name))
|
|
resultHTML.WriteString(fmt.Sprintf("<span class=\"file-message\">%s</span>", message))
|
|
resultHTML.WriteString("</div>")
|
|
}
|
|
|
|
resultHTML.WriteString("</div>") // End of file-results
|
|
} else {
|
|
resultHTML.WriteString("<p>No files processed for this job.</p>")
|
|
}
|
|
|
|
resultHTML.WriteString("</div>") // End of job-result
|
|
}
|
|
|
|
resultHTML.WriteString("</div>") // End of job-results
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write(resultHTML.Bytes())
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|