mantenimento-app 2.4.9 → 2.4.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.js +67 -22
- package/frontend/public/app.js +67 -22
- package/frontend/public/index.html +1 -1
- package/frontend/public/styles.css +14 -0
- package/package.json +1 -1
package/app.js
CHANGED
|
@@ -31,6 +31,7 @@ const defaultExpenseItems = [
|
|
|
31
31
|
const EXPENSE_DETAIL_MAX_CHARS = 560;
|
|
32
32
|
const EXPENSE_DETAIL_MAX_LINES = 10;
|
|
33
33
|
const EXPENSE_DETAIL_MAX_ROWS = 8;
|
|
34
|
+
const EXPENSE_DETAIL_NOTE_SEPARATOR = "\n---\n";
|
|
34
35
|
|
|
35
36
|
const QUOTA_MANTENIMENTO_PERC = 35;
|
|
36
37
|
|
|
@@ -256,6 +257,7 @@ const defaultExpenseItems = [
|
|
|
256
257
|
expenseDetailColDue: "Scadenza",
|
|
257
258
|
expenseDetailAddRow: "Aggiungi riga",
|
|
258
259
|
expenseDetailRemoveRow: "Rimuovi riga",
|
|
260
|
+
expenseDetailFreeTextPlaceholder: "Note libere aggiuntive...",
|
|
259
261
|
expenseDetailCharsRemaining: "Caratteri rimanenti: {count}",
|
|
260
262
|
expenseRemoveTitle: "Rimuovi voce spesa",
|
|
261
263
|
expenseRemoveBtn: "Rimuovi",
|
|
@@ -623,6 +625,7 @@ const defaultExpenseItems = [
|
|
|
623
625
|
expenseDetailColDue: "Due date",
|
|
624
626
|
expenseDetailAddRow: "Add row",
|
|
625
627
|
expenseDetailRemoveRow: "Remove row",
|
|
628
|
+
expenseDetailFreeTextPlaceholder: "Additional free notes...",
|
|
626
629
|
expenseDetailCharsRemaining: "Remaining characters: {count}",
|
|
627
630
|
expenseRemoveTitle: "Remove expense item",
|
|
628
631
|
expenseRemoveBtn: "Remove",
|
|
@@ -1471,25 +1474,48 @@ const defaultExpenseItems = [
|
|
|
1471
1474
|
syncExpenseDetailTableFromStore(textarea);
|
|
1472
1475
|
}
|
|
1473
1476
|
|
|
1474
|
-
function
|
|
1475
|
-
const
|
|
1477
|
+
function parseExpenseDetailPayload(raw) {
|
|
1478
|
+
const source = String(raw || "").trim();
|
|
1479
|
+
let tablePart = source;
|
|
1480
|
+
let notePart = "";
|
|
1481
|
+
const sepIdx = source.indexOf(EXPENSE_DETAIL_NOTE_SEPARATOR);
|
|
1482
|
+
if (sepIdx >= 0) {
|
|
1483
|
+
tablePart = source.slice(0, sepIdx).trim();
|
|
1484
|
+
notePart = source.slice(sepIdx + EXPENSE_DETAIL_NOTE_SEPARATOR.length).trim();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const tableLines = tablePart
|
|
1476
1488
|
.split(/\r?\n/)
|
|
1477
1489
|
.map((line) => line.trim())
|
|
1478
1490
|
.filter(Boolean);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1491
|
+
const rows = [];
|
|
1492
|
+
const looseNoteLines = [];
|
|
1493
|
+
tableLines.forEach((line) => {
|
|
1494
|
+
const cols = line.split("|").map((part) => part.trim());
|
|
1495
|
+
if (cols.length >= 2) {
|
|
1496
|
+
rows.push({
|
|
1497
|
+
what: cols[0] || "",
|
|
1498
|
+
amount: cols[1] || "",
|
|
1499
|
+
due: cols.slice(2).join(" | ") || ""
|
|
1500
|
+
});
|
|
1501
|
+
} else {
|
|
1502
|
+
looseNoteLines.push(line);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
const resolvedNote = [notePart, looseNoteLines.join("\n")]
|
|
1507
|
+
.filter(Boolean)
|
|
1508
|
+
.join(notePart && looseNoteLines.length ? "\n" : "")
|
|
1509
|
+
.trim();
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
rows: (rows.length ? rows : [{ what: "", amount: "", due: "" }]).slice(0, EXPENSE_DETAIL_MAX_ROWS),
|
|
1513
|
+
note: resolvedNote
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function parseExpenseDetailRows(raw) {
|
|
1518
|
+
return parseExpenseDetailPayload(raw).rows;
|
|
1493
1519
|
}
|
|
1494
1520
|
|
|
1495
1521
|
function sanitizeExpenseDetailCell(value) {
|
|
@@ -1510,7 +1536,14 @@ const defaultExpenseItems = [
|
|
|
1510
1536
|
return compact.map((row) => `${row.what} | ${row.amount} | ${row.due}`.trim()).join("\n");
|
|
1511
1537
|
}
|
|
1512
1538
|
|
|
1513
|
-
function
|
|
1539
|
+
function serializeExpenseDetailPayload(rows, note) {
|
|
1540
|
+
const tablePart = serializeExpenseDetailRows(rows);
|
|
1541
|
+
const safeNote = String(note || "").trim();
|
|
1542
|
+
if (!safeNote) return tablePart;
|
|
1543
|
+
return `${tablePart}${tablePart ? EXPENSE_DETAIL_NOTE_SEPARATOR : ""}${safeNote}`;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function buildExpenseDetailTableHtml(textareaId, rows, note = "") {
|
|
1514
1547
|
const safeRows = (Array.isArray(rows) && rows.length ? rows : [{ what: "", amount: "", due: "" }])
|
|
1515
1548
|
.slice(0, EXPENSE_DETAIL_MAX_ROWS);
|
|
1516
1549
|
const rowsHtml = safeRows.map((row) => {
|
|
@@ -1535,6 +1568,7 @@ const defaultExpenseItems = [
|
|
|
1535
1568
|
<tbody>${rowsHtml}</tbody>
|
|
1536
1569
|
</table>
|
|
1537
1570
|
<button type="button" class="btn-secondary spese-detail-add-row" data-row-add="${textareaId}">${escapeHtml(tr("expenseDetailAddRow"))}</button>
|
|
1571
|
+
<textarea class="spese-detail-note" data-detail-note-for="${textareaId}" rows="2" maxlength="360" placeholder="${escapeHtml(tr("expenseDetailFreeTextPlaceholder"))}">${escapeHtml(note || "")}</textarea>
|
|
1538
1572
|
</div>`;
|
|
1539
1573
|
}
|
|
1540
1574
|
|
|
@@ -1552,7 +1586,9 @@ const defaultExpenseItems = [
|
|
|
1552
1586
|
const textareaId = String(tableWrap.getAttribute("data-detail-table") || "");
|
|
1553
1587
|
const textarea = textareaId ? document.getElementById(textareaId) : null;
|
|
1554
1588
|
if (!textarea) return;
|
|
1555
|
-
const
|
|
1589
|
+
const noteEl = tableWrap.querySelector(`textarea[data-detail-note-for='${textareaId}']`);
|
|
1590
|
+
const note = String(noteEl && noteEl.value ? noteEl.value : "");
|
|
1591
|
+
const serialized = serializeExpenseDetailPayload(readExpenseDetailRowsFromTable(tableWrap), note);
|
|
1556
1592
|
textarea.value = serialized.slice(0, EXPENSE_DETAIL_MAX_CHARS);
|
|
1557
1593
|
updateExpenseDetailCounter(textarea);
|
|
1558
1594
|
}
|
|
@@ -1562,10 +1598,12 @@ const defaultExpenseItems = [
|
|
|
1562
1598
|
const host = document.getElementById(`${textarea.id}TableHost`);
|
|
1563
1599
|
if (!host) return;
|
|
1564
1600
|
const existing = host.querySelector(`[data-detail-table='${textarea.id}']`);
|
|
1565
|
-
const
|
|
1601
|
+
const existingNote = existing ? String(existing.querySelector(`textarea[data-detail-note-for='${textarea.id}']`)?.value || "") : "";
|
|
1602
|
+
const existingSerialized = serializeExpenseDetailPayload(existing ? readExpenseDetailRowsFromTable(existing) : [], existingNote);
|
|
1566
1603
|
const storeSerialized = String(textarea.value || "").trim();
|
|
1567
1604
|
if (existing && existingSerialized === storeSerialized) return;
|
|
1568
|
-
|
|
1605
|
+
const payload = parseExpenseDetailPayload(storeSerialized);
|
|
1606
|
+
host.innerHTML = buildExpenseDetailTableHtml(textarea.id, payload.rows, payload.note);
|
|
1569
1607
|
syncExpenseDetailStoreFromTable(host.querySelector(`[data-detail-table='${textarea.id}']`));
|
|
1570
1608
|
}
|
|
1571
1609
|
|
|
@@ -6446,10 +6484,11 @@ ${scenarioLab.length ? `
|
|
|
6446
6484
|
const textareaId = String(addRowBtn.getAttribute("data-row-add") || "");
|
|
6447
6485
|
const textarea = textareaId ? document.getElementById(textareaId) : null;
|
|
6448
6486
|
if (!textarea) return;
|
|
6449
|
-
const
|
|
6487
|
+
const payload = parseExpenseDetailPayload(textarea.value);
|
|
6488
|
+
const rows = payload.rows;
|
|
6450
6489
|
if (rows.length < EXPENSE_DETAIL_MAX_ROWS) {
|
|
6451
6490
|
rows.push({ what: "", amount: "", due: "" });
|
|
6452
|
-
textarea.value =
|
|
6491
|
+
textarea.value = serializeExpenseDetailPayload(rows, payload.note);
|
|
6453
6492
|
syncExpenseDetailTableFromStore(textarea);
|
|
6454
6493
|
refreshExpenseDetailButtonState();
|
|
6455
6494
|
}
|
|
@@ -6491,6 +6530,12 @@ ${scenarioLab.length ? `
|
|
|
6491
6530
|
const tableWrap = e.target.closest("[data-detail-table]");
|
|
6492
6531
|
syncExpenseDetailStoreFromTable(tableWrap);
|
|
6493
6532
|
refreshExpenseDetailButtonState();
|
|
6533
|
+
return;
|
|
6534
|
+
}
|
|
6535
|
+
if (e.target && e.target.matches(".spese-detail-note")) {
|
|
6536
|
+
const tableWrap = e.target.closest("[data-detail-table]");
|
|
6537
|
+
syncExpenseDetailStoreFromTable(tableWrap);
|
|
6538
|
+
refreshExpenseDetailButtonState();
|
|
6494
6539
|
}
|
|
6495
6540
|
});
|
|
6496
6541
|
|
package/frontend/public/app.js
CHANGED
|
@@ -31,6 +31,7 @@ const defaultExpenseItems = [
|
|
|
31
31
|
const EXPENSE_DETAIL_MAX_CHARS = 560;
|
|
32
32
|
const EXPENSE_DETAIL_MAX_LINES = 10;
|
|
33
33
|
const EXPENSE_DETAIL_MAX_ROWS = 8;
|
|
34
|
+
const EXPENSE_DETAIL_NOTE_SEPARATOR = "\n---\n";
|
|
34
35
|
|
|
35
36
|
const QUOTA_MANTENIMENTO_PERC = 35;
|
|
36
37
|
|
|
@@ -256,6 +257,7 @@ const defaultExpenseItems = [
|
|
|
256
257
|
expenseDetailColDue: "Scadenza",
|
|
257
258
|
expenseDetailAddRow: "Aggiungi riga",
|
|
258
259
|
expenseDetailRemoveRow: "Rimuovi riga",
|
|
260
|
+
expenseDetailFreeTextPlaceholder: "Note libere aggiuntive...",
|
|
259
261
|
expenseDetailCharsRemaining: "Caratteri rimanenti: {count}",
|
|
260
262
|
expenseRemoveTitle: "Rimuovi voce spesa",
|
|
261
263
|
expenseRemoveBtn: "Rimuovi",
|
|
@@ -623,6 +625,7 @@ const defaultExpenseItems = [
|
|
|
623
625
|
expenseDetailColDue: "Due date",
|
|
624
626
|
expenseDetailAddRow: "Add row",
|
|
625
627
|
expenseDetailRemoveRow: "Remove row",
|
|
628
|
+
expenseDetailFreeTextPlaceholder: "Additional free notes...",
|
|
626
629
|
expenseDetailCharsRemaining: "Remaining characters: {count}",
|
|
627
630
|
expenseRemoveTitle: "Remove expense item",
|
|
628
631
|
expenseRemoveBtn: "Remove",
|
|
@@ -1471,25 +1474,48 @@ const defaultExpenseItems = [
|
|
|
1471
1474
|
syncExpenseDetailTableFromStore(textarea);
|
|
1472
1475
|
}
|
|
1473
1476
|
|
|
1474
|
-
function
|
|
1475
|
-
const
|
|
1477
|
+
function parseExpenseDetailPayload(raw) {
|
|
1478
|
+
const source = String(raw || "").trim();
|
|
1479
|
+
let tablePart = source;
|
|
1480
|
+
let notePart = "";
|
|
1481
|
+
const sepIdx = source.indexOf(EXPENSE_DETAIL_NOTE_SEPARATOR);
|
|
1482
|
+
if (sepIdx >= 0) {
|
|
1483
|
+
tablePart = source.slice(0, sepIdx).trim();
|
|
1484
|
+
notePart = source.slice(sepIdx + EXPENSE_DETAIL_NOTE_SEPARATOR.length).trim();
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const tableLines = tablePart
|
|
1476
1488
|
.split(/\r?\n/)
|
|
1477
1489
|
.map((line) => line.trim())
|
|
1478
1490
|
.filter(Boolean);
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1491
|
+
const rows = [];
|
|
1492
|
+
const looseNoteLines = [];
|
|
1493
|
+
tableLines.forEach((line) => {
|
|
1494
|
+
const cols = line.split("|").map((part) => part.trim());
|
|
1495
|
+
if (cols.length >= 2) {
|
|
1496
|
+
rows.push({
|
|
1497
|
+
what: cols[0] || "",
|
|
1498
|
+
amount: cols[1] || "",
|
|
1499
|
+
due: cols.slice(2).join(" | ") || ""
|
|
1500
|
+
});
|
|
1501
|
+
} else {
|
|
1502
|
+
looseNoteLines.push(line);
|
|
1503
|
+
}
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
const resolvedNote = [notePart, looseNoteLines.join("\n")]
|
|
1507
|
+
.filter(Boolean)
|
|
1508
|
+
.join(notePart && looseNoteLines.length ? "\n" : "")
|
|
1509
|
+
.trim();
|
|
1510
|
+
|
|
1511
|
+
return {
|
|
1512
|
+
rows: (rows.length ? rows : [{ what: "", amount: "", due: "" }]).slice(0, EXPENSE_DETAIL_MAX_ROWS),
|
|
1513
|
+
note: resolvedNote
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
function parseExpenseDetailRows(raw) {
|
|
1518
|
+
return parseExpenseDetailPayload(raw).rows;
|
|
1493
1519
|
}
|
|
1494
1520
|
|
|
1495
1521
|
function sanitizeExpenseDetailCell(value) {
|
|
@@ -1510,7 +1536,14 @@ const defaultExpenseItems = [
|
|
|
1510
1536
|
return compact.map((row) => `${row.what} | ${row.amount} | ${row.due}`.trim()).join("\n");
|
|
1511
1537
|
}
|
|
1512
1538
|
|
|
1513
|
-
function
|
|
1539
|
+
function serializeExpenseDetailPayload(rows, note) {
|
|
1540
|
+
const tablePart = serializeExpenseDetailRows(rows);
|
|
1541
|
+
const safeNote = String(note || "").trim();
|
|
1542
|
+
if (!safeNote) return tablePart;
|
|
1543
|
+
return `${tablePart}${tablePart ? EXPENSE_DETAIL_NOTE_SEPARATOR : ""}${safeNote}`;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
function buildExpenseDetailTableHtml(textareaId, rows, note = "") {
|
|
1514
1547
|
const safeRows = (Array.isArray(rows) && rows.length ? rows : [{ what: "", amount: "", due: "" }])
|
|
1515
1548
|
.slice(0, EXPENSE_DETAIL_MAX_ROWS);
|
|
1516
1549
|
const rowsHtml = safeRows.map((row) => {
|
|
@@ -1535,6 +1568,7 @@ const defaultExpenseItems = [
|
|
|
1535
1568
|
<tbody>${rowsHtml}</tbody>
|
|
1536
1569
|
</table>
|
|
1537
1570
|
<button type="button" class="btn-secondary spese-detail-add-row" data-row-add="${textareaId}">${escapeHtml(tr("expenseDetailAddRow"))}</button>
|
|
1571
|
+
<textarea class="spese-detail-note" data-detail-note-for="${textareaId}" rows="2" maxlength="360" placeholder="${escapeHtml(tr("expenseDetailFreeTextPlaceholder"))}">${escapeHtml(note || "")}</textarea>
|
|
1538
1572
|
</div>`;
|
|
1539
1573
|
}
|
|
1540
1574
|
|
|
@@ -1552,7 +1586,9 @@ const defaultExpenseItems = [
|
|
|
1552
1586
|
const textareaId = String(tableWrap.getAttribute("data-detail-table") || "");
|
|
1553
1587
|
const textarea = textareaId ? document.getElementById(textareaId) : null;
|
|
1554
1588
|
if (!textarea) return;
|
|
1555
|
-
const
|
|
1589
|
+
const noteEl = tableWrap.querySelector(`textarea[data-detail-note-for='${textareaId}']`);
|
|
1590
|
+
const note = String(noteEl && noteEl.value ? noteEl.value : "");
|
|
1591
|
+
const serialized = serializeExpenseDetailPayload(readExpenseDetailRowsFromTable(tableWrap), note);
|
|
1556
1592
|
textarea.value = serialized.slice(0, EXPENSE_DETAIL_MAX_CHARS);
|
|
1557
1593
|
updateExpenseDetailCounter(textarea);
|
|
1558
1594
|
}
|
|
@@ -1562,10 +1598,12 @@ const defaultExpenseItems = [
|
|
|
1562
1598
|
const host = document.getElementById(`${textarea.id}TableHost`);
|
|
1563
1599
|
if (!host) return;
|
|
1564
1600
|
const existing = host.querySelector(`[data-detail-table='${textarea.id}']`);
|
|
1565
|
-
const
|
|
1601
|
+
const existingNote = existing ? String(existing.querySelector(`textarea[data-detail-note-for='${textarea.id}']`)?.value || "") : "";
|
|
1602
|
+
const existingSerialized = serializeExpenseDetailPayload(existing ? readExpenseDetailRowsFromTable(existing) : [], existingNote);
|
|
1566
1603
|
const storeSerialized = String(textarea.value || "").trim();
|
|
1567
1604
|
if (existing && existingSerialized === storeSerialized) return;
|
|
1568
|
-
|
|
1605
|
+
const payload = parseExpenseDetailPayload(storeSerialized);
|
|
1606
|
+
host.innerHTML = buildExpenseDetailTableHtml(textarea.id, payload.rows, payload.note);
|
|
1569
1607
|
syncExpenseDetailStoreFromTable(host.querySelector(`[data-detail-table='${textarea.id}']`));
|
|
1570
1608
|
}
|
|
1571
1609
|
|
|
@@ -6446,10 +6484,11 @@ ${scenarioLab.length ? `
|
|
|
6446
6484
|
const textareaId = String(addRowBtn.getAttribute("data-row-add") || "");
|
|
6447
6485
|
const textarea = textareaId ? document.getElementById(textareaId) : null;
|
|
6448
6486
|
if (!textarea) return;
|
|
6449
|
-
const
|
|
6487
|
+
const payload = parseExpenseDetailPayload(textarea.value);
|
|
6488
|
+
const rows = payload.rows;
|
|
6450
6489
|
if (rows.length < EXPENSE_DETAIL_MAX_ROWS) {
|
|
6451
6490
|
rows.push({ what: "", amount: "", due: "" });
|
|
6452
|
-
textarea.value =
|
|
6491
|
+
textarea.value = serializeExpenseDetailPayload(rows, payload.note);
|
|
6453
6492
|
syncExpenseDetailTableFromStore(textarea);
|
|
6454
6493
|
refreshExpenseDetailButtonState();
|
|
6455
6494
|
}
|
|
@@ -6491,6 +6530,12 @@ ${scenarioLab.length ? `
|
|
|
6491
6530
|
const tableWrap = e.target.closest("[data-detail-table]");
|
|
6492
6531
|
syncExpenseDetailStoreFromTable(tableWrap);
|
|
6493
6532
|
refreshExpenseDetailButtonState();
|
|
6533
|
+
return;
|
|
6534
|
+
}
|
|
6535
|
+
if (e.target && e.target.matches(".spese-detail-note")) {
|
|
6536
|
+
const tableWrap = e.target.closest("[data-detail-table]");
|
|
6537
|
+
syncExpenseDetailStoreFromTable(tableWrap);
|
|
6538
|
+
refreshExpenseDetailButtonState();
|
|
6494
6539
|
}
|
|
6495
6540
|
});
|
|
6496
6541
|
|
|
@@ -1592,6 +1592,20 @@
|
|
|
1592
1592
|
font-size: 0.67rem;
|
|
1593
1593
|
}
|
|
1594
1594
|
|
|
1595
|
+
.spese-detail-note {
|
|
1596
|
+
width: 100%;
|
|
1597
|
+
margin-top: 6px;
|
|
1598
|
+
border: 1px solid #bfd5d0;
|
|
1599
|
+
border-radius: 6px;
|
|
1600
|
+
background: #ffffff;
|
|
1601
|
+
color: #214b47;
|
|
1602
|
+
font-size: 0.72rem;
|
|
1603
|
+
line-height: 1.3;
|
|
1604
|
+
padding: 6px 7px;
|
|
1605
|
+
resize: vertical;
|
|
1606
|
+
min-height: 46px;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1595
1609
|
.spese-detail-counter {
|
|
1596
1610
|
margin-top: 3px;
|
|
1597
1611
|
font-size: 0.66rem;
|