From: Paul Hänsch Date: Wed, 15 May 2024 14:37:59 +0000 (+0200) Subject: date time helper X-Git-Url: https://git.plutz.net/?p=confetti;a=commitdiff_plain;h=refs%2Fheads%2Fmaster;hp=6e7496f7d5f9d5804ff071a53da789ca5ef9d4ba date time helper --- diff --git a/cards/l10n.sh b/cards/l10n.sh index 2d9dc06..fde98e0 100755 --- a/cards/l10n.sh +++ b/cards/l10n.sh @@ -37,6 +37,7 @@ l10n(){ X-ZACK-LEAVEDATE) printf %s "Abmelde­datum";; X-ZACK-JOINDATE_short) printf %s "Anm.";; X-ZACK-LEAVEDATE_short) printf %s "Abm.";; + X-IBAN) printf %s "IBAN";; *) l10n_global "$word";; esac diff --git a/cards/list.sh b/cards/list.sh index 519005c..e12d760 100755 --- a/cards/list.sh +++ b/cards/list.sh @@ -31,7 +31,10 @@ edit_card(){ [ $(pdi_count "$card" IMPP) -gt 0 ] && edit_item "$card" IMPP [ $(pdi_count "$card" URL ) -gt 0 ] && edit_item "$card" URL )] - [div .section .address $(edit_item "$card" ADR)] + [div .section .address $( + edit_item "$card" ADR + [ $(pdi_count "$card" X-IBAN) -gt 0 ] && edit_item "$card" X-IBAN + )] [div .section .note $(edit_item "$card" NOTE)] [div .section .attendance [h3 $(l10n course_attendance) ] [div .attendance $( @@ -65,7 +68,7 @@ edit_card(){ [div .item .newfield [select name="newfield" [option value="" disabled="disabled" selected="selected" $(l10n edit_addfieldtext)] - $(for f in NICKNAME EMAIL TEL IMPP ADR URL NOTE X-ZACK-LEAVEDATE; do + $(for f in NICKNAME EMAIL TEL IMPP ADR URL NOTE X-ZACK-LEAVEDATE X-IBAN; do printf '[option value="%s" %s] ' "$f" "$(l10n "$f")" done) ][button type="submit" name="action" value="addfield" $(l10n edit_addfield)] @@ -90,7 +93,7 @@ print_card(){ )] [div .section .phone . $(card_item "$card" TEL)] [div .section .message . $(card_item "$card" EMAIL IMPP URL)] - [div .section .address . $(card_item "$card" ADR)] + [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 @@ -101,6 +104,7 @@ print_card(){ $(card_item "$card" CATEGORIES) ] [div .control + [a .button .item href="${_BASE}/ledgers/account.sh?card=${cardfile##*/}" $(l10n ledger)] [a .button .item href="${_BASE}/cards/edit_card.sh?card=${cardfile##*/}" $(l10n edit)] [a .button .item href="${_BASE}/cards/export_card.sh?card=${cardfile##*/}" $(l10n vcf_export)] ] diff --git a/cards/update_card.sh b/cards/update_card.sh index 2b87632..d57f503 100755 --- a/cards/update_card.sh +++ b/cards/update_card.sh @@ -35,7 +35,7 @@ attfile="$_DATA/mappings/attendance" action="$(POST action)" newfield="$(POST newfield |grep -m 1 -xE '[A-Z][A-Z0-9-]*')" -if printf '%s\n' "$action" |grep -qxE 'addfield [A-Z][A-Z0-9]*'; then +if printf '%s\n' "$action" |grep -qxE 'addfield [A-Z][A-Z0-9-]*'; then newfield="${action##* }" action=addfield fi diff --git a/cgilite/cgilite.awk b/cgilite/cgilite.awk index f16ed6a..ebf4411 100644 --- a/cgilite/cgilite.awk +++ b/cgilite/cgilite.awk @@ -1,5 +1,7 @@ #!/bin/env awk -f +function debug(t) { printf "%s\n", t >>"/dev/stderr"; } + function PATH( str, seg, out ) { while ( str ) { seg = str; @@ -151,8 +153,9 @@ BEGIN { split("", _GET); split("", _POST); split("", _REF); split("", _HEADER); split("", _COOKIE); - if ( ENVIRON["REQUEST_METHOD"] ) + if ( ENVIRON["REQUEST_METHOD"] ) { _cgilite_headers(); - else + } else { _cgilite_request(); + } } diff --git a/cgilite/common.css b/cgilite/common.css index 30c3942..16e99f2 100644 --- a/cgilite/common.css +++ b/cgilite/common.css @@ -31,7 +31,7 @@ a { color: #068; word-break: break-word; } -a.button { +a.button, label.button { font-style: inherit; text-decoration: inherit; color: inherit; @@ -86,7 +86,7 @@ h1 { } h2 { font-size: 1.125em; } -select, input, button, textarea, a.button { +select, input, button, textarea, a.button, label.button { display: inline-block; color: #000; background-color: #FFF; border: .5pt solid; @@ -103,7 +103,7 @@ input[type=radio], input[type=checkbox] { } input[type=number] { text-align: right; padding-right: 0; } -button, input[type=button], a.button { +button, input[type=button], a.button, label.button { box-shadow: .125em .125em .25em; cursor: pointer; } diff --git a/cgilite/storage.sh b/cgilite/storage.sh index 17ea0d0..5c61df0 100755 --- a/cgilite/storage.sh +++ b/cgilite/storage.sh @@ -94,6 +94,21 @@ UNSTRING(){ printf '%s\n' "$out" } +RXLITERAL(){ + # sed -E 's;[].*+?^${}()|\[];\\&;g' + local in out='' + [ $# -gt 0 ] && in="$*" || in="$(cat)" + while [ "$in" ]; do case $in in + [.+^\$\{\}\(\)\[\]\*\?\|\\]*) + out="${out}\\${in%"${in#?}"}"; in="${in#?}"; + ;; + *)out="${out}${in%%[.+^\$\{\}\(\)\[\]\*\?\|\\]*}" + in="${in#"${in%%[.+^\$\{\}\(\)\[\]\*\?\|\\]*}"}" + ;; + esac; done + printf '%s\n' "$out" +} + DBM() { local file="$1" cmd="$2" local k v key value 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/index.cgi b/index.cgi index 276965f..71edd85 100755 --- a/index.cgi +++ b/index.cgi @@ -34,6 +34,8 @@ _PATH="$(PATH "/${PATH_INFO}")" _PATH="${_PATH#${_BASE}}" ACTION="$(GET a)" +SESSION_COOKIE + message="$(COOKIE message)" [ "$message" ] && SET_COOKIE 0 message='' @@ -64,7 +66,8 @@ yield_page() { printf ' ] [body #top class="%s" ' "$class" - printf '[ul .menu [li [a "%s/cards/" . %s]][li [a "%s/courses/" . %s]]]' "${_BASE}" "$(l10n cards)" "${_BASE}" "$(l10n courses)" + printf '[ul .menu [li [a "%s/cards/" . %s]][li [a "%s/courses/" . %s]][li [a "%s/ledgers/" . %s]]]' \ + "${_BASE}" "$(l10n cards)" "${_BASE}" "$(l10n courses)" "${_BASE}" "$(l10n ledgers)" [ "$message" ] && printf '[p #message\n%s\n]' "$(l10n "$message")" cat printf '] ]' diff --git a/l10n.sh b/l10n.sh index bddc7b7..4f13ad4 100755 --- a/l10n.sh +++ b/l10n.sh @@ -26,6 +26,7 @@ l10n_global() { # Nav Menu cards) printf %s "Teil­neh­mende";; courses) printf %s "Kurse";; + ledgers) printf %s "Bei­trä­ge";; # VCF Default PHOTO) printf %s "Foto";; @@ -84,6 +85,7 @@ l10n_global() { edit) printf %s "Bearbeiten";; edit_categories) printf %s "Kategorien Bearbeiten";; vcf_export) printf %s "Vcard Exportieren";; + ledger) printf %s "Buchungen";; control) printf %s "Aktionen";; delete) printf %s "entfernen";; edit_update) printf %s "Daten übernehmen";; @@ -114,6 +116,28 @@ l10n_global() { filter_cancel) printf %s "Filter löschen";; export_csv) printf %s "Liste als CSV-Datei";; + # Accounting page + Ledgers) printf %s "Buchungslisten";; + '%i IBANs are unassigned') printf %s "%i IBANs sind nicht zugewiesen";; + 'IBAN Assignments') printf %s "IBAN Zuweisung";; + 'Assign IBANs') printf %s "IBANs Zuweisen";; + 'Account') printf %s "Konto";; + + 'Accept Suggestions') printf %s "Vorschläge Akzeptieren";; + 'Ignore Suggestions') printf %s "Vorschläge Ignorieren";; + 'Submit Changes') printf %s "Änderungen Übernehmen";; + + 'Payments') printf %s "Zahlungen";; + 'Date') printf %s "Datum";; + 'Originator') printf %s "Auftraggeber";; + 'Reference Text') printf %s "Verwendungszweck";; + 'Amount') printf %s "Betrag";; + 'Balance') printf %s "Stand";; + 'Manual Record') printf %s "Manueller Eintrag";; + 'once') printf %s "einmalig";; + 'monthly until') printf %s "monatlich bis";; + 'until') printf %s "bis";; + # UI Labels Special course_attendance) printf %s "Kurs­teil­nahme";; vcf_seed_label) printf "Anmeld. Vorn. Nachn. Geb.Tag Geb.Monat Geb.Jahr Tel. Mobil () EMail () Notiz";; diff --git a/ledgers/account.sh b/ledgers/account.sh new file mode 100755 index 0000000..d9d3f4b --- /dev/null +++ b/ledgers/account.sh @@ -0,0 +1,88 @@ +#!/bin/sh + +credit() { + printf '%+03i\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)" + cfile="$(grep -lxF "UID;:${uid}" "${_DATA}/vcard/"*.vcf || grep -lxF "UID:${uid}" "${_DATA}/vcard/"*.vcf)" + REDIRECT "${_BASE}/ledgers/account.sh?card=${cfile##*/}" +fi + +. "${_EXEC}/cgilite/storage.sh" +. "${_EXEC}/pdiread.sh" +. "${_EXEC}/cards/l10n.sh" +. "${_EXEC}/cards/widgets.sh" + +cardfile="${_DATA}/vcard/$(GET card |PATH)" +if [ ! -f "$cardfile" ]; then + SET_COOKIE 0 message="Invalid account: $cardfile" + REDIRECT "${_BASE}/ledgers/" +fi + +cledger="${cardfile##*/}" +cledger="${_DATA}/ledgers/vcf.${cledger%.vcf}.account" + +{ card="$(pdi_load "$cardfile")" + cat <<-EOF + [h1 $(l10n Payments)] + [div .card #${cardfile##*/} + [div .section .basic . $( + card_item "$card" FN GENDER NICKNAME BDAY X-ZACK-JOINDATE X-ZACK-LEAVEDATE SOUND PHOTO LOGO + )] + [div .section .phone . $(card_item "$card" TEL)] + [div .section .message . $(card_item "$card" EMAIL IMPP URL)] + [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 + 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)" + done |sort -k7)] + $(card_item "$card" CATEGORIES) + ] + ] + 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' + 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 + done + if [ -f "$cledger" ]; then + : + fi + } \ + | sort -n -k2 \ + | { total=0 + while read -r date dtstamp iban accname subject 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")" + 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 ']]]' +} \ +| yield_page ledgers diff --git a/ledgers/csv_upload.awk b/ledgers/csv_upload.awk new file mode 100755 index 0000000..04de4d5 --- /dev/null +++ b/ledgers/csv_upload.awk @@ -0,0 +1,100 @@ +#!/bin/awk -f + +function STRING( inp ) { + gsub(/\\/, "\\\\", inp); + gsub(/\n/, "\\n", inp); + gsub(/\r/, "\\r", inp); + gsub(/\t/, "\\t", inp); + gsub(/\+/, "\\+", inp); + gsub(/ /, "+", inp); + return inp ? inp : "\\"; +} + +function UNSTRING( inp, out, tmp ) { + while ( inp ) { + if ( inp ~ /^\\\\/) { out = out "\\"; sub(/^\\\\/, "", inp); } + else if ( inp ~ /^\\n/) { out = out "\n"; sub(/^\\n/, "", inp); } + else if ( inp ~ /^\\r/) { out = out "\r"; sub(/^\\r/, "", inp); } + else if ( inp ~ /^\\t/) { out = out "\t"; sub(/^\\t/, "", inp); } + else if ( inp ~ /^\\+/) { out = out "+"; sub(/^\\+/, "", inp); } + else if ( inp ~ /^\+/) { out = out " "; sub(/^\+/, "", inp); } + else if ( inp ~ /^\\/) { out = out ""; sub(/^\+/, "", inp); } + else { tmp = inp; sub(/[\\+].*$/, "", tmp); out = out tmp; sub(/^[^\\+]*/, "", inp); } + } + return out; +} + +function isdate( date, dt, y, m, d ) { + if ( match( date, + /^[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]))$/ )) { + split( date, dt, "-"); + y = dt[1]; m = dt[2]; d = dt[3]; + + } else if ( match( date, + /^((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})$/ )) { + split( date, dt, "/"); + m = dt[1]; d = dt[2]; y = dt[3]; + + } else if ( match( date, + /^((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})$/ )) { + split( date, dt, /[\.\/]/); + d = dt[1]; m = dt[2]; y = dt[3]; + + } else return ""; + + if ( y < 100 && y > 50 ) y = y + 1900; + if ( y <= 50 ) y = y + 2000; + + # leap year + if ( m == 2 && d == 29 ) { + if ( y % 400 == 0 ) y = y; + else if ( y % 100 == 0 ) return ""; + else if ( y % 4 == 0 ) y = y; + else return ""; + } + + return sprintf("%04i-%02i-%02i", y, m, d); +} + +function cents( val ) { + gsub(/\./, "", val); sub(/,/, ".", val); + return val * 100; +} + +BEGIN { + FS = ";"; + dtrange_end = dt_from = dt_to = balance_start = balance_end = ""; + split("", rec); + rec[0] = "Date DateU IBAN Name Subject Amount" +} + +/^([012]?[0-9]|30|31).(0?[1-9]|1[012]).[0-9]{4} - ([012]?[0-9]|30|31).(0?[1-9]|1[012]).[0-9]{4}$/ { + dtrange_end = $0; sub(/^.* - /, "", dtrange_end); + dt_from = $0; sub(/ - .*$/, "", dt_from); dt_from = isdate(dt_from); + dt_to = $0; sub(/^.* - /, "", dt_to ); dt_to = isdate(dt_to ); +} + +/^Letzter Kontostand;;;;[0-9\.,]+;EUR$/ { + balance_start = cents($5); +} + +/Kontostand;[^;]+;;;[0-9\.,]+;EUR/ { + if ( $2 = dtrange_end ) balance_end = cents($5) +} + +$18 == "EUR" { + rec_date = isdate($1); gsub(/-/, " ", rec_date); rec_date = mktime(rec_date " 00 00 00", "UTC"); + rec[length(rec)] = sprintf("%s %i %s %s %s %i", + isdate($1), rec_date, $6 ? $6 : "\\", STRING($4), STRING($5), cents($12)); +} + +END { + if ( dt_from && dt_to ) { + dtu_from = dt_from; gsub(/-/, " ", dtu_from); dtu_from = mktime( dtu_from " 00 00 00", "UTC"); + dtu_to = dt_to ; gsub(/-/, " ", dtu_to ); dtu_to = mktime( dtu_to " 00 00 00", "UTC"); + + printf "%i %s %i %s %i %i\n", + dtu_from, dt_from, dtu_to, dt_to, balance_start, balance_end; + for ( k = 1; k < length(rec); k++ ) print rec[k]; + } +} diff --git a/ledgers/csv_upload.sh b/ledgers/csv_upload.sh new file mode 100755 index 0000000..4d25b5c --- /dev/null +++ b/ledgers/csv_upload.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +# Copyright 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. + +if [ "${CONTENT_TYPE%%;*}" != "multipart/form-data" ]; then + SET_COOKIE 0 message="Not an upload" + REDIRECT "${_BASE}/ledgers/" +fi + +. "$_EXEC/multipart.sh" +multipart_cache + +# Validate session id from form to prevent CSRF +if [ "$(multipart session_id)" != "$SESSION_ID" ]; then + rm -- "$multipart_cachefile" + SET_COOKIE 0 message="INVALID SESSION ID IN FORM" + REDIRECT "${_BASE}/ledgers/" +fi + +mkdir -p "$_DATA/ledgers/" +CSV="$(multipart "csv" 1 | "$_EXEC/ledgers/csv_upload.awk")" +rm -- "$multipart_cachefile" + +read dtu_start dt_start dtu_end dt_end balance_start balance_end <<-EOF + ${CSV%%${BR}*} +EOF + +if [ ! "$dtu_end" -o ! "$dtu_start" ] || [ "$dtu_end" -lt "$dtu_start" ]; then + SET_COOKIE 0 message="No valid date range in upload" +else + num=0; while [ ! "$filename" -o -f "$_DATA/ledgers/$filename" ]; do + num=$((num + 1)); filename="${dt_start} - ${dt_end} - $(printf '%04i' $num).tbl" + done + printf '%s\n' "$CSV" >"$_DATA/ledgers/$filename" +fi + +REDIRECT "${_BASE}/ledgers/" diff --git a/ledgers/delete.sh b/ledgers/delete.sh new file mode 100755 index 0000000..7b41921 --- /dev/null +++ b/ledgers/delete.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +delete="$(POST delete |PATH)" +dtable="${_DATA}/ledgers/${delete##*/}" + +if [ "$dtable" ]; then + rm -- "${dtable}" \ + || SET_COOKIE 0 message="Unable to delete ledger: \"$(HTML "$delete")\"" +else + SET_COOKIE 0 message="No such ledger: \"$(HTML "$delete")\"" +fi + +REDIRECT "${_BASE}/ledgers/" diff --git a/ledgers/iban_assign.awk b/ledgers/iban_assign.awk new file mode 100755 index 0000000..0491617 --- /dev/null +++ b/ledgers/iban_assign.awk @@ -0,0 +1,92 @@ +#!/bin/awk -f + +function dbg( text ) { print text >>"/dev/stderr"; } + +function STRING( inp ) { + gsub(/\\/, "\\\\", inp); + gsub(/\n/, "\\n", inp); + gsub(/\r/, "\\r", inp); + gsub(/\t/, "\\t", inp); + gsub(/\+/, "\\+", inp); + gsub(/ /, "+", inp); + return inp ? inp : "\\"; +} + +function UNSTRING( inp, out, tmp ) { + while ( inp ) { + if ( inp ~ /^\\\\/) { out = out "\\"; sub(/^\\\\/, "", inp); } + else if ( inp ~ /^\\n/) { out = out "\n"; sub(/^\\n/, "", inp); } + else if ( inp ~ /^\\r/) { out = out "\r"; sub(/^\\r/, "", inp); } + else if ( inp ~ /^\\t/) { out = out "\t"; sub(/^\\t/, "", inp); } + else if ( inp ~ /^\\+/) { out = out "+"; sub(/^\\+/, "", inp); } + else if ( inp ~ /^\+/) { out = out " "; sub(/^\+/, "", inp); } + else if ( inp ~ /^\\/) { out = out ""; sub(/^\+/, "", inp); } + else { tmp = inp; sub(/[\\+].*$/, "", tmp); out = out tmp; sub(/^[^\\+]*/, "", inp); } + } + return out; +} + +function rx( regex ) { + gsub(/[].*+?^${}()|\\[]/, "\\\\&", regex); + return regex; +} + +BEGIN { + FS = " "; ledger = 0; + fn = n = uid = iban = tmp = ""; + split("", uid_n); split("", uid_fn); split("", uid_iban); + split("", iban_uid); split("", ibans); split("", uids); + + split("", sure); split("", unsure); split("", unknown); + split("", unsure_rec) +} + +/^BEGIN;:VCARD$/ { fn = n = id = iban = tmp = ""; } + + /^UID;[^:]*:/ { uid = $0; sub(/^[^;]+;[^:]*:/, "", uid); } + /^FN;[^:]*:/ { fn = $0; sub(/^[^;]+;[^:]*:/, "", fn); } + /^N;[^:]*:/ { n = $0; sub(/^[^;]+;[^:]*:/, "", n); sub(/;.*$/, "", n); } +/^X-IBAN;[^:]*:/ { iban = $0; sub(/^[^;]+;[^:]*:/, "", iban); ibans[length(ibans)] = iban; } + +/^END;:VCARD$/ { + uid_n[uid] = n; uid_fn[uid] = fn; uid_iban[uid] = iban; + for (iban in ibans) iban_uid[ibans[iban]] = iban_uid[ibans[iban]] ? iban_uid[ibans[iban]] " " uid : uid; + fn = n = uid = iban = tmp = ""; split("", ibans); +} + +/^BEGIN:LEDGERS$/ { ledger = 1; } + +ledger && strftime("%Y-%m-%d", $2, "UTC") == $1 { + if ($3 in iban_uid) { + sure[$3] = iban_uid[$3]; + } else { + for (uid in uid_fn) if ( match(UNSTRING($5), rx(uid_fn[uid])) ) { + if (! match(unsure[$3], rx(uid))) unsure[$3] = unsure[$3] ? unsure[$3] " " uid : uid; + unsure_rec[$3] = $0 + } + for (uid in uid_n) if ( uid_n[uid] && match(UNSTRING($4), rx(uid_n[uid])) ) { + if (! match(unsure[$3], rx(uid))) unsure[$3] = unsure[$3] ? unsure[$3] " " uid : uid; + unsure_rec[$3] = $0 + } + } + if (!($3 in sure) && !($3 in unsure)) unknown[$3] = $0; +} + +END { + for (iban in unsure) { + line = "guess " iban " " STRING(unsure_rec[iban]); + split(unsure[iban], uids, / /); + for (k in uids) line = line " " STRING(uids[k] "/" uid_fn[uids[k]]); + print line; + } + for (iban in unknown) { + line = "unknown " iban " " unknown[iban]; + print line; + } + for (iban in sure) { + line = "sure " iban; + split(sure[iban], uids, / /); + for (k in uids) line = line " " STRING(uids[k] "/" uid_fn[uids[k]]); + print line; + } +} diff --git a/ledgers/iban_assign.sh b/ledgers/iban_assign.sh new file mode 100755 index 0000000..778d279 --- /dev/null +++ b/ledgers/iban_assign.sh @@ -0,0 +1,81 @@ +#!/bin/sh + +credit() { + printf '%03i\n' "$1" \ + | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;' +} + +{ printf ' + [h1 . %s] + [form .ibanassign action="%s/ledgers/set_iban.sh" method=POST + [input type=hidden name=session_id value="%s"] + ' "$(l10n "IBAN Assignments")" "${_BASE}" "$SESSION_ID" + printf '[datalist id=lattendants .' + pdi_load "${_DATA}"/vcard/*.vcf |sed -n '/^FN\;:/!b; s;^FN\;:;;; p;' \ + | while read name; do + printf '[option value="%s"]' "$(HTML "$name")" + done + printf ']' + l10n_attendant="$(l10n attendant)" + printf %s\\n "$IBAN_ASSIGN" \ + | while read -r state iban data; do + iban="$(UNSTRING "$iban")" + [ ! "$iban" ] && iban="??????????" + printf '[input type=checkbox id="use_%s" name="use_%s" value=true]' "$iban" "$iban" + printf '[fieldset .iban .%s [legend . %s ]' \ + "$state" "$iban" + if [ $state = sure ]; then + for card in $data; do + uid="${card%%/*}" name="$(UNSTRING "${card#*/}")" + printf '[span .card . %s]' "$(HTML "${name}")" + done + : + elif [ $state = guess ]; then + record="$(UNSTRING "${data%% *}")" + cards="${data#* }" + date="${record%% *}" + principal="${record#* * * }" principal="${principal%% *}" + subject="${record#* * * * }" subject="${subject%% *}" + amount="${record#* * * * * }" amount="${amount%% *}" + printf '[p .principal . %s][p .date %s][p .amount %s][p .subject . %s]' \ + "$(UNSTRING "$principal" |HTML)" "$date" "$(credit "$amount")" "$(UNSTRING "$subject" |HTML)" + n=0; for card in $cards; do + n=$((n+1)); uid="${card%%/*}" name="$(UNSTRING "${card#*/}")" + cat <<-EOF + [input type=checkbox id="check_${iban}_$n" name="check_${iban}_$n" value=true checked=checked] + [input .card name="fn_${iban}_$n" value="$(HTML "$name")" .disabled tabindex="-1"] + [label .del for="check_${iban}_$n" . -] + EOF + done + for m in 1 2 3 4 5 6 7 8; do + cat <<-EOF + [input type=checkbox id="check_${iban}_$((n+m))" name="check_${iban}_$((n+m))" value=true] + [input .card name="fn_${iban}_$((n+m))" value="" placeholder="${l10n_attendant}" list="lattendants"] + [label .add for="check_${iban}_$((n+m))" . +] + EOF + done + printf '[label .button for="use_%s" . %s]' "$iban" "$(l10n Accept Suggestions)" + printf '[label .button for="use_%s" . %s]' "$iban" "$(l10n Ignore Suggestions)" + elif [ $state = unknown ]; then + date="${data%% *}" + principal="${data#* * * }" principal="${principal%% *}" + subject="${data#* * * * }" subject="${subject%% *}" + amount="${data#* * * * * }" amount="${amount%% *}" + printf '[p .principal . %s][p .date %s][p .amount %s][p .subject . %s]' \ + "$(UNSTRING "$principal" |HTML)" "$date" "$(credit "$amount")" "$(UNSTRING "$subject" |HTML)" + printf '[input name="check_" type=hidden][input type=hidden][label .del]' + n=0; for m in 1 2 3 4 5 6 7 8; do + cat <<-EOF + [input type=checkbox id="check_${iban}_$((n+m))" name="check_${iban}_$((n+m))" value=false] + [input .card name="fn_${iban}_$((n+m))" value="" placeholder="${l10n_attendant}" list="lattendants"] + [label .add for="check_${iban}_$((n+m))" . +] + EOF + done + printf '[label .button for="use_%s" . %s]' "$iban" "$(l10n Accept Suggestions)" + printf '[label .button for="use_%s" . %s]' "$iban" "$(l10n Ignore Suggestions)" + fi + printf ']' + done + printf '[button type=submit . %s]' "$(l10n Submit Changes)" + printf ' ]' +} | yield_page ledgers diff --git a/ledgers/index.cgi b/ledgers/index.cgi new file mode 100755 index 0000000..f5ac6b5 --- /dev/null +++ b/ledgers/index.cgi @@ -0,0 +1,76 @@ +#!/bin/sh + +. "$_EXEC/cgilite/storage.sh" +. "$_EXEC/pdiread.sh" + +if [ "$(POST show_account)" ]; then + uid="$(POST show_account)" + cfile="$(grep -lxF "UID;:${uid}" "${_DATA}/vcard/"*.vcf || grep -lxF "UID:${uid}" "${_DATA}/vcard/"*.vcf)" + REDIRECT "${_BASE}/ledgers/account.sh?card=${cfile##*/}" + exit 0; +fi + +credit() { + printf '%03i\n' "$1" \ + | sed -E 's;[0-9]{2}$;d&;; :0 s;([0-9])([0-9]{3}[dm]);\1m\2;; t0; y;dm;,.;' +} + +IBAN_ASSIGN="$( + { pdi_load "${_DATA}"/vcard/*.vcf + printf 'BEGIN:LEDGERS\n' + cat "${_DATA}"/ledgers/????-??-??\ -\ ????-??-??\ -\ ????.tbl + } | "${_EXEC}"/ledgers/iban_assign.awk + printf '\n' +)" + +if [ "${PATH_INFO%/iban_assign/}" != "${PATH_INFO}" ]; then + . "${_EXEC}/ledgers/iban_assign.sh" + exit 0 +fi + +{ printf ' + [form .upload action="%s/ledgers/csv_upload.sh" method="POST" enctype="multipart/form-data" + [label for=ledger_upload . %s:] + [input #ledger_upload type="file" name="csv" accept=".csv,text/csv"] + [input type=hidden name=session_id value="%s"] + [button type="submit" %s] + ]' \ + "${_BASE}" "$(l10n "Postbank CSV")" "$SESSION_ID" "$(l10n Upload)" + printf ' + [form .ledgers action="%s/ledgers/delete.sh" method=POST + [input type=hidden name=session_id value="%s"] + [h3 . %s] + ' "${_BASE}" "$SESSION_ID" "$(l10n Ledgers)" + for ledger in "$_DATA"/ledgers/????-??-??\ -\ ????-??-??\ -\ ????.tbl; do + ledger="${ledger##*/}" + [ "$ledger" = "????-??-?? - ????-??-?? - ????.tbl" ] && continue + printf '[p .ledger . %s [button type=submit name=delete value="%s" . %s]]' \ + "$(HTML "${ledger% - ????.tbl}")" "$(HTML "$ledger")" "$(l10n delete)" + done + printf ' ]' + unassigned="$(printf %s\\n "$IBAN_ASSIGN" |grep -E '^guess|^unknown' |wc -l)" + cat <<-EOF + [div + [h1 . $(l10n IBAN Assignments)] + $(printf "$(l10n "%i IBANs are unassigned")" "$unassigned") + [a href="${_BASE}/ledgers/iban_assign/" . $(l10n Assign IBANs)] + ] + [form action="${_BASE}/ledgers/account.sh" method=POST + [select name=uid + $(printf %s\\n "$IBAN_ASSIGN" \ + | sed -E ' + /^sure /!d; + s;^sure [^\t]+;;; + s;([^\t]+)/([^\t]+);\1 \2\n;g; + s;\n$;; + ' \ + | while read uid fn; do + uid="$(HTML "$uid")" + fn="$(UNSTRING "$fn" |HTML)" + printf '[option value="%s" . %s]' "$uid" "$fn" + done) + ] + [button type="submit" . $(l10n Account)] + ] + EOF +} | yield_page ledgers diff --git a/ledgers/set_iban.sh b/ledgers/set_iban.sh new file mode 100755 index 0000000..08c9a3b --- /dev/null +++ b/ledgers/set_iban.sh @@ -0,0 +1,49 @@ +#!/bin/sh + +. "$_EXEC/pdiread.sh" +. "$_EXEC/session_lock.sh" + +UIDLIST="$( + pdi_load "$_DATA/vcard/"*.vcf \ + | sed -Ez ' + s/\nBEGIN;:VCARD\n([^\n]+\n)*FN;:([^\n]+)\n([^\n]+\n)*UID;:([^\n]+)\n([^\n]+\n)*END;:VCARD\n/UID:\4 FN:\2/g + ' + echo +)" + +for key in $(POST_KEYS); do case $key in + use_*) + use_iban="${use_iban} ${key#use_} " + ;; +esac; done + +[ "$use_iban" ] && for key in $(POST_KEYS); do case $key in + check_*_*) + iban="${key#check_}" iban="${iban%_*}" + [ ! "${use_iban##* "${iban}" *}" ] && check="${check} ${key#check_} " + ;; +esac; done + +{ printf '[ul .results' + for use in $check; do + iban="${use%_*}" + fn="$(POST "fn_${use}")" + uid="${UIDLIST%% FN:"$fn"${BR}*}" uid="${uid##*${BR}UID:}" + + # cfile="${_DATA}/vcard/${uid}.vcf" + cfile="$(grep -lxF "UID;:${uid}" "${_DATA}/vcard/"*.vcf || grep -lxF "UID:${uid}" "${_DATA}/vcard/"*.vcf)" + if SLOCK "$cfile" >/dev/null; then + card="$(pdi_load "$cfile")" + cnum="$(pdi_count "$card" X-IBAN)" + pdi_update_value "$card" X-IBAN "$((cnum + 1))" "$iban" >"$cfile" + printf '[li .success . [span .name . %s] [span .uid . (UID: %s)] assigned IBAN [span .iban . %s]]' \ + "$(HTML "$fn")" "$(HTML "$uid")" "$(HTML "$iban")" + RELEASE_SLOCK "$cfile" + else + printf '[li .error . [span .name . %s] [span .uid . (UID: %s)] is being edited elsewhere]' + "$(HTML "$fn")" "$(HTML "$uid")" + fi + done + printf ']' + printf '[a .button href="%s/ledgers/iban_assign/" . %s]' "${_BASE}" "$(l10n Back)" +} | yield_page ledgers_assign diff --git a/multipart.sh b/multipart.sh new file mode 100755 index 0000000..02f7dfb --- /dev/null +++ b/multipart.sh @@ -0,0 +1,105 @@ +#!/bin/sh + +[ "$include_multipart" ] && return 0 +inlude_multipart="$0" + +# Copyright 2022 - 2023 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. + +if [ "${CONTENT_TYPE}" -a ! "${CONTENT_TYPE##multipart/form-data;*}" ]; then + multipart_boundary="${CONTENT_TYPE#*; boundary=}" + multipart_boundary="${multipart_boundary%%;*}" + multipart_boundary="${multipart_boundary%${CR}}" +fi +multipart_cachefile="/tmp/multipart.$$" + +readbytes(){ + # read n bytes, like `head -c` but do not consume input + local size="$1" block + + for block in 65536 32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1; do + if [ $size -ge $block ]; then + dd status=none bs="$block" count="$((size / block))" + size="$((size % block))" + fi + done +} + +multipart_cache() { + multipart_cachefile="${1:-${multipart_cachefile}}" # global + + if [ "${multipart_boundary}" ]; then + # readbytes "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}" + head -c "$(( CONTENT_LENGTH ))" >"${multipart_cachefile}" + else + return 1 + fi +} + +multipart(){ + local name="$1" count="${2:-1}" + local formdata state=begin + + while IFS='' read -r formdata; do case "$formdata" in + "--${multipart_boundary}--${CR}") + [ $state = data ] && return 0 \ + || return 1 + ;; + "--${multipart_boundary}${CR}") + [ $state = data ] && return 0 \ + || state=header + ;; + "Content-Disposition: form-data; name=\"${name}\""*"${CR}") + [ $state = header -a $count -eq 1 ] && state=dheader + [ $state = header -a $count -gt 1 ] && count=$((count - 1)) + [ $state = data ] && printf "%s\n" "$formdata" + ;; + "${CR}") + if [ $state = dheader ]; then + # Do not use `sed -n` (or busybox sed will "convert" NULL to LF) + sed "/--${multipart_boundary}\(--\)\?${CR}/{x;q;}" \ + | head -c-3 + return 0; + fi + [ $state = header ] && state=junk + ;; + esac; done <"${multipart_cachefile}" +} + +multipart_filename(){ + local name="$1" count="${2:-1}" + local formdata state=begin + + while read -r formdata; do case "$formdata" in + "--${multipart_boundary}--${CR}") + return 1 + ;; + "--${multipart_boundary}${CR}") + state=header + ;; + "Content-Disposition: form-data; name=\"${name}\"; filename=\""*"\""*"${CR}") + [ $state = header -a $count -eq 1 ] && break + [ $state = header -a $count -gt 1 ] && count=$((count - 1)) + ;; + "${CR}") + [ $state = header ] && state=junk + ;; + esac; done <"${multipart_cachefile}" + + filename="${formdata#*; filename=\"}" + filename="${filename%%\"${CR}}" + filename="${filename%%\";*}" + + HEX_DECODE % "$filename" +} diff --git a/style.css b/style.css index 298ae3d..c00829e 100644 --- a/style.css +++ b/style.css @@ -25,8 +25,9 @@ body > .menu a { padding: .5em 3em; box-shadow: inset 0 0 .5em #000; } -body.cards > .menu a[href$="/cards/"], -body.courses > .menu a[href$="/courses/"] { +body.ledgers > .menu a[href$="/ledgers/"], +body.courses > .menu a[href$="/courses/"], +body.cards > .menu a[href$="/cards/"] { color: #000; background-color: #FFF; box-shadow: none; @@ -34,9 +35,9 @@ body.courses > .menu a[href$="/courses/"] { /* =========== FILTER AND SEARCH Headers ========= */ -form.categories, +form.upload, form.categories, form.search, form.sort, form.filter, form.newcard, form.newcourses { - margin-top: 1em; padding: 0 1em; + margin-top: 1em; padding: .125em 1em 0 1em; z-index: 1; } form.filter > h1 { display: none; } @@ -64,10 +65,17 @@ form.filter button[type=submit] { form.filter button[value=export_csv] { margin-left: 1em; } body.courses form .order { display: inline-block; margin-right: 2em;} +body.courses form.search.sort fieldset { margin-top: .5em; } body.cards form.newcard { display: flex; } body.cards form.newcard input[name=seed] { flex: 1; } +form.upload label { + display: block; + font-weight: bold; + margin-top: .5em; +} + /* ============ LIST ITEMS, Generic ============= */ @@ -359,3 +367,167 @@ body.categories form.namelist ul.namelist > li h2 { body.categories form.namelist ul.namelist > li ul li { display: inline-block; } + + +/* ======== Ledgers Page ======== */ + +form.ibanassign, +form.ledgers { + padding: .125em 1em 0 1em; +} + +.ibanassign fieldset.iban.sure { background-color: #DFD; } +.ibanassign fieldset.iban.guess { background-color: #FFD; } +.ibanassign fieldset.iban.unknown { background-color: #FDD; } + +.ibanassign fieldset.iban { + padding: 0 .75em; + margin-top: -.5em; + box-shadow: .25em .25em .25em #AAA; +} +.ibanassign fieldset.iban legend { + top: .75em; +} +.ibanassign fieldset.iban p.principal { + font-size: .875em; +} +.ibanassign fieldset.iban p.date, +.ibanassign fieldset.iban p.amount { + font-size: .875em; + display: inline-block; + vertical-align: top; + margin-right: .75em; + margin-bottom: 0; +} +.ibanassign fieldset.iban p.amount { + font-weight: bold; +} + +.ibanassign fieldset.iban.sure .card { margin-right: 1em; } + +.ibanassign fieldset.iban input[name^="fn_"].disabled { + pointer-events: none; +} +.ibanassign fieldset.iban input[name^="check_"], +.ibanassign fieldset.iban input[name^="check_"] + input, +.ibanassign fieldset.iban input[name^="check_"] + input + label { + display: none; +} +.ibanassign fieldset.iban input[name^="check_"]:checked + input, +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label.del, +.ibanassign fieldset.iban input[name^="check_"] + input + label.del + input + input + label.add, +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label + input + input + label.add { + display: inline; +} +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label.add, +.ibanassign fieldset.iban input[name^="check_"] + input + label.del + input:checked + input + label.add, +.ibanassign fieldset.iban input[name^="check_"]:checked + input + label + input:checked + input + label.add { + display: none; +} + +.ibanassign fieldset.iban input[name^="check_"] + input + label { + vertical-align: bottom; + line-height: 2.5em; + padding: .375em .625em; + border: .5pt solid; +} +.ibanassign fieldset.iban input[name^="check_"] + input + label.add { + background-color: #DFD; + border-radius: 2pt; +} +.ibanassign fieldset.iban input[name^="check_"] + input + label.del { + margin: 0 .5em 0 -.25em; + background-color: #FDD; + border-radius: 0 2pt 2pt 0; +} + +.ibanassign input[name^="use_"] { + display: none; +} +.ibanassign input[name^="use_"]:checked + fieldset.iban.guess { background-color: #EFD; } +.ibanassign input[name^="use_"]:checked + fieldset.iban.unknown { background-color: #FED; } +.ibanassign input[name^="use_"]:checked + fieldset.iban input { + background-color: #DFD; + pointer-events: none; + border-color: #888; +} +.ibanassign input[name^="use_"]:checked + fieldset.iban label.del, +.ibanassign input[name^="use_"]:checked + fieldset.iban label.add { + display: none !important; +} + +.ibanassign fieldset.iban label[for^="use_"] { + display: block; + float: right; + padding: .25em .5em; + background-color: #DDF; + border: 1pt solid; +} + +.ibanassign input[name^="use_"] + fieldset.iban label[for^="use_"] { display: block; } +.ibanassign input[name^="use_"] + fieldset.iban label[for^="use_"] + label[for^="use_"] { display: none; } +.ibanassign input[name^="use_"]:checked + fieldset.iban label[for^="use_"] { display: none; } +.ibanassign input[name^="use_"]:checked + fieldset.iban label[for^="use_"] + label[for^="use_"] { display: block; } + +.ibanassign > button { + position: sticky; + bottom: 0; + margin: auto; + display: block; +} + +body.ledgers .transactions { + width: 98%; + width: calc(100% - 2em); + margin: auto; +} +body.ledgers .transactions thead { + position: sticky; + top: 0; + z-index: 1; +} +body.ledgers .transactions th { + text-align: left; + background-color: #FFF; +} +body.ledgers .transactions td { + vertical-align: top; + font-family: monospace; + font-size: 12pt; +} + +body.ledgers .transactions td:nth-child(2n) { + background-color: #DDD; +} +body.ledgers .transactions td:nth-child(2n + 1) { + background-color: #EEE; +} + +body.ledgers .transactions .date { + min-width: 8em; +} +body.ledgers .transactions .orig span { + display: block; +} +body.ledgers .transactions .amount, +body.ledgers .transactions .balance { + vertical-align: bottom; + min-width: 6em; + text-align: right; +} + +body.ledgers .transactions .reference textarea { + width: 100%; +} +body.ledgers .transactions .orig input[type=date], +body.ledgers .transactions .date input[type=date], +body.ledgers .transactions .amount input[type=number] { + display: block; + width: 100%; +} + +body.ledgers .transactions .orig input:not(:checked) + label + input[type=date] { + background-color: #DDD; + border-color: #888; + pointer-events: none; +}