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.
444 lines
13 KiB
444 lines
13 KiB
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/csv"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"path/filepath"
|
|
"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
|
|
}
|
|
|
|
if len(csvData) < 2 {
|
|
http.Error(w, "CSV file must contain at least a header row and one data row", http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Find the index of the 'id' column
|
|
headerRow := csvData[0]
|
|
idColumnIndex := -1
|
|
for i, header := range headerRow {
|
|
if strings.ToLower(strings.TrimSpace(header)) == "id" {
|
|
idColumnIndex = i
|
|
break
|
|
}
|
|
}
|
|
|
|
// If 'id' column not found, try the first column
|
|
if idColumnIndex == -1 {
|
|
idColumnIndex = 0
|
|
log.Printf("No 'id' column found in CSV, using first column (header: %s)", headerRow[0])
|
|
} else {
|
|
log.Printf("Found 'id' column at index %d", idColumnIndex)
|
|
}
|
|
|
|
// Extract job numbers from the CSV
|
|
var jobNumbers []string
|
|
for rowIndex, row := range csvData {
|
|
// Skip header row
|
|
if rowIndex == 0 {
|
|
continue
|
|
}
|
|
|
|
if len(row) > idColumnIndex {
|
|
// Extract and clean up the job ID
|
|
jobID := strings.TrimSpace(row[idColumnIndex])
|
|
if jobID != "" {
|
|
jobNumbers = append(jobNumbers, jobID)
|
|
log.Printf("Added job ID: %s", jobID)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("Extracted %d job IDs from CSV", len(jobNumbers))
|
|
|
|
// Create a list of valid job numbers
|
|
var validJobNumbers []string
|
|
validJobNumbers = append(validJobNumbers, jobNumbers...)
|
|
|
|
// Generate HTML for job list
|
|
var jobListHTML string
|
|
if len(validJobNumbers) > 0 {
|
|
// Create a hidden input with the job IDs
|
|
jobsValue := strings.Join(validJobNumbers, ",")
|
|
|
|
// Insert a hidden input for job numbers and show the job list
|
|
jobListHTML = fmt.Sprintf(`
|
|
<input type="hidden" name="jobNumbers" value="%s">
|
|
|
|
<style>
|
|
#csv-preview { display: block !important; }
|
|
</style>
|
|
|
|
<script>
|
|
// Update the job list display
|
|
document.getElementById("csv-preview-content").innerHTML = '';
|
|
var ul = document.createElement("ul");
|
|
ul.className = "job-list";
|
|
%s
|
|
document.getElementById("csv-preview-content").appendChild(ul);
|
|
</script>
|
|
`, jobsValue, buildJobListJS(validJobNumbers))
|
|
} else {
|
|
jobListHTML = `
|
|
<p>No valid job numbers found in the CSV file.</p>
|
|
`
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
w.Write([]byte(jobListHTML))
|
|
}
|
|
|
|
// Helper function to build JavaScript for job list
|
|
func buildJobListJS(jobIDs []string) string {
|
|
var js strings.Builder
|
|
for _, id := range jobIDs {
|
|
js.WriteString(fmt.Sprintf(`
|
|
var li = document.createElement("li");
|
|
li.setAttribute("data-job-id", "%s");
|
|
li.textContent = "Job #%s";
|
|
ul.appendChild(li);
|
|
`, id, id))
|
|
}
|
|
return js.String()
|
|
}
|
|
|
|
// 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, fmt.Sprintf("Unable to parse form: %v", err), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Get the job numbers from either of the possible form fields
|
|
jobNumbers := r.FormValue("jobNumbers")
|
|
if jobNumbers == "" {
|
|
jobNumbers = r.FormValue("job-ids")
|
|
if jobNumbers == "" {
|
|
log.Printf("No job numbers provided. Form data: %+v", r.Form)
|
|
http.Error(w, "No job numbers provided", http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Log the form data for debugging
|
|
log.Printf("Form data: %+v", r.Form)
|
|
log.Printf("Job numbers: %s", jobNumbers)
|
|
|
|
// 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
|
|
} else {
|
|
// If a custom name is provided without extension, add the original file extension
|
|
if !strings.Contains(documentName, ".") {
|
|
extension := filepath.Ext(fileHeader.Filename)
|
|
if extension != "" {
|
|
documentName = documentName + extension
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("Using document name: %s (original filename: %s)", documentName, fileHeader.Filename)
|
|
|
|
// Get document type
|
|
documentType := r.FormValue(typeKey)
|
|
if documentType == "" {
|
|
log.Printf("No document type for file %s", fileHeader.Filename)
|
|
continue
|
|
}
|
|
|
|
// Log the document type for debugging
|
|
log.Printf("Document type for %s: '%s'", documentName, documentType)
|
|
|
|
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>")
|
|
|
|
if len(results) == 0 {
|
|
resultHTML.WriteString("<p class='error'>No documents were uploaded. Please check that you have selected files and document types.</p>")
|
|
} else {
|
|
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>")
|
|
|
|
// Add JavaScript to scroll to results
|
|
resultHTML.WriteString("<script>document.getElementById('upload-results').scrollIntoView({behavior: 'smooth'});</script>")
|
|
|
|
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="01" selected>Job Paperwork</option>
|
|
<option value="02">Job Vendor Bill</option>
|
|
<option value="07">Generic Attachment</option>
|
|
<option value="10">Blank Paperwork</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(""))
|
|
}
|
|
|