mirror of
https://github.com/rickbarrette/redmine_qbo_lineitems.git
synced 2026-04-02 07:01:59 -04:00
Compare commits
4 Commits
2026.3.11
...
4b561ef4e3
| Author | SHA1 | Date | |
|---|---|---|---|
| 4b561ef4e3 | |||
| 1dadcf37b6 | |||
| ffaee10fef | |||
| 58994e3c7d |
13
README.md
13
README.md
@@ -10,8 +10,6 @@ This plugin allows **billable line items** to be attached to a Redmine issue. Wh
|
||||
|
||||
* **Redmine:** 6.1+
|
||||
|
||||
* **Ruby:** 3.2+
|
||||
|
||||
* **Parent Plugin:** [Redmine QuickBooks Online](https://github.com/rickbarrette/redmine_qbo) (must be installed and configured)
|
||||
|
||||
|
||||
@@ -19,9 +17,9 @@ This plugin allows **billable line items** to be attached to a Redmine issue. Wh
|
||||
|
||||
## Compatibility
|
||||
|
||||
| Plugin Version | Redmine Version | Ruby Version |
|
||||
| Plugin Version | Redmine Version | Parent Plugin Version |
|
||||
| --- | --- | --- |
|
||||
| 2026.3.6+ | 6.1.x | 3.2+ |
|
||||
| 2026.3.8+ | 6.1.x | 2026.3.9+ |
|
||||
|
||||
---
|
||||
|
||||
@@ -87,9 +85,10 @@ Before using this plugin:
|
||||
1. Install and configure the parent plugin.
|
||||
|
||||
2. Ensure your **QuickBooks Online** company file is connected.
|
||||
|
||||
3. Verify that the products or services referenced in line items exist in QuickBooks.
|
||||
|
||||
|
||||
3. Sync Accounts & Items via plugin settings
|
||||
|
||||
4. Set default income account for new items via plugin settings
|
||||
|
||||
---
|
||||
|
||||
|
||||
BIN
Screenshots/accounts.png
Normal file
BIN
Screenshots/accounts.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
BIN
Screenshots/edit item.png
Normal file
BIN
Screenshots/edit item.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
Screenshots/item.png
Normal file
BIN
Screenshots/item.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
BIN
Screenshots/items.png
Normal file
BIN
Screenshots/items.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 368 KiB |
BIN
Screenshots/settings.png
Normal file
BIN
Screenshots/settings.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 166 KiB |
@@ -1,3 +1,9 @@
|
||||
let lastKeyWasTab = false;
|
||||
|
||||
document.addEventListener("keydown", function (e) {
|
||||
lastKeyWasTab = (e.key === "Tab");
|
||||
});
|
||||
|
||||
(function () {
|
||||
function initNestedForms() {
|
||||
document.querySelectorAll("[data-nested-form]").forEach(function (wrapper) {
|
||||
@@ -22,11 +28,28 @@
|
||||
Date.now().toString()
|
||||
);
|
||||
|
||||
//container.insertAdjacentHTML("beforeend", content);
|
||||
container.insertAdjacentHTML("beforeend", content);
|
||||
|
||||
// initialize autocomplete on the new row
|
||||
initLineItemAutocomplete(container.lastElementChild);
|
||||
const newRow = container.lastElementChild;
|
||||
|
||||
// Ensure clean state
|
||||
newRow.dataset.autoAdded = "false";
|
||||
|
||||
// Reset defaults
|
||||
const qty = newRow.querySelector(".qty-field");
|
||||
if (qty && !qty.value) qty.value = 1;
|
||||
|
||||
const price = newRow.querySelector(".price-field");
|
||||
if (price) price.value = "";
|
||||
|
||||
// initialize autocomplete
|
||||
initLineItemAutocomplete(newRow);
|
||||
|
||||
// Only focus if NOT tabbing
|
||||
if (!lastKeyWasTab) {
|
||||
const desc = newRow.querySelector(".line-item-description");
|
||||
if (desc) desc.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE
|
||||
@@ -56,9 +79,77 @@
|
||||
document.addEventListener("turbo:load", initNestedForms);
|
||||
})();
|
||||
|
||||
$(document).on("input", ".line-item-description", function(){
|
||||
|
||||
// Keep your existing behavior
|
||||
$(document).on("input", ".line-item-description", function () {
|
||||
let row = $(this).closest(".line-item");
|
||||
row.find(".item-id-field").val("");
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// -------------------------------
|
||||
// AUTO-ADD NEW ROW LOGIC
|
||||
// -------------------------------
|
||||
|
||||
// Reset autoAdded flag if cleared
|
||||
document.addEventListener("input", function (e) {
|
||||
if (!e.target.classList.contains("line-item-description")) return;
|
||||
|
||||
const row = e.target.closest(".line-item");
|
||||
if (!row) return;
|
||||
|
||||
if (e.target.value.trim() === "") {
|
||||
row.dataset.autoAdded = "false";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Add row when leaving last description (without breaking TAB flow)
|
||||
document.addEventListener("blur", function (e) {
|
||||
if (!e.target.classList.contains("line-item-description")) return;
|
||||
|
||||
const input = e.target;
|
||||
const row = input.closest(".line-item");
|
||||
if (!row) return;
|
||||
|
||||
const wrapper = input.closest("[data-nested-form]");
|
||||
if (!wrapper) return;
|
||||
|
||||
const container = wrapper.querySelector("[data-nested-form-container]");
|
||||
if (!container) return;
|
||||
|
||||
// Active (visible + not destroyed) rows only
|
||||
const rows = Array.from(
|
||||
container.querySelectorAll(wrapper.dataset.wrapperSelector)
|
||||
).filter(r => {
|
||||
const destroy = r.querySelector("input[name*='[_destroy]']");
|
||||
const hidden = window.getComputedStyle(r).display === "none";
|
||||
return !(destroy && destroy.value === "1") && !hidden;
|
||||
});
|
||||
|
||||
const lastRow = rows[rows.length - 1];
|
||||
|
||||
// Only last row
|
||||
if (row !== lastRow) return;
|
||||
|
||||
// Must have content
|
||||
if (input.value.trim() === "") return;
|
||||
|
||||
// Prevent duplicate firing
|
||||
if (row.dataset.autoAdded === "true") return;
|
||||
|
||||
// If TAB, ensure user is leaving the row entirely
|
||||
if (lastKeyWasTab) {
|
||||
const next = document.activeElement;
|
||||
|
||||
if (row.contains(next)) {
|
||||
return; // still inside row → allow normal tabbing
|
||||
}
|
||||
}
|
||||
|
||||
row.dataset.autoAdded = "true";
|
||||
|
||||
const addButton = wrapper.querySelector("[data-nested-form-add]");
|
||||
if (addButton) addButton.click();
|
||||
|
||||
}, true); // capture phase required for blur
|
||||
Reference in New Issue
Block a user