From 6bd57c10be038d86cc8ebd1ae0338736e98a4555 Mon Sep 17 00:00:00 2001 From: nic Date: Fri, 4 Apr 2025 16:18:33 -0400 Subject: [PATCH] feat: bulk document removal working except for Job Invoice --- internal/handlers/web/document_remove.go | 495 ++++++++++++++++++- internal/handlers/web/documents.go | 6 +- templates/partials/document_remove.html | 18 +- templates/partials/document_upload_form.html | 8 +- 4 files changed, 480 insertions(+), 47 deletions(-) diff --git a/internal/handlers/web/document_remove.go b/internal/handlers/web/document_remove.go index 3073a09..c1f6792 100644 --- a/internal/handlers/web/document_remove.go +++ b/internal/handlers/web/document_remove.go @@ -36,8 +36,8 @@ func DocumentRemoveHandler(w http.ResponseWriter, r *http.Request) { "DocumentTypes": []map[string]string{ {"value": "1", "label": "Job Paperwork"}, {"value": "2", "label": "Job Vendor Bill"}, - {"value": "4", "label": "Generic Attachment"}, - {"value": "7", "label": "Blank Paperwork"}, + {"value": "7", "label": "Generic Attachment"}, + {"value": "10", "label": "Blank Paperwork"}, {"value": "14", "label": "Job Invoice"}, }, } @@ -326,15 +326,17 @@ func RemoveJobAttachmentsHandler(w http.ResponseWriter, r *http.Request) { } } - // Delete the attachment - err = session.DeleteAttachment(id) + // For all attachment types, we'll use the attachment endpoint for deletion + // The API endpoint is /attachment/{id} as defined in DeleteAttachment method + log.Printf("Deleting attachment %s (ID: %s) using attachment endpoint", fileResult.Name, id) + deleteErr := session.DeleteAttachment(id) mu.Lock() defer mu.Unlock() - if err != nil { + if deleteErr != nil { fileResult.Success = false - fileResult.Error = err.Error() + fileResult.Error = deleteErr.Error() results.ErrorCount++ results.Success = false } else { @@ -564,6 +566,271 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { // Only proceed if we have access or couldn't determine access if err != nil || hasAccess { + // Try job paperwork endpoint + if stringInSlice("1", docTypes) || len(docTypes) == 0 { + log.Printf("**** JOB %s: Trying the job paperwork endpoint", jobID) + paperworkURL := fmt.Sprintf("%s/job/%s/paperwork", api.BaseURL, jobID) + paperworkReq, err := http.NewRequest("GET", paperworkURL, nil) + if err == nil { + paperworkReq.Header.Set("Cookie", session.Cookie) + paperworkReq.Header.Set("Accept", "application/json") + + log.Printf("**** JOB %s: Sending request to job paperwork endpoint: %s", jobID, paperworkURL) + paperworkResp, err := session.Client.Do(paperworkReq) + + if err == nil && paperworkResp.StatusCode == http.StatusOK { + defer paperworkResp.Body.Close() + paperworkBody, _ := io.ReadAll(paperworkResp.Body) + + // Log preview of the response + responsePreview := string(paperworkBody) + if len(responsePreview) > 300 { + responsePreview = responsePreview[:300] + "... [truncated]" + } + log.Printf("**** JOB %s: Job paperwork response preview: %s", jobID, responsePreview) + + var paperworkResult map[string]interface{} + if err := json.Unmarshal(paperworkBody, &paperworkResult); err == nil { + // Process objects array if it exists + if objects, ok := paperworkResult["objects"].([]interface{}); ok && len(objects) > 0 { + log.Printf("**** JOB %s: Found %d paperwork items in objects array", jobID, len(objects)) + + for _, obj := range objects { + if paperworkMap, ok := obj.(map[string]interface{}); ok { + // Set purposeId to 1 for job paperwork + paperworkMap["purposeId"] = float64(1) + attachments = append(attachments, paperworkMap) + log.Printf("**** JOB %s: Added job paperwork to attachments", jobID) + } + } + } else if data, ok := paperworkResult["data"].(map[string]interface{}); ok { + // Check in data for attachments + if attachmentsArray, ok := data["attachments"].([]interface{}); ok && len(attachmentsArray) > 0 { + log.Printf("**** JOB %s: Found %d paperwork items in data.attachments", jobID, len(attachmentsArray)) + + for _, att := range attachmentsArray { + if attMap, ok := att.(map[string]interface{}); ok { + // Ensure purposeId is set correctly + attMap["purposeId"] = float64(1) + attachments = append(attachments, attMap) + log.Printf("**** JOB %s: Added job paperwork from data.attachments", jobID) + } + } + } + + // Also check other locations in data + possibleKeys := []string{"paperwork", "objects"} + for _, key := range possibleKeys { + if items, ok := data[key].([]interface{}); ok && len(items) > 0 { + log.Printf("**** JOB %s: Found %d paperwork items in data.%s", jobID, len(items), key) + + for _, item := range items { + if itemMap, ok := item.(map[string]interface{}); ok { + // Set purposeId to 1 for job paperwork + itemMap["purposeId"] = float64(1) + attachments = append(attachments, itemMap) + log.Printf("**** JOB %s: Added job paperwork from data.%s", jobID, key) + } + } + } + } + } + } + } else { + log.Printf("**** JOB %s: Job paperwork endpoint failed or returned non-200 status: %v", jobID, err) + } + } + } + + // Try job invoice endpoint + if stringInSlice("14", docTypes) || len(docTypes) == 0 { + log.Printf("**** JOB %s: Trying the job invoice endpoint", jobID) + invoiceURL := fmt.Sprintf("%s/job/%s/invoice", api.BaseURL, jobID) + invoiceReq, err := http.NewRequest("GET", invoiceURL, nil) + if err == nil { + invoiceReq.Header.Set("Cookie", session.Cookie) + invoiceReq.Header.Set("Accept", "application/json") + + log.Printf("**** JOB %s: Sending request to job invoice endpoint: %s", jobID, invoiceURL) + invoiceResp, err := session.Client.Do(invoiceReq) + + if err == nil && invoiceResp.StatusCode == http.StatusOK { + defer invoiceResp.Body.Close() + invoiceBody, _ := io.ReadAll(invoiceResp.Body) + + // Log preview of the response + responsePreview := string(invoiceBody) + if len(responsePreview) > 300 { + responsePreview = responsePreview[:300] + "... [truncated]" + } + log.Printf("**** JOB %s: Job invoice response preview: %s", jobID, responsePreview) + + var invoiceResult map[string]interface{} + if err := json.Unmarshal(invoiceBody, &invoiceResult); err == nil { + // Process objects array if it exists + if objects, ok := invoiceResult["objects"].([]interface{}); ok && len(objects) > 0 { + log.Printf("**** JOB %s: Found %d job invoices in objects array", jobID, len(objects)) + + for _, obj := range objects { + if invoiceMap, ok := obj.(map[string]interface{}); ok { + // Set purposeId to 14 for job invoices + invoiceMap["purposeId"] = float64(14) + attachments = append(attachments, invoiceMap) + log.Printf("**** JOB %s: Added job invoice to attachments", jobID) + } + } + } else if data, ok := invoiceResult["data"].(map[string]interface{}); ok { + // Check in data for attachments + possibleKeys := []string{"invoices", "attachments", "objects"} + for _, key := range possibleKeys { + if items, ok := data[key].([]interface{}); ok && len(items) > 0 { + log.Printf("**** JOB %s: Found %d invoices in data.%s", jobID, len(items), key) + + for _, item := range items { + if itemMap, ok := item.(map[string]interface{}); ok { + // Set purposeId to 14 for job invoices + itemMap["purposeId"] = float64(14) + attachments = append(attachments, itemMap) + log.Printf("**** JOB %s: Added job invoice from data.%s", jobID, key) + } + } + } + } + } + } + } else { + log.Printf("**** JOB %s: Job invoice endpoint failed or returned non-200 status: %v", jobID, err) + } + } + } + + // Try generic attachment endpoint + if stringInSlice("7", docTypes) || len(docTypes) == 0 { + log.Printf("**** JOB %s: Trying the generic attachment endpoint", jobID) + genericURL := fmt.Sprintf("%s/job/%s/attachment", api.BaseURL, jobID) + genericReq, err := http.NewRequest("GET", genericURL, nil) + if err == nil { + genericReq.Header.Set("Cookie", session.Cookie) + genericReq.Header.Set("Accept", "application/json") + + log.Printf("**** JOB %s: Sending request to generic attachment endpoint: %s", jobID, genericURL) + genericResp, err := session.Client.Do(genericReq) + + if err == nil && genericResp.StatusCode == http.StatusOK { + defer genericResp.Body.Close() + genericBody, _ := io.ReadAll(genericResp.Body) + + // Log preview of the response + responsePreview := string(genericBody) + if len(responsePreview) > 300 { + responsePreview = responsePreview[:300] + "... [truncated]" + } + log.Printf("**** JOB %s: Generic attachment response preview: %s", jobID, responsePreview) + + var genericResult map[string]interface{} + if err := json.Unmarshal(genericBody, &genericResult); err == nil { + // Process objects array if it exists + if objects, ok := genericResult["objects"].([]interface{}); ok && len(objects) > 0 { + log.Printf("**** JOB %s: Found %d generic attachments in objects array", jobID, len(objects)) + + for _, obj := range objects { + if attachMap, ok := obj.(map[string]interface{}); ok { + // Set purposeId to 7 for generic attachments + attachMap["purposeId"] = float64(7) + attachments = append(attachments, attachMap) + log.Printf("**** JOB %s: Added generic attachment to attachments", jobID) + } + } + } else if data, ok := genericResult["data"].(map[string]interface{}); ok { + // Check in data for attachments + possibleKeys := []string{"attachments", "objects"} + for _, key := range possibleKeys { + if items, ok := data[key].([]interface{}); ok && len(items) > 0 { + log.Printf("**** JOB %s: Found %d generic attachments in data.%s", jobID, len(items), key) + + for _, item := range items { + if itemMap, ok := item.(map[string]interface{}); ok { + // Set purposeId to 7 for generic attachments + itemMap["purposeId"] = float64(7) + attachments = append(attachments, itemMap) + log.Printf("**** JOB %s: Added generic attachment from data.%s", jobID, key) + } + } + } + } + } + } + } else { + log.Printf("**** JOB %s: Generic attachment endpoint failed or returned non-200 status: %v", jobID, err) + } + } + } + + // Try vendor bill endpoint + if stringInSlice("2", docTypes) || len(docTypes) == 0 { + log.Printf("**** JOB %s: Trying the vendor invoice endpoint", jobID) + vendorInvoiceURL := fmt.Sprintf("%s/job/%s/vendorinvoice", api.BaseURL, jobID) + vendorInvoiceReq, err := http.NewRequest("GET", vendorInvoiceURL, nil) + if err == nil { + vendorInvoiceReq.Header.Set("Cookie", session.Cookie) + vendorInvoiceReq.Header.Set("Accept", "application/json") + + log.Printf("**** JOB %s: Sending request to vendor invoice endpoint: %s", jobID, vendorInvoiceURL) + vendorInvoiceResp, err := session.Client.Do(vendorInvoiceReq) + + if err == nil && vendorInvoiceResp.StatusCode == http.StatusOK { + defer vendorInvoiceResp.Body.Close() + vendorInvoiceBody, _ := io.ReadAll(vendorInvoiceResp.Body) + + // Log preview of the response + responsePreview := string(vendorInvoiceBody) + if len(responsePreview) > 300 { + responsePreview = responsePreview[:300] + "... [truncated]" + } + log.Printf("**** JOB %s: Vendor invoice response preview: %s", jobID, responsePreview) + + var vendorInvoiceResult map[string]interface{} + if err := json.Unmarshal(vendorInvoiceBody, &vendorInvoiceResult); err == nil { + // Process objects array if it exists + if objects, ok := vendorInvoiceResult["objects"].([]interface{}); ok && len(objects) > 0 { + log.Printf("**** JOB %s: Found %d vendor invoices in objects array", jobID, len(objects)) + + for _, obj := range objects { + if invoiceMap, ok := obj.(map[string]interface{}); ok { + // Set purposeId to 2 for vendor bills + invoiceMap["purposeId"] = float64(2) + attachments = append(attachments, invoiceMap) + log.Printf("**** JOB %s: Added vendor invoice to attachments", jobID) + } + } + } else if data, ok := vendorInvoiceResult["data"].(map[string]interface{}); ok { + // Check in data for attachments + possibleKeys := []string{"invoices", "vendorInvoices", "attachments", "objects"} + for _, key := range possibleKeys { + if items, ok := data[key].([]interface{}); ok && len(items) > 0 { + log.Printf("**** JOB %s: Found %d vendor bills in data.%s", jobID, len(items), key) + + for _, item := range items { + if itemMap, ok := item.(map[string]interface{}); ok { + // Set purposeId to 2 for vendor bills + itemMap["purposeId"] = float64(2) + attachments = append(attachments, itemMap) + log.Printf("**** JOB %s: Added vendor bill from data.%s", jobID, key) + } + } + } + } + } + } + } else { + log.Printf("**** JOB %s: Vendor invoice endpoint failed or returned non-200 status: %v", jobID, err) + } + } + } + + // Then continue with general paperwork endpoint to catch any we might have missed + log.Printf("**** JOB %s: Trying general paperwork endpoint", jobID) + // Directly try to get paperwork using the specialized API log.Printf("**** JOB %s: Using specialized paperwork API", jobID) paperworkItems, err := session.GetJobPaperwork(jobID) @@ -648,7 +915,117 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { if len(attachments) == 0 { log.Printf("**** JOB %s: No attachments found yet, trying direct paperwork endpoint") - // Make a direct call to the paperwork endpoint + // Directly try to get vendor bills using a specific endpoint + log.Printf("**** JOB %s: Trying vendor bill specific endpoint", jobID) + vendorBillURL := fmt.Sprintf("%s/job/%s/vendor-bill", api.BaseURL, jobID) + vendorBillReq, err := http.NewRequest("GET", vendorBillURL, nil) + if err == nil { + vendorBillReq.Header.Set("Cookie", session.Cookie) + vendorBillReq.Header.Set("Accept", "application/json") + + log.Printf("**** JOB %s: Sending request to vendor bill endpoint: %s", jobID, vendorBillURL) + vendorBillResp, err := session.Client.Do(vendorBillReq) + + if err == nil && vendorBillResp.StatusCode == http.StatusOK { + defer vendorBillResp.Body.Close() + vendorBillBody, _ := io.ReadAll(vendorBillResp.Body) + + // Log full response structure for debugging + log.Printf("**** JOB %s: Full vendor bill response: %s", jobID, string(vendorBillBody)) + + var vendorBillResult map[string]interface{} + if err := json.Unmarshal(vendorBillBody, &vendorBillResult); err == nil { + // Log all root keys in the response + rootKeys := make([]string, 0) + for k := range vendorBillResult { + rootKeys = append(rootKeys, k) + } + log.Printf("**** JOB %s: Vendor bill response root keys: %s", + jobID, strings.Join(rootKeys, ", ")) + + // Check if data exists and log all its keys + if data, ok := vendorBillResult["data"].(map[string]interface{}); ok { + dataKeys := make([]string, 0) + for k := range data { + dataKeys = append(dataKeys, k) + } + log.Printf("**** JOB %s: Vendor bill data keys: %s", + jobID, strings.Join(dataKeys, ", ")) + + // First try vendorBills directly + if vendorBills, ok := data["vendorBills"].([]interface{}); ok && len(vendorBills) > 0 { + log.Printf("**** JOB %s: Found %d vendor bills in data.vendorBills", jobID, len(vendorBills)) + + for _, bill := range vendorBills { + if billMap, ok := bill.(map[string]interface{}); ok { + // Set purposeId to 2 for vendor bills + billMap["purposeId"] = float64(2) + attachments = append(attachments, billMap) + } + } + } else { + // Try other possible locations + log.Printf("**** JOB %s: No vendorBills found in data, checking other locations", jobID) + + // Try each possible location for the vendor bills + possibleKeys := []string{"objects", "attachments", "bills", "paperwork", "documents"} + for _, key := range possibleKeys { + if items, ok := data[key].([]interface{}); ok && len(items) > 0 { + log.Printf("**** JOB %s: Found %d items in data.%s", jobID, len(items), key) + + // Log the structure of the first item + if itemMap, ok := items[0].(map[string]interface{}); ok { + itemKeys := make([]string, 0) + for k := range itemMap { + itemKeys = append(itemKeys, k) + } + log.Printf("**** JOB %s: First item in data.%s has keys: %s", + jobID, key, strings.Join(itemKeys, ", ")) + + // Log the first item as JSON for inspection + if itemJSON, err := json.Marshal(itemMap); err == nil { + log.Printf("**** JOB %s: First item in data.%s: %s", + jobID, key, string(itemJSON)) + } + } + + // Add all items as attachments + for _, item := range items { + if itemMap, ok := item.(map[string]interface{}); ok { + // Set purposeId to 2 for vendor bills + itemMap["purposeId"] = float64(2) + attachments = append(attachments, itemMap) + log.Printf("**** JOB %s: Added vendor bill from data.%s", jobID, key) + } + } + } + } + } + } else { + // If data is not a map, check for top-level objects + log.Printf("**** JOB %s: No data object in vendor bill response or it's not a map", jobID) + + if objects, ok := vendorBillResult["objects"].([]interface{}); ok && len(objects) > 0 { + log.Printf("**** JOB %s: Found %d objects at root level", jobID, len(objects)) + + for _, obj := range objects { + if objMap, ok := obj.(map[string]interface{}); ok { + // Set purposeId to 2 for vendor bills + objMap["purposeId"] = float64(2) + attachments = append(attachments, objMap) + log.Printf("**** JOB %s: Added vendor bill from root.objects", jobID) + } + } + } + } + } + } else { + log.Printf("**** JOB %s: Vendor bill endpoint failed or returned non-200 status: %v", jobID, err) + } + } + + // Also try direct paperwork endpoint + log.Printf("**** JOB %s: Trying direct paperwork endpoint", jobID) paperworkURL := fmt.Sprintf("%s/job/%s/paperwork", api.BaseURL, jobID) paperworkReq, err := http.NewRequest("GET", paperworkURL, nil) if err == nil { @@ -720,6 +1097,14 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { // Log attachment count after direct endpoint log.Printf("**** JOB %s: Attachments found after direct paperwork call: %d", jobID, len(attachments)) + // Deduplicate attachments to avoid processing the same ones multiple times + originalCount := len(attachments) + attachments = deduplicateAttachments(attachments) + if len(attachments) < originalCount { + log.Printf("**** JOB %s: Removed %d duplicate attachments, %d unique attachments remain", + jobID, originalCount-len(attachments), len(attachments)) + } + // Now actually apply the filters filteredAttachments := make([]map[string]interface{}, 0) @@ -800,14 +1185,17 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { } } - // Get document name/filename + // Get filename from any available field, don't assume description exists var filename string - if nameVal, ok := attachment["fileName"].(string); ok { - filename = nameVal - } else if nameVal, ok := attachment["name"].(string); ok { - filename = nameVal - } else if nameVal, ok := attachment["description"].(string); ok { - filename = nameVal + if desc, ok := attachment["description"].(string); ok && desc != "" { + filename = desc + } else if name, ok := attachment["name"].(string); ok && name != "" { + filename = name + } else if fname, ok := attachment["fileName"].(string); ok && fname != "" { + filename = fname + } else { + // If no name is available, use the ID as the name + filename = fmt.Sprintf("Attachment ID: %s", attachment["id"]) } log.Printf("**** JOB %s: Attachment filename: %s", jobID, filename) @@ -876,6 +1264,14 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { if len(attachments) == 0 { log.Printf("**** JOB %s: WARNING! No attachments found after all retrieval attempts") + } else { + // Deduplicate again before continuing with deletion, in case multiple methods found the same attachments + originalCount := len(attachments) + attachments = deduplicateAttachments(attachments) + if len(attachments) < originalCount { + log.Printf("**** JOB %s: Final deduplication removed %d duplicates, %d unique attachments remain", + jobID, originalCount-len(attachments), len(attachments)) + } } } @@ -890,14 +1286,17 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { } attachmentIDStr := fmt.Sprintf("%.0f", attachmentIDRaw) - // Get the filename + // Safely get filename from any available field var filename string - if nameVal, ok := attachment["fileName"].(string); ok { - filename = nameVal - } else if nameVal, ok := attachment["name"].(string); ok { - filename = nameVal - } else if nameVal, ok := attachment["description"].(string); ok { - filename = nameVal + if desc, ok := attachment["description"].(string); ok && desc != "" { + filename = desc + } else if name, ok := attachment["name"].(string); ok && name != "" { + filename = name + } else if fname, ok := attachment["fileName"].(string); ok && fname != "" { + filename = fname + } else { + // If no name is available, use the ID as the name + filename = fmt.Sprintf("Attachment ID: %s", attachmentIDStr) } if filename == "" { @@ -982,7 +1381,19 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { attachmentIDFloat := att["id"].(float64) attachmentIDStr := fmt.Sprintf("%.0f", attachmentIDFloat) // Convert to string without decimal - filename := att["description"].(string) + + // Safely get filename from any available field + var filename string + if desc, ok := att["description"].(string); ok && desc != "" { + filename = desc + } else if name, ok := att["name"].(string); ok && name != "" { + filename = name + } else if fname, ok := att["fileName"].(string); ok && fname != "" { + filename = fname + } else { + // If no name is available, use the ID as the name + filename = fmt.Sprintf("Attachment ID: %s", attachmentIDStr) + } fileResult := struct { Name string @@ -992,16 +1403,18 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) { Name: filename, } - // Delete the attachment - err := session.DeleteAttachment(attachmentIDStr) + // For all attachment types, we'll use the attachment endpoint for deletion + // The API endpoint is /attachment/{id} as defined in DeleteAttachment method + log.Printf("Deleting attachment %s (ID: %s) using attachment endpoint", filename, attachmentIDStr) + deleteErr := session.DeleteAttachment(attachmentIDStr) mu.Lock() defer mu.Unlock() - if err != nil { + if deleteErr != nil { fileResult.Success = false - fileResult.Error = err.Error() - log.Printf("Error deleting attachment %s: %v", filename, err) + fileResult.Error = deleteErr.Error() + log.Printf("Error deleting attachment %s: %v", filename, deleteErr) jobResult.Success = false } else { fileResult.Success = true @@ -1174,3 +1587,31 @@ func logAttachmentDetails(jobID string, attachment map[string]interface{}) { } log.Printf("***** END ATTACHMENT DETAILS *****") } + +// Helper function to deduplicate attachments based on ID +func deduplicateAttachments(attachments []map[string]interface{}) []map[string]interface{} { + seen := make(map[string]bool) + uniqueAttachments := make([]map[string]interface{}, 0) + + for _, attachment := range attachments { + // Get the ID as a string for deduplication + var idStr string + if id, ok := attachment["id"].(float64); ok { + idStr = fmt.Sprintf("%.0f", id) + } else if id, ok := attachment["id"].(string); ok { + idStr = id + } else { + // If no valid ID, just add it (should not happen) + uniqueAttachments = append(uniqueAttachments, attachment) + continue + } + + // Only add if we haven't seen this ID before + if !seen[idStr] { + seen[idStr] = true + uniqueAttachments = append(uniqueAttachments, attachment) + } + } + + return uniqueAttachments +} diff --git a/internal/handlers/web/documents.go b/internal/handlers/web/documents.go index 3f30469..6d00647 100644 --- a/internal/handlers/web/documents.go +++ b/internal/handlers/web/documents.go @@ -555,9 +555,9 @@ func DocumentFieldAddHandler(w http.ResponseWriter, r *http.Request) { diff --git a/templates/partials/document_remove.html b/templates/partials/document_remove.html index da2c415..c96ad3e 100644 --- a/templates/partials/document_remove.html +++ b/templates/partials/document_remove.html @@ -27,29 +27,21 @@
-
- - -
- +
- -
-
- - +
- +
- - + +
diff --git a/templates/partials/document_upload_form.html b/templates/partials/document_upload_form.html index 15107c8..78a45b4 100644 --- a/templates/partials/document_upload_form.html +++ b/templates/partials/document_upload_form.html @@ -19,10 +19,10 @@