diff --git a/internal/handlers/web/documents.go b/internal/handlers/web/documents.go index 40146c3..d8c7f60 100644 --- a/internal/handlers/web/documents.go +++ b/internal/handlers/web/documents.go @@ -586,7 +586,7 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { } // Return first page of results with configurable page size - renderUploadResultsPage(w, sessionID, utils.DefaultPage, limit) + renderUploadResultsPage(w, sessionID, utils.DefaultPage, limit, "all") } // UploadResultsHandler handles pagination for upload results @@ -607,18 +607,93 @@ func UploadResultsHandler(w http.ResponseWriter, r *http.Request) { limit = utils.DefaultPageSize } - renderUploadResultsPage(w, sessionID, page, limit) + // Optional filter: all|success|failed + filter := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("filter"))) + if filter != "success" && filter != "failed" { + filter = "all" + } + + renderUploadResultsPage(w, sessionID, page, limit, filter) } // renderUploadResultsPage renders a paginated page of upload results -func renderUploadResultsPage(w http.ResponseWriter, sessionID string, page, limit int) { +func renderUploadResultsPage(w http.ResponseWriter, sessionID string, page, limit int, filter string) { uploadSession, exists := uploadSessions[sessionID] if !exists { http.Error(w, "Upload session not found", http.StatusNotFound) return } - totalResults := len(uploadSession.GroupedResults) + // Build a filtered view per job at the file level + var filteredJobs []struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + } + + for _, jr := range uploadSession.GroupedResults { + // Filter files per job + var files []struct { + Name string + Success bool + Error string + FileSize int64 + } + for _, f := range jr.Files { + if filter == "success" && !f.Success { + continue + } + if filter == "failed" && f.Success { + continue + } + files = append(files, f) + } + if len(files) == 0 { + // Skip jobs that have no files matching the filter + if filter != "all" { + continue + } + } + // Copy job with filtered files + copy := struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + }{ + JobID: jr.JobID, + FilesFound: jr.FilesFound, + FilesUploaded: jr.FilesUploaded, + Success: jr.Success, + ErrorMsg: jr.ErrorMsg, + Files: func() []struct { + Name string + Success bool + Error string + FileSize int64 + } { + return files + }(), + } + filteredJobs = append(filteredJobs, copy) + } + + totalResults := len(filteredJobs) pagination := utils.CalculatePagination(totalResults, page, limit) // Get results for this page @@ -627,21 +702,25 @@ func renderUploadResultsPage(w http.ResponseWriter, sessionID string, page, limi if endIndex > totalResults { endIndex = totalResults } - pageResults := utils.GetPageResults(uploadSession.GroupedResults, startIndex, endIndex) + pageResults := utils.GetPageResults(filteredJobs, startIndex, endIndex) // Add pagination info to each job result for the template var resultsWithPagination []map[string]interface{} for _, jobResult := range pageResults { + filesLen := len(jobResult.Files) + displaySuccess := (filter == "success") || (filter != "failed" && jobResult.Success) resultMap := map[string]interface{}{ - "JobID": jobResult.JobID, - "FilesFound": jobResult.FilesFound, - "FilesUploaded": jobResult.FilesUploaded, - "Success": jobResult.Success, - "ErrorMsg": jobResult.ErrorMsg, - "Files": jobResult.Files, - "FilePage": 1, // Default to first file - "TotalFiles": len(jobResult.Files), - "SessionID": sessionID, + "JobID": jobResult.JobID, + "FilesFound": jobResult.FilesFound, + "FilesUploaded": jobResult.FilesUploaded, + "Success": jobResult.Success, + "ErrorMsg": jobResult.ErrorMsg, + "Files": jobResult.Files, + "FilePage": 1, // Default to first file + "TotalFiles": filesLen, + "SessionID": sessionID, + "Filter": filter, + "DisplaySuccess": displaySuccess, } resultsWithPagination = append(resultsWithPagination, resultMap) } @@ -662,13 +741,12 @@ func renderUploadResultsPage(w http.ResponseWriter, sessionID string, page, limi "StartPage": pagination.StartPage, "EndPage": pagination.EndPage, "SessionID": sessionID, + "Filter": filter, } 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 } } @@ -692,7 +770,6 @@ func (r *readCloserWithSize) Close() error { return nil // Allow closing nil reader safely } -// Size returns the current size of data read func (r *readCloserWithSize) Size() int64 { return r.size } @@ -711,6 +788,10 @@ func UploadJobFileHandler(w http.ResponseWriter, r *http.Request) { filePage = parsed } } + filter := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("filter"))) + if filter != "success" && filter != "failed" { + filter = "all" + } uploadSession, exists := uploadSessions[sessionID] if !exists { @@ -743,19 +824,37 @@ func UploadJobFileHandler(w http.ResponseWriter, r *http.Request) { return } - totalFiles := len(jobResult.Files) + // Apply file-level filter + var filteredFiles []struct { + Name string + Success bool + Error string + FileSize int64 + } + for _, f := range jobResult.Files { + if filter == "success" && !f.Success { + continue + } + if filter == "failed" && f.Success { + continue + } + filteredFiles = append(filteredFiles, f) + } + + totalFiles := len(filteredFiles) if totalFiles == 0 { - // No files to show data := map[string]interface{}{ - "JobID": jobResult.JobID, - "FilesFound": jobResult.FilesFound, - "FilesUploaded": jobResult.FilesUploaded, - "Success": jobResult.Success, - "ErrorMsg": jobResult.ErrorMsg, - "Files": nil, - "FilePage": 1, - "TotalFiles": 0, - "SessionID": sessionID, + "JobID": jobResult.JobID, + "FilesFound": jobResult.FilesFound, + "FilesUploaded": jobResult.FilesUploaded, + "Success": jobResult.Success, + "ErrorMsg": jobResult.ErrorMsg, + "Files": nil, + "FilePage": 1, + "TotalFiles": 0, + "SessionID": sessionID, + "Filter": filter, + "DisplaySuccess": (filter == "success") || (filter != "failed" && jobResult.Success), } tmpl := root.WebTemplates @@ -798,20 +897,22 @@ func UploadJobFileHandler(w http.ResponseWriter, r *http.Request) { Success bool Error string FileSize int64 - }{jobResult.Files[filePage-1]}, + }{filteredFiles[filePage-1]}, } // Add pagination info for the template data := map[string]interface{}{ - "JobID": jobResultCopy.JobID, - "FilesFound": jobResultCopy.FilesFound, - "FilesUploaded": jobResultCopy.FilesUploaded, - "Success": jobResultCopy.Success, - "ErrorMsg": jobResultCopy.ErrorMsg, - "Files": jobResultCopy.Files, - "FilePage": filePage, - "TotalFiles": totalFiles, - "SessionID": sessionID, + "JobID": jobResultCopy.JobID, + "FilesFound": jobResultCopy.FilesFound, + "FilesUploaded": jobResultCopy.FilesUploaded, + "Success": jobResultCopy.Success, + "ErrorMsg": jobResultCopy.ErrorMsg, + "Files": jobResultCopy.Files, + "FilePage": filePage, + "TotalFiles": totalFiles, + "SessionID": sessionID, + "Filter": filter, + "DisplaySuccess": (filter == "success") || (filter != "failed" && jobResult.Success), } tmpl := root.WebTemplates diff --git a/static/css/styles.css b/static/css/styles.css index 6c3fe9b..a505027 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -95,6 +95,9 @@ html { color: var(--text-color); } +/* Reserve vertical scrollbar space to prevent layout shift when content height changes */ +/* Removed scrollbar-gutter reservation to avoid visual misalignment */ + .flex { display: flex; } @@ -559,6 +562,10 @@ html { .error::before { content: "⚠️"; + display: inline-block; + width: 1em; + /* reserve horizontal space so layout doesn't shift */ + text-align: center; } .not-found::before { @@ -1736,9 +1743,10 @@ html { /* Upload Result Cards */ .upload-results-container { - background-color: var(--dashboard-bg); + background-color: var(--upload-card-bg); + border: var(--upload-card-border); border-radius: 0.5rem; - box-shadow: var(--dashboard-shadow); + box-shadow: var(--upload-card-shadow); padding: 1.5rem; margin-top: 1rem; max-width: 100%; @@ -1763,7 +1771,8 @@ html { } .stat-item { - background-color: rgba(30, 33, 43, 0.8); + background-color: var(--upload-stat-bg); + border: var(--upload-stat-border); border-radius: 0.5rem; padding: 1.25rem; text-align: center; @@ -1773,9 +1782,7 @@ html { justify-content: center; } -.stat-item:last-child { - grid-column: 1; -} +/* Do not force the last item into column 1; allow natural placement */ .stat-value { font-size: 2.5rem; @@ -1800,13 +1807,14 @@ html { .upload-results-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1rem; margin-top: 1.5rem; } .upload-result-card { - background-color: rgba(30, 33, 43, 0.8); + background-color: var(--upload-card-bg); + border: var(--upload-card-border); border-radius: 0.5rem; padding: 1.25rem; display: flex; @@ -1833,6 +1841,10 @@ html { padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.875rem; + display: inline-flex; + align-items: center; + gap: 0.25rem; + white-space: nowrap; } .upload-status.success { diff --git a/static/css/upload.css b/static/css/upload.css index e98f930..94b4f44 100644 --- a/static/css/upload.css +++ b/static/css/upload.css @@ -288,21 +288,23 @@ /* Upload Results Container */ .upload-results-container { background-color: var(--upload-card-bg); + border: var(--upload-card-border); border-radius: 0.5rem; box-shadow: var(--upload-card-shadow); padding: 1.5rem; margin-top: 1rem; max-width: 100%; + width: 100%; + box-sizing: border-box; + overflow-x: hidden; + /* Revert: avoid forcing gutter here to prevent content width reduction */ } .upload-results-header { margin-bottom: 1.5rem; -} - -.upload-results-header h3 { - color: var(--text-color); - font-size: 1.5rem; - margin: 0 0 1rem 0; + display: flex; + flex-direction: column; + gap: 0.75rem; } /* Upload Stats Grid */ @@ -312,10 +314,16 @@ gap: 1rem; margin-bottom: 1.5rem; max-width: 600px; + width: 100%; + box-sizing: border-box; + padding: 0 0.5rem; + /* ensure visual breathing room on both sides */ } +/* Stat item card styling (works in light and dark via CSS variables) */ .stat-item { background-color: var(--upload-stat-bg); + border: var(--upload-stat-border); border-radius: 0.5rem; padding: 1.25rem; text-align: center; @@ -326,10 +334,6 @@ min-width: 120px; } -.stat-item:last-child { - grid-column: 1; -} - .stat-value { font-size: 2.5rem; font-weight: bold; @@ -344,94 +348,17 @@ text-align: center; } -/* Stat item variants */ -.success-stat .stat-value { - color: var(--upload-success-color); -} - -.error-stat .stat-value { - color: var(--upload-error-color); -} - -.warning-stat .stat-value { - color: var(--upload-warning-color); -} - -/* Upload Results Grid */ -.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: var(--upload-card-bg); - 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; - font-weight: 500; -} - -.upload-status.success { - background-color: var(--upload-stat-success-bg); - color: var(--upload-success-color); -} - -.upload-status.error { - background-color: var(--upload-stat-error-bg); - color: var(--upload-error-color); -} - -.upload-details { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.upload-info { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -.upload-info p { - margin: 0; - font-size: 0.875rem; - color: var(--content-text); -} - -.success-text { - color: var(--upload-success-color); - font-weight: 500; +/* Clickable stat items (success/failed) */ +.upload-stats .success-stat, +.upload-stats .error-stat, +.upload-stats .all-stat { + cursor: pointer; } -.error-text { - color: var(--upload-error-color); - font-weight: 500; +.upload-stats .success-stat:hover, +.upload-stats .error-stat:hover, +.upload-stats .all-stat:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, .12); } /* Pagination Controls */ @@ -442,6 +369,7 @@ margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--content-border); + overflow-x: auto; } .pagination-info { @@ -453,6 +381,35 @@ display: flex; gap: 0.5rem; align-items: center; + flex-wrap: wrap; +} + +/* Prevent long error text from blowing out cards */ +.clamp-text { + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 5; + overflow: hidden; + word-break: break-word; + white-space: pre-wrap; + position: relative; +} + +/* show pointer and underline hint on hover */ +.error-text.clamp-text, +.error-message.clamp-text { + cursor: pointer; +} + +.error-text.clamp-text:hover, +.error-message.clamp-text:hover { + text-decoration: underline dotted; +} + +/* When expanded, remove clamping */ +.clamp-text.expanded { + -webkit-line-clamp: initial; + overflow: visible; } .pagination-btn { @@ -565,9 +522,12 @@ border-color: rgba(34, 197, 94, 0.3); } +/* Error bubble refinement */ .file-result.error { background-color: var(--upload-stat-error-bg); border-color: rgba(239, 68, 68, 0.3); + border-radius: 0.5rem; + /* rounder corners like old version */ } .status-icon { @@ -871,4 +831,14 @@ width: 100%; justify-content: center; } +} + +/* Ensure the results grid has inner gutters and doesn't touch the edges */ +.upload-results-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1rem; + margin-top: 1.5rem; + padding: 0 0.5rem; + box-sizing: border-box; } \ No newline at end of file diff --git a/templates/partials/upload_result_card.html b/templates/partials/upload_result_card.html index 65a6068..568ec20 100644 --- a/templates/partials/upload_result_card.html +++ b/templates/partials/upload_result_card.html @@ -2,8 +2,8 @@
Files Found: {{.FilesFound}}
Files Uploaded: {{.FilesUploaded}}
- {{if .Success}} + {{if .DisplaySuccess}}Successfully processed
{{else}}{{.ErrorMsg}}
@@ -19,7 +19,7 @@Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{.TotalTime}}!
- {{end}} - {{if gt .TotalFailure 0}} -Failed to upload {{.TotalFailure}} document(s). See details below.
- {{end}} - -