#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "tui.h" #include "formula.h" #include "util.h" #include "version.h" #include "yyjson.h" #include "ui.h" /* ------------------------------------------------------------------ */ /* bankavstämning */ /* ------------------------------------------------------------------ */ struct bank_match { int64_t voucher_id; char series[16]; int64_t number; }; struct bank_sugg { int64_t voucher_id; char series[16]; int64_t number; char date[16]; int64_t amount_ore; }; struct bank_tx { int64_t id; char account[16]; char booked_at[16]; char text[256]; int64_t amount_ore; struct bank_match *matches; size_t nmatches; struct bank_sugg *suggestions; size_t nsuggestions; }; static void bank_ver(char *buf, size_t n, const char *series, int64_t number) { snprintf(buf, n, "%s%lld", series ? series : "", (long long)number); } /* Unmatched first, then newest booked_at first. */ static int bank_tx_cmp(const void *pa, const void *pb) { const struct bank_tx *x = pa, *y = pb; int xm = x->nmatches > 0, ym = y->nmatches > 0; if (xm != ym) return xm - ym; int c = strcmp(y->booked_at, x->booked_at); if (c) return c; return x->id < y->id ? -1 : x->id > y->id; } static void bank_txs_free(struct bank_tx *txs, size_t n) { for (size_t i = 0; i < n; i++) { free(txs[i].matches); free(txs[i].suggestions); } free(txs); } static void bank_tx_row(const struct bank_tx *t, char *line, size_t cap) { char text[256], amt[48], tail[64] = ""; snprintf(text, sizeof text, "%s", t->text); tui_pad_field(text, sizeof text, 32); tui_amt_col(amt, sizeof amt, 13, t->amount_ore); if (t->nmatches > 0) { char ver[32]; bank_ver(ver, sizeof ver, t->matches[0].series, t->matches[0].number); if (t->nmatches > 1) snprintf(tail, sizeof tail, "→ %s +%zu", ver, t->nmatches - 1); else snprintf(tail, sizeof tail, "→ %s", ver); } else if (t->nsuggestions > 0) { char ver[32]; bank_ver(ver, sizeof ver, t->suggestions[0].series, t->suggestions[0].number); snprintf(tail, sizeof tail, "(förslag %s)", ver); } snprintf(line, cap, "%s %s %s %s %s", t->nmatches > 0 ? "✓" : "·", t->booked_at, text, amt, tail); } /* Serie+nummer for a posted voucher, via voucher.get. */ static void bank_voucher_ver(struct app *a, int64_t vid, char *buf, size_t n) { buf[0] = '\0'; char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)vid); char *resp = client_rpc(&a->conn, "voucher.get", a->session, a->org, args); if (resp && client_ok(resp)) { char *ser = jstr_dup(resp, "result.series"); bank_ver(buf, n, ser ? ser : "", jint_val(resp, "result.number", 0)); free(ser); } free(resp); } /* Prefill Nytt verifikat from the transaction, then match the posted voucher. Returns 1 when a voucher was posted (matched or not), 0 on cancel. */ static int bank_new_voucher(struct app *a, const struct bank_tx *t) { struct voucher_prefill p; memset(&p, 0, sizeof p); p.date = t->booked_at; p.description = t->text; p.bank_account = t->account; p.amount_ore = t->amount_ore; int64_t vid = vouchers_new_prefill(a, &p); if (vid <= 0) return 0; char args[64]; snprintf(args, sizeof args, "{\"transaction_id\":%lld,\"voucher_id\":%lld}", (long long)t->id, (long long)vid); char *resp = client_rpc(&a->conn, "bank.match", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Bankavstämning", resp); free(resp); return 1; } free(resp); char ver[32]; bank_voucher_ver(a, vid, ver, sizeof ver); tui_message("Bankavstämning", "Matchat med %s.", ver); return 1; } /* Enter on an unmatched row: suggest vouchers, or pick one freely. */ static void bank_match_tx(struct app *a, const struct bank_tx *t) { size_t nact = t->nsuggestions + 2; char **items = xcalloc(nact, sizeof(char *)); for (size_t i = 0; i < t->nsuggestions; i++) { const struct bank_sugg *s = &t->suggestions[i]; char ver[32], amt[48], diff[48], line[256]; bank_ver(ver, sizeof ver, s->series, s->number); tui_amt_col(amt, sizeof amt, 13, s->amount_ore); tui_amt_col(diff, sizeof diff, 13, t->amount_ore - s->amount_ore); snprintf(line, sizeof line, "%s %-8s %s %s", s->date, ver, amt, diff); items[i] = xstrdup(line); } items[t->nsuggestions] = xstrdup("Välj annat verifikat…"); items[t->nsuggestions + 1] = xstrdup("Skapa nytt verifikat… (^N)"); int sel = tui_select_list("Matcha transaktion", items, (int)nact, 0, 0, NULL, 0, NULL, 0); for (size_t i = 0; i < nact; i++) free(items[i]); free(items); if (sel < 0) return; if ((size_t)sel == nact - 1) { bank_new_voucher(a, t); return; } int64_t vid; if ((size_t)sel < t->nsuggestions) { vid = t->suggestions[sel].voucher_id; } else { vid = pick_voucher(a); if (!vid) return; } char args[64]; snprintf(args, sizeof args, "{\"transaction_id\":%lld,\"voucher_id\":%lld}", (long long)t->id, (long long)vid); char *resp = client_rpc(&a->conn, "bank.match", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Bankavstämning", resp); free(resp); return; } int64_t diff = jint_val(resp, "result.difference_ore", 0); if (diff) { char dbuf[48]; tui_kr_format(diff, dbuf, sizeof dbuf); tui_message("Bankavstämning", "Matchat. Differens: %s", dbuf); } else { tui_message("Bankavstämning", "Matchat."); } free(resp); } static void bank_unmatch_tx(struct app *a, const struct bank_tx *t) { if (t->nmatches == 0) return; if (t->nmatches == 1) { if (!tui_confirm("Bankavstämning", "Koppla bort transaktionen %s från verifikatet?", t->text)) return; } else if (!tui_confirm("Bankavstämning", "Koppla bort transaktionen %s från %zu verifikat?", t->text, t->nmatches)) { return; } int ok = 1; for (size_t i = 0; i < t->nmatches; i++) { char args[64]; snprintf(args, sizeof args, "{\"transaction_id\":%lld,\"voucher_id\":%lld}", (long long)t->id, (long long)t->matches[i].voucher_id); char *resp = client_rpc(&a->conn, "bank.unmatch", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Bankavstämning", resp); ok = 0; } free(resp); } if (ok) tui_message("Bankavstämning", "Matchningen borttagen."); } static void bank_import_file(struct app *a) { char *path = file_browser(a, a->attachment_dir); if (!path) return; char *b64 = read_file_b64(path); if (!b64) { tui_message("Fel", "Kunde inte läsa %s", path); free(path); return; } free(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, "format", "seb"); yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); yyjson_mut_obj_add_strcpy(d, o, "account", "1930"); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); free(b64); if (!args) return; char *resp = rpc_dry(a, "bank.import", args); if (!resp || !client_ok(resp)) { show_error("Bankavstämning", resp); free(resp); free(args); return; } int64_t imported = jint_val(resp, "result.imported", 0); int64_t duplicates = jint_val(resp, "result.duplicates", 0); free(resp); if (!tui_confirm("Bankavstämning", "%lld transaktioner (%lld dubbletter). Importera?", (long long)imported, (long long)duplicates)) { free(args); return; } resp = client_rpc(&a->conn, "bank.import", a->session, a->org, args); free(args); if (resp && client_ok(resp)) tui_message("Bankavstämning", "%lld transaktioner (%lld dubbletter).", (long long)jint_val(resp, "result.imported", 0), (long long)jint_val(resp, "result.duplicates", 0)); else show_error("Bankavstämning", resp); free(resp); } struct bankctx { struct app *a; struct bank_tx *txs; size_t n; int cursor; }; static int bank_key(void *ud, int ch) { struct bankctx *ctx = ud; if (ch == 'a' || ch == 'A') { bank_import_file(ctx->a); return TUI_HOOK_REFRESH; } if ((ch == 'u' || ch == 'U') && ctx->cursor >= 0 && (size_t)ctx->cursor < ctx->n) { bank_unmatch_tx(ctx->a, &ctx->txs[ctx->cursor]); return TUI_HOOK_REFRESH; } return TUI_HOOK_NONE; } void bank_screen(struct app *a) { int64_t keep_id = 0; for (;;) { if (g_quit) return; char *resp = client_rpc(&a->conn, "bank.list", a->session, a->org, "{\"status\":\"all\",\"limit\":500}"); if (!resp || !client_ok(resp)) { show_error("Bankavstämning", resp); free(resp); return; } yyjson_doc *doc = parse(resp); yyjson_val *items = doc ? jget(yyjson_doc_get_root(doc), "result.items") : NULL; size_t n = items && yyjson_is_arr(items) ? yyjson_arr_size(items) : 0; struct bank_tx *txs = xcalloc(n ? n : 1, sizeof *txs); for (size_t i = 0; i < n; i++) { yyjson_val *it = yyjson_arr_get(items, i); txs[i].id = vint(it, "id"); snprintf(txs[i].account, sizeof txs[i].account, "%s", vstr(it, "account")); snprintf(txs[i].booked_at, sizeof txs[i].booked_at, "%s", vstr(it, "booked_at")); snprintf(txs[i].text, sizeof txs[i].text, "%s", vstr(it, "text")); txs[i].amount_ore = vint(it, "amount_ore"); yyjson_val *ms = yyjson_obj_get(it, "matches"); size_t nm = ms && yyjson_is_arr(ms) ? yyjson_arr_size(ms) : 0; if (nm > 0) { txs[i].matches = xcalloc(nm, sizeof *txs[i].matches); txs[i].nmatches = nm; for (size_t k = 0; k < nm; k++) { yyjson_val *m = yyjson_arr_get(ms, k); txs[i].matches[k].voucher_id = vint(m, "voucher_id"); snprintf(txs[i].matches[k].series, sizeof txs[i].matches[k].series, "%s", vstr(m, "series")); txs[i].matches[k].number = vint(m, "number"); } } yyjson_val *ss = yyjson_obj_get(it, "suggestions"); size_t ns = ss && yyjson_is_arr(ss) ? yyjson_arr_size(ss) : 0; if (ns > 0) { txs[i].suggestions = xcalloc(ns, sizeof *txs[i].suggestions); txs[i].nsuggestions = ns; for (size_t k = 0; k < ns; k++) { yyjson_val *s = yyjson_arr_get(ss, k); txs[i].suggestions[k].voucher_id = vint(s, "voucher_id"); snprintf(txs[i].suggestions[k].series, sizeof txs[i].suggestions[k].series, "%s", vstr(s, "series")); txs[i].suggestions[k].number = vint(s, "number"); snprintf(txs[i].suggestions[k].date, sizeof txs[i].suggestions[k].date, "%s", vstr(s, "date")); txs[i].suggestions[k].amount_ore = vint(s, "amount_ore"); } } } qsort(txs, n, sizeof *txs, bank_tx_cmp); int start = 0; for (size_t i = 0; i < n; i++) if (txs[i].id == keep_id) start = (int)i; int64_t unmatched = jint_val(resp, "result.summary.unmatched", -1); if (unmatched < 0) { unmatched = 0; for (size_t i = 0; i < n; i++) if (txs[i].nmatches == 0) unmatched++; } char title[64]; snprintf(title, sizeof title, "Bankavstämning (%lld omatchade)", (long long)unmatched); size_t rows = n > 0 ? n : 1; char **lines = xcalloc(rows, sizeof(char *)); if (n == 0) { lines[0] = xstrdup("+ Importera bankfil (a)"); } else { for (size_t i = 0; i < n; i++) { char line[512]; bank_tx_row(&txs[i], line, sizeof line); lines[i] = xstrdup(line); } } struct bankctx ctx = { a, txs, n, start }; int sel = tui_select_list_hook(title, lines, (int)rows, start, 1, &ctx.cursor, 1, NULL, 0, bank_key, &ctx); for (size_t i = 0; i < rows; i++) free(lines[i]); free(lines); if (sel == -1) { bank_txs_free(txs, n); free(resp); yyjson_doc_free(doc); return; } if (sel == -4) { if (n > 0 && ctx.cursor >= 0 && (size_t)ctx.cursor < n) { keep_id = txs[ctx.cursor].id; if (txs[ctx.cursor].nmatches == 0) bank_new_voucher(a, &txs[ctx.cursor]); else tui_message("Bankavstämning", "Transaktionen är redan matchad."); } } else if (sel >= 0 && (size_t)sel < n) { keep_id = txs[sel].id; if (txs[sel].nmatches == 0) bank_match_tx(a, &txs[sel]); } else if (sel >= 0) { bank_import_file(a); } bank_txs_free(txs, n); free(resp); yyjson_doc_free(doc); } }