Browse Source

feat: bulk document removal 'working' ¯\_(ツ)_/

document-upload-removal-layout-update
nic 12 months ago
parent
commit
e1c4e8962b
  1. 3
      apps/web/main.go
  2. 74
      internal/api/attachments.go
  3. 245
      internal/api/jobs.go
  4. 210
      internal/api/paperwork.go
  5. 768
      internal/handlers/web/document_remove.go
  6. 69
      static/css/upload.css
  7. 114
      templates/partials/document_remove.html

3
apps/web/main.go

@ -70,7 +70,8 @@ func main() {
protected.HandleFunc("/documents/remove/process-csv", web.ProcessRemoveCSVHandler).Methods("POST")
protected.HandleFunc("/documents/remove/job-selection", web.JobSelectionHandler).Methods("POST")
protected.HandleFunc("/documents/remove/job/{jobID}", web.GetJobAttachmentsHandler).Methods("GET")
protected.HandleFunc("/documents/remove/job/{jobID}", web.RemoveJobAttachmentsHandler).Methods("POST")
protected.HandleFunc("/documents/remove/attachments/{jobID}", web.RemoveJobAttachmentsHandler).Methods("POST")
protected.HandleFunc("/documents/remove/bulk", web.BulkRemoveDocumentsHandler).Methods("POST")
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))

74
internal/api/attachments.go

@ -240,6 +240,7 @@ func (s *Session) DeleteAttachment(attachmentID string) error {
// GetJobAttachments retrieves all attachments for a given job ID
func (s *Session) GetJobAttachments(jobID string) ([]map[string]interface{}, error) {
log.Printf("GetJobAttachments: Fetching attachments for job %s", jobID)
url := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID)
req, err := http.NewRequest("GET", url, nil)
@ -249,6 +250,7 @@ func (s *Session) GetJobAttachments(jobID string) ([]map[string]interface{}, err
// Add authorization header
req.Header.Set("Cookie", s.Cookie)
log.Printf("GetJobAttachments: Authorization cookie length: %d", len(s.Cookie))
// Send the request
resp, err := s.Client.Do(req)
@ -257,19 +259,81 @@ func (s *Session) GetJobAttachments(jobID string) ([]map[string]interface{}, err
}
defer resp.Body.Close()
// Check for error response
// Read the full response body for complete error reporting
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
// Check for various error responses
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
// Check for authentication-related errors
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
log.Printf("GetJobAttachments: Authentication error (status %d) for job %s", resp.StatusCode, jobID)
return nil, fmt.Errorf("authentication error: %s - %s", resp.Status, string(body))
}
// Check for job not found
if resp.StatusCode == http.StatusNotFound {
log.Printf("GetJobAttachments: Job %s not found (status 404)", jobID)
return nil, fmt.Errorf("job not found: %s", jobID)
}
// Generic error
log.Printf("GetJobAttachments: API error (status %d) for job %s: %s", resp.StatusCode, jobID, string(body))
return nil, fmt.Errorf("API returned error: %d %s - %s", resp.StatusCode, resp.Status, string(body))
}
// Parse the response
// Parse the response - try multiple formats
var result struct {
Attachments []map[string]interface{} `json:"objects"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
// Try to parse as standard format first
if err := json.Unmarshal(body, &result); err != nil {
// If standard parse fails, try parsing as raw map
log.Printf("GetJobAttachments: Standard JSON parsing failed, trying alternative formats for job %s: %v", jobID, err)
var mapResult map[string]interface{}
if err := json.Unmarshal(body, &mapResult); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}
// Try to extract from various places in the map
attachments := make([]map[string]interface{}, 0)
// Check for objects array
if objects, ok := mapResult["objects"].([]interface{}); ok {
log.Printf("GetJobAttachments: Found %d attachments in 'objects' field for job %s", len(objects), jobID)
for _, obj := range objects {
if attachment, ok := obj.(map[string]interface{}); ok {
attachments = append(attachments, attachment)
}
}
return attachments, nil
}
// Check for data.attachments
if data, ok := mapResult["data"].(map[string]interface{}); ok {
if attList, ok := data["attachments"].([]interface{}); ok {
log.Printf("GetJobAttachments: Found %d attachments in 'data.attachments' field for job %s", len(attList), jobID)
for _, att := range attList {
if attachment, ok := att.(map[string]interface{}); ok {
attachments = append(attachments, attachment)
}
}
return attachments, nil
}
}
// If nothing found, return empty with a warning
log.Printf("GetJobAttachments: No attachments found in response for job %s: %s", jobID, string(body))
return attachments, nil
}
if len(result.Attachments) == 0 {
log.Printf("GetJobAttachments: No attachments found for job %s in standard format", jobID)
} else {
log.Printf("GetJobAttachments: Found %d attachments for job %s in standard format", len(result.Attachments), jobID)
}
return result.Attachments, nil

245
internal/api/jobs.go

@ -120,24 +120,257 @@ func (s *Session) GetJobDetails(jobID string) (map[string]interface{}, error) {
// GetAttachmentsForJob retrieves attachments for a specific job
func (s *Session) GetAttachmentsForJob(jobID string) (map[string]interface{}, error) {
resp, err := s.DoRequest("GET", fmt.Sprintf("/job/%s/paperwork", jobID), nil)
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 {
return nil, err
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()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
return nil, fmt.Errorf("failed to get attachments: %s, response: %s", resp.Status, string(body))
// 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 {
return nil, fmt.Errorf("error unmarshalling response: %v", err)
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)

210
internal/api/paperwork.go

@ -0,0 +1,210 @@
package api
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
)
// GetJobPaperwork retrieves a list of paperwork for a specific job
// This is a specialized endpoint to directly access paperwork
func (s *Session) GetJobPaperwork(jobID string) ([]map[string]interface{}, error) {
log.Printf("GetJobPaperwork: Fetching paperwork for job %s", jobID)
url := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}
// Add authorization header
req.Header.Set("Cookie", s.Cookie)
// Set headers for better compatibility
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
// Send the request
log.Printf("GetJobPaperwork: Sending request to %s", url)
resp, err := s.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
// Read the full response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
// Check for errors
if resp.StatusCode != http.StatusOK {
// Log detailed error information
log.Printf("GetJobPaperwork: API returned error %d for job %s", resp.StatusCode, jobID)
log.Printf("GetJobPaperwork: Error response: %s", string(body))
return nil, fmt.Errorf("API returned error: %s - %s", resp.Status, string(body))
}
// Log response preview
responsePreview := string(body)
if len(responsePreview) > 200 {
responsePreview = responsePreview[:200] + "... [truncated]"
}
log.Printf("GetJobPaperwork: Response preview for job %s: %s", jobID, responsePreview)
// Parse the response into a generic map
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}
// Debug the response structure
keys := make([]string, 0)
for k := range result {
keys = append(keys, k)
}
log.Printf("GetJobPaperwork: Response root keys for job %s: %v", jobID, keys)
// Check for data.attachments format (which is what the logs show)
paperworkItems := make([]map[string]interface{}, 0)
// Check if data key exists
if data, ok := result["data"].(map[string]interface{}); ok {
dataKeys := make([]string, 0)
for k := range data {
dataKeys = append(dataKeys, k)
}
log.Printf("GetJobPaperwork: Data keys for job %s: %v", jobID, dataKeys)
// Look for the attachments array in data
if attachments, ok := data["attachments"].([]interface{}); ok && len(attachments) > 0 {
log.Printf("GetJobPaperwork: Found %d attachments in data.attachments for job %s",
len(attachments), jobID)
// Convert each attachment to a map and add to our collection
for _, attachment := range attachments {
if attachmentMap, ok := attachment.(map[string]interface{}); ok {
// Log the structure of the first attachment to better understand format
if len(paperworkItems) == 0 {
log.Printf("GetJobPaperwork: First attachment structure: %+v", attachmentMap)
}
paperworkItems = append(paperworkItems, attachmentMap)
}
}
return paperworkItems, nil
}
// Check for objects in data
if objects, ok := data["objects"].([]interface{}); ok && len(objects) > 0 {
log.Printf("GetJobPaperwork: Found %d items in data.objects for job %s", len(objects), jobID)
for _, obj := range objects {
if objMap, ok := obj.(map[string]interface{}); ok {
paperworkItems = append(paperworkItems, objMap)
}
}
}
// Check for paperwork array in data
if paperwork, ok := data["paperwork"].([]interface{}); ok && len(paperwork) > 0 {
log.Printf("GetJobPaperwork: Found %d items in data.paperwork for job %s", len(paperwork), jobID)
for _, doc := range paperwork {
if docMap, ok := doc.(map[string]interface{}); ok {
paperworkItems = append(paperworkItems, docMap)
}
}
}
}
// Also try the old formats
// Check for objects at root level
if objects, ok := result["objects"].([]interface{}); ok && len(objects) > 0 {
log.Printf("GetJobPaperwork: Found %d items at root.objects for job %s", len(objects), jobID)
for _, obj := range objects {
if objMap, ok := obj.(map[string]interface{}); ok {
paperworkItems = append(paperworkItems, objMap)
}
}
}
if len(paperworkItems) > 0 {
log.Printf("GetJobPaperwork: Extracted %d total paperwork items from response for job %s",
len(paperworkItems), jobID)
return paperworkItems, nil
}
log.Printf("GetJobPaperwork: No paperwork items found in any format for job %s", jobID)
return nil, nil
}
// CheckJobPermissions verifies if the current user has permissions to access the specified job
func (s *Session) CheckJobPermissions(jobID string) (bool, string, error) {
log.Printf("CheckJobPermissions: Checking permissions for job %s", jobID)
// First try to get the job details - if this succeeds, the user has basic read access
url := fmt.Sprintf("%s/job/%s", BaseURL, jobID)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return false, "Error creating request", err
}
req.Header.Set("Cookie", s.Cookie)
resp, err := s.Client.Do(req)
if err != nil {
return false, "Error sending request", err
}
defer resp.Body.Close()
// Read response for detailed error information
body, _ := io.ReadAll(resp.Body)
// If we can't get the job, check the specific error
if resp.StatusCode != http.StatusOK {
errorMessage := string(body)
// Check for permission-related errors
if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized {
log.Printf("CheckJobPermissions: Permission denied for job %s: %s", jobID, errorMessage)
return false, fmt.Sprintf("Permission denied: %s", resp.Status), nil
}
// Check for job not found
if resp.StatusCode == http.StatusNotFound {
log.Printf("CheckJobPermissions: Job %s not found: %s", jobID, errorMessage)
return false, "Job not found", nil
}
// Other errors
log.Printf("CheckJobPermissions: Error accessing job %s: %s - %s", jobID, resp.Status, errorMessage)
return false, fmt.Sprintf("Error: %s", resp.Status), nil
}
// If we get here, the user can at least view the job
// Now try to check if they can access paperwork
paperworkURL := fmt.Sprintf("%s/job/%s/paperwork", BaseURL, jobID)
paperworkReq, err := http.NewRequest("GET", paperworkURL, nil)
if err != nil {
return true, "Can access job, but error checking paperwork permissions", err
}
paperworkReq.Header.Set("Cookie", s.Cookie)
paperworkResp, err := s.Client.Do(paperworkReq)
if err != nil {
return true, "Can access job, but error checking paperwork API", err
}
defer paperworkResp.Body.Close()
if paperworkResp.StatusCode != http.StatusOK {
log.Printf("CheckJobPermissions: User can access job %s but paperwork API returned %d",
jobID, paperworkResp.StatusCode)
return true, "Can access job, but paperwork access denied", nil
}
log.Printf("CheckJobPermissions: User has full access to job %s and its paperwork", jobID)
return true, "Full access", nil
}

768
internal/handlers/web/document_remove.go

@ -3,9 +3,13 @@ package web
import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@ -29,6 +33,13 @@ func DocumentRemoveHandler(w http.ResponseWriter, r *http.Request) {
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": "4", "label": "Generic Attachment"},
{"value": "7", "label": "Blank Paperwork"},
{"value": "14", "label": "Job Invoice"},
},
}
if r.Header.Get("HX-Request") == "true" {
@ -406,3 +417,760 @@ func renderErrorTemplate(w http.ResponseWriter, templateName, errorMsg string, j
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// Helper function to get map keys as a string for logging
func mapKeysStr(m map[string]interface{}) string {
if m == nil {
return "nil"
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
return strings.Join(keys, ", ")
}
// 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("session").(*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("ageFilter"); 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)
}
// 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) // Limit concurrent API calls
// 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
log.Printf("**** JOB %s: Checking permissions...", jobID)
hasAccess, reason, err := session.CheckJobPermissions(jobID)
if err != nil {
log.Printf("**** JOB %s: Error checking permissions: %v", jobID, err)
} else {
log.Printf("**** JOB %s: Permission check result: access=%v, reason=%s",
jobID, hasAccess, reason)
}
// Create the attachments array that will hold all found documents
var attachments []map[string]interface{}
// Only proceed if we have access or couldn't determine access
if err != nil || hasAccess {
// Directly try to get paperwork using the specialized API
log.Printf("**** JOB %s: Using specialized paperwork API", jobID)
paperworkItems, err := session.GetJobPaperwork(jobID)
if err != nil {
log.Printf("**** JOB %s: Error getting paperwork: %v", jobID, err)
} else if len(paperworkItems) > 0 {
log.Printf("**** JOB %s: GetJobPaperwork returned %d paperwork items",
jobID, len(paperworkItems))
// Add all paperwork items to attachments
for _, item := range paperworkItems {
log.Printf("**** JOB %s: Adding item from GetJobPaperwork: %v",
jobID, mapKeysStr(item))
attachments = append(attachments, item)
}
} else {
log.Printf("**** JOB %s: No paperwork found using specialized API", jobID)
}
} else {
log.Printf("**** JOB %s: WARNING: No access to this job - reason: %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
}
// Try alternate method (always try both methods to see what data is available)
log.Printf("**** JOB %s: Retrieving attachments using GetAttachmentsForJob", jobID)
apiResponse, err := session.GetAttachmentsForJob(jobID)
if err != nil {
log.Printf("**** JOB %s: Error in GetAttachmentsForJob: %v", jobID, err)
} else {
// Log the structure of the response to understand format
rootKeys := make([]string, 0)
for k := range apiResponse {
rootKeys = append(rootKeys, k)
}
log.Printf("**** JOB %s: GetAttachmentsForJob returned response with root keys: %s",
jobID, strings.Join(rootKeys, ", "))
// Check if we have a data object
if data, ok := apiResponse["data"].(map[string]interface{}); ok {
dataKeys := make([]string, 0)
for k := range data {
dataKeys = append(dataKeys, k)
}
log.Printf("**** JOB %s: data object keys: %s",
jobID, strings.Join(dataKeys, ", "))
}
// Check if we have paperwork_data
if paperworkData, ok := apiResponse["paperwork_data"].(map[string]interface{}); ok {
dataKeys := make([]string, 0)
for k := range paperworkData {
dataKeys = append(dataKeys, k)
}
log.Printf("**** JOB %s: paperwork_data keys: %s",
jobID, strings.Join(dataKeys, ", "))
// Check if paperwork_data.data.attachments exists
if paperworkDataInner, ok := paperworkData["data"].(map[string]interface{}); ok {
if attachmentsArray, ok := paperworkDataInner["attachments"].([]interface{}); ok && len(attachmentsArray) > 0 {
log.Printf("**** JOB %s: Found %d attachments in paperwork_data.data.attachments",
jobID, len(attachmentsArray))
// Process each attachment and add to our collection
for _, attachment := range attachmentsArray {
if attachmentMap, ok := attachment.(map[string]interface{}); ok {
attachments = append(attachments, attachmentMap)
}
}
}
}
}
log.Printf("**** JOB %s: Total attachments gathered: %d", jobID, len(attachments))
if len(attachments) == 0 {
log.Printf("**** JOB %s: No attachments found yet, trying direct paperwork endpoint")
// Make a direct call to the paperwork endpoint
paperworkURL := fmt.Sprintf("%s/job/%s/paperwork", api.BaseURL, jobID)
paperworkReq, err := http.NewRequest("GET", paperworkURL, nil)
if err == nil {
paperworkReq.Header.Set("Cookie", session.Cookie)
paperworkReq.Header.Set("Accept", "application/json")
log.Printf("**** JOB %s: Sending direct request to %s", jobID, paperworkURL)
paperworkResp, err := session.Client.Do(paperworkReq)
if err == nil && paperworkResp.StatusCode == http.StatusOK {
defer paperworkResp.Body.Close()
paperworkBody, _ := io.ReadAll(paperworkResp.Body)
// Log preview of the response
responsePreview := string(paperworkBody)
if len(responsePreview) > 200 {
responsePreview = responsePreview[:200] + "... [truncated]"
}
log.Printf("**** JOB %s: Direct paperwork response preview: %s", jobID, responsePreview)
// Parse the response
var paperworkResult map[string]interface{}
if err := json.Unmarshal(paperworkBody, &paperworkResult); err == nil {
// Log the structure of the response
rootKeys := make([]string, 0)
for k := range paperworkResult {
rootKeys = append(rootKeys, k)
}
log.Printf("**** JOB %s: Direct paperwork response keys: %s",
jobID, strings.Join(rootKeys, ", "))
// Check for data.attachments
if data, ok := paperworkResult["data"].(map[string]interface{}); ok {
dataKeys := make([]string, 0)
for k := range data {
dataKeys = append(dataKeys, k)
}
log.Printf("**** JOB %s: Direct paperwork data keys: %s",
jobID, strings.Join(dataKeys, ", "))
// Extract attachments from data.attachments
if attachmentsArray, ok := data["attachments"].([]interface{}); ok && len(attachmentsArray) > 0 {
log.Printf("**** JOB %s: Found %d attachments in direct paperwork response",
jobID, len(attachmentsArray))
// Loop through the attachments and add to our collection
for i, attachment := range attachmentsArray {
if attachmentMap, ok := attachment.(map[string]interface{}); ok {
// Log details of the first attachment to understand the structure
if i == 0 {
log.Printf("**** JOB %s: First attachment structure: %+v", jobID, attachmentMap)
attKeys := make([]string, 0)
for k := range attachmentMap {
attKeys = append(attKeys, k)
}
log.Printf("**** JOB %s: First attachment keys: %s", jobID, strings.Join(attKeys, ", "))
}
// Add to our attachments collection
attachments = append(attachments, attachmentMap)
}
}
}
}
}
}
}
// Log attachment count after direct endpoint
log.Printf("**** JOB %s: Attachments found after direct paperwork call: %d", jobID, len(attachments))
// Now actually apply the filters
filteredAttachments := make([]map[string]interface{}, 0)
// Process each attachment
for _, attachment := range attachments {
log.Printf("**** JOB %s: Processing attachment ID: %v", jobID, attachment["id"])
// Check document types filter
if len(docTypes) > 0 {
typeMatches := false
// Log all attachment details for debugging
logAttachmentDetails(jobID, attachment)
// Log docTypes array as it comes from the form
log.Printf("**** JOB %s: Doc types from form: %v", jobID, docTypes)
// Get all possible attachment type info
var purposeId float64
var purpose string
var typeValue string
// Check all possible type fields
if val, ok := attachment["purposeId"].(float64); ok {
purposeId = val
log.Printf("**** JOB %s: Found purposeId=%.0f", jobID, purposeId)
}
if val, ok := attachment["purpose"].(string); ok {
purpose = val
log.Printf("**** JOB %s: Found purpose=%s", jobID, purpose)
}
if val, ok := attachment["type"].(string); ok {
typeValue = val
log.Printf("**** JOB %s: Found type=%s", jobID, typeValue)
}
// Now try to match with each document type from form
for _, docType := range docTypes {
// Clean up the doc type (remove leading zeros)
docTypeClean := strings.TrimLeft(docType, "0")
// Try to convert the cleaned doc type to a number
if docTypeNum, err := strconv.ParseFloat(docTypeClean, 64); err == nil {
// Compare with purposeId
if purposeId > 0 && purposeId == docTypeNum {
log.Printf("**** JOB %s: MATCH! docType=%s matches purposeId=%.0f",
jobID, docType, purposeId)
typeMatches = true
break
}
}
// Try string comparisons if no match yet
if !typeMatches && purpose != "" {
if docType == purpose || docTypeClean == purpose {
log.Printf("**** JOB %s: MATCH! docType=%s matches purpose=%s",
jobID, docType, purpose)
typeMatches = true
break
}
}
if !typeMatches && typeValue != "" {
if docType == typeValue || docTypeClean == typeValue {
log.Printf("**** JOB %s: MATCH! docType=%s matches type=%s",
jobID, docType, typeValue)
typeMatches = true
break
}
}
}
if !typeMatches {
log.Printf("**** JOB %s: No type match found, skipping attachment", jobID)
continue
}
}
// Get document name/filename
var filename string
if nameVal, ok := attachment["fileName"].(string); ok {
filename = nameVal
} else if nameVal, ok := attachment["name"].(string); ok {
filename = nameVal
} else if nameVal, ok := attachment["description"].(string); ok {
filename = nameVal
}
log.Printf("**** JOB %s: Attachment filename: %s", jobID, filename)
// Check filename pattern
if len(filenamePatterns) > 0 && !matchesAnyPattern(filename, filenamePatterns) {
log.Printf("**** JOB %s: Skipping attachment - filename '%s' doesn't match patterns",
jobID, filename)
continue
}
// Check age filter if applicable
if ageFilterDays > 0 {
// Get creation time
var createdAt time.Time
var createdOn string
var hasDate bool
// Try to get the creation date
if created, ok := attachment["createdOn"].(string); ok {
createdOn = created
hasDate = true
} else if created, ok := attachment["created"].(string); ok {
createdOn = created
hasDate = true
} else if lastModified, ok := attachment["lastModified"].(string); ok {
createdOn = lastModified
hasDate = true
} else if createdVal, ok := attachment["created"].(float64); ok {
createdAt = time.Unix(int64(createdVal), 0)
createdOn = createdAt.Format(time.RFC3339)
hasDate = true
}
if hasDate {
if parsedTime, err := time.Parse(time.RFC3339, createdOn); err == nil {
createdAt = parsedTime
if createdAt.After(cutoffDate) {
log.Printf("Skipping attachment %s - created on %s is newer than cutoff %s",
filename, createdAt.Format("2006-01-02"), cutoffDate.Format("2006-01-02"))
continue // Skip if not old enough
}
}
}
}
// Use a new variable with a different name for the log message
var typeStr string = "unknown"
if pId, ok := attachment["purposeId"].(float64); ok {
typeStr = fmt.Sprintf("%.0f", pId)
}
log.Printf("Attachment %s (type: %s) matches all criteria - queued for deletion",
filename, typeStr)
// If we got here, the attachment passes all filters
filteredAttachments = append(filteredAttachments, attachment)
}
// Update the attachments with the filtered list
attachments = filteredAttachments
log.Printf("**** JOB %s: Final attachments to process after filtering: %d", jobID, len(attachments))
}
log.Printf("**** JOB %s: Final total attachments: %d", jobID, len(attachments))
if len(attachments) == 0 {
log.Printf("**** JOB %s: WARNING! No attachments found after all retrieval attempts")
}
}
// 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 {
log.Printf("Skipping attachment - missing ID field")
continue
}
attachmentIDStr := fmt.Sprintf("%.0f", attachmentIDRaw)
// Get the filename
var filename string
if nameVal, ok := attachment["fileName"].(string); ok {
filename = nameVal
} else if nameVal, ok := attachment["name"].(string); ok {
filename = nameVal
} else if nameVal, ok := attachment["description"].(string); ok {
filename = nameVal
}
if filename == "" {
log.Printf("Attachment %s is missing filename information", attachmentIDStr)
continue
}
log.Printf("Processing attachment ID: %s, filename: %s", attachmentIDStr, filename)
// Check document type using purposeId which IS available in the data
if len(docTypes) > 0 {
typeMatches := false
// Get purposeId
var purposeId float64
if val, ok := attachment["purposeId"].(float64); ok {
purposeId = val
log.Printf("Attachment %s has purposeId=%.0f", attachmentIDStr, purposeId)
} else {
log.Printf("Attachment %s has no purposeId field", attachmentIDStr)
continue
}
// Compare with selected document types
for _, docType := range docTypes {
// Form uses "01", "02", etc. but API uses 1, 2, etc. - handle both
docTypeClean := strings.TrimLeft(docType, "0")
if docTypeInt, err := strconv.Atoi(docTypeClean); err == nil {
if float64(docTypeInt) == purposeId {
log.Printf("✓ Type match for attachment %s: form value %s matches purposeId %.0f",
attachmentIDStr, docType, purposeId)
typeMatches = true
break
}
}
}
if !typeMatches {
// Get purposeId for error message
var purposeVal float64
if val, ok := attachment["purposeId"].(float64); ok {
purposeVal = val
log.Printf("Skipping attachment %s - purposeId %.0f doesn't match any selected types: %v",
attachmentIDStr, purposeVal, docTypes)
} else {
log.Printf("Skipping attachment %s - type doesn't match any selected types: %v",
attachmentIDStr, docTypes)
}
continue
}
}
// Comment out problematic log line
// log.Printf("Attachment %s (type: %v, created: %s) matches all criteria - queued for deletion",
// filename, purposeId, attachment["createdOn"])
// Log that we found an attachment to delete
log.Printf("Attachment %s matches criteria - will be deleted", filename)
// 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 {
// Use a separate goroutine for each deletion with its own semaphore slot
deletionWg.Add(1)
// Important: Create a copy of the attachment for the goroutine to avoid
// sharing the loop variable which can cause race conditions
attachmentCopy := attachment
go func(att map[string]interface{}) {
defer deletionWg.Done()
// Acquire a semaphore slot for this deletion operation
semaphore <- struct{}{}
defer func() { <-semaphore }() // Release when done
attachmentIDFloat := att["id"].(float64)
attachmentIDStr := fmt.Sprintf("%.0f", attachmentIDFloat) // Convert to string without decimal
filename := att["description"].(string)
fileResult := struct {
Name string
Success bool
Error string
}{
Name: filename,
}
// Delete the attachment
err := session.DeleteAttachment(attachmentIDStr)
mu.Lock()
defer mu.Unlock()
if err != nil {
fileResult.Success = false
fileResult.Error = err.Error()
log.Printf("Error deleting attachment %s: %v", filename, err)
jobResult.Success = false
} else {
fileResult.Success = true
jobResult.FilesRemoved++
log.Printf("Successfully deleted attachment %s", filename)
}
jobResult.Files = append(jobResult.Files, fileResult)
// Add a slight delay to avoid overwhelming the API
time.Sleep(300 * time.Millisecond)
}(attachmentCopy)
}
// Wait for all deletions for this job to complete
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 #%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 is in a slice
func stringInSlice(s string, slice []string) bool {
for _, item := range slice {
if item == s {
return true
}
}
return false
}
// Helper function to check if a string matches any pattern in a slice
func matchesAnyPattern(s string, patterns []string) bool {
for _, pattern := range patterns {
match, _ := regexp.MatchString("(?i)"+pattern, s)
if match {
return true
}
}
return false
}
// Enhanced debugging function to help understand attachment structure
func logAttachmentDetails(jobID string, attachment map[string]interface{}) {
// Create a detailed view of the attachment
attachmentID := "unknown"
if id, ok := attachment["id"].(float64); ok {
attachmentID = fmt.Sprintf("%.0f", id)
}
log.Printf("***** DETAILED ATTACHMENT %s *****", attachmentID)
for key, value := range attachment {
log.Printf(" %s = %v (type: %T)", key, value, value)
}
log.Printf("***** END ATTACHMENT DETAILS *****")
}

69
static/css/upload.css

@ -280,4 +280,73 @@
:root.dark-theme .loading-indicator {
border-color: rgba(255, 255, 255, 0.1);
border-top-color: var(--btn-primary-bg);
}
/* Tab buttons */
.tab-buttons {
display: flex;
margin-bottom: 1rem;
border-bottom: 1px solid var(--dropdown-border);
}
.tab-button {
padding: 0.5rem 1rem;
background: var(--card-bg);
border: 1px solid var(--dropdown-border);
border-bottom: none;
border-radius: 4px 4px 0 0;
color: var(--content-text);
cursor: pointer;
margin-right: 0.5rem;
position: relative;
bottom: -1px;
}
.tab-button.active {
background: var(--content-bg);
border-bottom: 1px solid var(--content-bg);
font-weight: bold;
}
/* Checkbox styles */
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1.5rem;
margin: 0.5rem 0 1rem 0;
}
.checkbox-item {
display: flex;
align-items: center;
}
.checkbox-item input[type="checkbox"] {
margin-right: 0.5rem;
}
/* Form groups */
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
color: var(--label-color);
font-weight: bold;
}
.form-actions {
margin-top: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
}
/* Job summary in results */
.job-summary {
margin: 0.5rem 0;
font-size: 0.9rem;
color: var(--soft-text);
}

114
templates/partials/document_remove.html

@ -11,7 +11,74 @@
<!-- Step 2: Document Selection -->
<div class="content">
<h3 class="submenu-header">Step 2: Select Documents to Remove</h3>
{{template "document_remove_form" .}}
<div class="tab-buttons">
<button id="individual-tab-btn" class="tab-button active">Individual Selection</button>
<button id="bulk-tab-btn" class="tab-button">Bulk Removal</button>
</div>
<div id="individual-selection-tab">
{{template "document_remove_form" .}}
</div>
<div id="bulk-removal-tab" style="display: none;">
<form id="bulk-removal-form" hx-post="/documents/remove/bulk" hx-target="#removal-results"
hx-indicator="#bulk-loading-indicator">
<div class="form-group">
<label>Document Types to Remove:</label>
<div class="checkbox-group">
<div class="checkbox-item">
<input type="checkbox" id="type-0" name="documentTypes" value="0">
<label for="type-0">Job Paperwork</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="type-1" name="documentTypes" value="1">
<label for="type-1">Job Vendor Bill</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="type-2" name="documentTypes" value="2">
<label for="type-2">Job Picture</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="type-4" name="documentTypes" value="4">
<label for="type-4">Generic Attachment</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="type-7" name="documentTypes" value="7">
<label for="type-7">Blank Paperwork</label>
</div>
<div class="checkbox-item">
<input type="checkbox" id="type-10" name="documentTypes" value="10">
<label for="type-10">Job Invoice</label>
</div>
</div>
</div>
<div class="form-group">
<label for="filename-patterns">Filename Patterns (comma-separated, use * as wildcard):</label>
<input type="text" id="filename-patterns" name="filenamePatterns" class="card-input"
placeholder="e.g. invoice*.pdf, report*.docx">
</div>
<div class="form-group">
<label for="age-filter">Remove Files Older Than (days):</label>
<input type="number" id="age-filter" name="ageFilter" class="card-input" min="0">
</div>
<input type="hidden" id="bulk-job-ids" name="jobIDs">
<div class="form-actions">
<button type="submit" class="warning-button" id="bulk-remove-btn" disabled>
Remove All Matching Documents
</button>
<div id="bulk-loading-indicator" class="htmx-indicator" style="display: none;">
<span>Processing Document Removal...</span>
<div class="loading-indicator"></div>
</div>
</div>
</form>
</div>
<!-- Job IDs container moved inside the form for better structure -->
<div id="job-ids-removal-container" style="display: none;">
@ -31,4 +98,49 @@
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
// Tab switching
const individualTabBtn = document.getElementById('individual-tab-btn');
const bulkTabBtn = document.getElementById('bulk-tab-btn');
const individualTab = document.getElementById('individual-selection-tab');
const bulkTab = document.getElementById('bulk-removal-tab');
individualTabBtn.addEventListener('click', function () {
individualTabBtn.classList.add('active');
bulkTabBtn.classList.remove('active');
individualTab.style.display = 'block';
bulkTab.style.display = 'none';
});
bulkTabBtn.addEventListener('click', function () {
bulkTabBtn.classList.add('active');
individualTabBtn.classList.remove('active');
bulkTab.style.display = 'block';
individualTab.style.display = 'none';
});
// Sync job IDs between forms
htmx.on('#job-ids-removal-container', 'htmx:afterSwap', function () {
// Get jobIDs from the container
const hiddenInput = document.querySelector('#job-ids-removal-container input[name="jobIDs"]');
if (hiddenInput) {
const jobIds = hiddenInput.value;
// Update bulk form hidden input
const bulkJobIds = document.getElementById('bulk-job-ids');
if (bulkJobIds) {
bulkJobIds.value = jobIds;
// Enable bulk remove button if we have job IDs
const bulkRemoveBtn = document.getElementById('bulk-remove-btn');
if (bulkRemoveBtn) {
bulkRemoveBtn.disabled = !jobIds;
}
}
}
});
});
</script>
{{end}}
Loading…
Cancel
Save