an updated and hopefully faster version of the ST Toolbox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

363 lines
11 KiB

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("<input type='hidden' name='jobNumbers' value='%s'>", strings.Join(validJobNumbers, ","))
jobListHTML += "<ul class='job-list'>"
for _, jobNum := range validJobNumbers {
jobListHTML += fmt.Sprintf("<li data-job-id='%s'>Job #%s</li>", jobNum, jobNum)
}
jobListHTML += "</ul>"
// Add JavaScript to make the CSV preview visible
jobListHTML += "<script>document.getElementById('csv-preview').style.display = 'block';</script>"
} else {
jobListHTML = "<p>No valid job numbers found in the CSV file.</p>"
}
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("<div class='upload-results'>")
resultHTML.WriteString("<h4>Upload Results</h4>")
for jobID, jobResults := range results {
resultHTML.WriteString(fmt.Sprintf("<div class='job-result'><h5>Job #%s</h5><ul>", jobID))
for _, result := range jobResults {
filename := result["filename"].(string)
success := result["success"].(bool)
if success {
resultHTML.WriteString(fmt.Sprintf("<li class='success'>%s: Uploaded successfully</li>", filename))
} else {
errorMsg := result["error"].(string)
resultHTML.WriteString(fmt.Sprintf("<li class='error'>%s: %s</li>", filename, errorMsg))
}
}
resultHTML.WriteString("</ul></div>")
}
resultHTML.WriteString("</div>")
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(`
<div class="document-row" id="document-row-%s">
<div class="document-field">
<label>Select Document:</label>
<input class="card-input" type="file" id="document-file-%s" name="document-file-%s">
</div>
<div class="document-field-row">
<div class="document-field document-name-field">
<label>Document Name (optional):</label>
<input class="card-input" type="text" id="document-name-%s" name="document-name-%s"
placeholder="Document Name">
</div>
<div class="document-field document-type-field">
<label>Document Type:</label>
<select class="card-input" id="document-type-%s" name="document-type-%s">
<option value="">Select Document Type</option>
<option value="1">Job Paperwork</option>
<option value="2">Job Vendor Bill</option>
<option value="3">Job Quality Control Picture</option>
<option value="5">Deficiency Repair Proposal</option>
<option value="7">Generic Attachment</option>
<option value="8">Avatar Image</option>
<option value="9">Import</option>
<option value="10">Blank Paperwork</option>
<option value="11">Work Acknowledgement</option>
<option value="12">Logo</option>
<option value="14">Job Invoice</option>
</select>
</div>
</div>
<button type="button" class="remove-document warning-button"
hx-get="/document-field-remove?id=%s"
hx-target="#document-row-%s"
hx-swap="outerHTML">Remove</button>
</div>
`, 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(""))
}