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 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 parseExpenseDetailRows(raw) {
1475
- const lines = String(raw || "")
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
- if (!lines.length) return [{ what: "", amount: "", due: "" }];
1480
- return lines
1481
- .map((line) => {
1482
- const cols = line.split("|").map((part) => part.trim());
1483
- if (cols.length >= 2) {
1484
- return {
1485
- what: cols[0] || "",
1486
- amount: cols[1] || "",
1487
- due: cols.slice(2).join(" | ") || ""
1488
- };
1489
- }
1490
- return { what: line, amount: "", due: "" };
1491
- })
1492
- .slice(0, EXPENSE_DETAIL_MAX_ROWS);
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 buildExpenseDetailTableHtml(textareaId, rows) {
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 serialized = serializeExpenseDetailRows(readExpenseDetailRowsFromTable(tableWrap));
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 existingSerialized = serializeExpenseDetailRows(existing ? readExpenseDetailRowsFromTable(existing) : []);
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
- host.innerHTML = buildExpenseDetailTableHtml(textarea.id, parseExpenseDetailRows(storeSerialized));
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 rows = parseExpenseDetailRows(textarea.value);
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 = serializeExpenseDetailRows(rows);
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
 
@@ -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 parseExpenseDetailRows(raw) {
1475
- const lines = String(raw || "")
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
- if (!lines.length) return [{ what: "", amount: "", due: "" }];
1480
- return lines
1481
- .map((line) => {
1482
- const cols = line.split("|").map((part) => part.trim());
1483
- if (cols.length >= 2) {
1484
- return {
1485
- what: cols[0] || "",
1486
- amount: cols[1] || "",
1487
- due: cols.slice(2).join(" | ") || ""
1488
- };
1489
- }
1490
- return { what: line, amount: "", due: "" };
1491
- })
1492
- .slice(0, EXPENSE_DETAIL_MAX_ROWS);
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 buildExpenseDetailTableHtml(textareaId, rows) {
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 serialized = serializeExpenseDetailRows(readExpenseDetailRowsFromTable(tableWrap));
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 existingSerialized = serializeExpenseDetailRows(existing ? readExpenseDetailRowsFromTable(existing) : []);
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
- host.innerHTML = buildExpenseDetailTableHtml(textarea.id, parseExpenseDetailRows(storeSerialized));
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 rows = parseExpenseDetailRows(textarea.value);
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 = serializeExpenseDetailRows(rows);
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
 
@@ -630,7 +630,7 @@
630
630
  <script src="supabase.min.js"></script>
631
631
  <script src="fabric.min.js"></script>
632
632
  <script src="html2pdf.bundle.min.js"></script>
633
- <script src="app.js?v=2.4.9"></script>
633
+ <script src="app.js?v=2.4.10"></script>
634
634
  </body>
635
635
  </html>
636
636
 
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mantenimento-app",
3
- "version": "2.4.9",
3
+ "version": "2.4.10",
4
4
  "description": "Frontend + backend architecture for the mantenimento calculator",
5
5
  "type": "commonjs",
6
6
  "main": "backend/calculate-model.js",