Skip to content

Commit e17358d

Browse files
authored
Merge pull request #5462 from janeewheatley/5446-duplicate_item_entries_multiple_barcodes
5446 Duplicate Item Entries Related to Multiple Barcodes
2 parents d1c27f3 + 5f607b2 commit e17358d

File tree

4 files changed

+237
-1
lines changed

4 files changed

+237
-1
lines changed

app/assets/stylesheets/modal-dialog.scss

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,54 @@
3131
width: 700px;
3232
}
3333
}
34+
35+
/* DUPLICATE ITEMS MODAL */
36+
.duplicate-container {
37+
margin-bottom: 20px;
38+
padding: 10px;
39+
background-color: #f9f9f9;
40+
border: 1px solid #ddd;
41+
border-radius: 5px;
42+
}
43+
44+
.duplicate-entry {
45+
padding: 8px;
46+
margin: 4px 0;
47+
background-color: #fff3cd;
48+
border-left: 3px solid #ffc107;
49+
}
50+
51+
.duplicate-barcode {
52+
font-size: 0.85em;
53+
color: #666;
54+
margin-top: 2px;
55+
}
56+
57+
.duplicate-merged {
58+
padding: 10px;
59+
margin: 10px 0 0 0;
60+
background-color: #d4edda;
61+
border: 2px solid #28a745;
62+
border-radius: 4px;
63+
font-weight: bold;
64+
}
65+
66+
.duplicate-modal-footer {
67+
display: flex;
68+
justify-content: space-between;
69+
align-items: center;
70+
}
71+
72+
.duplicate-modal-text {
73+
margin: 0;
74+
font-size: 0.9em;
75+
margin-right: 20px;
76+
}
77+
78+
.duplicate-modal-buttons {
79+
margin-left: auto;
80+
}
81+
82+
.duplicate-items-list {
83+
margin-bottom: 20px;
84+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import { Controller } from "@hotwired/stimulus"
2+
3+
export default class extends Controller {
4+
connect() {
5+
this.boundHandleSubmit = this.handleSubmit.bind(this)
6+
this.element.addEventListener("submit", this.boundHandleSubmit)
7+
}
8+
9+
handleSubmit(event) {
10+
const submitter = event.submitter
11+
12+
if (!submitter?.name) return
13+
if (!submitter.name.includes('save_progress') &&
14+
!submitter.name.includes('confirm_audit')) {
15+
return
16+
}
17+
18+
event.preventDefault()
19+
20+
const duplicates = this.findDuplicates()
21+
22+
if (duplicates.length > 0) {
23+
this.showModal(duplicates, submitter.name)
24+
} else {
25+
this.submitForm(submitter.name)
26+
}
27+
}
28+
29+
findDuplicates() {
30+
const itemCounts = {}
31+
const itemData = {}
32+
33+
this.element.querySelectorAll('select[name*="[item_id]"]').forEach(select => {
34+
const itemId = select.value
35+
const itemText = select.options[select.selectedIndex]?.text
36+
const section = select.closest('.line_item_section')
37+
const quantityInput = section?.querySelector('input[name*="[quantity]"]')
38+
const quantity = parseInt(quantityInput?.value) || 0
39+
const barcodeValue = section?.querySelector('.__barcode_item_lookup')?.value || ''
40+
41+
if (!itemId || itemText === "Choose an item" || quantity === 0) return
42+
43+
itemCounts[itemId] = (itemCounts[itemId] || 0) + 1
44+
if (!itemData[itemId]) {
45+
itemData[itemId] = { name: itemText, entries: [] }
46+
}
47+
itemData[itemId].entries.push({ quantity, section, barcode: barcodeValue })
48+
})
49+
50+
return Object.keys(itemCounts)
51+
.filter(id => itemCounts[id] > 1)
52+
.map(id => itemData[id])
53+
}
54+
55+
showModal(duplicates, buttonName) {
56+
const itemRows = duplicates.map(item => {
57+
const entries = item.entries
58+
const total = entries.reduce((sum, entry) => sum + entry.quantity, 0)
59+
const rows = entries.map(entry => {
60+
const barcodeLine = entry.barcode ? `<div class="duplicate-barcode">Barcode: ${entry.barcode}</div>` : ''
61+
return `<div class="duplicate-entry">❐ ${item.name} : ${entry.quantity}${barcodeLine}</div>`
62+
}).join('')
63+
return `<div class="duplicate-container">${rows}<div class="duplicate-merged">→ Merged Result: ${item.name} : ${total}</div></div>`
64+
}).join('')
65+
66+
const modalHtml = `
67+
<div class="modal fade" id="duplicateItemsModal" tabindex="-1">
68+
<div class="modal-dialog modal-dialog-scrollable">
69+
<div class="modal-content">
70+
<div class="modal-header">
71+
<h5 class="modal-title">Multiple Item Entries Detected</h5>
72+
<button type="button" class="close" data-bs-dismiss="modal">
73+
<span>&times;</span>
74+
</button>
75+
</div>
76+
<div class="modal-body">
77+
<p><strong>The following items have multiple entries:</strong></p>
78+
<div class="duplicate-items-list">${itemRows}</div>
79+
</div>
80+
<div class="modal-footer duplicate-modal-footer">
81+
<p class="duplicate-modal-text">
82+
Choose <strong>Merge Items</strong> to combine quantities and continue, or <strong>Make Changes</strong> to go back and edit.
83+
</p>
84+
<div class="duplicate-modal-buttons">
85+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Make Changes</button>
86+
<button type="button" class="btn btn-success" id="confirmMerge">Merge Items</button>
87+
</div>
88+
</div>
89+
</div>
90+
</div>
91+
</div>
92+
`
93+
94+
document.getElementById('duplicateItemsModal')?.remove()
95+
document.body.insertAdjacentHTML('beforeend', modalHtml)
96+
97+
const modal = new bootstrap.Modal(document.getElementById('duplicateItemsModal'))
98+
modal.show()
99+
100+
document.getElementById('confirmMerge').addEventListener('click', () => {
101+
this.mergeAndSubmit(duplicates, buttonName)
102+
})
103+
}
104+
105+
mergeAndSubmit(duplicates, buttonName) {
106+
duplicates.forEach(item => {
107+
const total = item.entries.reduce((sum, entry) => sum + entry.quantity, 0)
108+
109+
// Separate the first entry from remaining entries
110+
const [firstEntry, ...remainingEntries] = item.entries
111+
112+
// Update the first entry with the merged total
113+
firstEntry.section.querySelector('input[name*="[quantity]"]').value = total
114+
115+
// Remove all duplicate entries from the form submission
116+
remainingEntries.forEach(entry => entry.section.remove())
117+
})
118+
119+
const modal = new bootstrap.Modal(document.getElementById('duplicateItemsModal'))
120+
modal.hide()
121+
122+
this.submitForm(buttonName)
123+
}
124+
125+
submitForm(buttonName) {
126+
this.element.removeEventListener('submit', this.boundHandleSubmit)
127+
128+
const input = document.createElement('input')
129+
input.type = 'hidden'
130+
input.name = buttonName
131+
input.value = '1'
132+
this.element.appendChild(input)
133+
134+
this.element.submit()
135+
}
136+
}

app/views/audits/_form.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<div class="box-header with-border">
1414
</div>
1515
<div class="box-body">
16-
<%= simple_form_for @audit, data: { controller: "form-input" }, html: {class: "storage-location-required"} do |f| %>
16+
<%= simple_form_for @audit, data: { controller: "form-input audit-duplicates" }, html: {class: "storage-location-required"} do |f| %>
1717
<%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?", include_omitted_items: true } %>
1818
<fieldset style="margin-bottom: 2rem;">
1919
<legend>Items in this audit</legend>

spec/system/audit_system_spec.rb

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,55 @@
195195
expect(page).to have_content("Delete Audit")
196196
expect(page).to have_content("Finalize Audit")
197197
end
198+
199+
it "detects duplicate items and shows modal", js: true do
200+
# Disable server-side validation to test JS modal
201+
allow_any_instance_of(Audit).to receive(:line_items_unique_by_item_id)
202+
visit subject
203+
click_link "New Audit"
204+
205+
await_select2("#audit_line_items_attributes_0_item_id") do
206+
select storage_location.name, from: "Storage location"
207+
end
208+
209+
# Add first entry for the item
210+
select item.name, from: "audit_line_items_attributes_0_item_id"
211+
fill_in "audit_line_items_attributes_0_quantity", with: "10"
212+
213+
# Add a new line item row
214+
find("[data-form-input-target='addButton']").click
215+
216+
# Add second entry for the same item
217+
within all('.line_item_section').last do
218+
item_select = find('select[name*="[item_id]"]')
219+
select item.name, from: item_select[:id]
220+
quantity_input = find('input[name*="[quantity]"]')
221+
fill_in quantity_input[:id], with: "15"
222+
end
223+
224+
# Try to save - should trigger duplicate detection modal
225+
click_button "Save Progress"
226+
227+
# JavaScript modal should appear
228+
expect(page).to have_css("#duplicateItemsModal", visible: true)
229+
expect(page).to have_content("Multiple Item Entries Detected")
230+
expect(page).to have_content("Merge Items")
231+
expect(page).to have_content("Make Changes")
232+
233+
# Test merge functionality
234+
audit_id = nil
235+
expect {
236+
click_button "Merge Items"
237+
expect(page).to have_content("Audit's progress was successfully saved.")
238+
audit_id = Audit.maximum(:id)
239+
}.to change { Audit.count }.by(1)
240+
241+
# Verify only one line item with merged quantity (10 + 15 = 25)
242+
created_audit = Audit.find(audit_id)
243+
line_item = created_audit.line_items.find_by(item_id: item.id)
244+
expect(created_audit.line_items.count).to eq(1)
245+
expect(line_item.quantity).to eq(25)
246+
end
198247
end
199248

200249
context "with an existing audit" do

0 commit comments

Comments
 (0)