package web import ( "bytes" "encoding/csv" "fmt" "io" "log" "mime/multipart" "net/http" "regexp" "strconv" "strings" "time" root "marmic/servicetrade-toolbox" "marmic/servicetrade-toolbox/internal/api" ) // DocumentsHandler handles the document upload page func DocumentsHandler(w http.ResponseWriter, r *http.Request) { session, ok := r.Context().Value("session").(*api.Session) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } tmpl := root.WebTemplates data := map[string]interface{}{ "Title": "Document Uploads", "Session": session, } if r.Header.Get("HX-Request") == "true" { // For HTMX requests, just send the document_upload partial if err := tmpl.ExecuteTemplate(w, "document_upload", data); err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } } else { // For full page requests, first render document_upload into a buffer var contentBuf bytes.Buffer if err := tmpl.ExecuteTemplate(&contentBuf, "document_upload", data); err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } // Add the rendered content to the data for the layout data["BodyContent"] = contentBuf.String() // Now render the layout with our content if err := tmpl.ExecuteTemplate(w, "layout.html", data); err != nil { log.Printf("Template execution error: %v", err) http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } } } // ProcessCSVHandler processes a CSV file with job numbers func ProcessCSVHandler(w http.ResponseWriter, r *http.Request) { _, ok := r.Context().Value("session").(*api.Session) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Check if the request method is POST if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse the multipart form data with a 10MB limit if err := r.ParseMultipartForm(10 << 20); err != nil { http.Error(w, "Unable to parse form: "+err.Error(), http.StatusBadRequest) return } // Get the file from the form file, _, err := r.FormFile("csvFile") if err != nil { http.Error(w, "Error retrieving file: "+err.Error(), http.StatusBadRequest) return } defer file.Close() // Read the CSV data csvData, err := csv.NewReader(file).ReadAll() if err != nil { http.Error(w, "Error reading CSV file: "+err.Error(), http.StatusBadRequest) return } // Extract job numbers from the CSV var jobNumbers []string for _, row := range csvData { if len(row) > 0 && row[0] != "" { // Trim whitespace and skip headers or empty lines jobNum := strings.TrimSpace(row[0]) if jobNum != "Job Number" && jobNum != "JobNumber" && jobNum != "Job" && jobNum != "" { jobNumbers = append(jobNumbers, jobNum) } } } // Validate each job number (optional: could make API calls to verify they exist) var validJobNumbers []string validJobNumbers = append(validJobNumbers, jobNumbers...) // Generate HTML for job list var jobListHTML string if len(validJobNumbers) > 0 { // Add a hidden input with comma-separated job IDs for form submission jobListHTML = fmt.Sprintf("", strings.Join(validJobNumbers, ",")) jobListHTML += "" // Add JavaScript to make the CSV preview visible jobListHTML += "" } else { jobListHTML = "

No valid job numbers found in the CSV file.

" } w.Header().Set("Content-Type", "text/html") w.Write([]byte(jobListHTML)) } // UploadDocumentsHandler handles document uploads to jobs func UploadDocumentsHandler(w http.ResponseWriter, r *http.Request) { session, ok := r.Context().Value("session").(*api.Session) if !ok { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } // Check if the request method is POST if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Parse the multipart form with a 30MB limit if err := r.ParseMultipartForm(30 << 20); err != nil { http.Error(w, "Unable to parse form: "+err.Error(), http.StatusBadRequest) return } // Get the job numbers jobNumbers := r.FormValue("jobNumbers") if jobNumbers == "" { http.Error(w, "No job numbers provided", http.StatusBadRequest) return } // Split the job numbers jobs := strings.Split(jobNumbers, ",") if len(jobs) == 0 { http.Error(w, "No valid job numbers provided", http.StatusBadRequest) return } // Regular expression to match file field patterns filePattern := regexp.MustCompile(`document-file-(\d+)`) // Collect document data type DocumentData struct { File multipart.File Header *multipart.FileHeader Name string Type string Index int } var documents []DocumentData // First, identify all available indices var indices []int for key := range r.MultipartForm.File { if matches := filePattern.FindStringSubmatch(key); len(matches) > 1 { if index, err := strconv.Atoi(matches[1]); err == nil { indices = append(indices, index) } } } // Process each document for _, index := range indices { fileKey := fmt.Sprintf("document-file-%d", index) nameKey := fmt.Sprintf("document-name-%d", index) typeKey := fmt.Sprintf("document-type-%d", index) fileHeaders := r.MultipartForm.File[fileKey] if len(fileHeaders) == 0 { continue // Skip if no file uploaded } fileHeader := fileHeaders[0] file, err := fileHeader.Open() if err != nil { log.Printf("Error opening file %s: %v", fileHeader.Filename, err) continue } // Get document name (use filename if not provided) documentName := r.FormValue(nameKey) if documentName == "" { documentName = fileHeader.Filename } // Get document type documentType := r.FormValue(typeKey) if documentType == "" { log.Printf("No document type for file %s", fileHeader.Filename) continue } documents = append(documents, DocumentData{ File: file, Header: fileHeader, Name: documentName, Type: documentType, Index: index, }) } // Process each file and upload to each job results := make(map[string][]map[string]interface{}) for _, doc := range documents { defer doc.File.Close() // Read file content fileContent, err := io.ReadAll(doc.File) if err != nil { http.Error(w, fmt.Sprintf("Error reading file %s: %v", doc.Header.Filename, err), http.StatusInternalServerError) return } // Upload to each job for _, jobID := range jobs { // Call the ServiceTrade API result, err := session.UploadAttachment(jobID, doc.Name, doc.Type, fileContent) if err != nil { log.Printf("Error uploading %s to job %s: %v", doc.Name, jobID, err) if _, exists := results[jobID]; !exists { results[jobID] = []map[string]interface{}{} } results[jobID] = append(results[jobID], map[string]interface{}{ "filename": doc.Name, "success": false, "error": err.Error(), }) continue } // Record the success if _, exists := results[jobID]; !exists { results[jobID] = []map[string]interface{}{} } results[jobID] = append(results[jobID], map[string]interface{}{ "filename": doc.Name, "success": true, "data": result, }) log.Printf("Successfully uploaded %s to job %s", doc.Name, jobID) } } // Generate HTML for results var resultHTML bytes.Buffer resultHTML.WriteString("
") resultHTML.WriteString("

Upload Results

") for jobID, jobResults := range results { resultHTML.WriteString(fmt.Sprintf("
Job #%s
") } resultHTML.WriteString("
") w.Header().Set("Content-Type", "text/html") w.Write(resultHTML.Bytes()) } // DocumentFieldAddHandler generates a new document field for the form func DocumentFieldAddHandler(w http.ResponseWriter, r *http.Request) { // Generate a random ID for the new field newId := fmt.Sprintf("%d", time.Now().UnixNano()) // Create HTML for a new document row html := fmt.Sprintf(`
`, newId, newId, newId, newId, newId, newId, newId, newId, newId) w.Header().Set("Content-Type", "text/html") w.Write([]byte(html)) } // DocumentFieldRemoveHandler handles the removal of a document field func DocumentFieldRemoveHandler(w http.ResponseWriter, r *http.Request) { // We read the ID but don't need to use it for simple removal _ = r.URL.Query().Get("id") // Count how many document rows exist // For simplicity, we'll just return an empty response to remove the field // In a complete implementation, we'd check if this is the last field and handle that case w.Header().Set("Content-Type", "text/html") w.Write([]byte("")) }