diff options
Diffstat (limited to 'clients')
| -rw-r--r-- | clients/bokftui.c | 5 | ||||
| -rw-r--r-- | clients/screens_vouchers.c | 351 | ||||
| -rw-r--r-- | clients/tui.c | 14 | ||||
| -rw-r--r-- | clients/tui.h | 3 | ||||
| -rw-r--r-- | clients/ui.h | 1 | ||||
| -rw-r--r-- | clients/vlist.c | 84 | ||||
| -rw-r--r-- | clients/vlist.h | 46 |
7 files changed, 395 insertions, 109 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c index 69d6690..867e63e 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -20,6 +20,7 @@ #include "version.h" #include "yyjson.h" #include "ui.h" +#include "vlist.h" /* Screen names for ^R restoration and --screen. Only the top-level views; sub-screens unwind to their parent. */ @@ -149,6 +150,8 @@ static void config_load(struct app *a) else if (strcmp(line, "attachment_dir") == 0 && !a->attachment_dir[0]) snprintf(a->attachment_dir, sizeof a->attachment_dir, "%s", val); + else if (strcmp(line, "voucher_sort") == 0) + a->voucher_sort = vlist_sort_parse(val); } fclose(f); } @@ -166,6 +169,8 @@ void config_save(struct app *a) fprintf(f, "user=%s\n", a->username); if (a->attachment_dir[0]) fprintf(f, "attachment_dir=%s\n", a->attachment_dir); + if (a->voucher_sort != VSORT_NUMBER) + fprintf(f, "voucher_sort=%s\n", vlist_sort_name(a->voucher_sort)); fclose(f); chmod(path, 0600); } diff --git a/clients/screens_vouchers.c b/clients/screens_vouchers.c index 4cb6d9d..3c33990 100644 --- a/clients/screens_vouchers.c +++ b/clients/screens_vouchers.c @@ -20,6 +20,7 @@ #include "version.h" #include "yyjson.h" #include "ui.h" +#include "vlist.h" struct vdctx { struct app *a; @@ -30,12 +31,21 @@ struct vdctx { int want_refresh; int posted_new; int64_t posted_id; + int can_prev, can_next; /* a neighbour exists in the list order */ + int nav; /* -1/+1: show the previous/next voucher */ }; static int vd_key(void *ud, int ch) { struct vdctx *ctx = ud; struct app *a = ctx->a; + if (ch == KEY_UP || ch == KEY_DOWN) { + int dir = ch == KEY_UP ? -1 : 1; + if (!(dir < 0 ? ctx->can_prev : ctx->can_next)) + return TUI_HOOK_STAY; + ctx->nav = dir; + return TUI_HOOK_BACK; + } if (ch == TUI_KEY_CTRL_N) { int64_t nid = vouchers_new(a); if (nid > 0) { @@ -175,10 +185,48 @@ static int vd_key(void *ud, int ch) return TUI_HOOK_NONE; } -static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) +/* Column widths of the voucher detail; header and rows share them. */ +enum { VD_ACCT_W = 6, VD_NAME_W = 40, VD_AMT_W = 14 }; + +static void vd_amount(int64_t ore, char *buf, size_t n) +{ + if (ore) + tui_kr_format(ore, buf, n); + else + buf[0] = '\0'; +} + +/* One detail line: " konto benämning debet kredit text". Every column + is padded by display width so åäö never shift the ones after it. */ +static void vd_line(struct buf *out, const char *mark, const char *acct, + const char *name, const char *debit, const char *credit, + const char *text) +{ + char acol[64], ncol[256], dcol[64], ccol[64], line[1024]; + snprintf(acol, sizeof acol, "%s", acct); + tui_pad_field(acol, sizeof acol, VD_ACCT_W); + snprintf(ncol, sizeof ncol, "%s", name); + tui_pad_field(ncol, sizeof ncol, VD_NAME_W); + tui_pad_hdr(dcol, sizeof dcol, VD_AMT_W, debit); + tui_pad_hdr(ccol, sizeof ccol, VD_AMT_W, credit); + int n = snprintf(line, sizeof line, "%s %s %s %s %s %s", mark, acol, + ncol, dcol, ccol, text); + while (n > 0 && line[n - 1] == ' ') + n--; + buf_append(out, line, (size_t)n); + buf_append(out, "\n", 1); +} + +/* Returns 0 back, 1 after a correction, 2 when a new voucher was posted + (*out_new), 3 to show the neighbour in *nav (-1/+1) in the list order. + pos/total place the voucher in that order (pos -1: not in the list). */ +static int voucher_detail(struct app *a, int64_t id, int64_t *out_new, + int pos, int total, int *nav) { if (out_new) *out_new = 0; + if (nav) + *nav = 0; char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); for (;;) { @@ -201,34 +249,38 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) series ? series : "", (long long)number, date ? date : "", desc ? desc : ""); buf_append(&text, line, strlen(line)); - int hdr = snprintf( - line, sizeof line, MK_HEAD " %-6s %-34s D %12s K %12s\n", - "Konto", "Benämning", "Debet", "Kredit"); - buf_append(&text, line, (size_t)hdr); + vd_line(&text, MK_HEAD, "Konto", "Benämning", "Debet", "Kredit", + "Text"); size_t nrows = jarr_size(resp, "result.rows"); + int64_t sum_d = 0, sum_c = 0; for (size_t i = 0; i < nrows; i++) { char path[64]; snprintf(path, sizeof path, "result.rows.%zu.account", i); char *acc = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.name", i); char *name = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.rows.%zu.description", i); + char *rtext = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.debit_ore", i); int64_t debit = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.rows.%zu.credit_ore", i); int64_t credit = jint_val(resp, path, 0); + sum_d += debit; + sum_c += credit; char d[32], c[32]; - tui_kr_format(debit, d, sizeof d); - tui_kr_format(credit, c, sizeof c); - char acol[32], ncol[256]; - snprintf(acol, sizeof acol, "%s", acc ? acc : ""); - tui_pad_field(acol, sizeof acol, 6); - snprintf(ncol, sizeof ncol, "%s", name ? name : ""); - tui_pad_field(ncol, sizeof ncol, 34); - snprintf(line, sizeof line, " %s %s D %12s K %12s\n", acol, - ncol, d, c); - buf_append(&text, line, strlen(line)); + vd_amount(debit, d, sizeof d); + vd_amount(credit, c, sizeof c); + vd_line(&text, "", acc ? acc : "", name ? name : "", d, c, + rtext ? rtext : ""); free(acc); free(name); + free(rtext); + } + { + char d[32], c[32]; + tui_kr_format(sum_d, d, sizeof d); + tui_kr_format(sum_c, c, sizeof c); + vd_line(&text, MK_DIM, "", "Summa", d, c, ""); } size_t natts = jarr_size(resp, "result.attachments"); if (natts) { @@ -267,13 +319,22 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) ctx.id = id; ctx.natts = natts; ctx.resp = resp; - char hint[256]; + ctx.can_prev = pos > 0; + ctx.can_next = pos >= 0 && pos + 1 < total; + char hint[256], title[96]; snprintf(hint, sizeof hint, - "^N = nytt verifikat c = rätta ^F = bifoga%s", + "%s^N = nytt verifikat c = rätta ^F = bifoga%s", + pos >= 0 ? "upp/ned = föregående/nästa " : "", natts ? " f = underlag" : ""); + if (pos >= 0) + snprintf(title, sizeof title, "Verifikat %s%lld (%d av %d)", + series ? series : "", (long long)number, pos + 1, total); + else + snprintf(title, sizeof title, "Verifikat %s%lld", + series ? series : "", (long long)number); int want_refresh = 0, corrected = 0; - int ret = tui_pager_hook("Verifikat", (const char *)text.p, hint, 0, - vd_key, &ctx); + int ret = tui_pager_hook(title, (const char *)text.p, hint, + TUI_PAGER_NO_ARROWS, vd_key, &ctx); if (ret == 1) want_refresh = 1; if (ctx.want_refresh) @@ -294,6 +355,11 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) } if (want_refresh) continue; + if (ctx.nav && !corrected) { + if (nav) + *nav = ctx.nav; + return 3; + } return corrected; } } @@ -882,99 +948,167 @@ int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p) } } +struct vlctx { + struct app *a; + int resort; +}; + +static int vlist_key(void *ud, int ch) +{ + struct vlctx *l = ud; + if (ch != 's') + return TUI_HOOK_NONE; + l->a->voucher_sort = (l->a->voucher_sort + 1) % VSORT_COUNT; + config_save(l->a); + l->resort = 1; + return TUI_HOOK_REFRESH; +} + +static void vlist_free(struct vlist_item *v, size_t n) +{ + for (size_t i = 0; i < n; i++) + free(v[i].desc); + free(v); +} + +/* All vouchers of the current year, page by page. NULL after an error + (already shown). */ +static struct vlist_item *vlist_fetch(struct app *a, size_t *out_n) +{ + struct vlist_item *v = NULL; + size_t n = 0, cap = 0; + int64_t cursor = 0; + for (;;) { + char largs[128]; + snprintf(largs, sizeof largs, + "{\"limit\":1000,\"fiscal_year\":%lld,\"cursor\":%lld}", + (long long)a->fy, (long long)cursor); + char *resp = client_rpc(&a->conn, "voucher.list", a->session, a->org, + largs); + if (!resp || !client_ok(resp)) { + show_error("Verifikat", resp); + free(resp); + vlist_free(v, n); + return NULL; + } + size_t m = jarr_size(resp, "result.items"); + if (n + m > cap) { + cap = (n + m) * 2; + v = xrealloc(v, cap * sizeof *v); + } + for (size_t i = 0; i < m; i++) { + struct vlist_item *it = &v[n + i]; + memset(it, 0, sizeof *it); + char path[64]; + snprintf(path, sizeof path, "result.items.%zu.id", i); + it->id = jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.number", i); + it->number = jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.attachment_count", + i); + it->attachments = (int)jint_val(resp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.series", i); + char *ser = jstr_dup(resp, path); + snprintf(it->series, sizeof it->series, "%s", ser ? ser : ""); + free(ser); + snprintf(path, sizeof path, "result.items.%zu.date", i); + char *date = jstr_dup(resp, path); + snprintf(it->date, sizeof it->date, "%s", date ? date : ""); + free(date); + snprintf(path, sizeof path, "result.items.%zu.description", i); + it->desc = jstr_dup(resp, path); + } + n += m; + int64_t next = jint_val(resp, "result.next_cursor", 0); + free(resp); + if (!next || m == 0) + break; + cursor = next; + } + *out_n = n; + return v ? v : xcalloc(1, sizeof *v); +} + +/* List lines for v (plus the trailing "new" row); the caller frees them. */ +static char **vlist_lines(const struct vlist_item *v, size_t n) +{ + int idw = 4; + for (size_t i = 0; i < n; i++) { + char idbuf[48]; + int l = snprintf(idbuf, sizeof idbuf, "%s%lld", v[i].series, + (long long)v[i].number); + if (l > idw) + idw = l; + } + char **items = xcalloc(n + 1, sizeof(char *)); + for (size_t i = 0; i < n; i++) { + char idbuf[48], line[512]; + snprintf(idbuf, sizeof idbuf, "%s%lld", v[i].series, + (long long)v[i].number); + snprintf(line, sizeof line, " %c %-*s %s %s", + v[i].attachments ? 'x' : ' ', idw, idbuf, v[i].date, + v[i].desc ? v[i].desc : ""); + items[i] = xstrdup(line); + } + items[n] = xstrdup("+ Nytt verifikat (^N)"); + return items; +} + +static void vlist_lines_free(char **items, size_t n) +{ + for (size_t i = 0; items && i <= n; i++) + free(items[i]); + free(items); +} + void vouchers_screen(struct app *a) { + struct vlist_item *v = NULL; char **items = NULL; - int64_t *ids = NULL; size_t n = 0; - int fetch = 1; + int fetch = 1, resort = 0; for (;;) { - if (g_quit) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - return; - } + if (g_quit) + break; if (fetch) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); + vlist_lines_free(items, n); + vlist_free(v, n); items = NULL; - ids = NULL; n = 0; - fetch = 0; - char largs[96]; - snprintf(largs, sizeof largs, - "{\"limit\":200,\"fiscal_year\":%lld}", - (long long)a->fy); - char *resp = client_rpc(&a->conn, "voucher.list", a->session, a->org, - largs); - if (!resp || !client_ok(resp)) { - show_error("Verifikat", resp); - free(resp); + v = vlist_fetch(a, &n); + if (!v) return; - } - n = jarr_size(resp, "result.items"); - items = xcalloc(n + 1, sizeof(char *)); - ids = xcalloc(n ? n : 1, sizeof(int64_t)); - char **idstr = xcalloc(n ? n : 1, sizeof(char *)); - int idw = 4; - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.series", i); - char *ser = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.number", i); - int64_t number = jint_val(resp, path, 0); - char idbuf[32]; - snprintf(idbuf, sizeof idbuf, "%s%lld", ser ? ser : "", - (long long)number); - idstr[i] = xstrdup(idbuf); - int l = (int)strlen(idbuf); - if (l > idw) - idw = l; - free(ser); - } - for (size_t i = 0; i < n; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.date", i); - char *date = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.description", i); - char *desc = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.items.%zu.attachment_count", - i); - int atts = (int)jint_val(resp, path, 0); - snprintf(line, sizeof line, " %c %-*s %s %s", - atts ? 'x' : ' ', idw, idstr[i], date ? date : "", - desc ? desc : ""); - items[i] = xstrdup(line); - free(date); - free(desc); - free(idstr[i]); - } - free(idstr); - free(resp); - items[n] = xstrdup("+ Nytt verifikat (^N)"); + fetch = 0; + resort = 1; + } + if (resort) { + vlist_sort(v, n, a->voucher_sort); + vlist_lines_free(items, items ? n : 0); + items = vlist_lines(v, n); + resort = 0; } - size_t total = n + 1; int start = 0; if (a->voucher_sel) { - for (size_t i = 0; i < n; i++) - if (ids[i] == a->voucher_sel) { - start = (int)i; - break; - } + int i = vlist_index(v, n, a->voucher_sel); + if (i >= 0) + start = i; } + char title[96]; + snprintf(title, sizeof title, "Verifikat (sorterade på %s, s = byt)", + vlist_sort_label(a->voucher_sort)); + struct vlctx lctx = { a, 0 }; int cur = start; - int sel = tui_select_list("Verifikat", items, (int)total, start, 1, &cur, - 1, NULL, 0); + tui_list_hint_extra("s = sortera"); + int sel = tui_select_list_hook(title, items, (int)n + 1, start, 1, + &cur, 1, NULL, 0, vlist_key, &lctx); + tui_list_hint_extra(NULL); if (cur >= 0 && (size_t)cur < n) - a->voucher_sel = ids[cur]; + a->voucher_sel = v[cur].id; if (sel == -2) { - fetch = 1; + if (lctx.resort) + resort = 1; + else + fetch = 1; continue; } int64_t open_id = 0; @@ -982,14 +1116,10 @@ void vouchers_screen(struct app *a) if (sel >= 0) { if ((size_t)sel == n) want_new = 1; - else if ((size_t)sel < n) - open_id = ids[sel]; + else + open_id = v[sel].id; } else if (sel != -4) { - for (size_t i = 0; i < n; i++) - free(items[i]); - free(items); - free(ids); - return; + break; } if (want_new) { open_id = vouchers_new(a); @@ -1000,7 +1130,18 @@ void vouchers_screen(struct app *a) } for (;;) { int64_t nv = 0; - int act = voucher_detail(a, open_id, &nv); + int nav = 0; + /* a voucher posted since the fetch is not in v: no stepping */ + int pos = fetch ? -1 : vlist_index(v, n, open_id); + int act = voucher_detail(a, open_id, &nv, pos, (int)n, &nav); + if (act == 3) { + int next = vlist_step(n, pos, nav); + if (next < 0) + continue; + open_id = v[next].id; + a->voucher_sel = open_id; + continue; + } if (act == 2) { open_id = nv; a->voucher_sel = nv; @@ -1012,4 +1153,6 @@ void vouchers_screen(struct app *a) break; } } + vlist_lines_free(items, n); + vlist_free(v, n); } diff --git a/clients/tui.c b/clients/tui.c index 211df40..aa859e2 100644 --- a/clients/tui.c +++ b/clients/tui.c @@ -1410,15 +1410,16 @@ int tui_pager_hook(const char *title, const char *text, pager_line(2 + row, NULL); } char hint[512]; + const char *scroll = (flags & TUI_PAGER_NO_ARROWS) + ? "PgUp/PgDn/Home/End rullar" + : "piltangenter/PgUp/PgDn rullar"; if (extra_hint && *extra_hint) snprintf(hint, sizeof hint, - "piltangenter/PgUp/PgDn rullar F5 = uppdatera %s " - "q = tillbaka", + "%s F5 = uppdatera %s q = tillbaka", scroll, extra_hint); else - snprintf(hint, sizeof hint, - "piltangenter/PgUp/PgDn rullar F5 = uppdatera " - "q = tillbaka"); + snprintf(hint, sizeof hint, "%s F5 = uppdatera q = tillbaka", + scroll); tui_hints(hint); refresh(); int ch = in_key(); @@ -1443,6 +1444,9 @@ int tui_pager_hook(const char *title, const char *text, if (h == TUI_HOOK_STAY) continue; } + if ((flags & TUI_PAGER_NO_ARROWS) && + (ch == KEY_UP || ch == KEY_DOWN)) + continue; if (ch == KEY_DOWN && top + vbody < nb) top++; else if (ch == KEY_UP && top > 0) diff --git a/clients/tui.h b/clients/tui.h index 5ede574..c4fe116 100644 --- a/clients/tui.h +++ b/clients/tui.h @@ -162,6 +162,9 @@ void tui_frame(const char *title); void tui_hints(const char *s); void tui_redraw(void); #define TUI_PAGER_SAVE 0x1 /* 's' returns 2 (the save action) */ +/* Up/Down do not scroll (the key hook owns them, e.g. previous/next + record); PgUp/PgDn/Home/End still scroll and the hint says so. */ +#define TUI_PAGER_NO_ARROWS 0x2 int tui_pager(const char *title, const char *text, const char *action_hint); int tui_pager_hook(const char *title, const char *text, const char *extra_hint, unsigned flags, diff --git a/clients/ui.h b/clients/ui.h index c546c91..8574121 100644 --- a/clients/ui.h +++ b/clients/ui.h @@ -37,6 +37,7 @@ struct app { char role[32]; int64_t fy; int64_t voucher_sel; /* last selected voucher id in the list view */ + int voucher_sort; /* VSORT_* for the list, kept in tui.conf */ int64_t invoice_sel; /* last selected invoice id in the list view */ int64_t customer_sel; /* last selected customer id in the list view */ int64_t employee_sel; /* last selected employee id in the list view */ diff --git a/clients/vlist.c b/clients/vlist.c new file mode 100644 index 0000000..933b704 --- /dev/null +++ b/clients/vlist.c @@ -0,0 +1,84 @@ +#include <stdlib.h> +#include <string.h> + +#include "vlist.h" + +static const char *const SORT_NAMES[VSORT_COUNT] = { + "number", "number-desc", "date", "date-desc", +}; + +static const char *const SORT_LABELS[VSORT_COUNT] = { + "nummer, stigande", "nummer, fallande", "datum, stigande", + "datum, fallande", +}; + +static int cmp_i64(int64_t a, int64_t b) +{ + return a < b ? -1 : a > b; +} + +static int cmp_number(const struct vlist_item *a, const struct vlist_item *b) +{ + int c = strcmp(a->series, b->series); + if (c) + return c; + return cmp_i64(a->number, b->number); +} + +static int g_mode; /* qsort has no context argument in C11 */ + +static int cmp_items(const void *pa, const void *pb) +{ + const struct vlist_item *a = pa, *b = pb; + int desc = g_mode == VSORT_NUMBER_DESC || g_mode == VSORT_DATE_DESC; + int c = 0; + if (g_mode == VSORT_DATE || g_mode == VSORT_DATE_DESC) + c = strcmp(a->date, b->date); + if (!c) + c = cmp_number(a, b); + if (!c) + c = cmp_i64(a->id, b->id); + return desc ? -c : c; +} + +void vlist_sort(struct vlist_item *v, size_t n, int mode) +{ + if (!v || n < 2) + return; + g_mode = mode >= 0 && mode < VSORT_COUNT ? mode : VSORT_NUMBER; + qsort(v, n, sizeof *v, cmp_items); +} + +const char *vlist_sort_label(int mode) +{ + return SORT_LABELS[mode >= 0 && mode < VSORT_COUNT ? mode : 0]; +} + +const char *vlist_sort_name(int mode) +{ + return SORT_NAMES[mode >= 0 && mode < VSORT_COUNT ? mode : 0]; +} + +int vlist_sort_parse(const char *name) +{ + for (int i = 0; name && i < VSORT_COUNT; i++) + if (strcmp(name, SORT_NAMES[i]) == 0) + return i; + return VSORT_NUMBER; +} + +int vlist_index(const struct vlist_item *v, size_t n, int64_t id) +{ + for (size_t i = 0; i < n; i++) + if (v[i].id == id) + return (int)i; + return -1; +} + +int vlist_step(size_t n, int idx, int dir) +{ + if (idx < 0 || (size_t)idx >= n) + return -1; + int next = idx + (dir < 0 ? -1 : 1); + return next >= 0 && (size_t)next < n ? next : -1; +} diff --git a/clients/vlist.h b/clients/vlist.h new file mode 100644 index 0000000..8324ade --- /dev/null +++ b/clients/vlist.h @@ -0,0 +1,46 @@ +#ifndef BOKF_VLIST_H +#define BOKF_VLIST_H + +#include <stddef.h> +#include <stdint.h> + +/* Voucher list ordering and stepping for the Verifikat view. Pure, so it is + unit-tested in tests/test_tui.c. */ + +struct vlist_item { + int64_t id; + char series[16]; + int64_t number; + char date[16]; + char *desc; /* owned by the caller */ + int attachments; +}; + +enum { + VSORT_NUMBER = 0, /* series, then number, ascending */ + VSORT_NUMBER_DESC, + VSORT_DATE, /* date, then series and number, ascending */ + VSORT_DATE_DESC, + VSORT_COUNT, +}; + +/* Sorts in place; ties fall back to the id so the order is stable. An + out-of-range mode sorts by number. */ +void vlist_sort(struct vlist_item *v, size_t n, int mode); + +/* Swedish label for the list title, e.g. "datum, fallande". */ +const char *vlist_sort_label(int mode); + +/* Config value ("number", "number-desc", "date", "date-desc") and back; + an unknown name gives VSORT_NUMBER. */ +const char *vlist_sort_name(int mode); +int vlist_sort_parse(const char *name); + +/* Index of the item with this id, or -1. */ +int vlist_index(const struct vlist_item *v, size_t n, int64_t id); + +/* Index dir steps from idx, or -1 past either end or when idx is not in + the list. */ +int vlist_step(size_t n, int idx, int dir); + +#endif |
