Browse Source

feat: added pagination to results

document-upload-removal-layout-update
nic 9 months ago
parent
commit
0a169b8b8b
  1. 2
      apps/web/main.go
  2. BIN
      apps/web/web-app
  3. 221
      internal/handlers/web/document_remove.go
  4. 241
      internal/handlers/web/documents.go
  5. 226
      static/css/styles.css
  6. 1092
      static/css/upload.css
  7. 52
      templates/partials/removal_result_card.html
  8. 77
      templates/partials/removal_results.html
  9. 24
      templates/partials/removal_stats.html
  10. 113
      templates/partials/upload_result_card.html
  11. 54
      templates/partials/upload_results_pagination.html
  12. 24
      templates/partials/upload_stats.html
  13. 19
      web_templates.go

2
apps/web/main.go

@ -64,6 +64,7 @@ func main() {
protected.HandleFunc("/documents", web.DocumentsHandler).Methods("GET")
protected.HandleFunc("/process-csv", web.ProcessCSVHandler).Methods("POST")
protected.HandleFunc("/upload-documents", web.UploadDocumentsHandler).Methods("POST")
protected.HandleFunc("/documents/upload/results", web.UploadResultsHandler).Methods("GET")
// Document removal routes
protected.HandleFunc("/documents/remove", web.DocumentRemoveHandler).Methods("GET")
@ -72,6 +73,7 @@ func main() {
protected.HandleFunc("/documents/remove/job/{jobID}", web.GetJobAttachmentsHandler).Methods("GET")
protected.HandleFunc("/documents/remove/attachments/{jobID}", web.RemoveJobAttachmentsHandler).Methods("POST")
protected.HandleFunc("/documents/remove/bulk", web.BulkRemoveDocumentsHandler).Methods("POST")
protected.HandleFunc("/documents/remove/results", web.RemovalResultsHandler).Methods("GET")
port := os.Getenv("PORT")
if port == "" {

BIN
apps/web/web-app

Binary file not shown.

221
internal/handlers/web/document_remove.go

@ -7,7 +7,6 @@ import (
"log"
"net/http"
"regexp"
"sort"
"strconv"
"strings"
"sync"
@ -20,6 +19,34 @@ import (
"github.com/gorilla/mux"
)
// RemovalResult represents the result of a single job removal
type RemovalResult struct {
JobID string
FilesFound int
FilesRemoved int
Success bool
ErrorMsg string
Files []struct {
Name string
Success bool
Error string
}
}
// RemovalSession stores removal results for pagination
type RemovalSession struct {
Results []RemovalResult
TotalJobs int
SuccessCount int
ErrorCount int
TotalFiles int
TotalTime time.Duration
CreatedAt time.Time
}
// Global map to store removal sessions (in production, use Redis or database)
var removalSessions = make(map[string]RemovalSession)
// DocumentRemoveHandler handles the document removal page
func DocumentRemoveHandler(w http.ResponseWriter, r *http.Request) {
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
@ -731,114 +758,126 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
log.Printf("Document removal completed in %v", totalDuration)
// Generate HTML for results
var resultHTML bytes.Buffer
// Convert results to RemovalResult format for session storage
var removalResults []RemovalResult
for _, jobResult := range results.JobResults {
removalResult := RemovalResult{
JobID: jobResult.JobID,
FilesFound: jobResult.FilesFound,
FilesRemoved: jobResult.FilesRemoved,
Success: jobResult.Success,
ErrorMsg: jobResult.ErrorMsg,
Files: jobResult.Files,
}
removalResults = append(removalResults, removalResult)
}
// 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>")
// Duration stat
resultHTML.WriteString("<div class=\"stat-box\">")
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-value\">%v</div>", totalDuration))
resultHTML.WriteString("<div class=\"stat-label\">Total Time</div>")
resultHTML.WriteString("</div>")
resultHTML.WriteString("</div>") // End of upload-stats
// Add completion message with timing
if results.ErrorCount == 0 {
resultHTML.WriteString(fmt.Sprintf("<p>All documents were successfully removed from ServiceTrade in %v!</p>", totalDuration))
} else {
resultHTML.WriteString(fmt.Sprintf("<p>Some documents failed to be removed. Process completed in %v. See details below.</p>", totalDuration))
// Store results in session for pagination
sessionID := fmt.Sprintf("removal_%d", time.Now().UnixNano())
removalSession := RemovalSession{
Results: removalResults,
TotalJobs: results.JobsProcessed,
SuccessCount: results.SuccessCount,
ErrorCount: results.ErrorCount,
TotalFiles: results.TotalFiles,
TotalTime: totalDuration,
CreatedAt: time.Now(),
}
resultHTML.WriteString("</div>") // End of upload-summary
// Store in global map (in production, use Redis or database)
removalSessions[sessionID] = removalSession
// Add detailed job results
resultHTML.WriteString("<div class=\"job-results\">")
// Return first page of results
renderRemovalResultsPage(w, sessionID, 1, 20) // Default to page 1, 20 items per page
}
// Sort job IDs for consistent display
sort.Slice(results.JobResults, func(i, j int) bool {
return results.JobResults[i].JobID < results.JobResults[j].JobID
})
// RemovalResultsHandler handles pagination for removal results
func RemovalResultsHandler(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
http.Error(w, "Session ID required", http.StatusBadRequest)
return
}
for _, jobResult := range results.JobResults {
// Job result row
jobClass := "success"
if !jobResult.Success {
jobClass = "error"
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
resultHTML.WriteString(fmt.Sprintf("<div class=\"job-result %s\">", jobClass))
resultHTML.WriteString(fmt.Sprintf("<span class=\"job-id\">Job ID: %s</span>", jobResult.JobID))
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit < 1 {
limit = 20
}
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))
}
renderRemovalResultsPage(w, sessionID, page, limit)
}
// File results
if len(jobResult.Files) > 0 {
resultHTML.WriteString("<div class=\"file-results\">")
// renderRemovalResultsPage renders a paginated page of removal results
func renderRemovalResultsPage(w http.ResponseWriter, sessionID string, page, limit int) {
removalSession, exists := removalSessions[sessionID]
if !exists {
http.Error(w, "Removal session not found", http.StatusNotFound)
return
}
for _, file := range jobResult.Files {
fileClass := "success"
icon := "✓"
message := "Successfully removed"
totalResults := len(removalSession.Results)
totalPages := (totalResults + limit - 1) / limit // Ceiling division
if !file.Success {
fileClass = "error"
icon = "✗"
message = file.Error
}
if page > totalPages && totalPages > 0 {
page = totalPages
}
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>")
}
startIndex := (page - 1) * limit
endIndex := startIndex + limit
if endIndex > totalResults {
endIndex = totalResults
}
resultHTML.WriteString("</div>") // End of file-results
// Get results for this page
var pageResults []RemovalResult
if startIndex < totalResults {
pageResults = removalSession.Results[startIndex:endIndex]
}
// Calculate pagination info
startPage := 1
endPage := totalPages
if totalPages > 10 {
if page <= 5 {
endPage = 10
} else if page >= totalPages-4 {
startPage = totalPages - 9
} else {
resultHTML.WriteString("<p>No files processed for this job.</p>")
startPage = page - 4
endPage = page + 5
}
resultHTML.WriteString("</div>") // End of job-result
}
resultHTML.WriteString("</div>") // End of job-results
data := map[string]interface{}{
"Results": pageResults,
"TotalJobs": removalSession.TotalJobs,
"SuccessCount": removalSession.SuccessCount,
"ErrorCount": removalSession.ErrorCount,
"TotalFiles": removalSession.TotalFiles,
"TotalTime": removalSession.TotalTime,
"TotalResults": totalResults,
"TotalPages": totalPages,
"CurrentPage": page,
"Limit": limit,
"StartIndex": startIndex + 1,
"EndIndex": endIndex,
"StartPage": startPage,
"EndPage": endPage,
"SessionID": sessionID,
}
w.Header().Set("Content-Type", "text/html")
w.Write(resultHTML.Bytes())
tmpl := root.WebTemplates
if err := tmpl.ExecuteTemplate(w, "removal_results", data); err != nil {
log.Printf("Template execution error: %v", err)
// Don't call http.Error here as the response may have already started
// Just log the error and return
return
}
}
// Helper function to check if a string matches any pattern in a slice

241
internal/handlers/web/documents.go

@ -9,7 +9,7 @@ import (
"log"
"math"
"net/http"
"sort"
"strconv"
"strings"
"sync"
"time"
@ -19,6 +19,30 @@ import (
"marmic/servicetrade-toolbox/internal/middleware"
)
// UploadResult represents the result of a single file upload
type UploadResult struct {
JobID string
DocName string
Success bool
Error string
Data map[string]interface{}
FileSize int64
}
// UploadSession stores upload results for pagination
type UploadSession struct {
Results []UploadResult
TotalJobs int
TotalSuccess int
TotalFailure int
TotalBytesUploaded int64
TotalTime time.Duration
CreatedAt time.Time
}
// Global map to store upload sessions (in production, use Redis or database)
var uploadSessions = make(map[string]UploadSession)
// DocumentsHandler handles the document upload page
func DocumentsHandler(w http.ResponseWriter, r *http.Request) {
session, ok := r.Context().Value(middleware.SessionKey).(*api.Session)
@ -312,15 +336,6 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) {
const maxConcurrent = 5
const requestDelay = 300 * time.Millisecond
type UploadResult struct {
JobID string
DocName string
Success bool
Error string
Data map[string]interface{}
FileSize int64
}
totalUploads := len(jobs) * activeFilesProcessedCount
resultsChan := make(chan UploadResult, totalUploads)
var wg sync.WaitGroup
@ -412,115 +427,139 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) {
log.Println("All upload goroutines finished.")
}()
results := make(map[string][]UploadResult)
resultsCount := 0
// Collect all results
var allResults []UploadResult
var totalBytesUploaded int64
for result := range resultsChan {
resultsCount++
log.Printf("Received result %d/%d: Job %s, File %s, Success: %v, Size: %.2f MB",
resultsCount, totalUploads, result.JobID, result.DocName, result.Success,
float64(result.FileSize)/(1024*1024))
log.Printf("Received result: Job %s, File %s, Success: %v, Size: %.2f MB",
result.JobID, result.DocName, result.Success, float64(result.FileSize)/(1024*1024))
if result.Success {
totalBytesUploaded += result.FileSize
}
if _, exists := results[result.JobID]; !exists {
results[result.JobID] = []UploadResult{}
}
results[result.JobID] = append(results[result.JobID], result)
allResults = append(allResults, result)
}
// Calculate total duration
totalDuration := time.Since(startTime)
log.Printf("All results collected. Total: %d, Total bytes uploaded: %.2f MB, Total time: %v",
resultsCount, float64(totalBytesUploaded)/(1024*1024), totalDuration)
var resultHTML bytes.Buffer
var totalSuccess, totalFailure int
for _, jobResults := range results {
for _, result := range jobResults {
if result.Success {
totalSuccess++
} else {
totalFailure++
}
len(allResults), float64(totalBytesUploaded)/(1024*1024), totalDuration)
// Store results in session for pagination
sessionID := fmt.Sprintf("upload_%d", time.Now().UnixNano())
uploadSession := UploadSession{
Results: allResults,
TotalJobs: len(jobs),
TotalSuccess: 0,
TotalFailure: 0,
TotalBytesUploaded: totalBytesUploaded,
TotalTime: totalDuration,
CreatedAt: time.Now(),
}
// Calculate totals
for _, result := range allResults {
if result.Success {
uploadSession.TotalSuccess++
} else {
uploadSession.TotalFailure++
}
}
resultHTML.WriteString("<div class=\"upload-summary\">")
resultHTML.WriteString("<h3>Upload Results</h3>")
resultHTML.WriteString("<div class=\"upload-stats\">")
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-box\"><div class=\"stat-value\">%d</div><div class=\"stat-label\">Total Jobs</div></div>", len(results)))
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-box success-stat\"><div class=\"stat-value\">%d</div><div class=\"stat-label\">Successful Uploads</div></div>", totalSuccess))
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-box error-stat\"><div class=\"stat-value\">%d</div><div class=\"stat-label\">Failed Uploads</div></div>", totalFailure))
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-box\"><div class=\"stat-value\">%d</div><div class=\"stat-label\">Files Processed</div></div>", resultsCount))
resultHTML.WriteString(fmt.Sprintf("<div class=\"stat-box\"><div class=\"stat-value\">%v</div><div class=\"stat-label\">Total Time</div></div>", totalDuration))
// Store in global map (in production, use Redis or database)
uploadSessions[sessionID] = uploadSession
if totalFailure == 0 && resultsCount > 0 {
resultHTML.WriteString(fmt.Sprintf("<p>All documents were successfully uploaded to ServiceTrade in %v!</p>", totalDuration))
} else if resultsCount == 0 {
resultHTML.WriteString("<p>No documents were processed for upload.</p>")
} else {
resultHTML.WriteString(fmt.Sprintf("<p>Some documents failed to upload. Process completed in %v. See details below.</p>", totalDuration))
}
resultHTML.WriteString("</div>")
resultHTML.WriteString("<div class=\"job-results\">")
sortedJobs := make([]string, 0, len(results))
for jobID := range results {
sortedJobs = append(sortedJobs, jobID)
}
sort.Strings(sortedJobs)
for _, jobID := range sortedJobs {
jobResults := results[jobID]
jobHasSuccess := false
jobHasFailure := false
for _, result := range jobResults {
if result.Success {
jobHasSuccess = true
} else {
jobHasFailure = true
}
}
jobClass := "neutral"
if jobHasSuccess && !jobHasFailure {
jobClass = "success"
} else if jobHasFailure {
jobClass = "error"
}
resultHTML.WriteString(fmt.Sprintf("<div class=\"job-result %s\">", jobClass))
resultHTML.WriteString(fmt.Sprintf("<div class=\"job-id\">Job ID: %s</div>", jobID))
if len(jobResults) > 0 {
resultHTML.WriteString("<div class=\"file-results\">")
sort.Slice(jobResults, func(i, j int) bool {
return jobResults[i].DocName < jobResults[j].DocName
})
for _, result := range jobResults {
fileClass := "success"
icon := "✓"
message := "Successfully uploaded"
if !result.Success {
fileClass = "error"
icon = "✗"
message = strings.ReplaceAll(result.Error, "<", "&lt;")
message = strings.ReplaceAll(message, ">", "&gt;")
}
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>")
// Return first page of results
renderUploadResultsPage(w, sessionID, 1, 20) // Default to page 1, 20 items per page
}
// UploadResultsHandler handles pagination for upload results
func UploadResultsHandler(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
http.Error(w, "Session ID required", http.StatusBadRequest)
return
}
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
if page < 1 {
page = 1
}
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
if limit < 1 {
limit = 20
}
renderUploadResultsPage(w, sessionID, page, limit)
}
// renderUploadResultsPage renders a paginated page of upload results
func renderUploadResultsPage(w http.ResponseWriter, sessionID string, page, limit int) {
uploadSession, exists := uploadSessions[sessionID]
if !exists {
http.Error(w, "Upload session not found", http.StatusNotFound)
return
}
totalResults := len(uploadSession.Results)
totalPages := (totalResults + limit - 1) / limit // Ceiling division
if page > totalPages && totalPages > 0 {
page = totalPages
}
startIndex := (page - 1) * limit
endIndex := startIndex + limit
if endIndex > totalResults {
endIndex = totalResults
}
// Get results for this page
var pageResults []UploadResult
if startIndex < totalResults {
pageResults = uploadSession.Results[startIndex:endIndex]
}
// Calculate pagination info
startPage := 1
endPage := totalPages
if totalPages > 10 {
if page <= 5 {
endPage = 10
} else if page >= totalPages-4 {
startPage = totalPages - 9
} else {
resultHTML.WriteString("<p>No file upload results for this job.</p>")
startPage = page - 4
endPage = page + 5
}
resultHTML.WriteString("</div>")
}
resultHTML.WriteString("</div>")
w.Header().Set("Content-Type", "text/html")
w.Write(resultHTML.Bytes())
data := map[string]interface{}{
"Results": pageResults,
"TotalJobs": uploadSession.TotalJobs,
"TotalSuccess": uploadSession.TotalSuccess,
"TotalFailure": uploadSession.TotalFailure,
"TotalBytesUploaded": uploadSession.TotalBytesUploaded,
"TotalTime": uploadSession.TotalTime,
"TotalResults": totalResults,
"TotalPages": totalPages,
"CurrentPage": page,
"Limit": limit,
"StartIndex": startIndex + 1,
"EndIndex": endIndex,
"StartPage": startPage,
"EndPage": endPage,
"SessionID": sessionID,
}
tmpl := root.WebTemplates
if err := tmpl.ExecuteTemplate(w, "upload_results_pagination", data); err != nil {
log.Printf("Template execution error: %v", err)
// Don't call http.Error here as the response may have already started
// Just log the error and return
return
}
}
// readCloserWithSize is a custom io.Reader that counts the bytes read

226
static/css/styles.css

@ -1731,5 +1731,229 @@ html {
}
.btn-secondary:hover {
background: var(--input-border);
background-color: var(--btn-caution-hover);
}
/* Upload Result Cards */
.upload-results-container {
background-color: var(--dashboard-bg);
border-radius: 0.5rem;
box-shadow: var(--dashboard-shadow);
padding: 1.5rem;
margin-top: 1rem;
max-width: 100%;
}
.upload-results-header {
margin-bottom: 1.5rem;
}
.upload-results-header h3 {
color: var(--text-color);
font-size: 1.5rem;
margin: 0 0 1rem 0;
}
.upload-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1.5rem;
max-width: 600px;
}
.stat-item {
background-color: rgba(30, 33, 43, 0.8);
border-radius: 0.5rem;
padding: 1.25rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.stat-item:last-child {
grid-column: 1;
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: var(--text-color);
line-height: 1;
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-muted);
}
.success-stat .stat-value {
color: var(--success-color, #4CAF50);
}
.error-stat .stat-value {
color: var(--error-color, #f44336);
}
.upload-results-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.upload-result-card {
background-color: rgba(30, 33, 43, 0.8);
border-radius: 0.5rem;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.upload-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.upload-header h4 {
margin: 0;
color: var(--text-color);
font-size: 1rem;
font-weight: 500;
flex: 1;
}
.upload-status {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
}
.upload-status.success {
background-color: rgba(76, 175, 80, 0.1);
color: var(--success-color, #4CAF50);
}
.upload-status.error {
background-color: rgba(244, 67, 54, 0.1);
color: var(--error-color, #f44336);
}
.upload-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.upload-info {
font-size: 0.875rem;
color: var(--text-muted);
}
.upload-info p {
margin: 0;
}
.success-text {
color: var(--success-color, #4CAF50);
}
.error-text {
color: var(--error-color, #f44336);
}
/* Pagination Controls */
.pagination-controls {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid var(--content-border);
}
.pagination-info {
font-size: 0.875rem;
color: var(--label-color);
}
.pagination-buttons {
display: flex;
gap: 0.5rem;
align-items: center;
}
.pagination-btn {
padding: 0.5rem 0.75rem;
border: 1px solid var(--content-border);
background-color: var(--input-bg);
color: var(--text-color);
border-radius: 0.25rem;
cursor: pointer;
font-size: 0.875rem;
transition: all 0.2s ease;
}
.pagination-btn:hover {
background-color: var(--btn-primary-bg);
color: white;
border-color: var(--btn-primary-bg);
}
.pagination-btn.active {
background-color: var(--btn-primary-bg);
color: white;
border-color: var(--btn-primary-bg);
}
.pagination-btn:disabled {
background-color: var(--btn-disabled);
color: var(--label-color);
cursor: not-allowed;
border-color: var(--btn-disabled);
}
/* Responsive Design */
@media (max-width: 768px) {
.upload-results-header {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.upload-stats {
flex-wrap: wrap;
gap: 0.5rem;
}
.upload-results-grid {
grid-template-columns: 1fr;
}
.upload-details {
flex-direction: column;
gap: 1rem;
}
.upload-actions {
margin-left: 0;
align-items: flex-start;
}
.pagination-controls {
flex-direction: column;
gap: 1rem;
align-items: center;
}
.pagination-buttons {
flex-wrap: wrap;
justify-content: center;
}
}

1092
static/css/upload.css

File diff suppressed because it is too large

52
templates/partials/removal_result_card.html

@ -0,0 +1,52 @@
{{define "removal_result_card"}}
<div id="removal-card-{{.JobID}}" class="upload-result-card" data-job-id="{{.JobID}}">
<div class="upload-header">
<h4>Job #{{.JobID}}</h4>
<div class="upload-status {{if .Success}}success{{else}}error{{end}}">
{{if .Success}}✓ Success{{else}}✗ Failed{{end}}
</div>
</div>
<div class="upload-details">
<div class="upload-info">
<p><strong>Files Found:</strong> {{.FilesFound}}</p>
<p><strong>Files Removed:</strong> {{.FilesRemoved}}</p>
{{if .Success}}
<p class="success-text">Successfully processed</p>
{{else}}
<p class="error-text">{{.ErrorMsg}}</p>
{{end}}
</div>
<div class="upload-actions">
{{if .Success}}
<div class="success-indicator">
<span class="icon"></span>
<span>Removal Complete</span>
</div>
{{else}}
<div class="error-indicator">
<span class="icon"></span>
<span>Removal Failed</span>
</div>
{{end}}
</div>
</div>
{{if .Files}}
<div class="file-results">
{{range .Files}}
<div class="file-result {{if .Success}}success{{else}}error{{end}}">
<span class="file-name">{{.Name}}</span>
{{if .Success}}
<span class="success-icon"></span>
{{else}}
<span class="error-icon"></span>
<span class="error-message">{{.Error}}</span>
{{end}}
</div>
{{end}}
</div>
{{end}}
</div>
{{end}}

77
templates/partials/removal_results.html

@ -1,51 +1,54 @@
{{define "removal_results"}}
<div class="upload-summary">
<h3>Document Removal Results</h3>
<div class="upload-results-container">
<div class="upload-results-header">
<h3>Document Removal Results</h3>
{{template "removal_stats" .}}
</div>
{{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>
{{if gt .SuccessCount 0}}
<p>Successfully removed {{.SuccessCount}} document(s) from ServiceTrade in {{.TotalTime}}!</p>
{{end}}
{{if gt .ErrorCount 0}}
<p class="text-warning">Failed to remove {{.ErrorCount}} document(s). See details below.</p>
{{end}}
<div class="upload-results-grid">
{{range .Results}}
{{template "removal_result_card" .}}
{{end}}
</div>
{{if .Results}}
<div class="job-results">
{{range $job := .Results}}
<div class="job-result">
<h4>Job #{{$job.JobID}}</h4>
{{if gt .TotalPages 1}}
<div class="pagination-controls">
<div class="pagination-info">
Showing {{.StartIndex}}-{{.EndIndex}} of {{.TotalResults}} results
</div>
<div class="pagination-buttons">
{{if gt .CurrentPage 1}}
<button
hx-get="/documents/remove/results?page={{subtract .CurrentPage 1}}&limit={{.Limit}}&session_id={{.SessionID}}"
hx-target="#removal-results" class="pagination-btn">
← Previous
</button>
{{end}}
{{if $job.Success}}
<div class="success-message">Successfully processed</div>
{{else}}
<div class="error-message">Error: {{$job.Error}}</div>
{{range $i := sequence .StartPage .EndPage}}
<button hx-get="/documents/remove/results?page={{$i}}&limit={{$.Limit}}&session_id={{$.SessionID}}"
hx-target="#removal-results" class="pagination-btn {{if eq $i $.CurrentPage}}active{{end}}">
{{$i}}
</button>
{{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>
{{if lt .CurrentPage .TotalPages}}
<button
hx-get="/documents/remove/results?page={{add .CurrentPage 1}}&limit={{.Limit}}&session_id={{.SessionID}}"
hx-target="#removal-results" class="pagination-btn">
Next →
</button>
{{end}}
</div>
{{end}}
</div>
{{end}}
{{end}}
</div>
{{end}}

24
templates/partials/removal_stats.html

@ -0,0 +1,24 @@
{{define "removal_stats"}}
<div class="upload-stats">
<div class="stat-item">
<span class="stat-value">{{.TotalJobs}}</span>
<span class="stat-label">Total Jobs</span>
</div>
<div class="stat-item success-stat">
<span class="stat-value">{{.SuccessCount}}</span>
<span class="stat-label">Successful</span>
</div>
<div class="stat-item error-stat">
<span class="stat-value">{{.ErrorCount}}</span>
<span class="stat-label">Failed</span>
</div>
<div class="stat-item">
<span class="stat-value">{{.TotalFiles}}</span>
<span class="stat-label">Files Processed</span>
</div>
<div class="stat-item">
<span class="stat-value">{{.TotalTime}}</span>
<span class="stat-label">Total Time</span>
</div>
</div>
{{end}}

113
templates/partials/upload_result_card.html

@ -0,0 +1,113 @@
{{define "upload_result_card"}}
<div id="upload-card-{{.JobID}}-{{.DocName}}" class="upload-result-card" data-job-id="{{.JobID}}"
data-doc-name="{{.DocName}}">
<div class="upload-header">
<h4>{{.DocName}}</h4>
<div class="upload-status {{if .Success}}success{{else}}error{{end}}">
{{if .Success}}✓ Success{{else}}✗ Failed{{end}}
</div>
</div>
<div class="upload-details">
<div class="upload-info">
<p><strong>Job ID:</strong> {{.JobID}}</p>
<p><strong>File Size:</strong> {{printf "%.2f MB" (div .FileSize 1048576.0)}}</p>
{{if .Success}}
<p class="success-text">Successfully uploaded to ServiceTrade</p>
{{else}}
<p class="error-text">{{.Error}}</p>
{{end}}
</div>
<div class="upload-actions">
{{if .Success}}
<div class="success-indicator">
<span class="icon"></span>
<span>Upload Complete</span>
</div>
{{else}}
<div class="error-indicator">
<span class="icon"></span>
<span>Upload Failed</span>
</div>
{{end}}
</div>
</div>
</div>
{{end}}
{{define "upload_results_pagination"}}
<div class="upload-results-container">
<div class="upload-results-header">
<h3>Upload Results</h3>
<div class="upload-stats">
<div class="stat-item">
<span class="stat-value">{{.TotalJobs}}</span>
<span class="stat-label">Total Jobs</span>
</div>
<div class="stat-item success-stat">
<span class="stat-value">{{.TotalSuccess}}</span>
<span class="stat-label">Successful</span>
</div>
<div class="stat-item error-stat">
<span class="stat-value">{{.TotalFailure}}</span>
<span class="stat-label">Failed</span>
</div>
<div class="stat-item">
<span class="stat-value">{{printf "%.1f" (div .TotalBytesUploaded 1048576.0)}}</span>
<span class="stat-label">MB Uploaded</span>
</div>
<div class="stat-item">
<span class="stat-value">{{.TotalTime}}</span>
<span class="stat-label">Total Time</span>
</div>
</div>
</div>
{{if gt .TotalSuccess 0}}
<p>Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{.TotalTime}}!</p>
{{end}}
{{if gt .TotalFailure 0}}
<p class="text-warning">Failed to upload {{.TotalFailure}} document(s). See details below.</p>
{{end}}
<div class="upload-results-grid">
{{range .Results}}
{{template "upload_result_card" .}}
{{end}}
</div>
{{if gt .TotalPages 1}}
<div class="pagination-controls">
<div class="pagination-info">
Showing {{.StartIndex}}-{{.EndIndex}} of {{.TotalResults}} results
</div>
<div class="pagination-buttons">
{{if gt .CurrentPage 1}}
<button
hx-get="/documents/upload/results?page={{subtract .CurrentPage 1}}&limit={{.Limit}}&session_id={{.SessionID}}"
hx-target="#upload-results" class="pagination-btn">
← Previous
</button>
{{end}}
{{range $i := sequence .StartPage .EndPage}}
<button hx-get="/documents/upload/results?page={{$i}}&limit={{$.Limit}}&session_id={{$.SessionID}}"
hx-target="#upload-results" class="pagination-btn {{if eq $i $.CurrentPage}}active{{end}}">
{{$i}}
</button>
{{end}}
{{if lt .CurrentPage .TotalPages}}
<button
hx-get="/documents/upload/results?page={{add .CurrentPage 1}}&limit={{.Limit}}&session_id={{.SessionID}}"
hx-target="#upload-results" class="pagination-btn">
Next →
</button>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}

54
templates/partials/upload_results_pagination.html

@ -0,0 +1,54 @@
{{define "upload_results_pagination"}}
<div class="upload-results-container">
<div class="upload-results-header">
<h3>Upload Results</h3>
{{template "upload_stats" .}}
</div>
{{if gt .TotalSuccess 0}}
<p>Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{.TotalTime}}!</p>
{{end}}
{{if gt .TotalFailure 0}}
<p class="text-warning">Failed to upload {{.TotalFailure}} document(s). See details below.</p>
{{end}}
<div class="upload-results-grid">
{{range .Results}}
{{template "upload_result_card" .}}
{{end}}
</div>
{{if gt .TotalPages 1}}
<div class="pagination-controls">
<div class="pagination-info">
Showing {{.StartIndex}}-{{.EndIndex}} of {{.TotalResults}} results
</div>
<div class="pagination-buttons">
{{if gt .CurrentPage 1}}
<button
hx-get="/documents/upload/results?page={{subtract .CurrentPage 1}}&limit={{.Limit}}&session_id={{.SessionID}}"
hx-target="#upload-results" class="pagination-btn">
← Previous
</button>
{{end}}
{{range $i := sequence .StartPage .EndPage}}
<button hx-get="/documents/upload/results?page={{$i}}&limit={{$.Limit}}&session_id={{$.SessionID}}"
hx-target="#upload-results" class="pagination-btn {{if eq $i $.CurrentPage}}active{{end}}">
{{$i}}
</button>
{{end}}
{{if lt .CurrentPage .TotalPages}}
<button
hx-get="/documents/upload/results?page={{add .CurrentPage 1}}&limit={{.Limit}}&session_id={{.SessionID}}"
hx-target="#upload-results" class="pagination-btn">
Next →
</button>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}

24
templates/partials/upload_stats.html

@ -0,0 +1,24 @@
{{define "upload_stats"}}
<div class="upload-stats">
<div class="stat-item">
<span class="stat-value">{{.TotalJobs}}</span>
<span class="stat-label">Total Jobs</span>
</div>
<div class="stat-item success-stat">
<span class="stat-value">{{.TotalSuccess}}</span>
<span class="stat-label">Successful</span>
</div>
<div class="stat-item error-stat">
<span class="stat-value">{{.TotalFailure}}</span>
<span class="stat-label">Failed</span>
</div>
<div class="stat-item">
<span class="stat-value">{{printf "%.1f" (div .TotalBytesUploaded 1048576.0)}}</span>
<span class="stat-label">MB Uploaded</span>
</div>
<div class="stat-item">
<span class="stat-value">{{.TotalTime}}</span>
<span class="stat-label">Total Time</span>
</div>
</div>
{{end}}

19
web_templates.go

@ -19,6 +19,25 @@ var funcMap = template.FuncMap{
// This allows us to reference specific content blocks
return "{{template \"" + name + "-content\" .}}"
},
"add": func(a, b int) int {
return a + b
},
"subtract": func(a, b int) int {
return a - b
},
"div": func(a int64, b float64) float64 {
return float64(a) / b
},
"sequence": func(start, end int) []int {
if start > end {
return []int{}
}
result := make([]int, end-start+1)
for i := range result {
result[i] = start + i
}
return result
},
}
// InitializeWebTemplates parses all HTML templates in the embedded filesystem

Loading…
Cancel
Save