Browse Source

fix: removal now works on type filters and a combination of all filters

document-upload-removal-layout-update
nic 9 months ago
parent
commit
f427fc1831
  1. 101
      internal/api/attachments.go
  2. 160
      internal/handlers/web/document_remove.go
  3. 5
      static/css/upload.css
  4. 92
      templates/partials/document_upload.html

101
internal/api/attachments.go

@ -480,3 +480,104 @@ func (s *Session) UploadAttachmentFile(jobID, filename, purpose string, fileRead
log.Printf("Successfully uploaded streaming attachment %s to job %s", filename, jobID) log.Printf("Successfully uploaded streaming attachment %s to job %s", filename, jobID)
return result, nil return result, nil
} }
// GetJobAttachmentsDirect tries to get attachments directly using the attachment endpoint
func (s *Session) GetJobAttachmentsDirect(jobID string) ([]map[string]interface{}, error) {
log.Printf("GetJobAttachmentsDirect: Fetching attachments directly for job %s", jobID)
// Try to get attachments using the attachment endpoint with entityId filter
url := fmt.Sprintf("%s/attachment", BaseURL)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, fmt.Errorf("error creating request: %v", err)
}
// Add authorization header
req.Header.Set("Cookie", s.Cookie)
// Add query parameters to filter by job
q := req.URL.Query()
q.Add("entityType", "3") // 3 = Job
q.Add("entityId", jobID)
req.URL.RawQuery = q.Encode()
log.Printf("GetJobAttachmentsDirect: Using URL: %s", req.URL.String())
// Send the request
resp, err := s.Client.Do(req)
if err != nil {
return nil, fmt.Errorf("error sending request: %v", err)
}
defer resp.Body.Close()
// Read the response
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("error reading response body: %v", err)
}
// Check for errors
if resp.StatusCode != http.StatusOK {
log.Printf("GetJobAttachmentsDirect: API error (status %d) for job %s: %s", resp.StatusCode, jobID, string(body))
return nil, fmt.Errorf("API returned error: %d %s - %s", resp.StatusCode, resp.Status, string(body))
}
// Log response preview
responsePreview := string(body)
if len(responsePreview) > 200 {
responsePreview = responsePreview[:200] + "... [truncated]"
}
log.Printf("GetJobAttachmentsDirect: Response preview for job %s: %s", jobID, responsePreview)
// Parse the response
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("error parsing response: %v", err)
}
// Try to extract attachments from various response structures
attachments := make([]map[string]interface{}, 0)
// Check for data.attachments (this is what the API is actually returning)
if data, ok := result["data"].(map[string]interface{}); ok {
if attList, ok := data["attachments"].([]interface{}); ok {
log.Printf("GetJobAttachmentsDirect: Found %d attachments in data.attachments for job %s", len(attList), jobID)
for _, att := range attList {
if attachment, ok := att.(map[string]interface{}); ok {
attachments = append(attachments, attachment)
}
}
return attachments, nil
}
}
// Check for objects array at root level
if objects, ok := result["objects"].([]interface{}); ok {
log.Printf("GetJobAttachmentsDirect: Found %d objects at root level for job %s", len(objects), jobID)
for _, obj := range objects {
if attachment, ok := obj.(map[string]interface{}); ok {
attachments = append(attachments, attachment)
}
}
return attachments, nil
}
// Check for data.objects
if data, ok := result["data"].(map[string]interface{}); ok {
if objects, ok := data["objects"].([]interface{}); ok {
log.Printf("GetJobAttachmentsDirect: Found %d objects in data.objects for job %s", len(objects), jobID)
for _, obj := range objects {
if attachment, ok := obj.(map[string]interface{}); ok {
attachments = append(attachments, attachment)
}
}
return attachments, nil
}
}
log.Printf("GetJobAttachmentsDirect: No attachments found for job %s", jobID)
return attachments, nil
}
// Helper function to check if a string matches any pattern in a slice

160
internal/handlers/web/document_remove.go

@ -480,6 +480,8 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
if types := r.Form["documentTypes"]; len(types) > 0 { if types := r.Form["documentTypes"]; len(types) > 0 {
docTypes = types docTypes = types
log.Printf("Filtering by document types: %v", docTypes) log.Printf("Filtering by document types: %v", docTypes)
} else {
log.Printf("No document types specified, will process all document types")
} }
// Get filename patterns to match (optional) // Get filename patterns to match (optional)
@ -582,21 +584,54 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
// Get attachments using the most reliable method first // Get attachments using the most reliable method first
var attachments []map[string]interface{} var attachments []map[string]interface{}
// Try the specialized GetJobAttachments method first // Try the direct attachment endpoint first - this should return ALL attachment types
attachments, err = session.GetJobAttachments(jobID) directAttachments, err := session.GetJobAttachmentsDirect(jobID)
if err != nil { if err != nil {
log.Printf("Error getting attachments for job %s: %v", jobID, err) log.Printf("Error getting direct attachments for job %s: %v", jobID, err)
attachments = []map[string]interface{}{} // Ensure it's initialized attachments = []map[string]interface{}{} // Ensure it's initialized
} else {
attachments = directAttachments
if len(attachments) > 0 {
log.Printf("Found %d attachments via GetJobAttachmentsDirect for job %s", len(attachments), jobID)
}
}
// If no attachments found via direct method, try the comprehensive GetAttachmentsForJob method
if len(attachments) == 0 {
jobData, err := session.GetAttachmentsForJob(jobID)
if err != nil {
log.Printf("Error getting attachments for job %s: %v", jobID, err)
attachments = []map[string]interface{}{} // Ensure it's initialized
} else {
// Extract attachments from the job data response
attachments = extractAttachmentsFromJobData(jobData, jobID)
if len(attachments) > 0 {
log.Printf("Found %d attachments via GetAttachmentsForJob for job %s", len(attachments), jobID)
}
}
} }
// If no attachments found, try the paperwork endpoint as fallback // If still no attachments found, try the specialized GetJobAttachments method as fallback
if len(attachments) == 0 {
attachments, err = session.GetJobAttachments(jobID)
if err != nil {
log.Printf("Error getting attachments via GetJobAttachments for job %s: %v", jobID, err)
attachments = []map[string]interface{}{} // Ensure it's initialized
} else if len(attachments) > 0 {
log.Printf("Found %d attachments via GetJobAttachments for job %s", len(attachments), jobID)
}
}
// If still no attachments found, try the paperwork endpoint as final fallback
if len(attachments) == 0 { if len(attachments) == 0 {
log.Printf("No attachments found via GetJobAttachments for job %s, trying paperwork endpoint", jobID)
paperworkItems, err := session.GetJobPaperwork(jobID) paperworkItems, err := session.GetJobPaperwork(jobID)
if err != nil { if err != nil {
log.Printf("Error getting paperwork for job %s: %v", jobID, err) log.Printf("Error getting paperwork for job %s: %v", jobID, err)
} else { } else {
attachments = paperworkItems attachments = paperworkItems
if len(attachments) > 0 {
log.Printf("Found %d attachments via GetJobPaperwork for job %s", len(attachments), jobID)
}
} }
} }
@ -624,6 +659,15 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
filename = fmt.Sprintf("Attachment ID: %s", attachmentIDStr) filename = fmt.Sprintf("Attachment ID: %s", attachmentIDStr)
} }
// Get purposeId for debugging
var purposeId float64
if pid, ok := attachment["purposeId"].(float64); ok {
purposeId = pid
}
// Track if this attachment should be included
includeAttachment := true
// Check document type filter // Check document type filter
if len(docTypes) > 0 { if len(docTypes) > 0 {
typeMatches := false typeMatches := false
@ -639,17 +683,21 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
if !typeMatches { if !typeMatches {
continue log.Printf("Job %s: Skipping '%s' (type %v) - no document type match", jobID, filename, purposeId)
includeAttachment = false
} }
} }
// Check filename pattern // Check filename pattern filter
if len(filenamePatterns) > 0 && !matchesAnyPattern(filename, filenamePatterns) { if includeAttachment && len(filenamePatterns) > 0 {
continue if !matchesAnyPattern(filename, filenamePatterns) {
log.Printf("Job %s: Skipping '%s' - no filename pattern match", jobID, filename)
includeAttachment = false
}
} }
// Check age filter if applicable // Check age filter if applicable
if ageFilterDays > 0 { if includeAttachment && ageFilterDays > 0 {
var createdAt time.Time var createdAt time.Time
var hasDate bool var hasDate bool
@ -670,14 +718,20 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
} }
if hasDate && createdAt.After(cutoffDate) { if hasDate && createdAt.After(cutoffDate) {
continue // Skip if not old enough log.Printf("Job %s: Skipping '%s' - not old enough (%s)", jobID, filename, createdAt.Format("2006-01-02"))
includeAttachment = false
} }
} }
// This attachment matches all criteria // Add attachment if it passed all filters
attachmentsToDelete = append(attachmentsToDelete, attachment) if includeAttachment {
log.Printf("Job %s: Including '%s' (type %v) for deletion", jobID, filename, purposeId)
attachmentsToDelete = append(attachmentsToDelete, attachment)
}
} }
log.Printf("Job %s: Found %d attachments to delete out of %d total attachments", jobID, len(attachmentsToDelete), len(attachments))
jobResult.FilesFound = len(attachmentsToDelete) jobResult.FilesFound = len(attachmentsToDelete)
// Process deletions with rate limiting // Process deletions with rate limiting
@ -737,6 +791,8 @@ func BulkRemoveDocumentsHandler(w http.ResponseWriter, r *http.Request) {
deletionWg.Wait() deletionWg.Wait()
log.Printf("Job %s: Deletion completed. Files found: %d, Files removed: %d, Success: %v", jobID, jobResult.FilesFound, jobResult.FilesRemoved, jobResult.Success)
mu.Lock() mu.Lock()
results.JobsProcessed++ results.JobsProcessed++
if jobResult.Success { if jobResult.Success {
@ -1008,3 +1064,81 @@ func RemovalJobFileHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
} }
// Helper function to extract attachments from job data response
func extractAttachmentsFromJobData(jobData map[string]interface{}, jobID string) []map[string]interface{} {
attachments := make([]map[string]interface{}, 0)
// Check if data key exists
if data, ok := jobData["data"].(map[string]interface{}); ok {
// Look for attachments directly in data
if attList, ok := data["attachments"].([]interface{}); ok && len(attList) > 0 {
for _, att := range attList {
if attachment, ok := att.(map[string]interface{}); ok {
attachments = append(attachments, attachment)
}
}
return attachments
}
// Check for paperwork in data
if paperwork, ok := data["paperwork"].([]interface{}); ok && len(paperwork) > 0 {
for _, doc := range paperwork {
if docMap, ok := doc.(map[string]interface{}); ok {
attachments = append(attachments, docMap)
}
}
return attachments
}
// Check for paperwork_objects (from fallback paperwork endpoint)
if paperworkObjects, ok := data["paperwork_objects"].([]interface{}); ok && len(paperworkObjects) > 0 {
for _, obj := range paperworkObjects {
if objMap, ok := obj.(map[string]interface{}); ok {
attachments = append(attachments, objMap)
}
}
return attachments
}
// Check for other potential containers
for _, key := range []string{"objects", "components", "items", "records"} {
if items, ok := data[key].([]interface{}); ok && len(items) > 0 {
for _, item := range items {
if itemMap, ok := item.(map[string]interface{}); ok {
attachments = append(attachments, itemMap)
}
}
return attachments
}
}
}
// Check for objects at root level
if objects, ok := jobData["objects"].([]interface{}); ok && len(objects) > 0 {
for _, obj := range objects {
if objMap, ok := obj.(map[string]interface{}); ok {
attachments = append(attachments, objMap)
}
}
return attachments
}
return attachments
}
// Helper function to get map keys
func getMapKeys(m interface{}) []string {
keys := make([]string, 0)
switch m := m.(type) {
case map[string]interface{}:
for k := range m {
keys = append(keys, k)
}
case map[string]string:
for k := range m {
keys = append(keys, k)
}
}
return keys
}

5
static/css/upload.css

@ -225,7 +225,7 @@
.modal { .modal {
display: none; display: none;
position: fixed; position: fixed;
z-index: 1001; z-index: 1002;
left: 0; left: 0;
top: 0; top: 0;
width: 100%; width: 100%;
@ -609,7 +609,7 @@
.form-actions { .form-actions {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
justify-content: flex-end; justify-content: center;
margin-top: 2rem; margin-top: 2rem;
padding-top: 1rem; padding-top: 1rem;
border-top: 1px solid var(--content-border); border-top: 1px solid var(--content-border);
@ -826,6 +826,7 @@
display: flex; display: flex;
gap: 0.5rem; gap: 0.5rem;
align-items: center; align-items: center;
justify-content: center;
} }
.file-pagination-buttons .pagination-btn { .file-pagination-buttons .pagination-btn {

92
templates/partials/document_upload.html

@ -134,6 +134,8 @@
</div> </div>
<script> <script>
console.log('Document upload script loaded');
// Check if variable already exists to avoid redeclaration error with HTMX // Check if variable already exists to avoid redeclaration error with HTMX
if (typeof selectedFilesData === 'undefined') { if (typeof selectedFilesData === 'undefined') {
var selectedFilesData = []; // To store {originalFile, displayName, documentType, isActive, originalIndex} var selectedFilesData = []; // To store {originalFile, displayName, documentType, isActive, originalIndex}
@ -145,13 +147,17 @@
document.getElementById('document-files').addEventListener('change', handleFileSelectionChange); document.getElementById('document-files').addEventListener('change', handleFileSelectionChange);
var continueToStep3Button = document.getElementById('continue-to-step3-button'); var continueToStep3Button = document.getElementById('continue-to-step3-button');
console.log('Event listeners attached');
function handleFileSelectionChange(event) { function handleFileSelectionChange(event) {
console.log('File selection changed');
selectedFilesData = []; // Reset selectedFilesData = []; // Reset
const filesArea = document.getElementById('selected-files-area'); const filesArea = document.getElementById('selected-files-area');
filesArea.innerHTML = ''; // Clear previous chips filesArea.innerHTML = ''; // Clear previous chips
const noFilesPlaceholder = document.getElementById('no-files-selected-placeholder'); const noFilesPlaceholder = document.getElementById('no-files-selected-placeholder');
const files = event.target.files; const files = event.target.files;
console.log('Files selected:', files.length);
if (files.length === 0) { if (files.length === 0) {
if (noFilesPlaceholder) noFilesPlaceholder.style.display = 'block'; if (noFilesPlaceholder) noFilesPlaceholder.style.display = 'block';
filesArea.innerHTML = '<p id="no-files-selected-placeholder">No files selected yet.</p>'; // Re-add if cleared filesArea.innerHTML = '<p id="no-files-selected-placeholder">No files selected yet.</p>'; // Re-add if cleared
@ -197,8 +203,20 @@
return `${start}...${end}`; return `${start}...${end}`;
} }
function getDocumentTypeName(typeValue) {
const typeMap = {
"1": "Job Paperwork",
"2": "Job Vendor Bill",
"7": "Generic Attachment",
"10": "Blank Paperwork",
"14": "Job Invoice"
};
return typeMap[typeValue] || `Type: ${typeValue}`;
}
function renderFileChip(fileMetadata, index) { function renderFileChip(fileMetadata, index) {
console.log('renderFileChip called for index:', index, 'file:', fileMetadata.displayName);
const filesArea = document.getElementById('selected-files-area'); const filesArea = document.getElementById('selected-files-area');
const chip = document.createElement('div'); const chip = document.createElement('div');
chip.className = `file-chip ${fileMetadata.isActive ? '' : 'removed'}`; chip.className = `file-chip ${fileMetadata.isActive ? '' : 'removed'}`;
@ -206,16 +224,18 @@
const icon = getFileIcon(fileMetadata.displayName); const icon = getFileIcon(fileMetadata.displayName);
const truncatedName = truncateFilename(fileMetadata.displayName); const truncatedName = truncateFilename(fileMetadata.displayName);
const documentTypeName = getDocumentTypeName(fileMetadata.documentType);
chip.innerHTML = ` chip.innerHTML = `
<span class="file-chip-icon">${icon}</span> <span class="file-chip-icon">${icon}</span>
<span class="file-chip-name" title="${fileMetadata.displayName}">${truncatedName}</span> <span class="file-chip-name" title="${fileMetadata.displayName}" onclick="openEditModal(${index})" style="cursor: pointer;">${truncatedName}</span>
<span class="file-chip-doctype">Type: ${fileMetadata.documentType}</span> <span class="file-chip-doctype">${documentTypeName}</span>
<button type="button" class="file-chip-edit" onclick="openEditModal(${index})" title="Edit file details">✏️</button> <button type="button" class="file-chip-edit" onclick="openEditModal(${index})" title="Edit file details">✏️</button>
<button type="button" class="file-chip-remove" onclick="toggleFileActive(${index})" title="${fileMetadata.isActive ? 'Remove from upload' : 'Add back to upload'}">${fileMetadata.isActive ? '❌' : '➕'}</button> <button type="button" class="file-chip-remove" onclick="toggleFileActive(${index})" title="${fileMetadata.isActive ? 'Remove from upload' : 'Add back to upload'}">${fileMetadata.isActive ? '❌' : '➕'}</button>
`; `;
filesArea.appendChild(chip); filesArea.appendChild(chip);
console.log('File chip rendered for:', fileMetadata.displayName);
} }
function toggleFileActive(index) { function toggleFileActive(index) {
@ -234,17 +254,50 @@
} }
function openEditModal(index) { function openEditModal(index) {
console.log('openEditModal called with index:', index);
console.log('selectedFilesData length:', selectedFilesData.length);
if (index >= 0 && index < selectedFilesData.length) { if (index >= 0 && index < selectedFilesData.length) {
const fileData = selectedFilesData[index]; const fileData = selectedFilesData[index];
document.getElementById('editFileOriginalIndex').value = index; console.log('File data:', fileData);
document.getElementById('editDisplayName').value = fileData.displayName;
document.getElementById('editDocumentType').value = fileData.documentType; const originalIndexInput = document.getElementById('editFileOriginalIndex');
document.getElementById('editFileModal').style.display = 'block'; const displayNameInput = document.getElementById('editDisplayName');
const documentTypeInput = document.getElementById('editDocumentType');
const modal = document.getElementById('editFileModal');
console.log('Modal element:', modal);
console.log('Input elements:', { originalIndexInput, displayNameInput, documentTypeInput });
if (originalIndexInput && displayNameInput && documentTypeInput && modal) {
originalIndexInput.value = index;
displayNameInput.value = fileData.displayName;
documentTypeInput.value = fileData.documentType;
// Ensure modal is displayed properly
modal.style.display = 'flex'; // Use flex instead of block for better centering
modal.style.zIndex = '1002'; // Ensure it's above the overlay
console.log('Modal opened for file:', fileData.displayName);
console.log('Modal display style:', modal.style.display);
console.log('Modal z-index:', modal.style.zIndex);
} else {
console.error('Missing modal elements:', { originalIndexInput, displayNameInput, documentTypeInput, modal });
}
} else {
console.error('Invalid index or selectedFilesData:', { index, selectedFilesDataLength: selectedFilesData.length });
} }
} }
function closeEditModal() { function closeEditModal() {
document.getElementById('editFileModal').style.display = 'none'; console.log('closeEditModal called');
const modal = document.getElementById('editFileModal');
if (modal) {
modal.style.display = 'none';
console.log('Modal closed');
} else {
console.error('Modal element not found');
}
} }
function saveFileChanges() { function saveFileChanges() {
@ -258,11 +311,12 @@
if (chip) { if (chip) {
const icon = getFileIcon(selectedFilesData[index].displayName); const icon = getFileIcon(selectedFilesData[index].displayName);
const truncatedName = truncateFilename(selectedFilesData[index].displayName); const truncatedName = truncateFilename(selectedFilesData[index].displayName);
const documentTypeName = getDocumentTypeName(selectedFilesData[index].documentType);
chip.innerHTML = ` chip.innerHTML = `
<span class="file-chip-icon">${icon}</span> <span class="file-chip-icon">${icon}</span>
<span class="file-chip-name" title="${selectedFilesData[index].displayName}">${truncatedName}</span> <span class="file-chip-name" title="${selectedFilesData[index].displayName}" onclick="openEditModal(${index})" style="cursor: pointer;">${truncatedName}</span>
<span class="file-chip-doctype">Type: ${selectedFilesData[index].documentType}</span> <span class="file-chip-doctype">${documentTypeName}</span>
<button type="button" class="file-chip-edit" onclick="openEditModal(${index})" title="Edit file details">✏️</button> <button type="button" class="file-chip-edit" onclick="openEditModal(${index})" title="Edit file details">✏️</button>
<button type="button" class="file-chip-remove" onclick="toggleFileActive(${index})" title="${selectedFilesData[index].isActive ? 'Remove from upload' : 'Add back to upload'}">${selectedFilesData[index].isActive ? '❌' : '➕'}</button> <button type="button" class="file-chip-remove" onclick="toggleFileActive(${index})" title="${selectedFilesData[index].isActive ? 'Remove from upload' : 'Add back to upload'}">${selectedFilesData[index].isActive ? '❌' : '➕'}</button>
`; `;
@ -325,6 +379,26 @@
// Add event listeners for the upload overlay // Add event listeners for the upload overlay
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Add click outside modal to close functionality
const modal = document.getElementById('editFileModal');
if (modal) {
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeEditModal();
}
});
}
// Add escape key to close modal
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') {
const modal = document.getElementById('editFileModal');
if (modal && modal.style.display !== 'none') {
closeEditModal();
}
}
});
document.querySelectorAll('form').forEach(form => { document.querySelectorAll('form').forEach(form => {
form.addEventListener('htmx:beforeRequest', function (evt) { form.addEventListener('htmx:beforeRequest', function (evt) {
if (evt.detail.pathInfo.requestPath.includes('/upload-documents')) { if (evt.detail.pathInfo.requestPath.includes('/upload-documents')) {

Loading…
Cancel
Save