diff --git a/apps/web/main.go b/apps/web/main.go
index 8a4d19d..40feb0a 100644
--- a/apps/web/main.go
+++ b/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 == "" {
diff --git a/apps/web/web-app b/apps/web/web-app
new file mode 100644
index 0000000..c89706a
Binary files /dev/null and b/apps/web/web-app differ
diff --git a/internal/handlers/web/document_remove.go b/internal/handlers/web/document_remove.go
index 8fd5400..31ae675 100644
--- a/internal/handlers/web/document_remove.go
+++ b/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("
")
- resultHTML.WriteString("
Upload Results
")
- resultHTML.WriteString("
")
- resultHTML.WriteString(fmt.Sprintf("
", len(results)))
- resultHTML.WriteString(fmt.Sprintf("
", totalSuccess))
- resultHTML.WriteString(fmt.Sprintf("
", totalFailure))
- resultHTML.WriteString(fmt.Sprintf("
", resultsCount))
- resultHTML.WriteString(fmt.Sprintf("
", totalDuration))
+ // Store in global map (in production, use Redis or database)
+ uploadSessions[sessionID] = uploadSession
- if totalFailure == 0 && resultsCount > 0 {
- resultHTML.WriteString(fmt.Sprintf("
All documents were successfully uploaded to ServiceTrade in %v!
", totalDuration))
- } else if resultsCount == 0 {
- resultHTML.WriteString("
No documents were processed for upload.
")
- } else {
- resultHTML.WriteString(fmt.Sprintf("
Some documents failed to upload. Process completed in %v. See details below.
", totalDuration))
- }
- resultHTML.WriteString("
")
-
- resultHTML.WriteString("
")
- 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("
", jobClass))
- resultHTML.WriteString(fmt.Sprintf("
Job ID: %s
", jobID))
- if len(jobResults) > 0 {
- resultHTML.WriteString("
")
- 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, "<", "<")
- message = strings.ReplaceAll(message, ">", ">")
- }
- resultHTML.WriteString(fmt.Sprintf("
", fileClass))
- resultHTML.WriteString(fmt.Sprintf("%s", icon))
- resultHTML.WriteString(fmt.Sprintf("%s:", result.DocName))
- resultHTML.WriteString(fmt.Sprintf("%s", message))
- resultHTML.WriteString("
")
- }
- resultHTML.WriteString("
")
+ // 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("
No file upload results for this job.
")
+ startPage = page - 4
+ endPage = page + 5
}
- resultHTML.WriteString("
")
}
- resultHTML.WriteString("
")
- 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
diff --git a/static/css/styles.css b/static/css/styles.css
index 4520130..6c3fe9b 100644
--- a/static/css/styles.css
+++ b/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;
+ }
}
\ No newline at end of file
diff --git a/static/css/upload.css b/static/css/upload.css
index 165d757..53e5dc2 100644
--- a/static/css/upload.css
+++ b/static/css/upload.css
@@ -1,698 +1,806 @@
-/* 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-specific CSS Variables (extending styles.css) */
+:root {
+ /* Upload-specific colors */
+ --upload-success-color: #22c55e;
+ --upload-error-color: #ef4444;
+ --upload-warning-color: #eab308;
+ --upload-info-color: #4299e1;
+
+ /* Upload overlay colors */
+ --upload-overlay-bg: rgba(0, 0, 0, 0.7);
+ --upload-overlay-content-bg: transparent;
+
+ /* Upload card colors */
+ --upload-card-bg: var(--content-bg);
+ --upload-card-border: var(--content-border);
+ --upload-card-shadow: var(--dashboard-shadow);
+
+ /* Upload stats colors */
+ --upload-stat-bg: var(--input-bg);
+ --upload-stat-border: var(--input-border);
+ --upload-stat-success-bg: rgba(34, 197, 94, 0.1);
+ --upload-stat-error-bg: rgba(239, 68, 68, 0.1);
+ --upload-stat-warning-bg: rgba(234, 179, 8, 0.1);
}
-.upload-stats {
- display: flex;
- gap: 1.5rem;
- margin-bottom: 1rem;
+@media (prefers-color-scheme: dark) {
+ :root {
+ /* Upload-specific colors for dark theme */
+ --upload-overlay-bg: rgba(0, 0, 0, 0.8);
+ --upload-overlay-content-bg: transparent;
+
+ /* Upload card colors for dark theme */
+ --upload-card-bg: rgba(30, 33, 43, 0.8);
+ --upload-card-border: none;
+ --upload-card-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+
+ /* Upload stats colors for dark theme */
+ --upload-stat-bg: rgba(30, 33, 43, 0.8);
+ --upload-stat-border: none;
+ --upload-stat-success-bg: rgba(52, 211, 153, 0.1);
+ --upload-stat-error-bg: rgba(248, 113, 113, 0.1);
+ --upload-stat-warning-bg: rgba(245, 158, 11, 0.1);
+ }
}
-.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;
+/* Upload Container */
+.upload-container {
+ position: relative;
+ min-height: 400px;
}
-.stat-value {
- font-size: 2rem;
- font-weight: 600;
- margin-bottom: 0.25rem;
- color: var(--content-text);
+/* Upload Overlay - Fixed positioning to be contained within upload section */
+.upload-overlay,
+.removal-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: var(--upload-overlay-bg);
+ display: none;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ border-radius: inherit;
+ backdrop-filter: blur(2px);
}
-.stat-label {
- font-size: 0.875rem;
- color: var(--label-color);
+.upload-overlay.htmx-request,
+.removal-overlay.htmx-request {
+ display: flex;
}
-.success-stat .stat-value {
- color: var(--btn-success-bg);
+.upload-overlay-content {
+ background-color: var(--upload-overlay-content-bg);
+ padding: 2rem;
+ border-radius: 0.5rem;
+ text-align: center;
+ max-width: 400px;
+ width: 90%;
}
-.error-stat .stat-value {
- color: var(--btn-warning-bg);
+.upload-overlay h3,
+.removal-overlay h3 {
+ margin: 0 0 1rem 0;
+ color: white;
+ font-size: 1.5rem;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
-/* Job Results Styles */
-.job-results {
- margin-top: 1.5rem;
+.upload-overlay p,
+.removal-overlay p {
+ margin: 0 0 1.5rem 0;
+ color: white;
+ text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
-.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);
+.upload-overlay .overlay-spinner,
+.removal-overlay .overlay-spinner {
+ margin-bottom: 1rem;
}
-.job-result.success {
- border-left-color: var(--btn-success-bg);
+/* Overlay Spinner */
+.overlay-spinner {
+ width: 50px;
+ height: 50px;
+ margin: 0 auto;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-radius: 50%;
+ border-top-color: white;
+ animation: spin 1s ease-in-out infinite;
}
-.job-result.error {
- border-left-color: var(--btn-warning-bg);
-}
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
-.job-id {
- font-weight: 600;
- display: block;
- margin-bottom: 0.5rem;
- color: var(--dashboard-header-color);
+ 100% {
+ transform: rotate(360deg);
+ }
}
-.file-results {
- margin-left: 1rem;
- margin-top: 0.5rem;
+/* File Chip and Modal Styles */
+.selected-files-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem;
+ margin-top: 1rem;
+ margin-bottom: 1rem;
}
-.file-result {
- padding: 0.75rem;
- margin-bottom: 0.5rem;
- border-radius: 4px;
+.file-chip {
+ background-color: var(--input-bg);
+ border: 1px solid var(--input-border);
+ border-radius: 16px;
+ padding: 0.4rem 0.8rem;
display: flex;
align-items: center;
- background-color: var(--input-bg);
- border: var(--input-border);
+ font-size: 0.875rem;
+ box-shadow: var(--dashboard-shadow);
+ transition: background-color 0.2s, opacity 0.2s, box-shadow 0.2s;
+ color: var(--content-text);
}
-.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-chip:hover {
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
-.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);
+.file-chip.removed {
+ opacity: 0.65;
+ background-color: var(--progress-bg);
+ text-decoration: line-through;
+ box-shadow: none;
}
-.status-icon {
- font-size: 1.25rem;
- margin-right: 0.75rem;
+.file-chip.removed .file-chip-name {
+ color: var(--label-color);
}
-.success .status-icon {
- color: var(--btn-success-bg);
+.file-chip-icon {
+ margin-right: 0.5rem;
+ font-size: 1.1em;
+ color: var(--btn-primary-bg);
}
-.error .status-icon {
- color: var(--btn-warning-bg);
+.file-chip.removed .file-chip-icon {
+ color: var(--label-color);
}
-.file-name {
- font-weight: 500;
+.file-chip-name {
margin-right: 0.5rem;
- color: var(--content-text);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 150px;
+ cursor: pointer;
}
-.file-message {
+.file-chip-doctype {
+ font-size: 0.8em;
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);
+ padding: 0.1em 0.4em;
+ border-radius: 4px;
+ margin-right: 0.5rem;
+ white-space: nowrap;
}
-.progress-info {
- display: flex;
- align-items: center;
- justify-content: space-between;
- margin-bottom: 0.75rem;
+.file-chip-edit,
+.file-chip-remove {
+ background: none;
+ border: none;
+ color: var(--label-color);
+ cursor: pointer;
+ font-size: 1em;
+ padding: 0 0.25rem;
+ margin-left: 0.25rem;
+ line-height: 1;
+ opacity: 0.8;
}
-.progress-info span {
- font-weight: 500;
+.file-chip-edit:hover,
+.file-chip-remove:hover {
color: var(--content-text);
+ opacity: 1;
}
-/* Original Spinner - used for general loading indicators */
-.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;
+.file-chip.removed .file-chip-edit,
+.file-chip.removed .file-chip-remove {
+ color: var(--label-color);
+ cursor: not-allowed;
+ opacity: 0.5;
}
-/* Larger spinner specifically for overlays */
-.overlay-spinner {
- width: 50px;
- height: 50px;
- margin: 0 auto;
- border: 3px solid rgba(255, 255, 255, 0.3);
- border-radius: 50%;
- border-top-color: white;
- animation: spin 1s ease-in-out infinite;
+#no-files-selected-placeholder {
+ color: var(--label-color);
+ font-style: italic;
+ width: 100%;
+ text-align: center;
+ padding: 1rem 0;
}
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
-
- 100% {
- transform: rotate(360deg);
- }
+/* Modal Styles */
+.modal {
+ display: none;
+ position: fixed;
+ z-index: 1001;
+ left: 0;
+ top: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+ background-color: rgba(0, 0, 0, 0.4);
+ justify-content: center;
+ align-items: center;
}
-.progress {
- height: 0.5rem;
- background-color: var(--progress-bg, #e2e8f0);
- border-radius: 999px;
- overflow: hidden;
+.modal-content {
+ background-color: var(--content-bg);
+ color: var(--content-text);
+ margin: auto;
+ padding: 20px;
+ border: 1px solid var(--input-border);
+ border-radius: 8px;
+ width: 90%;
+ max-width: 500px;
+ box-shadow: var(--dashboard-shadow);
}
-.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;
+.modal-content .form-group label {
+ font-weight: bold;
}
-@keyframes progress-animation {
- 0% {
- background-position: 1rem 0;
- }
-
- 100% {
- background-position: 0 0;
- }
+.modal-content .card-input {
+ width: 100%;
+ box-sizing: border-box;
}
-#upload-status {
- margin-top: 0.75rem;
- font-size: 0.875rem;
+.close-button {
color: var(--label-color);
+ float: right;
+ font-size: 1.75rem;
+ font-weight: bold;
+ line-height: 1;
+ padding: 0;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ opacity: 0.7;
}
-.pulsing {
- animation: pulse 2s infinite;
+.close-button:hover {
+ color: var(--content-text);
+ opacity: 1;
}
-@keyframes pulse {
- 0% {
- opacity: 0.6;
- }
-
- 50% {
- opacity: 1;
- }
-
- 100% {
- opacity: 0.6;
- }
+.modal-content .form-actions {
+ margin-top: 1.5rem;
+ text-align: right;
}
-.advice {
- font-weight: 500;
- color: var(--btn-primary-bg);
+.modal-content .btn-primary,
+.modal-content .btn-secondary {
+ margin-left: 0.5rem;
}
-/* CSV Preview styles */
-#csv-preview,
-#csv-preview-removal {
+/* Upload Results Container */
+.upload-results-container {
+ background-color: var(--upload-card-bg);
+ border-radius: 0.5rem;
+ box-shadow: var(--upload-card-shadow);
+ padding: 1.5rem;
margin-top: 1rem;
- padding: 1rem;
- background-color: var(--content-bg);
- border-radius: 6px;
- border-left: 4px solid var(--btn-primary-bg);
+ max-width: 100%;
}
-.csv-sample {
- margin-top: 0.5rem;
- padding: 0.75rem;
- background-color: var(--input-bg);
- border-radius: 4px;
- box-shadow: var(--dashboard-shadow);
+.upload-results-header {
+ margin-bottom: 1.5rem;
}
-#csv-preview p,
-#csv-preview-removal p {
- margin: 0.5rem 0;
- color: var(--content-text);
+.upload-results-header h3 {
+ color: var(--text-color);
+ font-size: 1.5rem;
+ margin: 0 0 1rem 0;
}
-/* HTMX indicator styles */
-.htmx-indicator {
- display: none;
- opacity: 0;
- transition: opacity 200ms ease-in;
+/* Upload Stats Grid */
+.upload-stats {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1rem;
+ margin-bottom: 1.5rem;
+ max-width: 600px;
}
-.htmx-request .htmx-indicator {
- opacity: 1;
+.stat-item {
+ background-color: var(--upload-stat-bg);
+ border-radius: 0.5rem;
+ padding: 1.25rem;
+ text-align: center;
display: flex;
+ flex-direction: column;
align-items: center;
justify-content: center;
+ min-width: 120px;
}
-.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;
+.stat-item:last-child {
+ grid-column: 1;
}
-:root.dark-theme .spinner,
-:root.dark-theme .loading-indicator {
- border-color: rgba(255, 255, 255, 0.1);
- border-top-color: var(--btn-primary-bg);
+.stat-value {
+ font-size: 2.5rem;
+ font-weight: bold;
+ color: var(--text-color);
+ line-height: 1.2;
+ margin-bottom: 0.5rem;
}
-/* Tab buttons */
-.tab-buttons {
- display: flex;
- margin-bottom: 1rem;
- border-bottom: 1px solid var(--dropdown-border);
+.stat-label {
+ font-size: 0.875rem;
+ color: var(--label-color);
+ text-align: center;
}
-.tab-button {
- padding: 0.5rem 1rem;
- background: var(--card-bg);
- border: 1px solid var(--dropdown-border);
- border-bottom: none;
- border-radius: 4px 4px 0 0;
- color: var(--content-text);
- cursor: pointer;
- margin-right: 0.5rem;
- position: relative;
- bottom: -1px;
+/* Stat item variants */
+.success-stat .stat-value {
+ color: var(--upload-success-color);
}
-.tab-button.active {
- background: var(--content-bg);
- border-bottom: 1px solid var(--content-bg);
- font-weight: bold;
+.error-stat .stat-value {
+ color: var(--upload-error-color);
}
-/* Checkbox styles */
-.checkbox-group {
+.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-wrap: wrap;
- gap: 0.5rem 1.5rem;
- margin: 0.5rem 0 1rem 0;
+ flex-direction: column;
+ gap: 0.75rem;
}
-.checkbox-item {
+.upload-header {
display: flex;
+ justify-content: space-between;
align-items: center;
+ gap: 1rem;
}
-.checkbox-item input[type="checkbox"] {
- margin-right: 0.5rem;
+.upload-header h4 {
+ margin: 0;
+ color: var(--text-color);
+ font-size: 1rem;
+ font-weight: 500;
+ flex: 1;
}
-/* Form groups */
-.form-group {
- margin-bottom: 1rem;
+.upload-status {
+ padding: 0.25rem 0.75rem;
+ border-radius: 1rem;
+ font-size: 0.875rem;
+ font-weight: 500;
}
-.form-group label {
- display: block;
- margin-bottom: 0.5rem;
- color: var(--label-color);
- font-weight: bold;
+.upload-status.success {
+ background-color: var(--upload-stat-success-bg);
+ color: var(--upload-success-color);
}
-.form-actions {
- margin-top: 1.5rem;
+.upload-status.error {
+ background-color: var(--upload-stat-error-bg);
+ color: var(--upload-error-color);
+}
+
+.upload-details {
display: flex;
- align-items: center;
- gap: 1rem;
+ flex-direction: column;
+ gap: 0.5rem;
}
-/* Job summary in results */
-.job-summary {
- margin: 0.5rem 0;
- font-size: 0.9rem;
- color: var(--soft-text);
+.upload-info {
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
}
-/* Upload Overlay */
-.upload-container {
- position: relative;
- min-height: 200px;
- width: 100%;
+.upload-info p {
+ margin: 0;
+ font-size: 0.875rem;
+ color: var(--content-text);
}
-.upload-overlay,
-.removal-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(135deg, rgba(255, 255, 255, 0.95), rgba(240, 240, 240, 0.95));
- display: none;
- justify-content: center;
- align-items: center;
- z-index: 1000;
- border-radius: inherit;
+.success-text {
+ color: var(--upload-success-color);
+ font-weight: 500;
}
-.upload-overlay.htmx-request,
-.removal-overlay.htmx-request {
+.error-text {
+ color: var(--upload-error-color);
+ font-weight: 500;
+}
+
+/* Pagination Controls */
+.pagination-controls {
display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 2rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--content-border);
}
-.upload-overlay-content {
+.pagination-info {
+ font-size: 0.875rem;
+ color: var(--label-color);
+}
+
+.pagination-buttons {
display: flex;
- flex-direction: column;
+ gap: 0.5rem;
align-items: center;
- justify-content: center;
- text-align: center;
- max-width: 80%;
}
-.upload-overlay h3,
-.removal-overlay h3 {
- margin-bottom: 0.5rem;
- color: #333;
+.pagination-btn {
+ padding: 0.5rem 1rem;
+ border: 1px solid var(--input-border);
+ background-color: var(--input-bg);
+ color: var(--input-text);
+ border-radius: 0.25rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ transition: all 0.2s ease;
}
-.upload-overlay p,
-.removal-overlay p {
- margin-top: 0.5rem;
- color: #555;
+.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 {
+ opacity: 0.5;
+ cursor: not-allowed;
+ background-color: var(--btn-disabled);
+ border-color: var(--btn-disabled);
+}
+
+/* Upload Summary (for removal results) */
+.upload-summary {
+ margin: 1.5rem 0;
+ padding: 1.5rem;
+ border-radius: 0.5rem;
+ background-color: var(--upload-card-bg);
+ box-shadow: var(--upload-card-shadow);
+ color: var(--content-text);
}
-.upload-overlay .overlay-spinner,
-.removal-overlay .overlay-spinner {
- width: 50px;
- height: 50px;
- border: 5px solid #f3f3f3;
- border-top: 5px solid #3498db;
- border-radius: 50%;
- animation: spin 1s linear infinite;
+.upload-summary h3 {
+ margin-top: 0;
+ font-size: 1.25rem;
+ color: var(--dashboard-header-color);
margin-bottom: 1rem;
}
-/* Dark theme support */
-@media (prefers-color-scheme: dark) {
+/* Job Results (for removal results) */
+.job-results {
+ margin-top: 1.5rem;
+}
- .upload-overlay,
- .removal-overlay {
- background: linear-gradient(135deg, rgba(30, 30, 30, 0.95), rgba(20, 20, 20, 0.95));
- }
+.job-result {
+ padding: 1rem;
+ margin-bottom: 0.5rem;
+ border-radius: 0.5rem;
+ border: var(--upload-card-border);
+ border-left-width: 4px;
+ background-color: var(--upload-card-bg);
+ box-shadow: var(--upload-card-shadow);
+ color: var(--content-text);
+}
- .upload-overlay h3,
- .removal-overlay h3 {
- color: #fff;
- }
+.job-result.success {
+ border-left-color: var(--upload-success-color);
+}
- .upload-overlay p,
- .removal-overlay p {
- color: #ccc;
- }
+.job-result.error {
+ border-left-color: var(--upload-error-color);
}
-@keyframes spin {
- 0% {
- transform: rotate(0deg);
- }
+.job-id {
+ font-weight: 600;
+ display: block;
+ margin-bottom: 0.5rem;
+ color: var(--dashboard-header-color);
+}
- 100% {
- transform: rotate(360deg);
- }
+.job-summary {
+ font-size: 0.875rem;
+ color: var(--label-color);
+ margin-bottom: 0.5rem;
}
-/* Styles for Modal and File Chips */
+.error-message {
+ color: var(--upload-error-color);
+ font-size: 0.875rem;
+ margin-bottom: 0.5rem;
+}
-/* Basic Modal Styling */
-.modal {
- display: none;
- /* Hidden by default */
- position: fixed;
- /* Stay in place */
- z-index: 1001;
- /* Sit on top, above overlays which are 1000 */
- left: 0;
- top: 0;
- width: 100%;
- /* Full width */
- height: 100%;
- /* Full height */
- overflow: auto;
- /* Enable scroll if needed */
- background-color: rgba(0, 0, 0, 0.4);
- /* Semi-transparent black backdrop for light theme */
- justify-content: center;
- align-items: center;
+.file-results {
+ margin-left: 1rem;
+ margin-top: 0.5rem;
}
-.modal-content {
- background-color: var(--content-bg);
- color: var(--content-text);
- margin: auto;
- padding: 20px;
- /* Standard padding */
- border: 1px solid var(--input-border);
- border-radius: 8px;
- /* Consistent border-radius */
- width: 90%;
- /* Responsive width */
- max-width: 500px;
- /* Max width for the modal */
- box-shadow: var(--dashboard-shadow);
+.file-result {
+ padding: 0.75rem;
+ margin-bottom: 0.5rem;
+ border-radius: 0.25rem;
+ display: flex;
+ align-items: center;
+ background-color: var(--upload-stat-bg);
+ border: var(--upload-stat-border);
}
-/* Modal uses existing .form-group, ensure label is bold if that's the standard */
-.modal-content .form-group label {
- /* font-weight: bold; Inherits from global .form-group label if already bold */
+.file-result.success {
+ background-color: var(--upload-stat-success-bg);
+ border-color: rgba(34, 197, 94, 0.3);
}
-.modal-content .card-input {
- /* Ensure modal inputs are full width and box-sizing */
- width: 100%;
- box-sizing: border-box;
+.file-result.error {
+ background-color: var(--upload-stat-error-bg);
+ border-color: rgba(239, 68, 68, 0.3);
}
-.close-button {
- color: var(--label-color);
- float: right;
- font-size: 1.75rem;
- /* Clearer size */
- font-weight: bold;
- line-height: 1;
- padding: 0;
- background: transparent;
- border: none;
- cursor: pointer;
- opacity: 0.7;
+.status-icon {
+ font-size: 1.25rem;
+ margin-right: 0.75rem;
}
-.close-button:hover {
- color: var(--content-text);
- opacity: 1;
+.success .status-icon {
+ color: var(--upload-success-color);
}
-/* File Chip Styling */
-.selected-files-grid {
- display: flex;
- flex-wrap: wrap;
- gap: 0.75rem;
- /* padding: 0.5rem 0; No specific padding, gap handles spacing */
- margin-top: 1rem;
- /* Retain consistent margins */
- margin-bottom: 1rem;
+.error .status-icon {
+ color: var(--upload-error-color);
}
-.file-chip {
- background-color: var(--input-bg);
- border: 1px solid var(--input-border);
- border-radius: 16px;
- /* Pill shape for chips */
- padding: 0.4rem 0.8rem;
- display: flex;
- align-items: center;
- font-size: 0.875rem;
- /* Slightly smaller font for chips */
- box-shadow: var(--dashboard-shadow);
- transition: background-color 0.2s, opacity 0.2s, box-shadow 0.2s;
+.file-name {
+ font-weight: 500;
+ margin-right: 0.5rem;
color: var(--content-text);
}
-.file-chip:hover {
- box-shadow: var(--button-shadow);
- /* Use button-shadow for hover if defined and suitable */
+.file-message {
+ color: var(--label-color);
+ font-size: 0.875rem;
}
-.file-chip.removed {
- opacity: 0.65;
- /* Make it visibly less prominent */
- background-color: var(--progress-bg);
- /* Using a neutral theme color */
- text-decoration: line-through;
- box-shadow: none;
- /* No shadow for removed items */
+/* Form Elements */
+.form-group {
+ margin-bottom: 1.5rem;
}
-.file-chip.removed .file-chip-name {
+.form-group label {
+ display: block;
+ margin-bottom: 0.5rem;
+ font-weight: 500;
color: var(--label-color);
- /* Dim the text of removed items */
}
-.file-chip-icon {
- margin-right: 0.5rem;
- font-size: 1.1em;
- /* Relative to chip's font size */
- color: var(--btn-primary-bg);
- /* Use a theme color for the icon */
+.form-actions {
+ display: flex;
+ gap: 1rem;
+ justify-content: flex-end;
+ margin-top: 2rem;
+ padding-top: 1rem;
+ border-top: 1px solid var(--content-border);
}
-.file-chip.removed .file-chip-icon {
- color: var(--label-color);
- /* Dimmed icon for removed items */
+.checkbox-group {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 1rem;
}
-.file-chip-name {
- margin-right: 0.5rem;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 150px;
- /* Adjust as needed */
- cursor: pointer;
- /* For the title attribute & clickability */
+.checkbox-item {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
}
-.file-chip-doctype {
- font-size: 0.8em;
- /* Smaller than the filename */
- color: var(--label-color);
- background-color: var(--content-bg);
- /* Slightly different background */
- padding: 0.1em 0.4em;
- border-radius: 4px;
- margin-right: 0.5rem;
- white-space: nowrap;
+.checkbox-item input[type="checkbox"] {
+ margin: 0;
}
-.file-chip-edit,
-.file-chip-remove {
- background: none;
+/* Warning Button */
+.warning-button {
+ background-color: var(--btn-warning-bg);
+ color: white;
+ padding: 0.75rem 1.5rem;
border: none;
- color: var(--label-color);
- /* Subtler color for actions */
+ border-radius: 0.25rem;
cursor: pointer;
- font-size: 1em;
- /* Relative to chip's font size */
- padding: 0 0.25rem;
- margin-left: 0.25rem;
- /* Slight spacing */
- line-height: 1;
- opacity: 0.8;
+ font-size: 1rem;
+ font-weight: 500;
+ transition: background-color 0.2s ease;
}
-.file-chip-edit:hover,
-.file-chip-remove:hover {
- color: var(--content-text);
- /* Darker on hover for clarity */
- opacity: 1;
+.warning-button:hover {
+ background-color: var(--btn-warning-hover);
}
-.file-chip.removed .file-chip-edit,
-.file-chip.removed .file-chip-remove {
- /* Actions on removed chips */
- color: var(--label-color);
+.warning-button:disabled {
+ background-color: var(--btn-disabled);
cursor: not-allowed;
- opacity: 0.5;
- /* Make them more faded */
}
-#no-files-selected-placeholder {
- color: var(--label-color);
- font-style: italic;
- width: 100%;
- /* Take full width */
- text-align: center;
- /* Center placeholder text */
- padding: 1rem 0;
- /* Give it some space */
+/* CSV Preview */
+#csv-preview,
+#csv-preview-removal {
+ background-color: var(--upload-stat-success-bg);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ margin: 1rem 0;
+ display: none;
}
-/* Modal Buttons Styling */
-.modal-content .form-actions {
- /* If you have a .form-actions div in modal */
- margin-top: 1.5rem;
- /* Space above action buttons */
- text-align: right;
- /* Align buttons to the right */
+#csv-preview h4,
+#csv-preview-removal h4 {
+ color: var(--upload-success-color);
+ margin: 0 0 0.5rem 0;
+ font-size: 1.125rem;
}
-.modal-content .btn-primary,
-.modal-content .btn-secondary {
- margin-left: 0.5rem;
- /* Space between buttons if aligned right */
+#csv-preview p,
+#csv-preview-removal p {
+ margin: 0;
+ color: var(--content-text);
+ font-size: 0.875rem;
}
-/* If not using .form-actions, direct styling: */
-/* .modal-content > button { margin-top: 1rem; margin-right: 0.5rem; } */
-
+.csv-preview-active {
+ display: block;
+}
-/* Dark theme adjustments for modal backdrop & specific modal elements */
-/* Using @media (prefers-color-scheme: dark) as per existing structure at end of file */
+/* Dark theme adjustments for modal */
@media (prefers-color-scheme: dark) {
.modal {
background-color: rgba(20, 20, 20, 0.75);
- /* Darker, more opaque backdrop for dark theme */
}
.close-button {
- /* Ensure close button is visible in dark mode */
color: var(--label-color);
- /* Should pick up dark theme var */
}
.close-button:hover {
color: var(--content-text);
- /* Should pick up dark theme var */
}
- /* Specific modal button styling for dark theme if needed, e.g., for btn-secondary */
.modal-content .btn-secondary {
- /* Assuming global .btn-secondary might not be fully dark-theme aware */
- /* background-color: var(--input-bg); Picks up dark theme variable */
- /* color: var(--content-text); Picks up dark theme variable */
- /* border: 1px solid var(--input-border); Picks up dark theme variable */
+ background-color: var(--input-bg);
+ color: var(--content-text);
+ border: 1px solid var(--input-border);
+ }
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .upload-results-header {
+ flex-direction: column;
+ gap: 1rem;
+ }
+
+ .upload-stats {
+ grid-template-columns: 1fr;
+ max-width: none;
+ }
+
+ .upload-results-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .upload-details {
+ flex-direction: column;
+ }
+
+ .upload-actions {
+ flex-direction: column;
+ gap: 0.5rem;
}
- /* .modal-content .btn-secondary:hover { */
- /* background-color: var(--content-bg); Slightly different hover for dark */
- /* } */
+ .pagination-controls {
+ flex-direction: column;
+ gap: 1rem;
+ align-items: stretch;
+ }
+
+ .pagination-buttons {
+ justify-content: center;
+ flex-wrap: wrap;
+ }
+
+ .checkbox-group {
+ flex-direction: column;
+ gap: 0.5rem;
+ }
+
+ .form-actions {
+ flex-direction: column;
+ }
+
+ .selected-files-grid {
+ gap: 0.5rem;
+ }
+
+ .file-chip {
+ padding: 0.3rem 0.6rem;
+ font-size: 0.8rem;
+ }
+
+ .file-chip-name {
+ max-width: 100px;
+ }
+}
+
+@media (max-width: 480px) {
+ .upload-overlay-content {
+ padding: 1.5rem;
+ margin: 1rem;
+ }
+
+ .upload-results-container {
+ padding: 1rem;
+ }
+
+ .stat-item {
+ padding: 1rem;
+ }
+
+ .stat-value {
+ font-size: 2rem;
+ }
+
+ .upload-result-card {
+ padding: 1rem;
+ }
+
+ .upload-header {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 0.5rem;
+ }
+
+ .modal-content {
+ padding: 1rem;
+ margin: 1rem;
+ }
+
+ .file-chip {
+ padding: 0.25rem 0.5rem;
+ font-size: 0.75rem;
+ }
+
+ .file-chip-name {
+ max-width: 80px;
+ }
}
\ No newline at end of file
diff --git a/templates/partials/removal_result_card.html b/templates/partials/removal_result_card.html
new file mode 100644
index 0000000..1674ef6
--- /dev/null
+++ b/templates/partials/removal_result_card.html
@@ -0,0 +1,52 @@
+{{define "removal_result_card"}}
+
+
+
+
+
+
Files Found: {{.FilesFound}}
+
Files Removed: {{.FilesRemoved}}
+ {{if .Success}}
+
Successfully processed
+ {{else}}
+
{{.ErrorMsg}}
+ {{end}}
+
+
+
+ {{if .Success}}
+
+ ✓
+ Removal Complete
+
+ {{else}}
+
+ ✗
+ Removal Failed
+
+ {{end}}
+
+
+
+ {{if .Files}}
+
+ {{range .Files}}
+
+ {{.Name}}
+ {{if .Success}}
+ ✓
+ {{else}}
+ ✗
+ {{.Error}}
+ {{end}}
+
+ {{end}}
+
+ {{end}}
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/removal_results.html b/templates/partials/removal_results.html
index e43ab1f..c5de7dd 100644
--- a/templates/partials/removal_results.html
+++ b/templates/partials/removal_results.html
@@ -1,51 +1,54 @@
{{define "removal_results"}}
-
-
Document Removal Results
+
+
- {{if .Error}}
-
Error: {{.Error}}
- {{else}}
-
-
Successfully removed {{.SuccessCount}} document(s).
- {{if gt .ErrorCount 0}}
-
Failed to remove {{.ErrorCount}} document(s).
- {{end}}
- {{if gt .JobsProcessed 0}}
-
Processed {{.JobsProcessed}} job(s).
+ {{if gt .SuccessCount 0}}
+
Successfully removed {{.SuccessCount}} document(s) from ServiceTrade in {{.TotalTime}}!
+ {{end}}
+ {{if gt .ErrorCount 0}}
+
Failed to remove {{.ErrorCount}} document(s). See details below.
+ {{end}}
+
+
+ {{range .Results}}
+ {{template "removal_result_card" .}}
{{end}}
- {{if .Results}}
-
- {{range $job := .Results}}
-
-
Job #{{$job.JobID}}
+ {{if gt .TotalPages 1}}
+
{{end}}
- {{end}}
{{end}}
\ No newline at end of file
diff --git a/templates/partials/removal_stats.html b/templates/partials/removal_stats.html
new file mode 100644
index 0000000..ff88020
--- /dev/null
+++ b/templates/partials/removal_stats.html
@@ -0,0 +1,24 @@
+{{define "removal_stats"}}
+
+
+ {{.TotalJobs}}
+ Total Jobs
+
+
+ {{.SuccessCount}}
+ Successful
+
+
+ {{.ErrorCount}}
+ Failed
+
+
+ {{.TotalFiles}}
+ Files Processed
+
+
+ {{.TotalTime}}
+ Total Time
+
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/upload_result_card.html b/templates/partials/upload_result_card.html
new file mode 100644
index 0000000..225d1b2
--- /dev/null
+++ b/templates/partials/upload_result_card.html
@@ -0,0 +1,113 @@
+{{define "upload_result_card"}}
+
+
+
+
+
+
Job ID: {{.JobID}}
+
File Size: {{printf "%.2f MB" (div .FileSize 1048576.0)}}
+ {{if .Success}}
+
Successfully uploaded to ServiceTrade
+ {{else}}
+
{{.Error}}
+ {{end}}
+
+
+
+ {{if .Success}}
+
+ ✓
+ Upload Complete
+
+ {{else}}
+
+ ✗
+ Upload Failed
+
+ {{end}}
+
+
+
+{{end}}
+
+{{define "upload_results_pagination"}}
+
+
+
+ {{if gt .TotalSuccess 0}}
+
Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{.TotalTime}}!
+ {{end}}
+ {{if gt .TotalFailure 0}}
+
Failed to upload {{.TotalFailure}} document(s). See details below.
+ {{end}}
+
+
+ {{range .Results}}
+ {{template "upload_result_card" .}}
+ {{end}}
+
+
+ {{if gt .TotalPages 1}}
+
+ {{end}}
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/upload_results_pagination.html b/templates/partials/upload_results_pagination.html
new file mode 100644
index 0000000..e8bd3ca
--- /dev/null
+++ b/templates/partials/upload_results_pagination.html
@@ -0,0 +1,54 @@
+{{define "upload_results_pagination"}}
+
+
+
+ {{if gt .TotalSuccess 0}}
+
Successfully uploaded {{.TotalSuccess}} document(s) to ServiceTrade in {{.TotalTime}}!
+ {{end}}
+ {{if gt .TotalFailure 0}}
+
Failed to upload {{.TotalFailure}} document(s). See details below.
+ {{end}}
+
+
+ {{range .Results}}
+ {{template "upload_result_card" .}}
+ {{end}}
+
+
+ {{if gt .TotalPages 1}}
+
+ {{end}}
+
+{{end}}
\ No newline at end of file
diff --git a/templates/partials/upload_stats.html b/templates/partials/upload_stats.html
new file mode 100644
index 0000000..ac8c972
--- /dev/null
+++ b/templates/partials/upload_stats.html
@@ -0,0 +1,24 @@
+{{define "upload_stats"}}
+
+
+ {{.TotalJobs}}
+ Total Jobs
+
+
+ {{.TotalSuccess}}
+ Successful
+
+
+ {{.TotalFailure}}
+ Failed
+
+
+ {{printf "%.1f" (div .TotalBytesUploaded 1048576.0)}}
+ MB Uploaded
+
+
+ {{.TotalTime}}
+ Total Time
+
+
+{{end}}
\ No newline at end of file
diff --git a/web_templates.go b/web_templates.go
index 40688ee..7ddf9b4 100644
--- a/web_templates.go
+++ b/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