home · contact · privacy
To structured editing view, add addition/deletion/movement of lines.
authorChristian Heller <c.heller@plomlompom.de>
Sun, 26 Jan 2025 21:39:19 +0000 (22:39 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Sun, 26 Jan 2025 21:39:19 +0000 (22:39 +0100)
ledger.py
templates/_base.tmpl
templates/edit_structured.tmpl

index 364ca1f4cdf024e260567de680f6478847311d76..a3bc7af58b35f431007bcf5407a15ec2e87090ee 100755 (executable)
--- a/ledger.py
+++ b/ledger.py
@@ -79,6 +79,14 @@ class DatLine:
         self.code = halves[0]
         self.booking_line: Optional[BookingLine] = None
 
+    @property
+    def as_dict(self) -> dict:
+        """Return as JSON-ready dict."""
+        assert isinstance(self.booking_line, (IntroLine, TransferLine))
+        return {'comment': self.comment, 'code': self.code,
+                'is_intro': self.is_intro, 'error': self.error,
+                'booking_line': self.booking_line.as_dict}
+
     @property
     def is_intro(self) -> bool:
         """Return if intro line of a Booking."""
@@ -144,20 +152,24 @@ class IntroLine(BookingLine):
         else:
             self.date = toks[0]
 
+    @property
+    def as_dict(self) -> dict:
+        """Return as JSON-ready dict."""
+        return {'date': self.date, 'target': self.target}
+
 
 class TransferLine(BookingLine):
     """Non-first Booking line, expected to carry value movement."""
 
     def __init__(self, booking: 'Booking', code: str) -> None:
         super().__init__(booking)
+        self.currency = ''
         if not code[0].isspace():
             self.errors += ['transfer line not indented']
         toks = code.lstrip().split()
         self.account = toks[0]
         self.amount: Optional[Decimal] = None
-        if 1 == len(toks):
-            self.currency = ''
-        elif 3 <= len(toks):
+        if 3 <= len(toks):
             self.currency = toks[2]
             try:
                 self.amount = Decimal(toks[1])
@@ -175,6 +187,12 @@ class TransferLine(BookingLine):
             return f'{self.amount:.1f}…' if exp < -2 else f'{self.amount:.2f}'
         return ''
 
+    @property
+    def as_dict(self) -> dict:
+        """Return as JSON-ready dict."""
+        return {'account': self.account, 'currency': self.currency,
+                'amount': str(self.amount)}
+
 
 class Booking:
     """Represents lines of individual booking."""
@@ -273,15 +291,17 @@ class Handler(PlomHttpHandler):
         ctx = {'tainted': self.server.tainted}
         if self.pagename == 'booking' or self.pagename.startswith('edit_'):
             ctx['id'] = int(self.path_toks[2])
-            ctx['dat_lines'] = self.server.bookings[ctx['id']].dat_lines
         if self.pagename == 'balance':
             valid, balance_roots = self.server.balance_roots(
                     int(self.params.first('cutoff') or '0'))
             self.send_rendered(Path('balance.tmpl'),
                                ctx | {'roots': balance_roots, 'valid': valid})
         elif self.pagename in {'booking', 'edit_structured'}:
+            ctx['dat_lines'] = [dl.as_dict for dl
+                                in self.server.bookings[ctx['id']].dat_lines]
             self.send_rendered(Path('edit_structured.tmpl'), ctx)
         elif self.pagename == 'edit_raw':
+            ctx['dat_lines'] = self.server.bookings[ctx['id']].dat_lines
             self.send_rendered(Path('edit_raw.tmpl'), ctx)
         elif self.pagename == 'ledger_raw':
             self.send_rendered(Path('ledger_raw.tmpl'),
index d6881d24bfb5f3c126f28392cf19d41419285971..c90f1aba17bd1e802b60e27dc2b5ae67e908dfce 100644 (file)
@@ -3,6 +3,9 @@
 <html>
 <head>
 <meta charset="UTF-8">
+<script>
+{% block script %}{% endblock %}
+</script>
 <style>
 body { background-color: white; font-family: sans-serif; }
 tr:nth-child(odd) { background-color: #dcdcdc; }
index 89919e51bf60d7259d2350b067361fdfd1b3fcbb..af0c6348d499d6407a6560cb6b28b38eb9b44d6a 100644 (file)
@@ -6,32 +6,94 @@
 {{ macros.css_errors() }}
 {% endblock %}
 
+
+{% block script %}
+var dat_lines = {{dat_lines|tojson|safe}};
+
+function update_form() {
+  const table = document.getElementById("dat_lines");
+  table.innerHTML = "";
+  function add_button(td, text, disable, f) {
+    const btn = document.createElement("button");
+    td.appendChild(btn);
+    btn.textContent = text;
+    btn.type = "button";  // otherwise will act as form submit
+    btn.disabled = disable;
+    btn.onclick = function() {f(); update_form();};
+  }
+  function add_td(tr, colspan=1) {
+    const td = document.createElement("td");
+    tr.appendChild(td);
+    td.colSpan = colspan;
+    return td;
+  }
+  for (let i = 0; i < dat_lines.length; i++) {
+    const dat_line = dat_lines[i];
+    const tr = document.createElement("tr");
+    table.appendChild(tr);
+    function add_input(name, value, colspan=1) {
+      const td = add_td(tr, colspan);
+      const input = document.createElement("input");
+      td.appendChild(input);
+      input.name = `line_${i}_${name}`
+      input.value = value.trim();
+      if (dat_line.error) {
+        td.classList.add("invalid");
+      }
+    }
+    if (dat_line.is_intro) {
+      add_input('date', dat_line.booking_line.date)
+      add_input('target', dat_line.booking_line.target, 2)
+    } else if (!dat_line.error) {
+      add_input('account', dat_line.booking_line.account);
+      add_input('amount', dat_line.booking_line.amount == 'None' ? '' : dat_line.booking_line.amount);
+      add_input('currency', dat_line.booking_line.currency);
+    } else {
+      add_input('error', dat_line.code, 3)
+    }
+    add_input('comment', dat_line.comment);
+    const td_btns = add_td(tr);
+    add_button(td_btns, 'delete', false, function() {
+      dat_lines.splice(i, 1);
+    });
+    add_button(td_btns, 'move up', i > 1 ? false : true, function() {
+      const prev_line = dat_lines[i-1];
+      dat_lines.splice(i-1, 1);
+      dat_lines.splice(i, 0, prev_line);
+    });
+    add_button(td_btns, 'move down', i+1 < dat_lines.length ? false : true, function() {
+      const next_line = dat_lines[i];
+      dat_lines.splice(i, 1);
+      dat_lines.splice(i+1, 0, next_line);
+    });
+    if (dat_line.error) {
+      const tr = document.createElement("tr");
+      table.appendChild(tr);
+      const td = add_td(tr, 3);
+      tr.appendChild(document.createElement("td"));
+      td.textContent = dat_line.error;
+      td.classList.add("invalid");
+      tr.classList.add("warning");
+    }
+  }
+  const tr = document.createElement("tr");
+  table.appendChild(tr);
+  const td = add_td(tr, 5);
+  add_button(td, 'add line', false, function() {
+     new_line = {error: '', comment: '', booking_line: {account: '', amount: '', currency: ''}};
+     dat_lines.push(new_line);
+  });
+}
+
+window.onload = update_form;
+{% endblock %}
+
+
 {% block content %}
 <a href="/edit_raw/{{id}}">edit raw</a>
 <hr />
 <form action="/edit_structured/{{id}}" method="POST">
-<table>
-{% for dat_line in dat_lines %}
-  <tr>
-  {% if dat_line.is_intro %}
-    <td{% if dat_line.error %} class="invalid"{% endif %}><input name="line_{{loop.index0}}_date" value="{{dat_line.booking_line.date}}" /></td>
-    <td{% if dat_line.error %} class="invalid"{% endif %} colspan=2><input name="line_{{loop.index0}}_target" value="{{dat_line.booking_line.target}}" /></td>
-  {% elif not dat_line.error %}
-    <td><input name="line_{{loop.index0}}_account" value="{{dat_line.booking_line.account}}" /></td>
-    <td><input name="line_{{loop.index0}}_amount" value="{{dat_line.booking_line.amount or ''}}" /></td>
-    <td><input name="line_{{loop.index0}}_currency" value="{{dat_line.booking_line.currency or ''}}" /></td>
-  {% else %}
-    <td class="invalid" colspan=3><input name="line_{{loop.index0}}_error" value="{{dat_line.code|trim}}" /></td>
-  {% endif %}
-  <td><input name="line_{{loop.index0}}_comment" value="{{dat_line.comment|trim}}" /></td>
-  </tr>
-  {% if dat_line.error %}
-    <tr class="warning">
-    <td class="invalid" colspan=3>{{dat_line.error}}</td>
-    <td></td>
-    </tr>
-  {% endif %}
-{% endfor %}
+<table id="dat_lines">
 </table>
 <input type="submit" value="update" />
 </form>