Browse Source

fix: css junk that I made claude do

document-upload-removal-layout-update
nic 12 months ago
parent
commit
1f53830b94
  1. 300
      internal/handlers/web/documents.go
  2. 281
      static/css/upload.css
  3. 1
      templates/layout.html
  4. 23
      templates/partials/upload_actions.html

300
internal/handlers/web/documents.go

@ -10,8 +10,10 @@ import (
"net/http"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"time"
root "marmic/servicetrade-toolbox"
@ -131,62 +133,57 @@ func ProcessCSVHandler(w http.ResponseWriter, r *http.Request) {
jobID := strings.TrimSpace(row[idColumnIndex])
if jobID != "" {
jobNumbers = append(jobNumbers, jobID)
log.Printf("Added job ID: %s", jobID)
}
}
}
log.Printf("Extracted %d job IDs from CSV", len(jobNumbers))
totalJobs := len(jobNumbers)
log.Printf("Extracted %d job IDs from CSV", totalJobs)
// Create a list of valid job numbers
var validJobNumbers []string
validJobNumbers = append(validJobNumbers, jobNumbers...)
if totalJobs == 0 {
http.Error(w, "No valid job IDs found in the CSV file", http.StatusBadRequest)
return
}
// Generate HTML for job list
var jobListHTML string
if len(validJobNumbers) > 0 {
// Create a hidden input with the job IDs
jobsValue := strings.Join(validJobNumbers, ",")
jobsValue := strings.Join(jobNumbers, ",")
// Insert a hidden input for job numbers and show the job list
jobListHTML = fmt.Sprintf(`
// Generate HTML for job preview - don't show all IDs for large datasets
var jobPreviewHTML string
if totalJobs > 0 {
jobPreviewHTML = fmt.Sprintf(`
<input type="hidden" name="jobNumbers" value="%s">
<style>
#csv-preview { display: block !important; }
</style>
<script>
// Update the job list display
document.getElementById("csv-preview-content").innerHTML = '';
var ul = document.createElement("ul");
ul.className = "job-list";
%s
document.getElementById("csv-preview-content").appendChild(ul);
</script>
`, jobsValue, buildJobListJS(validJobNumbers))
<div id="csv-preview-content">
<p>Found <strong>%d</strong> job(s) in the CSV file</p>
<div class="csv-sample">
<p>Sample job IDs: %s</p>
</div>
</div>
`, jobsValue, totalJobs, getJobSampleDisplay(jobNumbers))
} else {
jobListHTML = `
jobPreviewHTML = `
<p>No valid job numbers found in the CSV file.</p>
`
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte(jobListHTML))
w.Write([]byte(jobPreviewHTML))
}
// Helper function to build JavaScript for job list
func buildJobListJS(jobIDs []string) string {
var js strings.Builder
for _, id := range jobIDs {
js.WriteString(fmt.Sprintf(`
var li = document.createElement("li");
li.setAttribute("data-job-id", "%s");
li.textContent = "Job #%s";
ul.appendChild(li);
`, id, id))
}
return js.String()
// Helper function to show sample job IDs with a limit
func getJobSampleDisplay(jobIDs []string) string {
const maxSamples = 5
if len(jobIDs) <= maxSamples {
return strings.Join(jobIDs, ", ")
}
sample := append([]string{}, jobIDs[:maxSamples]...)
return strings.Join(sample, ", ") + fmt.Sprintf(" and %d more...", len(jobIDs)-maxSamples)
}
// UploadDocumentsHandler handles document uploads to jobs
@ -308,79 +305,230 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) {
})
}
// Process each file and upload to each job
results := make(map[string][]map[string]interface{})
for _, doc := range documents {
defer doc.File.Close()
if len(documents) == 0 {
http.Error(w, "No valid documents selected for upload", http.StatusBadRequest)
return
}
// Read all file contents first to avoid keeping files open during concurrent uploads
type DocumentWithContent struct {
Name string
Type string
FileContent []byte
}
var docsWithContent []DocumentWithContent
for _, doc := range documents {
// Read file content
fileContent, err := io.ReadAll(doc.File)
if err != nil {
http.Error(w, fmt.Sprintf("Error reading file %s: %v", doc.Header.Filename, err), http.StatusInternalServerError)
return
log.Printf("Error reading file %s: %v", doc.Header.Filename, err)
continue
}
doc.File.Close() // Close the file as soon as we're done with it
docsWithContent = append(docsWithContent, DocumentWithContent{
Name: doc.Name,
Type: doc.Type,
FileContent: fileContent,
})
}
// Upload to each job
// Concurrent upload with throttling
// ServiceTrade API allows 30s of availability per minute (approximately 15 requests at 2s each)
const maxConcurrent = 5 // A conservative limit to avoid rate limiting
const requestDelay = 300 * time.Millisecond // Delay between requests
// Channel for collecting results
type UploadResult struct {
JobID string
DocName string
Success bool
Error string
Data map[string]interface{}
}
totalUploads := len(jobs) * len(docsWithContent)
resultsChan := make(chan UploadResult, totalUploads)
// Create a wait group to track when all uploads are done
var wg sync.WaitGroup
// Create a semaphore channel to limit concurrent uploads
semaphore := make(chan struct{}, maxConcurrent)
// Start the upload workers
for _, jobID := range jobs {
for _, doc := range docsWithContent {
wg.Add(1)
// Launch a goroutine for each job+document combination
go func(jobID string, doc DocumentWithContent) {
defer wg.Done()
// Acquire a semaphore slot
semaphore <- struct{}{}
defer func() { <-semaphore }() // Release when done
// Add a small delay to avoid overwhelming the API
time.Sleep(requestDelay)
// Call the ServiceTrade API
result, err := session.UploadAttachment(jobID, doc.Name, doc.Type, fileContent)
result, err := session.UploadAttachment(jobID, doc.Name, doc.Type, doc.FileContent)
if err != nil {
log.Printf("Error uploading %s to job %s: %v", doc.Name, jobID, err)
if _, exists := results[jobID]; !exists {
results[jobID] = []map[string]interface{}{}
resultsChan <- UploadResult{
JobID: jobID,
DocName: doc.Name,
Success: false,
Error: err.Error(),
}
results[jobID] = append(results[jobID], map[string]interface{}{
"filename": doc.Name,
"success": false,
"error": err.Error(),
})
continue
} else {
log.Printf("Successfully uploaded %s to job %s", doc.Name, jobID)
resultsChan <- UploadResult{
JobID: jobID,
DocName: doc.Name,
Success: true,
Data: result,
}
// Record the success
if _, exists := results[jobID]; !exists {
results[jobID] = []map[string]interface{}{}
}
results[jobID] = append(results[jobID], map[string]interface{}{
"filename": doc.Name,
"success": true,
"data": result,
})
log.Printf("Successfully uploaded %s to job %s", doc.Name, jobID)
}(jobID, doc)
}
}
// Close the results channel when all uploads are done
go func() {
wg.Wait()
close(resultsChan)
}()
// Collect results
results := make(map[string][]UploadResult)
for result := range resultsChan {
if _, exists := results[result.JobID]; !exists {
results[result.JobID] = []UploadResult{}
}
results[result.JobID] = append(results[result.JobID], result)
}
// Generate HTML for results
var resultHTML bytes.Buffer
resultHTML.WriteString("<div class='upload-results'>")
resultHTML.WriteString("<h4>Upload Results</h4>")
if len(results) == 0 {
resultHTML.WriteString("<p class='error'>No documents were uploaded. Please check that you have selected files and document types.</p>")
// Count successes and failures
var totalSuccess, totalFailure int
for _, jobResults := range results {
for _, result := range jobResults {
if result.Success {
totalSuccess++
} else {
for jobID, jobResults := range results {
resultHTML.WriteString(fmt.Sprintf("<div class='job-result'><h5>Job #%s</h5><ul>", jobID))
totalFailure++
}
}
}
for _, result := range jobResults {
filename := result["filename"].(string)
success := result["success"].(bool)
// Add summary section
resultHTML.WriteString("<div class=\"upload-summary\">")
resultHTML.WriteString("<h3>Upload 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>", len(results)))
resultHTML.WriteString("<div class=\"stat-label\">Total Jobs</div>")
resultHTML.WriteString("</div>")
if success {
resultHTML.WriteString(fmt.Sprintf("<li class='success'>%s: Uploaded successfully</li>", filename))
// Success stat
resultHTML.WriteString("<div class=\"stat-box success-stat\">")
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", totalSuccess))
resultHTML.WriteString("<div class=\"stat-label\">Successful Uploads</div>")
resultHTML.WriteString("</div>")
// Failure stat
resultHTML.WriteString("<div class=\"stat-box error-stat\">")
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", totalFailure))
resultHTML.WriteString("<div class=\"stat-label\">Failed Uploads</div>")
resultHTML.WriteString("</div>")
// File count stat
resultHTML.WriteString("<div class=\"stat-box\">")
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%d</div>", totalSuccess+totalFailure))
resultHTML.WriteString("<div class=\"stat-label\">Files Processed</div>")
resultHTML.WriteString("</div>")
resultHTML.WriteString("</div>") // End of upload-stats
// Add completion message
if totalFailure == 0 {
resultHTML.WriteString("<p>All documents were successfully uploaded to ServiceTrade!</p>")
} else {
errorMsg := result["error"].(string)
resultHTML.WriteString(fmt.Sprintf("<li class='error'>%s: %s</li>", filename, errorMsg))
resultHTML.WriteString("<p>Some documents failed to upload. 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
sortedJobs := make([]string, 0, len(results))
for jobID := range results {
sortedJobs = append(sortedJobs, jobID)
}
sort.Strings(sortedJobs)
for _, jobID := range sortedJobs {
jobResults := results[jobID]
resultHTML.WriteString("</ul></div>")
// Determine job success status
jobSuccess := true
for _, result := range jobResults {
if !result.Success {
jobSuccess = false
break
}
}
// Job result row
jobClass := "success"
if !jobSuccess {
jobClass = "error"
}
resultHTML.WriteString(fmt.Sprintf("<div class=\"job-result %s\">", jobClass))
resultHTML.WriteString(fmt.Sprintf("<span class=\"job-id\">Job #%s</span>", jobID))
// File results
if len(jobResults) > 0 {
resultHTML.WriteString("<div class=\"file-results\">")
for _, result := range jobResults {
fileClass := "success"
icon := "✓"
message := "Successfully uploaded"
if !result.Success {
fileClass = "error"
icon = "✗"
message = result.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>", result.DocName))
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
}
// Add JavaScript to scroll to results
resultHTML.WriteString("<script>document.getElementById('upload-results').scrollIntoView({behavior: 'smooth'});</script>")
resultHTML.WriteString("</div>") // End of job-results
w.Header().Set("Content-Type", "text/html")
w.Write(resultHTML.Bytes())

281
static/css/upload.css

@ -0,0 +1,281 @@
/* Upload Summary Styles */
.upload-summary {
margin: 1.5rem 0;
padding: 1.5rem;
border-radius: 6px;
background-color: var(--content-bg);
box-shadow: var(--dashboard-shadow);
color: var(--content-text);
}
.upload-summary h3 {
margin-top: 0;
font-size: 1.25rem;
color: var(--dashboard-header-color);
margin-bottom: 1rem;
}
.upload-stats {
display: flex;
gap: 1.5rem;
margin-bottom: 1rem;
}
.stat-box {
flex: 1;
padding: 1rem;
border-radius: 4px;
background-color: var(--input-bg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
text-align: center;
}
.stat-value {
font-size: 2rem;
font-weight: 600;
margin-bottom: 0.25rem;
color: var(--content-text);
}
.stat-label {
font-size: 0.875rem;
color: var(--label-color);
}
.success-stat .stat-value {
color: var(--btn-success-bg);
}
.error-stat .stat-value {
color: var(--btn-warning-bg);
}
/* Job Results Styles */
.job-results {
margin-top: 1.5rem;
}
.job-result {
padding: 1rem;
margin-bottom: 0.5rem;
border-radius: 4px;
border: var(--input-border);
border-left-width: 4px;
background-color: var(--content-bg);
box-shadow: var(--dashboard-shadow);
color: var(--content-text);
}
.job-result.success {
border-left-color: var(--btn-success-bg);
}
.job-result.error {
border-left-color: var(--btn-warning-bg);
}
.job-id {
font-weight: 600;
display: block;
margin-bottom: 0.5rem;
color: var(--dashboard-header-color);
}
.file-results {
margin-left: 1rem;
margin-top: 0.5rem;
}
.file-result {
padding: 0.75rem;
margin-bottom: 0.5rem;
border-radius: 4px;
display: flex;
align-items: center;
background-color: var(--input-bg);
border: var(--input-border);
}
.file-result.success {
background-color: rgba(var(--btn-success-rgb, 52, 211, 153), 0.1);
border-color: rgba(var(--btn-success-rgb, 52, 211, 153), 0.3);
}
.file-result.error {
background-color: rgba(var(--btn-warning-rgb, 248, 113, 113), 0.1);
border-color: rgba(var(--btn-warning-rgb, 248, 113, 113), 0.3);
}
.status-icon {
font-size: 1.25rem;
margin-right: 0.75rem;
}
.success .status-icon {
color: var(--btn-success-bg);
}
.error .status-icon {
color: var(--btn-warning-bg);
}
.file-name {
font-weight: 500;
margin-right: 0.5rem;
color: var(--content-text);
}
.file-message {
color: var(--label-color);
font-size: 0.875rem;
}
/* Upload Progress Styles */
.upload-progress {
background-color: var(--content-bg);
border-radius: 6px;
padding: 1.5rem;
margin: 1.5rem 0;
box-shadow: var(--dashboard-shadow);
}
.progress-info {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.progress-info span {
font-weight: 500;
color: var(--content-text);
}
.spinner {
border: 3px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 3px solid var(--btn-primary-bg);
width: 20px;
height: 20px;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.progress {
height: 0.5rem;
background-color: var(--progress-bg, #e2e8f0);
border-radius: 999px;
overflow: hidden;
}
.progress-bar {
height: 100%;
width: 100%;
background-color: var(--progress-fill, #4299e1);
background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent);
background-size: 1rem 1rem;
border-radius: 999px;
animation: progress-animation 1s linear infinite;
}
@keyframes progress-animation {
0% {
background-position: 1rem 0;
}
100% {
background-position: 0 0;
}
}
#upload-status {
margin-top: 0.75rem;
font-size: 0.875rem;
color: var(--label-color);
}
.pulsing {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
opacity: 0.6;
}
50% {
opacity: 1;
}
100% {
opacity: 0.6;
}
}
.advice {
font-weight: 500;
color: var(--btn-primary-bg);
}
/* CSV Preview styles */
#csv-preview {
margin-top: 1rem;
padding: 1rem;
background-color: var(--content-bg);
border-radius: 6px;
border-left: 4px solid var(--btn-primary-bg);
}
.csv-sample {
margin-top: 0.5rem;
padding: 0.75rem;
background-color: var(--input-bg);
border-radius: 4px;
box-shadow: var(--dashboard-shadow);
}
#csv-preview p {
margin: 0.5rem 0;
color: var(--content-text);
}
/* HTMX indicator styles */
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator {
opacity: 1;
}
.htmx-request.htmx-indicator {
opacity: 1;
}
.loading-indicator {
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--btn-primary-bg);
animation: spin 1s linear infinite;
margin-left: 0.5rem;
vertical-align: middle;
}
:root.dark-theme .spinner,
:root.dark-theme .loading-indicator {
border-color: rgba(255, 255, 255, 0.1);
border-top-color: var(--btn-primary-bg);
}

1
templates/layout.html

@ -7,6 +7,7 @@
<title>ServiceTrade Tools</title>
<script src="https://unpkg.com/htmx.org@1.9.6"></script>
<link rel="stylesheet" href="/static/css/styles.css" />
<link rel="stylesheet" href="/static/css/upload.css" />
</head>
<body class="flex h-screen bg-gray-100">

23
templates/partials/upload_actions.html

@ -4,24 +4,27 @@
<div>
<form id="upload-form" hx-post="/upload-documents" hx-encoding="multipart/form-data"
hx-include="[name='jobNumbers'],[name^='document-file'],[name^='document-name'],[name^='document-type']"
hx-target="#upload-results" hx-indicator="#upload-loading-indicator">
hx-target="#upload-results" hx-indicator="#upload-progress">
<input type="hidden" name="job-ids" id="job-ids-field">
<button type="submit" class="success-button">Upload Documents to Jobs</button>
<button type="submit" class="success-button" id="submit-button">Upload Documents to Jobs</button>
<div id="upload-progress" style="display: none; margin-top: 1rem;">
<div id="upload-progress" class="upload-progress htmx-indicator">
<div class="progress-info">
<span>Processing your uploads...</span>
<div class="spinner"></div>
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" style="width: 0%;"></div>
<div class="progress-bar" role="progressbar"></div>
</div>
<p id="upload-status">Preparing uploads...</p>
<div id="upload-loading-indicator" class="htmx-indicator">
<span>Uploading...</span>
<div class="loading-indicator"></div>
<p id="upload-status">
<span class="pulsing">This may take a while for large batches of jobs or multiple
documents.</span><br>
<span class="advice">Please don't refresh the page during the upload process.</span>
</p>
</div>
<div id="upload-results"></div>
</div>
</form>
</div>
</div>

Loading…
Cancel
Save