diff options
| -rw-r--r-- | clients/screens_attachments.c | 2 | ||||
| -rw-r--r-- | clients/screens_invoices.c | 4 | ||||
| -rw-r--r-- | clients/screens_payroll.c | 2 | ||||
| -rw-r--r-- | clients/screens_vouchers.c | 2 | ||||
| -rw-r--r-- | clients/ui.c | 98 | ||||
| -rw-r--r-- | clients/ui.h | 6 | ||||
| -rw-r--r-- | docs/PROTOCOL.md | 5 | ||||
| -rw-r--r-- | docs/TUI-GUIDELINES.md | 2 | ||||
| -rwxr-xr-x | scripts/tui-golden.py | 45 |
9 files changed, 132 insertions, 34 deletions
diff --git a/clients/screens_attachments.c b/clients/screens_attachments.c index 3eeb9e9..c72ee67 100644 --- a/clients/screens_attachments.c +++ b/clients/screens_attachments.c @@ -158,6 +158,6 @@ void inbox_screen(struct app *a) continue; } if (sel_id) - attachment_download(a, sel_id); + attachment_view_or_download(a, sel_id); } } diff --git a/clients/screens_invoices.c b/clients/screens_invoices.c index 8fb32eb..d685e83 100644 --- a/clients/screens_invoices.c +++ b/clients/screens_invoices.c @@ -476,7 +476,7 @@ static void inv_detail_pdf(struct invctx *ctx) char fname[800]; snprintf(fname, sizeof fname, "Faktura %lld %s.pdf", (long long)ctx->number, ctx->customer); - pdf_save_and_open(b64, fname, "Faktura PDF"); + file_save_and_open(b64, fname, "Faktura PDF"); free(b64); } free(resp); @@ -967,7 +967,7 @@ static void iform_preview(struct iform *f) } char *b64 = jstr_dup(resp, "result.content_base64"); if (b64) { - pdf_save_and_open(b64, "Faktura förhandsvisning.pdf", "Faktura PDF"); + file_save_and_open(b64, "Faktura förhandsvisning.pdf", "Faktura PDF"); free(b64); } free(resp); diff --git a/clients/screens_payroll.c b/clients/screens_payroll.c index 234778c..8a27ec8 100644 --- a/clients/screens_payroll.c +++ b/clients/screens_payroll.c @@ -898,7 +898,7 @@ static void payroll_run_payslip(struct payroll_run *r) char *fname = jstr_dup(resp, "result.filename"); free(resp); if (b64 && fname) - pdf_save_and_open(b64, fname, "Lönebesked"); + file_save_and_open(b64, fname, "Lönebesked"); free(b64); free(fname); } diff --git a/clients/screens_vouchers.c b/clients/screens_vouchers.c index d884b6e..52d29da 100644 --- a/clients/screens_vouchers.c +++ b/clients/screens_vouchers.c @@ -137,7 +137,7 @@ static int vd_key(void *ud, int ch) int asel = tui_select_list("Underlag", alines, (int)ctx->natts, 0, 0, &acursor, 0, "ta bort", 0); if (asel >= 0) { - attachment_download(a, aids[asel]); + attachment_view_or_download(a, aids[asel]); } else if (asel == -6 && acursor >= 0 && (size_t)acursor < ctx->natts) { char q[320]; diff --git a/clients/ui.c b/clients/ui.c index e6f6e07..cda0883 100644 --- a/clients/ui.c +++ b/clients/ui.c @@ -472,26 +472,9 @@ static void safe_fname(const char *in, char *out, size_t n) out[o] = '\0'; } -static int write_pdf_b64(const char *b64, const char *path) -{ - unsigned char *data = NULL; - size_t len = 0; - if (!b64 || util_b64_decode(b64, strlen(b64), &data, &len) != 0) - return -1; - FILE *fp = fopen(path, "wb"); - if (!fp) { - free(data); - return -1; - } - size_t wrote = fwrite(data, 1, len, fp); - fclose(fp); - free(data); - return wrote == len ? 0 : -1; -} - -/* Open a saved PDF with xdg-open when a desktop session is present; +/* Open a saved file with xdg-open when a desktop session is present; otherwise (or on fork failure) show where it was saved. */ -static void open_pdf(const char *path, const char *title) +static void open_saved(const char *path, const char *title) { const char *display = getenv("DISPLAY"); const char *wayland = getenv("WAYLAND_DISPLAY"); @@ -515,8 +498,8 @@ static void open_pdf(const char *path, const char *title) tui_message(title, "Sparad: %s", path); } -void pdf_save_and_open(const char *b64, const char *filename, - const char *title) +static void save_cache_and_open(const unsigned char *data, size_t n, + const char *filename, const char *title) { char dir[512], path[700], safe[600]; pdf_cache_dir(dir, sizeof dir); @@ -526,11 +509,80 @@ void pdf_save_and_open(const char *b64, const char *filename, return; } config_mkdirs(path); - if (write_pdf_b64(b64, path) != 0) { + FILE *fp = fopen(path, "wb"); + if (!fp || (n > 0 && fwrite(data, 1, n, fp) != n)) { + if (fp) + fclose(fp); tui_message(title, "Kunde inte spara %s", path); return; } - open_pdf(path, title); + fclose(fp); + open_saved(path, title); +} + +void file_save_and_open(const char *b64, const char *filename, + const char *title) +{ + unsigned char *data = NULL; + size_t n = 0; + if (!b64 || util_b64_decode(b64, strlen(b64), &data, &n) != 0) { + tui_message(title, "Kunde inte avkoda innehållet."); + return; + } + save_cache_and_open(data, n, filename, title); + free(data); +} + +/* Show one attachment: text inline in a pager, anything else through the + desktop viewer (or a message with the saved path). */ +void attachment_open(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 *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) { + tui_message("Underlag", "Kunde inte avkoda innehållet."); + goto done; + } + 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'; + tui_pager("Underlag", copy, NULL); + free(copy); + } else { + save_cache_and_open(data, n, fn && *fn ? fn : "underlag", "Underlag"); + } +done: + free(data); + free(b64); + free(mime); + free(fn); + free(resp); +} + +/* Enter on an attachment row: granska or ladda ned. */ +void attachment_view_or_download(struct app *a, int64_t att_id) +{ + static const char *const opts[] = { "Granska", "Ladda ned…" }; + int c = tui_choice_prompt("Underlag: ", opts, 2, 0); + if (c == 0) + attachment_open(a, att_id); + else if (c == 1) + attachment_download(a, att_id); } /* Fetch one attachment, save it to a prompted path and verify its hash. diff --git a/clients/ui.h b/clients/ui.h index b4ce5f5..393d5ff 100644 --- a/clients/ui.h +++ b/clients/ui.h @@ -79,9 +79,11 @@ char *rpc_dry(struct app *a, const char *cmd, const char *args); char *read_file_b64(const char *path); char *file_browser(struct app *a, const char *start_dir); void attachment_download(struct app *a, int64_t att_id); +void attachment_open(struct app *a, int64_t att_id); +void attachment_view_or_download(struct app *a, int64_t att_id); int64_t pick_voucher(struct app *a); -void pdf_save_and_open(const char *b64, const char *filename, - const char *title); +void file_save_and_open(const char *b64, const char *filename, + const char *title); /* generated-document helpers (reports, bokslut) */ void buf_line(struct buf *b, const char *fmt, ...) diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md index 6d99225..81eeb67 100644 --- a/docs/PROTOCOL.md +++ b/docs/PROTOCOL.md @@ -908,7 +908,10 @@ commands. Implemented screens (0.1.0-dev): - **Mallar** — list, create and edit templates in the same form style as vouchers (Tab, dynamic rows, F7 clear row, F5 validate, F9 save); archive keeps the template but hides it from the list. -- **Underlag** — inbox of unlinked attachments; `a` uploads a file. +- **Underlag** — inbox of unlinked attachments; `a` uploads a file and + Enter opens **Granska** (text in a pager, other files in the desktop + viewer) or **Ladda ned…**; the voucher detail's underlag list (`f`) works + the same way. - **Bankavstämning** — imported bank transactions (`bank.import`) matched against vouchers on the bank account, with suggestions; Enter matches the suggested voucher (or picks another), `u` unmatches, `a` imports a SEB CSV. diff --git a/docs/TUI-GUIDELINES.md b/docs/TUI-GUIDELINES.md index 470f6d8..433b0b2 100644 --- a/docs/TUI-GUIDELINES.md +++ b/docs/TUI-GUIDELINES.md @@ -29,7 +29,7 @@ there. | `a` | Add/upload (Underlag) | | `c` | Correct (voucher detail) | | `d` | Delete/arkivera the selected row (only where the action exists; asks for confirmation) | -| `f` | Voucher detail: list the voucher's underlag — Enter fetches, `d` removes the link (asks first). Underlag: Enter fetches | +| `f` | Voucher detail: list the voucher's underlag — Enter opens Granska (text in a pager, PDFs/images in the desktop viewer) or Ladda ned…, `d` removes the link (asks first). Underlag: Enter does the same | | `Ctrl+F` | Attach a file via the file browser (voucher form and voucher detail) | | `k` | Underlag: link the highlighted attachment to a voucher picked from a list | | `Ctrl+X` | Clear the current row — only inside row editors (never "new") | diff --git a/scripts/tui-golden.py b/scripts/tui-golden.py index db8a18e..6af4d4f 100755 --- a/scripts/tui-golden.py +++ b/scripts/tui-golden.py @@ -41,6 +41,7 @@ from pathlib import Path ORG_NAME = "Test AB" ORG_NR = "5560123456" +DOWNLOAD_PATH = f"/tmp/bokf-golden-dl-{os.getpid()}.txt" KEYS = { "enter": "\r", @@ -104,6 +105,42 @@ SCENARIOS = [ "expect": ["Benämning", "Debet", "Kredit", "Underlag", "kvitto.txt"], }, + { + "keys": ["f"], + "expect": ["Underlag", "kvitto.txt"], + }, + { + "keys": ["enter"], + "expect": ["Underlag: < Granska >"], + }, + { + "keys": ["enter"], + "expect": ["piltangenter/PgUp/PgDn rullar", "kvitto"], + }, + { + "keys": ["esc"], + "expect": ["Benämning", "Underlag"], + }, + { + "keys": ["f", "enter", "right"], + "expect": ["Underlag: < Ladda ned… >"], + }, + { + "keys": ["enter"], + "expect": ["Spara underlag: "], + }, + { + "keys": [DOWNLOAD_PATH, "enter"], + "expect": ["Sparat"], + }, + { + "keys": ["enter"], + "expect": ["piltangenter/PgUp/PgDn rullar", "kvitto"], + }, + { + "keys": ["esc"], + "expect": ["Benämning", "Underlag"], + }, ], }, { @@ -194,11 +231,11 @@ SCENARIOS = [ }, { "keys": ["enter"], - "expect": ["Matchat med A3"], + "expect": ["Matchat med A2"], }, { "keys": ["enter"], - "expect": ["→ A3", "0 omatchade"], + "expect": ["→ A2", "0 omatchade"], }, { "keys": ["ctrln"], @@ -1062,6 +1099,10 @@ def main(argv): file=sys.stderr) failures = max(failures, 1) finally: + try: + os.unlink(DOWNLOAD_PATH) + except OSError: + pass if daemon and daemon.poll() is None: try: os.killpg(daemon.pid, signal.SIGTERM) |
