diff --git a/apps/web/main.go b/apps/web/main.go index 40feb0a..45aa85b 100644 --- a/apps/web/main.go +++ b/apps/web/main.go @@ -65,11 +65,13 @@ func main() { 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") + protected.HandleFunc("/documents/upload/job/file", web.UploadJobFileHandler).Methods("GET") // Document removal routes protected.HandleFunc("/documents/remove", web.DocumentRemoveHandler).Methods("GET") 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/file", web.RemovalJobFileHandler).Methods("GET") 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") diff --git a/internal/handlers/web/document_remove.go b/internal/handlers/web/document_remove.go index 31ae675..f77b635 100644 --- a/internal/handlers/web/document_remove.go +++ b/internal/handlers/web/document_remove.go @@ -15,6 +15,7 @@ import ( root "marmic/servicetrade-toolbox" "marmic/servicetrade-toolbox/internal/api" "marmic/servicetrade-toolbox/internal/middleware" + "marmic/servicetrade-toolbox/internal/utils" "github.com/gorilla/mux" ) @@ -787,8 +788,21 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { // Store in global map (in production, use Redis or database) removalSessions[sessionID] = removalSession - // Return first page of results - renderRemovalResultsPage(w, sessionID, 1, 20) // Default to page 1, 20 items per page + // Get configurable page size from form, with fallback to default + limitStr := r.FormValue("limit") + limit := utils.DefaultPageSize + if limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { + if parsedLimit > utils.MaxPageSize { + limit = utils.MaxPageSize + } else { + limit = parsedLimit + } + } + } + + // Return first page of results with configurable page size + renderRemovalResultsPage(w, sessionID, utils.DefaultPage, limit) } // RemovalResultsHandler handles pagination for removal results @@ -801,12 +815,12 @@ func RemovalResultsHandler(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { - page = 1 + page = utils.DefaultPage } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 { - limit = 20 + limit = utils.DefaultPageSize } renderRemovalResultsPage(w, sessionID, page, limit) @@ -821,53 +835,48 @@ func renderRemovalResultsPage(w http.ResponseWriter, sessionID string, page, lim } totalResults := len(removalSession.Results) - totalPages := (totalResults + limit - 1) / limit // Ceiling division - - if page > totalPages && totalPages > 0 { - page = totalPages - } + pagination := utils.CalculatePagination(totalResults, page, limit) - startIndex := (page - 1) * limit - endIndex := startIndex + limit + // Get results for this page + startIndex := (pagination.CurrentPage - 1) * pagination.Limit + endIndex := startIndex + pagination.Limit if endIndex > totalResults { endIndex = totalResults } - - // 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 { - startPage = page - 4 - endPage = page + 5 + pageResults := utils.GetPageResults(removalSession.Results, startIndex, endIndex) + + // Add pagination info to each job result for the template + var resultsWithPagination []map[string]interface{} + for _, jobResult := range pageResults { + resultMap := map[string]interface{}{ + "JobID": jobResult.JobID, + "FilesFound": jobResult.FilesFound, + "FilesRemoved": jobResult.FilesRemoved, + "Success": jobResult.Success, + "ErrorMsg": jobResult.ErrorMsg, + "Files": jobResult.Files, + "FilePage": 1, // Default to first file + "TotalFiles": len(jobResult.Files), + "SessionID": sessionID, } + resultsWithPagination = append(resultsWithPagination, resultMap) } data := map[string]interface{}{ - "Results": pageResults, + "Results": resultsWithPagination, "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, + "TotalResults": pagination.TotalResults, + "TotalPages": pagination.TotalPages, + "CurrentPage": pagination.CurrentPage, + "Limit": pagination.Limit, + "StartIndex": pagination.StartIndex, + "EndIndex": pagination.EndIndex, + "StartPage": pagination.StartPage, + "EndPage": pagination.EndPage, "SessionID": sessionID, } @@ -903,3 +912,99 @@ func matchesAnyPattern(s string, patterns []string) bool { } return false } + +// New handler: Serve a single job card with only one file (per-job file pagination) +func RemovalJobFileHandler(w http.ResponseWriter, r *http.Request) { + sessionID := r.URL.Query().Get("session_id") + jobID := r.URL.Query().Get("job_id") + filePageStr := r.URL.Query().Get("file_page") + filePage := 1 + if filePageStr != "" { + if parsed, err := strconv.Atoi(filePageStr); err == nil && parsed > 0 { + filePage = parsed + } + } + + removalSession, exists := removalSessions[sessionID] + if !exists { + http.Error(w, "Removal session not found", http.StatusNotFound) + return + } + + // Find the job result + var jobResult *RemovalResult + for i := range removalSession.Results { + if removalSession.Results[i].JobID == jobID { + jobResult = &removalSession.Results[i] + break + } + } + if jobResult == nil { + http.Error(w, "Job not found in session", http.StatusNotFound) + return + } + + totalFiles := len(jobResult.Files) + if totalFiles == 0 { + // No files to show + data := map[string]interface{}{ + "JobID": jobResult.JobID, + "FilesFound": jobResult.FilesFound, + "FilesRemoved": jobResult.FilesRemoved, + "Success": jobResult.Success, + "ErrorMsg": jobResult.ErrorMsg, + "Files": nil, + "FilePage": 1, + "TotalFiles": 0, + "SessionID": sessionID, + } + + tmpl := root.WebTemplates + if err := tmpl.ExecuteTemplate(w, "removal_result_card", data); err != nil { + log.Printf("Template execution error: %v", err) + return + } + return + } + + // Ensure filePage is within bounds + if filePage > totalFiles { + filePage = totalFiles + } + if filePage < 1 { + filePage = 1 + } + + // Create a copy of the job result with only the requested file + jobResultCopy := RemovalResult{ + JobID: jobResult.JobID, + FilesFound: jobResult.FilesFound, + FilesRemoved: jobResult.FilesRemoved, + Success: jobResult.Success, + ErrorMsg: jobResult.ErrorMsg, + Files: []struct { + Name string + Success bool + Error string + }{jobResult.Files[filePage-1]}, + } + + // Add pagination info for the template + data := map[string]interface{}{ + "JobID": jobResultCopy.JobID, + "FilesFound": jobResultCopy.FilesFound, + "FilesRemoved": jobResultCopy.FilesRemoved, + "Success": jobResultCopy.Success, + "ErrorMsg": jobResultCopy.ErrorMsg, + "Files": jobResultCopy.Files, + "FilePage": filePage, + "TotalFiles": totalFiles, + "SessionID": sessionID, + } + + tmpl := root.WebTemplates + if err := tmpl.ExecuteTemplate(w, "removal_result_card", data); err != nil { + log.Printf("Template execution error: %v", err) + return + } +} diff --git a/internal/handlers/web/documents.go b/internal/handlers/web/documents.go index 24036e6..40146c3 100644 --- a/internal/handlers/web/documents.go +++ b/internal/handlers/web/documents.go @@ -17,6 +17,7 @@ import ( root "marmic/servicetrade-toolbox" "marmic/servicetrade-toolbox/internal/api" "marmic/servicetrade-toolbox/internal/middleware" + "marmic/servicetrade-toolbox/internal/utils" ) // UploadResult represents the result of a single file upload @@ -31,7 +32,20 @@ type UploadResult struct { // UploadSession stores upload results for pagination type UploadSession struct { - Results []UploadResult + Results []UploadResult + GroupedResults []struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + } TotalJobs int TotalSuccess int TotalFailure int @@ -445,10 +459,99 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { log.Printf("All results collected. Total: %d, Total bytes uploaded: %.2f MB, Total time: %v", len(allResults), float64(totalBytesUploaded)/(1024*1024), totalDuration) + // Group results by job for consistent display with removal results + type JobUploadResult struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + } + + // Group results by job + jobResults := make(map[string]*JobUploadResult) + for _, result := range allResults { + if jobResult, exists := jobResults[result.JobID]; exists { + // Add file to existing job + jobResult.FilesFound++ + if result.Success { + jobResult.FilesUploaded++ + } else { + jobResult.Success = false + if jobResult.ErrorMsg == "" { + jobResult.ErrorMsg = "Some files failed to upload" + } + } + jobResult.Files = append(jobResult.Files, struct { + Name string + Success bool + Error string + FileSize int64 + }{ + Name: result.DocName, + Success: result.Success, + Error: result.Error, + FileSize: result.FileSize, + }) + } else { + // Create new job result + jobResults[result.JobID] = &JobUploadResult{ + JobID: result.JobID, + FilesFound: 1, + FilesUploaded: 0, + Success: result.Success, + ErrorMsg: "", + Files: []struct { + Name string + Success bool + Error string + FileSize int64 + }{ + { + Name: result.DocName, + Success: result.Success, + Error: result.Error, + FileSize: result.FileSize, + }, + }, + } + if result.Success { + jobResults[result.JobID].FilesUploaded = 1 + } else { + jobResults[result.JobID].ErrorMsg = "Some files failed to upload" + } + } + } + + // Convert grouped results to slice + var groupedResults []struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + } + for _, jobResult := range jobResults { + groupedResults = append(groupedResults, *jobResult) + } + // Store results in session for pagination sessionID := fmt.Sprintf("upload_%d", time.Now().UnixNano()) uploadSession := UploadSession{ - Results: allResults, + Results: allResults, // Keep original results for backward compatibility + GroupedResults: groupedResults, TotalJobs: len(jobs), TotalSuccess: 0, TotalFailure: 0, @@ -469,8 +572,21 @@ func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { // Store in global map (in production, use Redis or database) uploadSessions[sessionID] = uploadSession - // Return first page of results - renderUploadResultsPage(w, sessionID, 1, 20) // Default to page 1, 20 items per page + // Get configurable page size from form, with fallback to default + limitStr := r.FormValue("limit") + limit := utils.DefaultPageSize + if limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 { + if parsedLimit > utils.MaxPageSize { + limit = utils.MaxPageSize + } else { + limit = parsedLimit + } + } + } + + // Return first page of results with configurable page size + renderUploadResultsPage(w, sessionID, utils.DefaultPage, limit) } // UploadResultsHandler handles pagination for upload results @@ -483,12 +599,12 @@ func UploadResultsHandler(w http.ResponseWriter, r *http.Request) { page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { - page = 1 + page = utils.DefaultPage } limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) if limit < 1 { - limit = 20 + limit = utils.DefaultPageSize } renderUploadResultsPage(w, sessionID, page, limit) @@ -502,54 +618,49 @@ func renderUploadResultsPage(w http.ResponseWriter, sessionID string, page, limi return } - totalResults := len(uploadSession.Results) - totalPages := (totalResults + limit - 1) / limit // Ceiling division + totalResults := len(uploadSession.GroupedResults) + pagination := utils.CalculatePagination(totalResults, page, limit) - if page > totalPages && totalPages > 0 { - page = totalPages - } - - startIndex := (page - 1) * limit - endIndex := startIndex + limit + // Get results for this page + startIndex := (pagination.CurrentPage - 1) * pagination.Limit + endIndex := startIndex + pagination.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 { - startPage = page - 4 - endPage = page + 5 + pageResults := utils.GetPageResults(uploadSession.GroupedResults, startIndex, endIndex) + + // Add pagination info to each job result for the template + var resultsWithPagination []map[string]interface{} + for _, jobResult := range pageResults { + 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, } + resultsWithPagination = append(resultsWithPagination, resultMap) } data := map[string]interface{}{ - "Results": pageResults, + "Results": resultsWithPagination, "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, + "TotalResults": pagination.TotalResults, + "TotalPages": pagination.TotalPages, + "CurrentPage": pagination.CurrentPage, + "Limit": pagination.Limit, + "StartIndex": pagination.StartIndex, + "EndIndex": pagination.EndIndex, + "StartPage": pagination.StartPage, + "EndPage": pagination.EndPage, "SessionID": sessionID, } @@ -588,3 +699,124 @@ func (r *readCloserWithSize) Size() int64 { // DocumentFieldAddHandler and DocumentFieldRemoveHandler are REMOVED // as they are no longer needed with the multi-file input and new chip UI. + +// New handler: Serve a single job card with only one file (per-job file pagination) +func UploadJobFileHandler(w http.ResponseWriter, r *http.Request) { + sessionID := r.URL.Query().Get("session_id") + jobID := r.URL.Query().Get("job_id") + filePageStr := r.URL.Query().Get("file_page") + filePage := 1 + if filePageStr != "" { + if parsed, err := strconv.Atoi(filePageStr); err == nil && parsed > 0 { + filePage = parsed + } + } + + uploadSession, exists := uploadSessions[sessionID] + if !exists { + http.Error(w, "Upload session not found", http.StatusNotFound) + return + } + + // Find the job result + var jobResult *struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + } + for i := range uploadSession.GroupedResults { + if uploadSession.GroupedResults[i].JobID == jobID { + jobResult = &uploadSession.GroupedResults[i] + break + } + } + if jobResult == nil { + http.Error(w, "Job not found in session", http.StatusNotFound) + return + } + + totalFiles := len(jobResult.Files) + 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, + } + + tmpl := root.WebTemplates + if err := tmpl.ExecuteTemplate(w, "upload_result_card", data); err != nil { + log.Printf("Template execution error: %v", err) + return + } + return + } + + // Ensure filePage is within bounds + if filePage > totalFiles { + filePage = totalFiles + } + if filePage < 1 { + filePage = 1 + } + + // Create a copy of the job result with only the requested file + jobResultCopy := struct { + JobID string + FilesFound int + FilesUploaded int + Success bool + ErrorMsg string + Files []struct { + Name string + Success bool + Error string + FileSize int64 + } + }{ + JobID: jobResult.JobID, + FilesFound: jobResult.FilesFound, + FilesUploaded: jobResult.FilesUploaded, + Success: jobResult.Success, + ErrorMsg: jobResult.ErrorMsg, + Files: []struct { + Name string + Success bool + Error string + FileSize int64 + }{jobResult.Files[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, + } + + tmpl := root.WebTemplates + if err := tmpl.ExecuteTemplate(w, "upload_result_card", data); err != nil { + log.Printf("Template execution error: %v", err) + return + } +} diff --git a/internal/utils/pagination.go b/internal/utils/pagination.go new file mode 100644 index 0000000..89aa057 --- /dev/null +++ b/internal/utils/pagination.go @@ -0,0 +1,88 @@ +package utils + +import ( + "math" +) + +// Default pagination constants +const ( + DefaultPage = 1 + DefaultPageSize = 5 + MaxPageSize = 100 +) + +// PaginationInfo contains all the information needed for pagination +type PaginationInfo struct { + TotalResults int + TotalPages int + CurrentPage int + Limit int + StartIndex int + EndIndex int + StartPage int + EndPage int +} + +// CalculatePagination calculates pagination information based on total results, current page, and limit +func CalculatePagination(totalResults, currentPage, limit int) PaginationInfo { + if currentPage < 1 { + currentPage = 1 + } + if limit < 1 { + limit = DefaultPageSize + } + + totalPages := int(math.Ceil(float64(totalResults) / float64(limit))) + if totalPages < 1 { + totalPages = 1 + } + + // Ensure current page is within bounds + if currentPage > totalPages { + currentPage = totalPages + } + + startIndex := (currentPage - 1) * limit + endIndex := startIndex + limit + if endIndex > totalResults { + endIndex = totalResults + } + + // Calculate pagination range for display + startPage := 1 + endPage := totalPages + + // Show up to 10 page numbers, centered around current page when possible + if totalPages > 10 { + if currentPage <= 5 { + endPage = 10 + } else if currentPage >= totalPages-4 { + startPage = totalPages - 9 + } else { + startPage = currentPage - 4 + endPage = currentPage + 5 + } + } + + return PaginationInfo{ + TotalResults: totalResults, + TotalPages: totalPages, + CurrentPage: currentPage, + Limit: limit, + StartIndex: startIndex + 1, // Convert to 1-based for display + EndIndex: endIndex, + StartPage: startPage, + EndPage: endPage, + } +} + +// GetPageResults extracts the results for the current page from a slice +func GetPageResults[T any](allResults []T, startIndex, endIndex int) []T { + if startIndex >= len(allResults) { + return []T{} + } + if endIndex > len(allResults) { + endIndex = len(allResults) + } + return allResults[startIndex:endIndex] +} diff --git a/static/css/upload.css b/static/css/upload.css index 53e5dc2..83b4d76 100644 --- a/static/css/upload.css +++ b/static/css/upload.css @@ -62,7 +62,7 @@ justify-content: center; align-items: center; z-index: 1000; - border-radius: inherit; + border-radius: 0.5rem; backdrop-filter: blur(2px); } @@ -803,4 +803,71 @@ .file-chip-name { max-width: 80px; } +} + +/* File pagination controls within job cards */ +.file-pagination-controls { + margin-top: 0.75rem; + padding-top: 0.75rem; + border-top: 1px solid var(--input-border); + display: flex; + flex-direction: column; + gap: 0.5rem; + align-items: center; +} + +.file-pagination-controls span { + font-size: 0.875rem; + color: var(--label-color); + font-weight: 500; +} + +.file-pagination-buttons { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.file-pagination-buttons .pagination-btn { + background-color: var(--input-bg); + border: 1px solid var(--input-border); + color: var(--content-text); + padding: 0.375rem 0.75rem; + border-radius: 0.375rem; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.25rem; +} + +.file-pagination-buttons .pagination-btn:hover { + background-color: var(--btn-primary-bg); + color: white; + border-color: var(--btn-primary-bg); +} + +.file-pagination-buttons .pagination-btn:disabled { + opacity: 0.5; + cursor: not-allowed; + background-color: var(--progress-bg); +} + +@media (max-width: 768px) { + .file-pagination-controls { + margin-top: 0.5rem; + padding-top: 0.5rem; + } + + .file-pagination-buttons { + flex-direction: column; + width: 100%; + } + + .file-pagination-buttons .pagination-btn { + width: 100%; + justify-content: center; + } } \ No newline at end of file diff --git a/templates/partials/document_upload.html b/templates/partials/document_upload.html index e7f6a88..ea76bfb 100644 --- a/templates/partials/document_upload.html +++ b/templates/partials/document_upload.html @@ -1,96 +1,103 @@ {{define "document_upload"}}

Document Uploads

-
- -
- -
-
-
-

Uploading Documents

-

Please wait while your documents are being uploaded...

-
+
+ +
+
+
+

Uploading Documents

+

Please wait while your documents are being uploaded...

+
- - + + - -
- -
- - - - - - -
+ +
+ +
+ + + + - - - diff --git a/templates/partials/upload_result_card.html b/templates/partials/upload_result_card.html index 225d1b2..65a6068 100644 --- a/templates/partials/upload_result_card.html +++ b/templates/partials/upload_result_card.html @@ -1,8 +1,7 @@ {{define "upload_result_card"}} -
+
-

{{.DocName}}

+

Job #{{.JobID}}

{{if .Success}}✓ Success{{else}}✗ Failed{{end}}
@@ -10,12 +9,12 @@
-

Job ID: {{.JobID}}

-

File Size: {{printf "%.2f MB" (div .FileSize 1048576.0)}}

+

Files Found: {{.FilesFound}}

+

Files Uploaded: {{.FilesUploaded}}

{{if .Success}} -

Successfully uploaded to ServiceTrade

+

Successfully processed

{{else}} -

{{.Error}}

+

{{.ErrorMsg}}

{{end}}
@@ -33,6 +32,42 @@ {{end}}
+ + {{if .Files}} +
+ {{with index .Files 0}} +
+ {{.Name}} + ({{printf "%.2f MB" (div .FileSize 1048576.0)}}) + {{if .Success}} + + {{else}} + + {{.Error}} + {{end}} +
+ {{end}} +
+ File {{.FilePage}} of {{.TotalFiles}} +
+ {{if gt .FilePage 1}} + + {{end}} + {{if lt .FilePage .TotalFiles}} + + {{end}} +
+
+
+ {{end}}
{{end}} @@ -87,14 +122,15 @@ {{if gt .CurrentPage 1}} {{end}} {{range $i := sequence .StartPage .EndPage}} {{end}} @@ -102,7 +138,7 @@ {{if lt .CurrentPage .TotalPages}} {{end}} diff --git a/templates/partials/upload_results_pagination.html b/templates/partials/upload_results_pagination.html index e8bd3ca..c86c6aa 100644 --- a/templates/partials/upload_results_pagination.html +++ b/templates/partials/upload_results_pagination.html @@ -6,7 +6,7 @@
{{if gt .TotalSuccess 0}} -

Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{.TotalTime}}!

+

Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{formatDuration .TotalTime}}!

{{end}} {{if gt .TotalFailure 0}}

Failed to upload {{.TotalFailure}} document(s). See details below.

@@ -28,14 +28,15 @@ {{if gt .CurrentPage 1}} {{end}} {{range $i := sequence .StartPage .EndPage}} {{end}} @@ -43,7 +44,7 @@ {{if lt .CurrentPage .TotalPages}} {{end}} diff --git a/templates/partials/upload_stats.html b/templates/partials/upload_stats.html index ac8c972..8f806f3 100644 --- a/templates/partials/upload_stats.html +++ b/templates/partials/upload_stats.html @@ -17,7 +17,7 @@ MB Uploaded
- {{.TotalTime}} + {{formatDuration .TotalTime}} Total Time
diff --git a/web_templates.go b/web_templates.go index 7ddf9b4..4a51af3 100644 --- a/web_templates.go +++ b/web_templates.go @@ -2,10 +2,12 @@ package root import ( "embed" + "fmt" "html/template" "io/fs" "log" "path/filepath" + "time" ) //go:embed templates static/* @@ -38,6 +40,21 @@ var funcMap = template.FuncMap{ } return result }, + "formatDuration": func(d time.Duration) string { + seconds := d.Seconds() + if seconds < 60 { + return fmt.Sprintf("%.2fs", seconds) + } else if seconds < 3600 { + minutes := int(seconds) / 60 + remainingSeconds := seconds - float64(minutes*60) + return fmt.Sprintf("%dm %.2fs", minutes, remainingSeconds) + } else { + hours := int(seconds) / 3600 + remainingMinutes := int(seconds) % 3600 / 60 + remainingSeconds := seconds - float64(hours*3600) - float64(remainingMinutes*60) + return fmt.Sprintf("%dh %dm %.2fs", hours, remainingMinutes, remainingSeconds) + } + }, } // InitializeWebTemplates parses all HTML templates in the embedded filesystem