summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-21 22:07:58 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-21 22:07:58 +0200
commitad8e1707374ca56bed3dbf472dc8b6bb6cb1f38b (patch)
tree16aa7a868b4a95a8ee12c8e371f6f4e57c37b6a6
parentf2c8f68763494f64a0a2d2e48d852bd83bd6df65 (diff)
downloadbokf-ad8e1707374ca56bed3dbf472dc8b6bb6cb1f38b.tar.gz
bokf-ad8e1707374ca56bed3dbf472dc8b6bb6cb1f38b.zip
tui: granska/ladda ned underlag via pagern eller skrivbordsvisarenv0.1.61
-rw-r--r--clients/screens_attachments.c2
-rw-r--r--clients/screens_invoices.c4
-rw-r--r--clients/screens_payroll.c2
-rw-r--r--clients/screens_vouchers.c2
-rw-r--r--clients/ui.c98
-rw-r--r--clients/ui.h6
-rw-r--r--docs/PROTOCOL.md5
-rw-r--r--docs/TUI-GUIDELINES.md2
-rwxr-xr-xscripts/tui-golden.py45
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)