From: Paul Hänsch Date: Sat, 7 Sep 2024 11:10:52 +0000 (+0200) Subject: process split fields as arrays X-Git-Url: https://git.plutz.net/?a=commitdiff_plain;ds=sidebyside;h=HEAD;hp=e5ac3bcdba86bd9e7967c4ce7177399d2f9bf69f;p=confetti process split fields as arrays --- diff --git a/courses/export_pdf.sh b/courses/export_pdf.sh index 92e10c5..10230b8 100755 --- a/courses/export_pdf.sh +++ b/courses/export_pdf.sh @@ -90,9 +90,10 @@ style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"' ] ][body lang="de_DE" [table width="100%" style="page-break-after: always;" - [col width=10*] [col width=5*] [col width=10*] [col width=15*] + [col width=2*] [col width=10*] [col width=5*] [col width=10*] [col width=15*] [thead - [tr [th $style_td . $(l10n N)] [th $style_td . $(l10n BDAY)] [th $style_td . $(l10n TEL)] [th $style_td . $(l10n NOTE)]] + [tr [th $style_td . $(l10n No.)] [th $style_td . $(l10n N)] [th $style_td . $(l10n BDAY)] + [th $style_td . $(l10n TEL)] [th $style_td . $(l10n NOTE)]] ][tbody $(grep -F "${coursefile##*/} " "$_DATA/mappings/attendance" |while read discard each; do vcf="$(pdi_load "$_DATA/vcard/$each")" @@ -101,12 +102,16 @@ style_td='style="border: 1pt solid; padding: 1mm 2mm; vertical-align: top;"' [ "$type" ] && type="$(l10n "TYPE=$type"):" printf '%s %s
' "$type" "$(pdi_value "$vcf" TEL $n)" done )" - printf '[tr valign=top [td %s .N . %s] [td %s .BDAY . %s] [td %s .TEL . %s] [td %s .NOTE . %s]]\n' \ + printf '[tr valign=top [td %s .No @@No@@] [td %s .N . %s] [td %s .BDAY . %s] [td %s .TEL . %s] [td %s .NOTE . %s]]\n' \ + "${style_td%\"} text-align: right;\"" \ "$style_td" "$(pdi_value "$vcf" FN |unescape |HTML)" \ "$style_td" "$(pdi_value "$vcf" BDAY |unescape |HTML)" \ "$style_td" "$tel" \ "$style_td" "$(pdi_value "$vcf" NOTE |unescape |HTML)" - done |sort)] + done |sort |while read -r line; do + attno=$((${attno-0} + 1)) + printf '%s%2i%s\n' "${line%%@@No@@*}" $attno "${line#*@@No@@}" + done)] ] [table width="100%" [col width=30*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] [col width=10*] diff --git a/datetime.sh b/datetime.sh new file mode 100755 index 0000000..2b4bba9 --- /dev/null +++ b/datetime.sh @@ -0,0 +1,101 @@ +#!/bin/sh + +[ "$include_datetime" ] && return 0 +include_datetime="$0" + +# Copyright 2023 - 2024 Paul Hänsch +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +# SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +# IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +isdate(){ + local date="$1" y m d + + if printf %s "$date" \ + | grep -xEq '[0-9]{4}-((01|03|05|07|08|10|12)-(0[1-9]|[12][0-9]|3[01])|(04|06|09|11)-(0[1-9]|[12][0-9]|30)|02-(0[1-9]|[12][0-9]))' + then # y-m-d (ISO Date) + y="${date%%-*}" d="${date##*-}" m="${date%-*}" m="${m#*-}" + elif printf %s "$date" \ + | grep -xEq '((0?1|0?3|0?5|0?7|0?8|10|12)/(0?[1-9]|[12][0-9]|3[01])|(0?4|0?6|0?9|11)/(0?[1-9]|[12][0-9]|30)|0?2-(0[1-9]|[12][0-9]))/([0-9]{2}|[0-9]{4})' + then # m/d/y (US Date) + y="${date##*/}" m="${date%%/*}" d="${date%/*}" d="${d#*/}" + elif printf %s "$date" \ + | grep -xEq '((0?[1-9]|[12][0-9]|3[01])[\./](0?1|0?3|0?5|0?7|0?8|10|12)|(0?[1-9]|[12][0-9]|30)[\./](0?4|0?6|0?9|11)|(0[1-9]|[12][0-9])[\./]0?2)[\./]([0-9]{2}|[0-9]{4})' + then # d/m/y or d.m.y (European Date / German Date) + y="${date##*.}" d="${date%%.*}" m="${date%.*}" m="${m#*.}" + else + return 1 + fi + [ $y -lt 100 -a $y -gt 50 ] && y=$((y + 1900)) + [ $y -lt 100 -a $y -le 50 ] && y=$((y + 2000)) + date="$(printf "%04i-%02i-%02i" $y ${m#0} ${d#0})" + + # leap year + if [ "$m" -eq 2 -a "$d" -eq 29 ]; then + if [ "$((y % 400))" -eq 0 ]; then + : + elif [ "$((y % 100))" -eq 0 ]; then + return 1 + elif [ "$((y % 4))" -eq 0 ]; then + : + else + return 1 + fi + fi + + printf '%04i-%02i-%02i\n' "$y" "${m#0}" "${d#0}" + return 0 +} + +istime(){ + time="$1" h= m= + + if printf %s "$time" | grep -xEq '(0?[1-9]|1[012])(:[0-5][0-9])? ?(am|AM)\.?'; then + time="${time%?[aA][mM]}" h="${time%:*}" h="$(h % 12)" + [ "$h" != "$time" ] && m="${time#*:}" || m=0 + elif printf %s "$time" | grep -xEq '(0?[1-9]|1[012])(:[0-5][0-9])? ?(pm|PM)\.?'; then + time="${time%?[aA][mM]}" h="${time%:*}" h="$(h % 12 + 12)" + [ "$h" != "$time" ] && m="${time#*:}" || m=0 + elif printf %s "$time" | grep -xEq '(0?[0-9]|1[0-9]|2[0-3]):[0-5][0-9]'; then + time="${time%?[aA][mM]}" h="${time%:*}" m="${time#*:}" + else + return 1 + fi + + printf '%02i:%02i\n' "${h#0}" "${m#0}" + return 0 +} + +numdays(){ + # return number of days in a month (i.e. 28, 29, 30, or 31) + local y="$1" m="${2#0}" + + case $m in + 1|3|5|7|10|12) + printf 31\\n + ;; + 4|6|8|9|11) + printf 30\\n + ;; + 2) if [ "$((y % 400))" -eq 0 ]; then + printf 29\\n + elif [ "$((y % 100))" -eq 0 ]; then + printf 28\\n + elif [ "$((y % 4))" -eq 0 ]; then + printf 29\\n + else + printf 28\\n + fi + ;; + *) return 1;; + esac +} diff --git a/l10n.sh b/l10n.sh index 4f13ad4..e1c29ea 100755 --- a/l10n.sh +++ b/l10n.sh @@ -79,6 +79,7 @@ l10n_global() { sYEARLY) printf "Jährlich";; # UI labels + No.) printf %s "Nr.";; year) printf %s "Jahr";; month) printf %s "Monat";; day) printf %s "Tag";; @@ -132,11 +133,11 @@ l10n_global() { 'Originator') printf %s "Auftraggeber";; 'Reference Text') printf %s "Verwendungszweck";; 'Amount') printf %s "Betrag";; - 'Balance') printf %s "Stand";; + 'Balance') printf %s "Kto.Stand";; 'Manual Record') printf %s "Manueller Eintrag";; - 'once') printf %s "einmalig";; - 'monthly until') printf %s "monatlich bis";; - 'until') printf %s "bis";; + 'Recur Monthly') printf %s "[strike monat­lich wie­der­holen]";; + 'Credit Account') printf %s "Guthaben­konto";; + 'Submit') printf %s "Eintragen";; # UI Labels Special course_attendance) printf %s "Kurs­teil­nahme";; diff --git a/ledgers/account.sh b/ledgers/account.sh index d9d3f4b..9d33299 100755 --- a/ledgers/account.sh +++ b/ledgers/account.sh @@ -1,12 +1,11 @@ #!/bin/sh credit() { - printf '%+03i\n' "$1" \ + printf '%+04i\n' "$1" \ | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;' } -if [ "$REQUEST_METHOD" = POST ]; then - uid="$(POST uid)" +if uid="$(POST uid)"; then cfile="$(grep -lxF "UID;:${uid}" "${_DATA}/vcard/"*.vcf || grep -lxF "UID:${uid}" "${_DATA}/vcard/"*.vcf)" REDIRECT "${_BASE}/ledgers/account.sh?card=${cfile##*/}" fi @@ -15,20 +14,45 @@ fi . "${_EXEC}/pdiread.sh" . "${_EXEC}/cards/l10n.sh" . "${_EXEC}/cards/widgets.sh" +. "${_EXEC}/datetime.sh" -cardfile="${_DATA}/vcard/$(GET card |PATH)" -if [ ! -f "$cardfile" ]; then +cardfile="$(GET card |PATH)" cardfile="${cardfile##*/}" +if [ ! -f "${_DATA}/vcard/$cardfile" ]; then SET_COOKIE 0 message="Invalid account: $cardfile" REDIRECT "${_BASE}/ledgers/" fi -cledger="${cardfile##*/}" -cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account" +cledger="${_DATA}/ledgers/vcf.${cardfile%.vcf}.account" -{ card="$(pdi_load "$cardfile")" +if tid="$(POST tid)"; then + if [ "$tid" != "$(transid "$cledger")" ]; then + SET_COOKIE 0 message="Ledger was changed since last edit" + REDIRECT "$REQUEST_URI" + fi + tdate="$(isdate "$(POST tdate)")" + tref="$(POST tref |grep -m1 -xE '.+')" + tamount="$(POST tamount \ + | sed -E ' + s;^([\+-]?[0-9]+)[\.,]([0-9][0-9])$;\1\2;; + s;^([\+-]?[0-9]+)$;&00;; + ' | grep -m1 -xE '[\+-]?[0-9]+')" + # debug "TDATE: $tdate TREF: $tref AMOUNT: $tamount" + if ! [ "$tdate" -a "$tref" -a "$tamount" ]; then + SET_COOKIE 0 message="Transaction info invalid" + REDIRECT "$REQUEST_URI" + fi + tdtstamp="$(date -ud "$tdate" +%s)" + printf '%s %i %s \ %s %i\n' \ + "${tdate}" "${tdtstamp}" "$(STRING "${cardfile%.vcf}")" \ + "$(STRING "$tref")" "${tamount}" \ + >>"${cledger}" + REDIRECT "$REQUEST_URI" +fi + +{ card="$(pdi_load "${_DATA}/vcard/$cardfile")" cat <<-EOF [h1 $(l10n Payments)] - [div .card #${cardfile##*/} + [div .card #${cardfile} [div .section .basic . $( card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO )] @@ -37,7 +61,7 @@ cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account" [div .section .address . $(card_item "$card" ADR X-IBAN)] [div .section .note . $(card_item "$card" NOTE)] [div .section .attendance [h3 $(l10n course_attendance) ] [ul - $(grep -F " ${cardfile##*/}" "$_DATA/mappings/attendance" |while read each discard; do + $(grep -F " ${cardfile}" "$_DATA/mappings/attendance" |while read each discard; do printf '[li [a .item .attendance href="%s/courses#%s" . %s]]' \ "${_BASE}" "$each" \ "$(pdi_value "$(pdi_load "$_DATA/ical/$each")" SUMMARY || l10n "(unnamed course)" |unescape |HTML)" @@ -46,43 +70,57 @@ cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account" ] ] EOF - printf '[table .transactions [thead - [tr [th .date . %s][th .orig . %s][th .reference . %s][th .amount . %s][th .balance . %s]]' \ - "$(l10n Date)" "$(l10n Originator)" "$(l10n "Reference Text")" "$(l10n Amount)" "$(l10n Balance)" - printf '][tbody' + cat <<-EOF + [form method=POST + [hidden "tid" "$(transid "${cledger}")"] + [table .transactions + [thead [tr + [th .date . $(l10n Date)][th .orig . $(l10n Originator)] + [th .reference . $(l10n "Reference Text")] + [th .amount . $(l10n Amount)][th .balance . $(l10n Balance)] + ]] + [tbody + EOF cnt="$(pdi_count "$card" X-IBAN)" - while [ "$cnt" -gt 0 ]; do - pdi_value "$card" X-IBAN "$cnt" |RXLITERAL - cnt=$((cnt - 1)) - done \ - | { - while read -r iban; do - grep -hE "^[^\t]+ [^\t]+ ${iban} " "${_DATA}/ledgers/"*.tbl + { while [ "$cnt" -gt 0 ]; do + pdi_value "$card" X-IBAN "$cnt" |RXLITERAL + cnt=$((cnt - 1)) done - if [ -f "$cledger" ]; then - : - fi - } \ + RXLITERAL "${cardfile%.vcf}"; echo + } |debug \ + | while read -r iban; do + grep -hE "^[^\t]+ [^\t]+ ${iban} " "${_DATA}/ledgers/"* + done \ | sort -n -k2 \ | { total=0 - while read -r date dtstamp iban accname subject amount; do + while read -r date dtstamp iban accname reftext amount; do total=$((total + amount)) - printf '[tr [td .date . %s][td .orig [span . %s][span . %s]][td .reference . %s][td .amount . %s][td .balance . %s]]' \ - "$date" "$(HTML "$iban")" \ - "$(UNSTRING "$accname" |HTML)" "$(UNSTRING "$subject" |HTML)" \ - "$(credit "$amount")" "$(credit "$total")" + if [ "$iban" = "${cardfile%.vcf}" ]; then + printf '[tr [td .date . %s][td .orig [span . %s][span . %s]][td .reference . %s] + [td .amount . %s][td .balance . %s]]' \ + "$date" "$(l10n "Credit Account")" \ + "$(UNSTRING "$accname" |HTML)" "$(UNSTRING "$reftext" |HTML)" \ + "$(credit "$amount")" "$(credit "$total")" + else + printf '[tr [td .date . %s][td .orig [span . %s][span . %s]][td .reference . %s] + [td .amount . %s][td .balance . %s]]' \ + "$date" "$(HTML "$iban")" \ + "$(UNSTRING "$accname" |HTML)" "$(UNSTRING "$reftext" |HTML)" \ + "$(credit "$amount")" "$(credit "$total")" + fi done } - printf '[tr [th colspan=5 . %s]]' "$(l10n 'Manual Record')" - printf '[tr [td .date [input type=date placeholder="%s" name=tdate]] - [td .orig [input id=trec_once type=radio name="trec" value="once" selected] - [label for=trec_once . %s]
- [input id=trec_month type=radio name="trec" value="month"] - [label for=trec_month . %s] - [input type=date name="trec_until" placeholder="%s"]] - [td .reference . [textarea placeholder="%s" name=treference]] - [td .amount colspan=2 [input type=number placeholder="%s" name=tamount value=0.00 step=.01]]' \ - "$(l10n Date)" "$(l10n once)" "$(l10n "monthly until")" "$(l10n until)" "$(l10n "Reference Text")" "$(l10n Amount)" - printf ']]]' + cat <<-EOF + [tr [th colspan=5 . $(l10n 'Manual Record')]] + [tr [td .date + [input type=date placeholder="$(l10n Date)" name=tdate value="$(date +%F)"] + [input type=checkbox id=rr_month name=recur value=monthly] + [label for=rr_month $(l10n Recur Monthly)] + ][td .orig ] + [td .reference . [textarea placeholder="$(l10n "Reference Text")" name=tref]] + [td .amount [input type=number placeholder="$(l10n Amount)" name=tamount value=0.00 step=.01]] + [td .balance [button type=submit . $(l10n Submit)]] + ]]]] + EOF } \ | yield_page ledgers diff --git a/lua/vcard.lua b/lua/vcard.lua new file mode 100644 index 0000000..3066737 --- /dev/null +++ b/lua/vcard.lua @@ -0,0 +1,171 @@ +#!/usr/bin/lua + +local Vcard = { + _component_fields = { N = true; GENDER = true; ADR = true; ORG = true; } +} + +-- constructor, synonymous to load() +function Vcard:new(path) + return self:load(path) +end + +-- constructor +-- load and parse vcard file, return new vcard object +function Vcard:load(path) + local file, data + + file = io.open(path) + if not file then return nil; end + data = file:read("a") + file:close() + + return self:parse(data) +end + +-- constructor +-- parse text block of a vcard, return new vcard object +function Vcard:parse(data) + local line, key, attr, value, i, v, n + local r = {} + setmetatable(r, { __index = self }) + + -- unwrap line continuations, remove carriage return at EOL + data = data:gsub("\r?\n[ \t]", "") + data = data:gsub("\r\n", "\n") + + -- parse lines into fields + for line in data:gmatch("[^\n]+") do + key, attr, value = self:_parse_line(line) + if not r[key] then r[key] = { attr = {} } end + if Vcard._component_fields[key] then + table.insert(r[key], self:components(value)); + r[key].attr[#r[key]] = attr + r[key][#r[key]][0] = value; + else for i,v in ipairs(self:_split_by(value, ",")) do + table.insert(r[key], v) + r[key].attr[#r[key]] = attr + end end + end + + -- try to ensure existence of FN field + if not r["FN"] and r["N"] then + n = r["N"][1] + r["FN"] = { (n[4] or " ") .. " " .. (n[2] or " ") .. " " .. + (n[3] or " ") .. " " .. (n[1] or " ") .. " " .. + (n[5] or " ") } + r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "") + end + if not r["FN"] and r["NICKNAME"] then + r["FN"] = { r["NICKNAME"][1] } + r["FN"][1] = r["FN"][1]:gsub("%s+", " "):gsub("^ ", ""):gsub(" $", "") + end + + return r +end + +-- split field components (i.e. the N field) +function Vcard:components(line) + return self:_split_by(line, ";") +end + +function Vcard:unescape(line) + local f local out = "" + + while line ~= "" and line ~= "\\" do + if (function() f = line:match("^\\n([^\\]*)") return f end)() then + out = out .. "\n" .. f + line = line:sub(#f + 3) + elseif (function() f = line:match("^\\(.[^\\]*)") return f end)() then + out = out .. f + line = line:sub(#f + 2) + elseif (function() f = line:match("^([^\\]+)") return f end)() then + out = out .. f + line = line:sub(#f + 1) + end + end + + return out +end + +function Vcard:escape(line) + return line:gsub("\\", "\\\\"):gsub(",", "\\,"):gsub(";", "\\;"):gsub("\n", "\\n") +end + +-- internal +-- split vcard line into key, value, and an array of attributes +function Vcard:_parse_line(line) + local key, value local attr = {} + + key = line:match('^([^:;]+)[;:].*$') + if not key then return nil end + + line = line:sub(#key + 1 ) + a = line:match('^;([^";:]+)[;:].*$') or line:match('^;([^"=;:]+="[^"]+")[;:].*$') + while a do + table.insert(attr, a) + line = line:sub(#a + 2) + a = line:match('^;([^";:]+)[;:].*$') or line:match('^;([^"=;:]+="[^"]+")[;:].*$') + end + + value = line:match("^:(.*)$") + if not value then return nil end + + key = key:upper() + return key, attr, value +end + +-- internal +-- split string by specified character, unless the character is ecapped by \ +function Vcard:_split_by(line, split) + local f local r = { "" } + + while line ~= "" and line ~= "\\" do + if line:match("^" .. split) then + table.insert(r, "") + line = line:sub(2) + end + f = line:match("^(\\.)") or line:match("^([^\\" .. split .. "]+)") + r[#r] = r[#r] .. f + line = line:sub(#f + 1) + end + return r +end + +-- internal +-- development tests +function Vcard:_test() + local vcf = self:parse([=[ +BEGIN:VCARD +VERSION:1.0 +UID:1 +N:Lastname;Firstname;Middle Names; Title; Suffix +NICKNAME: Bone Crusher, Cookie Monster +PHONE;TYPE=Home:123456789,666999 +PHONE;TYPE=Work;TYPE=Cell:987654321 +END:VCARD +]=]) + + assert( vcf["PHONE"][1] == "123456789", "Phone/1 wrong number" ) + assert( vcf["PHONE"][2] == "666999", "Phone/2 wrong number" ) + assert( vcf["PHONE"].attr[2][1] == vcf["PHONE"].attr[1][1] ) + assert( vcf["PHONE"][3] == "987654321", "Phone/3 wrong number" ) + assert( vcf["PHONE"].attr[3][2] == "TYPE=Cell", "Phone/3 attr type=cell" ) + assert( vcf["FN"][1] == "Title Firstname Middle Names Lastname Suffix" ) + assert( vcf["N"][1][1] == "Lastname" ) + assert( vcf["N"][1][4] == " Title" ) + + vcf = self:parse([=[ +BEGIN:VCARD +VERSION:1.0 +UID:1 +NICKNAME: Bone Crusher, Cookie Monster +PHONE;TYPE=Home:123456789,666999 +PHONE;TYPE=Work;TYPE=Cell:987654321 +END:VCARD +]=]) + assert( vcf["FN"][1] == "Bone Crusher" ) + + print("Vcard OK") +end + +return Vcard diff --git a/style.css b/style.css index c00829e..9e6bc02 100644 --- a/style.css +++ b/style.css @@ -504,7 +504,7 @@ body.ledgers .transactions td:nth-child(2n + 1) { } body.ledgers .transactions .date { - min-width: 8em; + width: 10em; } body.ledgers .transactions .orig span { display: block; @@ -512,14 +512,18 @@ body.ledgers .transactions .orig span { body.ledgers .transactions .amount, body.ledgers .transactions .balance { vertical-align: bottom; - min-width: 6em; + width: 8em; text-align: right; } body.ledgers .transactions .reference textarea { width: 100%; } -body.ledgers .transactions .orig input[type=date], +body.ledgers .transactions .date input + label { + display: inline-block; + vertical-align: middle; + width: 7em; +} body.ledgers .transactions .date input[type=date], body.ledgers .transactions .amount input[type=number] { display: block;