13 changed files with 693 additions and 24 deletions
@ -0,0 +1,408 @@ |
|||||
|
package web |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/csv" |
||||
|
"fmt" |
||||
|
"log" |
||||
|
"net/http" |
||||
|
"strconv" |
||||
|
"strings" |
||||
|
"sync" |
||||
|
"time" |
||||
|
|
||||
|
root "marmic/servicetrade-toolbox" |
||||
|
"marmic/servicetrade-toolbox/internal/api" |
||||
|
|
||||
|
"github.com/gorilla/mux" |
||||
|
) |
||||
|
|
||||
|
// DocumentRemoveHandler handles the document removal page
|
||||
|
func DocumentRemoveHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
session, ok := r.Context().Value("session").(*api.Session) |
||||
|
if !ok { |
||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
tmpl := root.WebTemplates |
||||
|
data := map[string]interface{}{ |
||||
|
"Title": "Document Removal", |
||||
|
"Session": session, |
||||
|
} |
||||
|
|
||||
|
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("session").(*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, ",") |
||||
|
jobSampleDisplay := getJobSampleDisplay(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)) |
||||
|
responseHTML.WriteString(fmt.Sprintf(`<div class="csv-sample"><p>Sample job IDs: %s</p></div>`, jobSampleDisplay)) |
||||
|
|
||||
|
// Generate out-of-band swap for the preview section
|
||||
|
responseHTML.WriteString(fmt.Sprintf(` |
||||
|
<div id="csv-preview-removal" class="fade-me-out" style="display: block !important; margin-top: 1rem;" hx-swap-oob="true"> |
||||
|
<h4>Detected Jobs</h4> |
||||
|
<div id="csv-preview-content-removal" class="job-list"> |
||||
|
<div class="csv-sample"> |
||||
|
<p>Sample job IDs: %s</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
`, jobSampleDisplay)) |
||||
|
|
||||
|
// Send the response with the hidden input and preview
|
||||
|
w.Header().Set("Content-Type", "text/html") |
||||
|
w.Write(responseHTML.Bytes()) |
||||
|
} |
||||
|
|
||||
|
// After the CSV is processed, a separate request should load job attachments
|
||||
|
// This handler is for Step 2
|
||||
|
func JobSelectionHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
// We don't use the session directly but check it for auth
|
||||
|
_, ok := r.Context().Value("session").(*api.Session) |
||||
|
if !ok { |
||||
|
http.Error(w, "Unauthorized", http.StatusUnauthorized) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
jobIDs := r.FormValue("jobIDs") |
||||
|
if jobIDs == "" { |
||||
|
http.Error(w, "No job IDs provided", http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
jobs := strings.Split(jobIDs, ",") |
||||
|
if len(jobs) == 0 { |
||||
|
http.Error(w, "No valid job IDs found", http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
var resultHTML bytes.Buffer |
||||
|
resultHTML.WriteString("<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 #%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("session").(*api.Session) |
||||
|
if !ok { |
||||
|
vars := mux.Vars(r) |
||||
|
jobID := vars["jobID"] |
||||
|
renderErrorTemplate(w, "job_attachments", "You must be logged in to use this feature", jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
vars := mux.Vars(r) |
||||
|
jobID := vars["jobID"] |
||||
|
if jobID == "" { |
||||
|
renderErrorTemplate(w, "job_attachments", "Job ID is required", jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get attachments for the job
|
||||
|
attachments, err := session.GetJobAttachments(jobID) |
||||
|
if err != nil { |
||||
|
renderErrorTemplate(w, "job_attachments", fmt.Sprintf("Failed to get attachments: %v", err), jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
tmpl := root.WebTemplates |
||||
|
data := map[string]interface{}{ |
||||
|
"JobID": jobID, |
||||
|
"Attachments": attachments, |
||||
|
"Session": session, |
||||
|
} |
||||
|
|
||||
|
if err := tmpl.ExecuteTemplate(w, "job_attachments", data); err != nil { |
||||
|
log.Printf("Template execution error: %v", err) |
||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// RemoveJobAttachmentsHandler handles the removal of attachments from a job
|
||||
|
func RemoveJobAttachmentsHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
session, ok := r.Context().Value("session").(*api.Session) |
||||
|
if !ok { |
||||
|
vars := mux.Vars(r) |
||||
|
jobID := vars["jobID"] |
||||
|
renderErrorTemplate(w, "job_attachments", "You must be logged in to use this feature", jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
vars := mux.Vars(r) |
||||
|
jobID := vars["jobID"] |
||||
|
if jobID == "" { |
||||
|
renderErrorTemplate(w, "job_attachments", "Job ID is required", jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Parse the form
|
||||
|
if err := r.ParseForm(); err != nil { |
||||
|
renderErrorTemplate(w, "job_attachments", fmt.Sprintf("Error parsing form: %v", err), jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Get the attachment IDs to remove
|
||||
|
attachmentIDs := r.PostForm["attachment_ids"] |
||||
|
if len(attachmentIDs) == 0 { |
||||
|
renderErrorTemplate(w, "job_attachments", "No attachments selected for deletion", jobID) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// Process deletion with rate limiting (max 5 concurrent requests)
|
||||
|
results := struct { |
||||
|
Success bool |
||||
|
SuccessCount int |
||||
|
ErrorCount int |
||||
|
Files []struct { |
||||
|
Name string |
||||
|
Success bool |
||||
|
Error string |
||||
|
} |
||||
|
}{ |
||||
|
Success: true, |
||||
|
Files: make([]struct { |
||||
|
Name string |
||||
|
Success bool |
||||
|
Error string |
||||
|
}, 0, len(attachmentIDs)), |
||||
|
} |
||||
|
|
||||
|
// Set up rate limiting
|
||||
|
semaphore := make(chan struct{}, 5) // Allow 5 concurrent requests
|
||||
|
var wg sync.WaitGroup |
||||
|
var mu sync.Mutex // Mutex for updating results
|
||||
|
|
||||
|
for _, attachmentID := range attachmentIDs { |
||||
|
wg.Add(1) |
||||
|
semaphore <- struct{}{} // Acquire semaphore
|
||||
|
|
||||
|
go func(id string) { |
||||
|
defer wg.Done() |
||||
|
defer func() { <-semaphore }() // Release semaphore
|
||||
|
|
||||
|
// Get attachment info first to get the name
|
||||
|
attachmentInfo, err := session.GetAttachmentInfo(id) |
||||
|
|
||||
|
fileResult := struct { |
||||
|
Name string |
||||
|
Success bool |
||||
|
Error string |
||||
|
}{ |
||||
|
Name: fmt.Sprintf("Attachment ID: %s", id), |
||||
|
} |
||||
|
|
||||
|
if err == nil { |
||||
|
// Get description if available
|
||||
|
if description, ok := attachmentInfo["description"].(string); ok { |
||||
|
fileResult.Name = description |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Delete the attachment
|
||||
|
err = session.DeleteAttachment(id) |
||||
|
|
||||
|
mu.Lock() |
||||
|
defer mu.Unlock() |
||||
|
|
||||
|
if err != nil { |
||||
|
fileResult.Success = false |
||||
|
fileResult.Error = err.Error() |
||||
|
results.ErrorCount++ |
||||
|
results.Success = false |
||||
|
} else { |
||||
|
fileResult.Success = true |
||||
|
results.SuccessCount++ |
||||
|
} |
||||
|
|
||||
|
results.Files = append(results.Files, fileResult) |
||||
|
|
||||
|
// Add a slight delay to avoid overwhelming the API
|
||||
|
time.Sleep(100 * time.Millisecond) |
||||
|
}(attachmentID) |
||||
|
} |
||||
|
|
||||
|
wg.Wait() // Wait for all deletions to complete
|
||||
|
|
||||
|
tmpl := root.WebTemplates |
||||
|
data := map[string]interface{}{ |
||||
|
"JobID": jobID, |
||||
|
"Session": session, |
||||
|
"SuccessCount": results.SuccessCount, |
||||
|
"ErrorCount": results.ErrorCount, |
||||
|
"JobsProcessed": 1, |
||||
|
"Results": []map[string]interface{}{ |
||||
|
{ |
||||
|
"JobID": jobID, |
||||
|
"Success": results.Success, |
||||
|
"Files": results.Files, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Generate HTML for results that will go to the removal_results div
|
||||
|
if err := tmpl.ExecuteTemplate(w, "removal_results", data); err != nil { |
||||
|
log.Printf("Template execution error: %v", err) |
||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// JobListHandler renders the job list for document removal
|
||||
|
func JobListHandler(w http.ResponseWriter, r *http.Request) { |
||||
|
session, ok := r.Context().Value("session").(*api.Session) |
||||
|
if !ok { |
||||
|
renderErrorTemplate(w, "job_list", "You must be logged in to use this feature") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if err := r.ParseForm(); err != nil { |
||||
|
renderErrorTemplate(w, "job_list", fmt.Sprintf("Error parsing form: %v", err)) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
tmpl := root.WebTemplates |
||||
|
data := map[string]interface{}{ |
||||
|
"JobIDs": r.PostForm["job_ids"], |
||||
|
"Session": session, |
||||
|
} |
||||
|
|
||||
|
if err := tmpl.ExecuteTemplate(w, "job_list", data); err != nil { |
||||
|
log.Printf("Template execution error: %v", err) |
||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Helper function to render error templates
|
||||
|
func renderErrorTemplate(w http.ResponseWriter, templateName, errorMsg string, jobID ...string) { |
||||
|
tmpl := root.WebTemplates |
||||
|
data := map[string]interface{}{ |
||||
|
"Error": errorMsg, |
||||
|
} |
||||
|
|
||||
|
if len(jobID) > 0 && jobID[0] != "" { |
||||
|
data["JobID"] = jobID[0] |
||||
|
} |
||||
|
|
||||
|
if err := tmpl.ExecuteTemplate(w, templateName, data); err != nil { |
||||
|
log.Printf("Template execution error: %v", err) |
||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError) |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,34 @@ |
|||||
|
{{define "document_remove"}} |
||||
|
<h2>Document Removal</h2> |
||||
|
|
||||
|
<div class="upload-container"> |
||||
|
<!-- Step 1: CSV Upload --> |
||||
|
<div class="content"> |
||||
|
<h3 class="submenu-header">Step 1: Upload CSV file with Job IDs</h3> |
||||
|
{{template "document_remove_csv" .}} |
||||
|
</div> |
||||
|
|
||||
|
<!-- Step 2: Document Selection --> |
||||
|
<div class="content"> |
||||
|
<h3 class="submenu-header">Step 2: Select Documents to Remove</h3> |
||||
|
{{template "document_remove_form" .}} |
||||
|
|
||||
|
<!-- Job IDs container moved inside the form for better structure --> |
||||
|
<div id="job-ids-removal-container" style="display: none;"> |
||||
|
<!-- Hidden input placeholder for job IDs --> |
||||
|
</div> |
||||
|
|
||||
|
<div id="job-results" class="job-selection"> |
||||
|
<!-- Jobs and document selection will appear here --> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Step 3: Results --> |
||||
|
<div class="content"> |
||||
|
<h3 class="submenu-header">Step 3: Removal Results</h3> |
||||
|
<div id="removal-results" class="upload-results"> |
||||
|
<!-- Results will appear here after removing documents --> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
{{end}} |
||||
@ -0,0 +1,25 @@ |
|||||
|
{{define "document_remove_csv"}} |
||||
|
<div> |
||||
|
<label>Select CSV file with job IDs:</label> |
||||
|
<input class="card-input" type="file" id="csv-file-removal" name="csv" accept=".csv" required> |
||||
|
|
||||
|
<button type="button" class="btn-primary" hx-post="/documents/remove/process-csv" |
||||
|
hx-target="#job-ids-removal-container" hx-encoding="multipart/form-data" hx-include="#csv-file-removal" |
||||
|
hx-indicator="#csv-loading-indicator-removal"> |
||||
|
Process CSV |
||||
|
</button> |
||||
|
|
||||
|
<div id="csv-loading-indicator-removal" class="htmx-indicator" style="display: none;"> |
||||
|
<span>Processing CSV...</span> |
||||
|
<div class="loading-indicator"></div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div id="csv-preview-removal" class="fade-me-out" style="display: none; margin-top: 1rem;"> |
||||
|
<h4>Detected Jobs</h4> |
||||
|
<div id="csv-preview-content-removal" class="job-list"> |
||||
|
<!-- Job numbers will be displayed here --> |
||||
|
<p>No jobs loaded yet</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
{{end}} |
||||
@ -0,0 +1,28 @@ |
|||||
|
{{define "document_remove_form"}} |
||||
|
<div id="document-selection-container"> |
||||
|
<form id="job-selection-form" hx-post="/documents/remove/job-selection" hx-target="#job-results" |
||||
|
hx-indicator="#job-loading-indicator" hx-include="#job-ids-removal-container"> |
||||
|
<!-- The hidden input with jobIDs will be inserted into job-ids-removal-container by the CSV process --> |
||||
|
|
||||
|
<button type="submit" id="load-jobs-btn" class="btn-primary"> |
||||
|
Load Jobs |
||||
|
</button> |
||||
|
|
||||
|
<div id="job-loading-indicator" class="htmx-indicator" style="display: none;"> |
||||
|
<span>Loading jobs...</span> |
||||
|
<div class="loading-indicator"></div> |
||||
|
</div> |
||||
|
</form> |
||||
|
|
||||
|
<div id="document-rows-container" class="document-rows"> |
||||
|
<!-- The job list will be loaded here after clicking "Load Jobs" --> |
||||
|
<p>After uploading a CSV and clicking "Load Jobs", you'll see jobs here.</p> |
||||
|
</div> |
||||
|
|
||||
|
<div class="document-actions"> |
||||
|
<button type="button" id="remove-selected-btn" class="btn warning-button" disabled> |
||||
|
Remove Selected Documents |
||||
|
</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
{{end}} |
||||
@ -0,0 +1,54 @@ |
|||||
|
{{define "job_attachments"}} |
||||
|
<div class="job-attachments" id="job-{{.JobID}}-attachments"> |
||||
|
<h4>Job #{{.JobID}}</h4> |
||||
|
|
||||
|
{{if .Error}} |
||||
|
<div class="error-message">Error: {{.Error}}</div> |
||||
|
{{else}} |
||||
|
{{if len .Attachments}} |
||||
|
<form hx-post="/documents/remove/job/{{.JobID}}" hx-target="#removal-results" |
||||
|
hx-indicator="#delete-loading-indicator-{{.JobID}}"> |
||||
|
<div class="attachments-list"> |
||||
|
{{range .Attachments}} |
||||
|
<div class="attachment-item"> |
||||
|
<label> |
||||
|
<input type="checkbox" name="attachment_ids" value="{{.id}}"> |
||||
|
<span class="attachment-name">{{.description}}</span> |
||||
|
<span class="attachment-info"> |
||||
|
<span class="attachment-type">{{template "document_type" .purposeId}}</span> |
||||
|
<span class="attachment-date">{{.lastModified}}</span> |
||||
|
</span> |
||||
|
</label> |
||||
|
</div> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
|
||||
|
<button type="submit" class="btn btn-danger">Delete Selected Documents</button> |
||||
|
|
||||
|
<div id="delete-loading-indicator-{{.JobID}}" class="htmx-indicator"> |
||||
|
<div class="spinner"></div> |
||||
|
<span>Deleting documents...</span> |
||||
|
</div> |
||||
|
</form> |
||||
|
{{else}} |
||||
|
<div class="info-message">No attachments found for this job.</div> |
||||
|
{{end}} |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
|
|
||||
|
{{define "document_type"}} |
||||
|
{{if eq . 0}}Job Paperwork |
||||
|
{{else if eq . 1}}Job Vendor Bill |
||||
|
{{else if eq . 2}}Job Picture |
||||
|
{{else if eq . 3}}Deficiency Repair Proposal |
||||
|
{{else if eq . 4}}Generic Attachment |
||||
|
{{else if eq . 5}}Avatar Image |
||||
|
{{else if eq . 6}}Import |
||||
|
{{else if eq . 7}}Blank Paperwork |
||||
|
{{else if eq . 8}}Work Acknowledgement |
||||
|
{{else if eq . 9}}Logo |
||||
|
{{else if eq . 10}}Job Invoice |
||||
|
{{else}}Unknown ({{.}}) |
||||
|
{{end}} |
||||
|
{{end}} |
||||
@ -0,0 +1,27 @@ |
|||||
|
{{define "job_list"}} |
||||
|
<div class="job-list-container"> |
||||
|
<h3>Jobs from CSV</h3> |
||||
|
|
||||
|
{{if .Error}} |
||||
|
<div class="error-message">Error: {{.Error}}</div> |
||||
|
{{else}} |
||||
|
{{if .JobIDs}} |
||||
|
<p>Found {{len .JobIDs}} job(s) in the CSV file. Click a job to view and manage its documents.</p> |
||||
|
|
||||
|
<div class="job-list"> |
||||
|
{{range .JobIDs}} |
||||
|
<div class="job-item"> |
||||
|
<a href="#job-{{.}}" hx-get="/documents/remove/job/{{.}}" hx-target="#job-{{.}}-attachments" |
||||
|
hx-swap="outerHTML" hx-trigger="click"> |
||||
|
Job #{{.}} |
||||
|
</a> |
||||
|
<div id="job-{{.}}-attachments"></div> |
||||
|
</div> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{else}} |
||||
|
<div class="info-message">No jobs found. Please upload a CSV file with job IDs.</div> |
||||
|
{{end}} |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
@ -0,0 +1,51 @@ |
|||||
|
{{define "removal_results"}} |
||||
|
<div class="upload-summary"> |
||||
|
<h3>Document Removal Results</h3> |
||||
|
|
||||
|
{{if .Error}} |
||||
|
<div class="error-message">Error: {{.Error}}</div> |
||||
|
{{else}} |
||||
|
<div class="results-summary"> |
||||
|
<p>Successfully removed {{.SuccessCount}} document(s).</p> |
||||
|
{{if gt .ErrorCount 0}} |
||||
|
<p class="text-warning">Failed to remove {{.ErrorCount}} document(s).</p> |
||||
|
{{end}} |
||||
|
{{if gt .JobsProcessed 0}} |
||||
|
<p>Processed {{.JobsProcessed}} job(s).</p> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
|
||||
|
{{if .Results}} |
||||
|
<div class="job-results"> |
||||
|
{{range $job := .Results}} |
||||
|
<div class="job-result"> |
||||
|
<h4>Job #{{$job.JobID}}</h4> |
||||
|
|
||||
|
{{if $job.Success}} |
||||
|
<div class="success-message">Successfully processed</div> |
||||
|
{{else}} |
||||
|
<div class="error-message">Error: {{$job.Error}}</div> |
||||
|
{{end}} |
||||
|
|
||||
|
{{if $job.Files}} |
||||
|
<div class="file-results"> |
||||
|
{{range $file := $job.Files}} |
||||
|
<div class="file-result {{if $file.Success}}success{{else}}error{{end}}"> |
||||
|
<span class="file-name">{{$file.Name}}</span> |
||||
|
{{if $file.Success}} |
||||
|
<span class="success-icon">✓</span> |
||||
|
{{else}} |
||||
|
<span class="error-icon">✗</span> |
||||
|
<span class="error-message">{{$file.Error}}</span> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
|
{{end}} |
||||
|
</div> |
||||
|
{{end}} |
||||
Loading…
Reference in new issue