diff options
Diffstat (limited to 'clients/bokftui.c')
| -rw-r--r-- | clients/bokftui.c | 346 |
1 files changed, 319 insertions, 27 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c index ddff00b..d446974 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -718,7 +718,7 @@ static int menu(const char *title, const char *const *items, int n, static int select_list(const char *title, char **items, int n, int start, int allow_refresh, int *cursor, int allow_new, - int allow_archive, int allow_toggle) + const char *archive_action, int allow_toggle) { int sel = start; if (sel < 0) @@ -763,13 +763,14 @@ static int select_list(const char *title, char **items, int n, int start, clrtoeol(); } { - char hint[256]; + char hint[256], dbuf[48] = ""; + if (archive_action) + snprintf(dbuf, sizeof dbuf, " d = %s", archive_action); snprintf(hint, sizeof hint, "upp/ned, 1-9 = hoppa, g = gå till, PgUp/PgDn, Home/End," " Enter%s%s%s%s Esc/q = tillbaka", allow_refresh ? " F5 = uppdatera" : "", - allow_new ? " ^N = ny" : "", - allow_archive ? " d = arkivera" : "", + allow_new ? " ^N = ny" : "", dbuf, allow_toggle ? " ^A = öppna/stäng" : ""); snprintf(hint + strlen(hint), sizeof hint - strlen(hint), " ^C = avsluta"); @@ -826,7 +827,7 @@ static int select_list(const char *title, char **items, int n, int start, return -2; else if (allow_new && ch == KEY_CTRL_N) return -4; - else if (allow_archive && (ch == 'd' || ch == 'D') && n > 0) + else if (archive_action && (ch == 'd' || ch == 'D') && n > 0) return -6; else if (allow_toggle && ch == 1 && n > 0) return -7; @@ -1278,7 +1279,7 @@ static void select_fiscal_year(struct app *a) if (start >= (int)n) start = (int)n - 1; int sel = select_list("Räkenskapsår", lines, (int)n, start, 0, - &cursor, 1, 0, 1); + &cursor, 1, NULL, 1); for (size_t i = 0; i < n; i++) free(lines[i]); free(lines); @@ -1471,7 +1472,7 @@ static char *file_browser(struct app *a, const char *start_dir) } char title[1200]; snprintf(title, sizeof title, "Välj fil — %.200s", dir); - int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0, 0, 0); + int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0, NULL, 0); for (size_t i = 0; i <= n; i++) free(items[i]); free(items); @@ -1501,6 +1502,114 @@ static char *file_browser(struct app *a, const char *start_dir) } } +/* Valid UTF-8 without NUL bytes: good enough to call a receipt text. */ +static int bytes_look_text(const unsigned char *p, size_t n) +{ + for (size_t i = 0; i < n;) { + unsigned char c = p[i]; + if (c == 0) + return 0; + if (c < 0x80) { + i++; + continue; + } + int len; + if ((c & 0xE0) == 0xC0) + len = 2; + else if ((c & 0xF0) == 0xE0) + len = 3; + else if ((c & 0xF8) == 0xF0) + len = 4; + else + return 0; + if (i + (size_t)len > n) + return 0; + for (int k = 1; k < len; k++) + if ((p[i + k] & 0xC0) != 0x80) + return 0; + i += (size_t)len; + } + return 1; +} + +/* Fetch one attachment, save it to a prompted path and verify its hash. + Text attachments are shown inline after saving. */ +static void attachment_download(struct app *a, int64_t att_id) +{ + char args[64]; + snprintf(args, sizeof args, "{\"id\":%lld}", (long long)att_id); + char *resp = + client_rpc(&a->conn, "attachment.get", a->session, a->org, args); + if (!resp || !client_ok(resp)) { + show_error("Kunde inte hämta underlaget", resp); + free(resp); + return; + } + char *fn = jstr_dup(resp, "result.filename"); + char *mime = jstr_dup(resp, "result.mime"); + char *sha = jstr_dup(resp, "result.sha256"); + char *b64 = jstr_dup(resp, "result.content_base64"); + unsigned char *data = NULL; + size_t n = 0; + if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { + message("Underlag", "Kunde inte avkoda innehållet."); + goto done; + } + const char *home = getenv("HOME"); + char def[600], dl[512]; + snprintf(dl, sizeof dl, "%s/Downloads", home && *home ? home : "."); + struct stat sb; + if (stat(dl, &sb) != 0 || !S_ISDIR(sb.st_mode)) + snprintf(dl, sizeof dl, "%s", home && *home ? home : "."); + snprintf(def, sizeof def, "%s/%s", dl, fn && *fn ? fn : "underlag"); + char *path = prompt("Spara underlag: ", def, 0); + if (!path || !*path) + goto done; + char full[600]; + if (path[0] == '~' && (path[1] == '/' || path[1] == '\0')) + snprintf(full, sizeof full, "%s%s", home ? home : "", path + 1); + else + snprintf(full, sizeof full, "%s", path); + if (stat(full, &sb) == 0) { + char *ans = prompt("Filen finns, skriv över? (j/n): ", "n", 0); + if (!ans || (ans[0] != 'j' && ans[0] != 'J')) + goto done; + } + FILE *f = fopen(full, "wb"); + if (!f || fwrite(data, 1, n, f) != n) { + message("Underlag", "Kunde inte skriva %s: %s", full, strerror(errno)); + if (f) + fclose(f); + goto done; + } + fclose(f); + unsigned char raw[32]; + char hex[65]; + util_sha256(data, n, raw); + util_hex(raw, sizeof raw, hex); + if (sha && *sha && strcmp(hex, sha) != 0) + message("Underlag", "VARNING: kontrollsumman stämmer inte.\nSparat: %s", + full); + else + message("Underlag", "Sparat: %s\n(%zu byte)", full, n); + int is_text = (mime && strncmp(mime, "text/", 5) == 0) || + (n > 0 && bytes_look_text(data, n)); + if (is_text && n <= 256 * 1024) { + char *copy = xmalloc(n + 1); + memcpy(copy, data, n); + copy[n] = '\0'; + text_view("Underlag", copy, NULL); + free(copy); + } +done: + free(data); + free(b64); + free(sha); + free(mime); + free(fn); + free(resp); +} + static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) { if (out_new) @@ -1597,11 +1706,15 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) int want_refresh = 0, corrected = 0, stop = 0, posted_new = 0; int64_t posted_id = 0; while (!stop) { + char hint[256]; + snprintf(hint, sizeof hint, + "upp/ned F5 = uppdatera ^N = nytt verifikat c =" + " rätta ^F = bifoga%s Esc/q = tillbaka", + natts ? " f = underlag" : ""); frame("Verifikat"); for (int i = 0; i < view && top + i < n; i++) mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); - hints("upp/ned F5 = uppdatera ^N = nytt verifikat c =" - " rätta Esc/q = tillbaka"); + hints(hint); refresh(); int ch = ui_getch(); if (ch == 'q' || ch == 27) { @@ -1643,6 +1756,95 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) free(ref); } stop = 1; + } else if (ch == 6) { /* ^F: attach a file to this voucher */ + char *path = file_browser(a, a->attachment_dir); + if (path) { + struct stat sb; + if (stat(path, &sb) == 0 && + (long)sb.st_size > a->max_attachment_bytes) { + message("Bilaga", "Filen är %.1f MB, max %ld MB.", + (double)sb.st_size / (1024 * 1024), + a->max_attachment_bytes / (1024 * 1024)); + free(path); + } else { + char *b64 = read_file_b64(path); + if (!b64) { + message("Fel", "Kunde inte läsa %s", path); + free(path); + } else { + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *o = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, o); + yyjson_mut_obj_add_strcpy(d, o, "filename", base); + yyjson_mut_obj_add_strcpy(d, o, "content_base64", + b64); + yyjson_mut_obj_add_int(d, o, "voucher_id", id); + char *fargs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + free(b64); + free(path); + char *r = client_rpc(&a->conn, "attachment.put", + a->session, a->org, fargs); + if (r && client_ok(r)) + message("Underlag", "Bifogat."); + else + show_error("Kunde inte bifoga", r); + free(r); + free(fargs); + want_refresh = 1; + stop = 1; + } + } + } + } else if (ch == 'f' && natts > 0) { + char **alines = xcalloc(natts, sizeof(char *)); + int64_t *aids = xcalloc(natts, sizeof(int64_t)); + for (size_t i = 0; i < natts; i++) { + char path[64]; + snprintf(path, sizeof path, "result.attachments.%zu.id", + i); + aids[i] = jint_val(resp, path, 0); + snprintf(path, sizeof path, + "result.attachments.%zu.filename", i); + char *fn = jstr_dup(resp, path); + alines[i] = xstrdup(fn ? fn : "(namnlös)"); + free(fn); + } + int acursor = 0; + int asel = select_list("Underlag", alines, (int)natts, 0, 0, + &acursor, 0, "ta bort", 0); + if (asel >= 0) + attachment_download(a, aids[asel]); + else if (asel == -6 && acursor >= 0 && + (size_t)acursor < natts) { + char q[320]; + snprintf(q, sizeof q, + "Ta bort '%s' från verifikatet? (j/n): ", + alines[acursor]); + char *ans = prompt(q, "n", 0); + if (ans && (ans[0] == 'j' || ans[0] == 'J')) { + char uargs[64]; + snprintf(uargs, sizeof uargs, + "{\"id\":%lld,\"voucher_id\":%lld}", + (long long)aids[acursor], (long long)id); + char *r = client_rpc(&a->conn, "attachment.unlink", + a->session, a->org, uargs); + if (r && client_ok(r)) { + message("Underlag", "Länken borttagen."); + want_refresh = 1; + stop = 1; + } else { + show_error("Kunde inte ta bort", r); + } + free(r); + } + } + for (size_t i = 0; i < natts; i++) + free(alines[i]); + free(alines); + free(aids); } else if (ch == KEY_DOWN && top + view < n) { top++; } else if (ch == KEY_UP && top > 0) { @@ -2021,7 +2223,7 @@ static int64_t vouchers_new(struct app *a) free(ser); free(ds); } - int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, 0, 0); + int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, NULL, 0); if (tsel >= 0) snprintf(tname, sizeof tname, "%s", names[tsel]); for (size_t i = 0; i < ln; i++) { @@ -2419,7 +2621,7 @@ static void vouchers_screen(struct app *a) } int cur = start; int sel = select_list("Verifikat", items, (int)total, start, 1, &cur, - 1, 0, 0); + 1, NULL, 0); if (cur >= 0 && (size_t)cur < n) a->voucher_sel = ids[cur]; if (sel == -2) { @@ -3547,9 +3749,62 @@ static void reports_screen(struct app *a) } } +/* Pick a voucher in the active fiscal year; returns its id or 0. */ +static int64_t pick_voucher(struct app *a) +{ + char args[96]; + snprintf(args, sizeof args, "{\"fiscal_year\":%lld,\"limit\":200}", + (long long)a->fy); + char *resp = + client_rpc(&a->conn, "voucher.list", a->session, a->org, args); + if (!resp || !client_ok(resp)) { + show_error("Verifikat", resp); + free(resp); + return 0; + } + size_t n = jarr_size(resp, "result.items"); + if (n == 0) { + message("Verifikat", "Inga verifikat i räkenskapsåret."); + free(resp); + return 0; + } + char **items = xcalloc(n, sizeof(char *)); + int64_t *ids = xcalloc(n, sizeof(int64_t)); + for (size_t i = 0; i < n; i++) { + char path[64], ver[32], line[512]; + 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 *series = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.items.%zu.number", i); + int64_t number = jint_val(resp, path, 0); + 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(ver, sizeof ver, "%s%lld", series ? series : "", + (long long)number); + snprintf(line, sizeof line, "%-8s %-10s %s", ver, date ? date : "", + desc ? desc : ""); + items[i] = xstrdup(line); + free(series); + free(date); + free(desc); + } + int sel = select_list("Välj verifikat", items, (int)n, 0, 1, NULL, 0, + NULL, 0); + int64_t out = sel >= 0 ? ids[sel] : 0; + for (size_t i = 0; i < n; i++) + free(items[i]); + free(items); + free(ids); + free(resp); + return out; +} + static void inbox_screen(struct app *a) { - int top = 0; + int top = 0, sel = 0; for (;;) { if (g_quit) return; @@ -3562,8 +3817,11 @@ static void inbox_screen(struct app *a) } size_t n = jarr_size(resp, "result.items"); char **items = xcalloc(n ? n : 1, sizeof(char *)); + int64_t *ids = xcalloc(n ? n : 1, sizeof(int64_t)); for (size_t i = 0; i < n; i++) { char path[64], line[512]; + snprintf(path, sizeof path, "result.items.%zu.id", i); + ids[i] = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.filename", i); char *fn = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.size_bytes", i); @@ -3577,39 +3835,73 @@ static void inbox_screen(struct app *a) free(fn); } int view = LINES - 4; + if (n == 0) + sel = 0; + else if (sel >= (int)n) + sel = (int)n - 1; + if (sel < top) + top = sel; + if (sel >= top + view) + top = sel - view + 1; if (top > (int)n - view) top = (int)n - view; if (top < 0) top = 0; frame("Underlag (inkorg)"); for (size_t i = (size_t)top; i < n && (int)(i - (size_t)top) < view; - i++) - mvaddnstr(2 + (int)(i - (size_t)top), 2, items[i], COLS - 4); + i++) { + int y = 2 + (int)(i - (size_t)top); + if ((int)i == sel) + attron(A_REVERSE); + mvaddnstr(y, 2, items[i], COLS - 4); + if ((int)i == sel) + attroff(A_REVERSE); + } if (n == 0) mvaddstr(2, 2, "Inga obokförda underlag."); - hints("a/^N = lägg till fil PgUp/PgDn, Home/End rullar F5 =" - " uppdatera Esc/q = tillbaka"); + hints("Enter = hämta k = koppla till verifikat a/^N = lägg till" + " fil PgUp/PgDn, Home/End rullar F5 = uppdatera Esc/q =" + " tillbaka"); refresh(); int ch = ui_getch(); + int64_t sel_id = (sel >= 0 && (size_t)sel < n) ? ids[sel] : 0; for (size_t i = 0; i < n; i++) free(items[i]); free(items); + free(ids); free(resp); if (ch == 27 || ch == 'q') return; - if (ch == KEY_DOWN && top + view < (int)n) - top++; - else if (ch == KEY_UP && top > 0) - top--; + if ((ch == '\n' || ch == '\r' || ch == KEY_ENTER) && sel_id) + attachment_download(a, sel_id); + else if (ch == KEY_DOWN && sel + 1 < (int)n) + sel++; + else if (ch == KEY_UP && sel > 0) + sel--; else if (ch == KEY_NPAGE) - top += view; + sel = sel + view < (int)n ? sel + view : (int)n - 1; else if (ch == KEY_PPAGE) - top -= view; + sel = sel - view > 0 ? sel - view : 0; else if (ch == KEY_HOME) - top = 0; + sel = 0; else if (ch == KEY_END) - top = (int)n > view ? (int)n - view : 0; - else if (ch == 'a' || ch == KEY_CTRL_N) { + sel = n > 0 ? (int)n - 1 : 0; + else if (ch == 'k' && sel_id) { + int64_t vid = pick_voucher(a); + if (vid) { + char kargs[64]; + snprintf(kargs, sizeof kargs, + "{\"id\":%lld,\"voucher_id\":%lld}", + (long long)sel_id, (long long)vid); + char *r = client_rpc(&a->conn, "attachment.link", a->session, + a->org, kargs); + if (r && client_ok(r)) + message("Underlag", "Kopplat till verifikatet."); + else + show_error("Kunde inte koppla", r); + free(r); + } + } else if (ch == 'a' || ch == KEY_CTRL_N) { char *path = file_browser(a, a->attachment_dir); if (!path) continue; @@ -4894,7 +5186,7 @@ static void templates_screen(struct app *a) names[n] = xstrdup(""); lines[n] = xstrdup("+ Ny mall (^N)"); int s = select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, - n > 0, 0); + n > 0 ? "arkivera" : NULL, 0); if (s == -1) { for (size_t i = 0; i <= n; i++) { free(names[i]); @@ -5348,7 +5640,7 @@ static int select_org(struct app *a) free(role); } int sel = select_list("Välj organisation att representera", items, (int)n, - 0, 0, NULL, 0, 0, 0); + 0, 0, NULL, 0, NULL, 0); for (size_t i = 0; i < n; i++) free(items[i]); free(items); |
