#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" #include "vlist.h" struct vdctx { struct app *a; int64_t id; size_t natts; const char *resp; int corrected; 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) { ctx->posted_id = nid; ctx->posted_new = 1; return TUI_HOOK_BACK; } return TUI_HOOK_STAY; } if (ch == 'c') { char reasonbuf[512]; char *reason = tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ", "", 0) ? reasonbuf : NULL; if (reason && *reason) { 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_int(d, o, "voucher", ctx->id); yyjson_mut_obj_add_strcpy(d, o, "description", reason); char *ref = util_random_id("tui-", 8); yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); char *cargs = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *r = client_rpc(&a->conn, "voucher.correct", a->session, a->org, cargs); if (r && client_ok(r)) { int64_t new_id = jint_val(r, "result.id", 0); tui_message("Rättat", "Ändringsverifikat %lld skapat", (long long)new_id); ctx->corrected = 1; } else { show_error("Kunde inte rätta", r); } free(r); free(cargs); free(ref); } return TUI_HOOK_BACK; } 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) { tui_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) { tui_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", ctx->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)) tui_message("Underlag", "Bifogat."); else show_error("Kunde inte bifoga", r); free(r); free(fargs); ctx->want_refresh = 1; return TUI_HOOK_BACK; } } } return TUI_HOOK_STAY; } if (ch == 'f' && ctx->natts > 0) { char **alines = xcalloc(ctx->natts, sizeof(char *)); int64_t *aids = xcalloc(ctx->natts, sizeof(int64_t)); for (size_t i = 0; i < ctx->natts; i++) { char path[64]; snprintf(path, sizeof path, "result.attachments.%zu.id", i); aids[i] = jint_val(ctx->resp, path, 0); snprintf(path, sizeof path, "result.attachments.%zu.filename", i); char *fn = jstr_dup(ctx->resp, path); alines[i] = xstrdup(fn ? fn : "(namnlös)"); free(fn); } int acursor = 0; int asel = tui_select_list("Underlag", alines, (int)ctx->natts, 0, 0, &acursor, 0, "ta bort", 0); if (asel >= 0) { attachment_view_or_download(a, aids[asel]); } else if (asel == -6 && acursor >= 0 && (size_t)acursor < ctx->natts) { char q[320]; snprintf(q, sizeof q, "Ta bort '%s' från verifikatet? (j/n): ", alines[acursor]); char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) ? ansbuf : NULL; 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)ctx->id); char *r = client_rpc(&a->conn, "attachment.unlink", a->session, a->org, uargs); if (r && client_ok(r)) { tui_message("Underlag", "Länken borttagen."); ctx->want_refresh = 1; for (size_t i = 0; i < ctx->natts; i++) free(alines[i]); free(alines); free(aids); return TUI_HOOK_REFRESH; } show_error("Kunde inte ta bort", r); free(r); } } for (size_t i = 0; i < ctx->natts; i++) free(alines[i]); free(alines); free(aids); return TUI_HOOK_STAY; } return TUI_HOOK_NONE; } /* 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 (;;) { char *resp = client_rpc(&a->conn, "voucher.get", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Verifikat", resp); free(resp); return 0; } struct buf text; buf_init(&text); char *series = jstr_dup(resp, "result.series"); char *date = jstr_dup(resp, "result.date"); char *desc = jstr_dup(resp, "result.description"); char *hash = jstr_dup(resp, "result.hash"); int64_t number = jint_val(resp, "result.number", 0); char line[1024]; snprintf(line, sizeof line, "%s%lld %s %s\n\n", series ? series : "", (long long)number, date ? date : "", desc ? desc : ""); buf_append(&text, line, strlen(line)); 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]; 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) { buf_append(&text, "\n", 1); line[0] = MK_RULE[0]; for (int i = 1; i <= 98; i++) line[i] = '-'; line[99] = '\n'; buf_append(&text, line, 100); buf_append(&text, MK_HEAD "Underlag:\n", 11); for (size_t i = 0; i < natts; i++) { char path[64]; snprintf(path, sizeof path, "result.attachments.%zu.filename", i); char *fn = jstr_dup(resp, path); snprintf(line, sizeof line, " %s\n", fn ? fn : ""); buf_append(&text, line, strlen(line)); free(fn); } } if (hash) { snprintf(line, sizeof line, "\nHash: %s\n", hash); buf_append(&text, line, strlen(line)); } int64_t corrects = jint_val(resp, "result.corrects_voucher_id", 0); if (corrects) { snprintf(line, sizeof line, "Rättar verifikat: %lld\n", (long long)corrects); buf_append(&text, line, strlen(line)); } buf_append(&text, "\0", 1); struct vdctx ctx; memset(&ctx, 0, sizeof ctx); ctx.a = a; ctx.id = id; ctx.natts = natts; ctx.resp = resp; ctx.can_prev = pos > 0; ctx.can_next = pos >= 0 && pos + 1 < total; char hint[256], title[96]; snprintf(hint, sizeof hint, "%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(title, (const char *)text.p, hint, TUI_PAGER_NO_ARROWS, vd_key, &ctx); if (ret == 1) want_refresh = 1; if (ctx.want_refresh) want_refresh = 1; corrected = ctx.corrected; int posted_new = ctx.posted_new; int64_t posted_id = ctx.posted_id; free(series); free(date); free(desc); free(hash); buf_free(&text); free(resp); if (posted_new) { if (out_new) *out_new = posted_id; return 2; } if (want_refresh) continue; if (ctx.nav && !corrected) { if (nav) *nav = ctx.nav; return 3; } return corrected; } } struct tui_vrow { char account[16]; char debit[32]; char credit[32]; char text[64]; }; struct acct_cache { int loaded; int64_t org; char **numbers; char **names; size_t n; }; static struct acct_cache g_accts; void acct_cache_free(void) { for (size_t i = 0; i < g_accts.n; i++) { free(g_accts.numbers[i]); free(g_accts.names[i]); } free(g_accts.numbers); free(g_accts.names); memset(&g_accts, 0, sizeof g_accts); } static void acct_cache_load(struct app *a) { if (g_accts.loaded && g_accts.org == a->org) return; acct_cache_free(); char *resp = client_rpc(&a->conn, "account.list", a->session, a->org, "{\"active_only\":true}"); if (!resp || !client_ok(resp)) { free(resp); return; } size_t n = jarr_size(resp, "result.items"); g_accts.numbers = xcalloc(n ? n : 1, sizeof(char *)); g_accts.names = xcalloc(n ? n : 1, sizeof(char *)); for (size_t i = 0; i < n; i++) { char path[64]; snprintf(path, sizeof path, "result.items.%zu.number", i); char *number = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.name", i); char *name = jstr_dup(resp, path); g_accts.numbers[i] = number ? number : xstrdup(""); g_accts.names[i] = name ? name : xstrdup(""); } g_accts.n = n; g_accts.org = a->org; g_accts.loaded = 1; free(resp); } const char *acct_name(struct app *a, const char *number) { if (!number || !*number) return NULL; acct_cache_load(a); for (size_t i = 0; i < g_accts.n; i++) if (strcmp(g_accts.numbers[i], number) == 0) return g_accts.names[i]; return NULL; } int acct_exists(struct app *a, const char *number) { if (!number || !*number) return 0; acct_cache_load(a); for (size_t i = 0; i < g_accts.n; i++) if (strcmp(g_accts.numbers[i], number) == 0) return 1; return 0; } struct vform { struct app *a; struct tui_rt rt; struct tui_vrow rows[64]; int64_t att_ids[32]; char att_names[32][80]; int natt; char date[16]; char series[16]; char desc[256]; char client_ref[64]; }; static void vform_cell(void *ud, int row, int col, char **buf, size_t *cap) { struct vform *vf = ud; struct tui_vrow *r = &vf->rows[row]; if (col == 0) { *buf = r->account; *cap = sizeof r->account; } else if (col == 2) { *buf = r->debit; *cap = sizeof r->debit; } else if (col == 3) { *buf = r->credit; *cap = sizeof r->credit; } else { *buf = r->text; *cap = sizeof r->text; } } static void vform_info(void *ud, int row, char *buf, size_t n) { struct vform *vf = ud; const char *nm = acct_name(vf->a, vf->rows[row].account); snprintf(buf, n, "%s", nm ? nm : ""); } static void vform_footer(void *ud, char *buf, size_t n) { struct vform *vf = ud; int64_t sum_d = 0, sum_c = 0; for (int i = 0; i < vf->rt.nrows; i++) { int64_t v; if (tui_parse_kr(vf->rows[i].debit, &v) == 0) sum_d += v; if (tui_parse_kr(vf->rows[i].credit, &v) == 0) sum_c += v; } char d1[32], c1[32], diff[32], line[1024]; tui_kr_format(sum_d, d1, sizeof d1); tui_kr_format(sum_c, c1, sizeof c1); tui_kr_format(sum_d - sum_c, diff, sizeof diff); int balanced = sum_d == sum_c; struct buf t; buf_init(&t); if (vf->natt > 0) { snprintf(line, sizeof line, "Bilagor: "); for (int i = 0; i < vf->natt; i++) { size_t used = strlen(line); if (used + 1 >= sizeof line) break; snprintf(line + used, sizeof line - used, "%.79s%s", vf->att_names[i], i + 1 < vf->natt ? ", " : ""); } buf_append(&t, line, strlen(line)); buf_append(&t, "\n", 1); } snprintf(line, sizeof line, "Summa debet %12s kredit %12s %s", d1, c1, balanced ? "I BALANS" : "DIFF"); buf_append(&t, line, strlen(line)); if (!balanced) { snprintf(line, sizeof line, "\nDifferens: %s", diff); buf_append(&t, line, strlen(line)); } buf_append(&t, "\0", 1); snprintf(buf, n, "%s", (char *)t.p); buf_free(&t); } static void vform_attach(struct vform *vf) { struct app *a = vf->a; char *path = file_browser(a, a->attachment_dir); if (!path) return; struct stat sb; if (stat(path, &sb) == 0 && (long)sb.st_size > a->max_attachment_bytes) { tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", (double)sb.st_size / (1024 * 1024), a->max_attachment_bytes / (1024 * 1024)); free(path); return; } char *b64 = read_file_b64(path); if (!b64) { tui_message("Bilaga", "Kunde inte läsa %s", path); free(path); return; } if (vf->natt >= 32) { tui_message("Bilaga", "Max 32 bilagor per verifikat."); free(b64); free(path); return; } 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); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); size_t need = strlen(args) + 256; char *raw = xmalloc(need); snprintf(raw, need, "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\"," "\"session\":\"%s\",\"org\":%lld,\"args\":%s}", a->session, (long long)a->org, args); free(args); char *r = NULL; if (client_send_line(&a->conn, raw) == 0) r = client_read_line(&a->conn); free(raw); if (r && client_ok(r)) { vf->att_ids[vf->natt] = jint_val(r, "result.id", 0); snprintf(vf->att_names[vf->natt], sizeof vf->att_names[vf->natt], "%s", base); vf->natt++; } else { show_error("Kunde inte bifoga filen", r); } free(r); free(b64); free(path); } static void vform_template(struct vform *vf) { struct app *a = vf->a; char *lresp = client_rpc(&a->conn, "template.list", a->session, a->org, "{\"active_only\":true}"); if (!lresp || !client_ok(lresp)) { show_error("Mallar", lresp); free(lresp); return; } size_t ln = jarr_size(lresp, "result.items"); if (ln == 0) { tui_message("Mallar", "Inga mallar ännu. Skapa en under Mallar först."); free(lresp); return; } char **names = xcalloc(ln, sizeof(char *)); char **lines = xcalloc(ln, sizeof(char *)); for (size_t i = 0; i < ln; i++) { char path[64], line[512]; snprintf(path, sizeof path, "result.items.%zu.name", i); char *nm = jstr_dup(lresp, path); snprintf(path, sizeof path, "result.items.%zu.series", i); char *ser = jstr_dup(lresp, path); snprintf(path, sizeof path, "result.items.%zu.row_count", i); int64_t rc = jint_val(lresp, path, 0); snprintf(path, sizeof path, "result.items.%zu.description", i); char *ds = jstr_dup(lresp, path); char nmbuf[128]; snprintf(nmbuf, sizeof nmbuf, "%s", nm ? nm : ""); tui_pad_field(nmbuf, sizeof nmbuf, 24); snprintf(line, sizeof line, "%s %-3s %2lld rader %s", nmbuf, ser ? ser : "", (long long)rc, ds ? ds : ""); names[i] = xstrdup(nm ? nm : ""); lines[i] = xstrdup(line); free(nm); free(ser); free(ds); } int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, NULL, 0); char tname[128] = ""; if (tsel >= 0) snprintf(tname, sizeof tname, "%s", names[tsel]); for (size_t i = 0; i < ln; i++) { free(names[i]); free(lines[i]); } free(names); free(lines); free(lresp); if (tsel < 0) return; yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *to = yyjson_mut_obj(d); yyjson_mut_doc_set_root(d, to); yyjson_mut_obj_add_strcpy(d, to, "name", tname); char *targs = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = client_rpc(&a->conn, "template.get", a->session, a->org, targs); free(targs); if (!resp || !client_ok(resp)) { show_error("Mall", resp); free(resp); return; } char xsbuf[512]; char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "", 0) ? xsbuf : NULL; double xv = 0; if (xs && *xs && !parse_x_double(xs, &xv)) { tui_message("Fel", "Ogiltigt belopp."); free(resp); return; } size_t n = jarr_size(resp, "result.rows"); char **accts = xcalloc(n ? n : 1, sizeof(char *)); char **forms = xcalloc(n ? n : 1, sizeof(char *)); char **descs = xcalloc(n ? n : 1, sizeof(char *)); struct template_row *in = xcalloc(n ? n : 1, sizeof *in); for (size_t i = 0; i < n; i++) { char path[64]; snprintf(path, sizeof path, "result.rows.%zu.account", i); accts[i] = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.formula", i); forms[i] = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.description", i); descs[i] = jstr_dup(resp, path); in[i].account = accts[i] ? accts[i] : ""; in[i].formula = forms[i] ? forms[i] : ""; in[i].description = descs[i] && *descs[i] ? descs[i] : NULL; } struct resolved_row *out = xcalloc(n ? n : 1, sizeof *out); size_t on = 0; char ferr[256] = ""; if (formula_resolve_rows(in, n, xv, out, &on, ferr, sizeof ferr) != 0) { tui_message("Mall", "%s", ferr[0] ? ferr : "kunde inte lösa mallen"); } else { memset(vf->rows, 0, sizeof vf->rows); int use = (int)(on > 64 ? 64 : on); for (int i = 0; i < use; i++) { snprintf(vf->rows[i].account, sizeof vf->rows[i].account, "%s", out[i].account); char tmp[32]; if (out[i].debit_ore) { tui_kr_format(out[i].debit_ore, tmp, sizeof tmp); snprintf(vf->rows[i].debit, sizeof vf->rows[i].debit, "%s", tmp); } else { tui_kr_format(out[i].credit_ore, tmp, sizeof tmp); snprintf(vf->rows[i].credit, sizeof vf->rows[i].credit, "%s", tmp); } if (out[i].description[0]) snprintf(vf->rows[i].text, sizeof vf->rows[i].text, "%s", out[i].description); } vf->rt.nrows = use > 0 ? use : 1; char *tds = jstr_dup(resp, "result.description"); char *tser = jstr_dup(resp, "result.series"); if (!vf->desc[0] && tds && *tds) replace_x_into(tds, xv, vf->desc, sizeof vf->desc); if (tser && *tser) snprintf(vf->series, sizeof vf->series, "%s", tser); free(tds); free(tser); vf->rt.row = 0; vf->rt.col = 0; vf->rt.focus = -1; vf->rt.fresh = 1; vf->rt.pos = (size_t)-1; tui_rt_normalize(&vf->rt); } free(out); for (size_t i = 0; i < n; i++) { free(accts[i]); free(forms[i]); free(descs[i]); } free(accts); free(forms); free(descs); free(in); free(resp); } /* Returns the posted voucher id, or 0 to stay in the form. */ static int64_t vform_post(struct vform *vf, int dry) { struct app *a = vf->a; 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, "date", vf->date); yyjson_mut_obj_add_strcpy(d, o, "description", vf->desc); yyjson_mut_obj_add_strcpy(d, o, "series", vf->series); yyjson_mut_obj_add_strcpy(d, o, "client_ref", vf->client_ref); yyjson_mut_val *arr = yyjson_mut_arr(d); for (int i = 0; i < vf->rt.nrows; i++) { if (!vf->rows[i].account[0]) continue; int64_t dv = 0, cv = 0; if (tui_parse_kr(vf->rows[i].debit, &dv) != 0 || tui_parse_kr(vf->rows[i].credit, &cv) != 0) { tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1); yyjson_mut_doc_free(d); return 0; } yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); yyjson_mut_obj_add_strcpy(d, ro, "account", vf->rows[i].account); yyjson_mut_obj_add_int(d, ro, "debit_ore", dv); yyjson_mut_obj_add_int(d, ro, "credit_ore", cv); if (vf->rows[i].text[0]) yyjson_mut_obj_add_strcpy(d, ro, "description", vf->rows[i].text); } yyjson_mut_obj_add_val(d, o, "rows", arr); if (vf->natt > 0) { yyjson_mut_val *aa = yyjson_mut_arr(d); for (int i = 0; i < vf->natt; i++) yyjson_mut_arr_add_int(d, aa, vf->att_ids[i]); yyjson_mut_obj_add_val(d, o, "attachment_ids", aa); } char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); if (dry) { char *raw = xmalloc(strlen(args) + 256); sprintf(raw, "{\"v\":1,\"id\":\"tui\",\"cmd\":\"voucher.post\"," "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true," "\"args\":%s}", a->session, (long long)a->org, args); if (client_send_line(&a->conn, raw) == 0) { char *r = client_read_line(&a->conn); if (r && client_ok(r)) { int64_t num = jint_val(r, "result.number", 0); tui_message("Validering OK", "Nummer %lld skulle tilldelas.", (long long)num); } else { show_error("Validering misslyckades", r); } free(r); } else { tui_message("Fel", "Kunde inte nå servern"); } free(raw); } else { char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org, args); if (r && client_ok(r)) { int64_t id = jint_val(r, "result.id", 0); int64_t num = jint_val(r, "result.number", 0); char *ser = jstr_dup(r, "result.series"); int replayed = jbool_val(r, "result.replayed", 0); tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "", (long long)num, (long long)id, replayed ? " — redan bokfört (idempotent)" : ""); free(ser); free(r); if (strcmp(vf->series, a->default_series) != 0) { yyjson_mut_doc *sd = yyjson_mut_doc_new(NULL); yyjson_mut_val *so = yyjson_mut_obj(sd); yyjson_mut_doc_set_root(sd, so); yyjson_mut_obj_add_strcpy(sd, so, "key", "series_voucher"); yyjson_mut_obj_add_strcpy(sd, so, "value", vf->series); char *sargs = yyjson_mut_write(sd, 0, NULL); yyjson_mut_doc_free(sd); if (sargs) { char *sr = client_rpc(&a->conn, "settings.set", a->session, a->org, sargs); free(sr); free(sargs); } snprintf(a->default_series, sizeof a->default_series, "%s", vf->series); } free(args); return id; } show_error("Kunde inte bokföra", r); free(r); } free(args); return 0; } static int vform_key(void *ud, int ch) { struct vform *vf = ud; if (ch == KEY_F(4)) { vform_template(vf); return TUI_HOOK_STAY; } if (ch == 6) { vform_attach(vf); return TUI_HOOK_STAY; } return TUI_HOOK_NONE; } int64_t vouchers_new(struct app *a) { return vouchers_new_prefill(a, NULL); } int64_t vouchers_new_prefill(struct app *a, const struct voucher_prefill *p) { struct vform vf; memset(&vf, 0, sizeof vf); vf.a = a; snprintf(vf.date, sizeof vf.date, "%s", a->fy_start[0] ? a->fy_start : "2026-01-01"); snprintf(vf.series, sizeof vf.series, "%s", a->default_series); char *ref = util_random_id("tui-", 8); snprintf(vf.client_ref, sizeof vf.client_ref, "%s", ref); free(ref); if (p) { if (p->date && *p->date && util_parse_iso_date(p->date)) snprintf(vf.date, sizeof vf.date, "%s", p->date); if (p->description && *p->description) snprintf(vf.desc, sizeof vf.desc, "%s", p->description); if (p->bank_account && *p->bank_account) { int64_t amt = p->amount_ore < 0 ? -p->amount_ore : p->amount_ore; char tmp[32]; tui_kr_format(amt, tmp, sizeof tmp); snprintf(vf.rows[0].account, sizeof vf.rows[0].account, "%s", p->bank_account); if (p->counter_account && *p->counter_account && p->amount_ore > 0) { snprintf(vf.rows[0].debit, sizeof vf.rows[0].debit, "%s", tmp); snprintf(vf.rows[1].account, sizeof vf.rows[1].account, "%s", p->counter_account); snprintf(vf.rows[1].credit, sizeof vf.rows[1].credit, "%s", tmp); } else if (p->amount_ore > 0) { snprintf(vf.rows[0].debit, sizeof vf.rows[0].debit, "%s", tmp); } else if (p->amount_ore < 0) { snprintf(vf.rows[0].credit, sizeof vf.rows[0].credit, "%s", tmp); } } if (p->description && *p->description) { snprintf(vf.rows[0].text, sizeof vf.rows[0].text, "%s", p->description); snprintf(vf.rows[1].text, sizeof vf.rows[1].text, "%s", p->description); } } int text_w = COLS - 60; if (text_w < 8) text_w = 8; struct tui_rt_col cols[5] = { { "Konto", 2, 6, TUI_RT_EDIT }, { "Namn", 9, 24, TUI_RT_INFO }, { "Debet", 34, 11, TUI_RT_EDIT }, { "Kredit", 46, 11, TUI_RT_EDIT }, { "Text", 58, text_w, TUI_RT_EDIT }, }; tui_rt_init(&vf.rt, 1, 64, 5, cols, 7, vform_cell, vform_info, &vf); tui_rt_set_footer(&vf.rt, vform_footer); tui_rt_set_key(&vf.rt, vform_key); struct tui_form_field ff[3]; memset(ff, 0, sizeof ff); ff[0].label = "Datum"; ff[0].value = vf.date; ff[0].cap = sizeof vf.date; ff[0].kind = TUI_F_DATE; ff[1].label = "Serie"; ff[1].value = vf.series; ff[1].cap = sizeof vf.series; ff[1].kind = TUI_F_TEXT; ff[2].label = "Text"; ff[2].value = vf.desc; ff[2].cap = sizeof vf.desc; ff[2].kind = TUI_F_TEXT; tui_rt_set_fields(&vf.rt, ff, 3); tui_rt_normalize(&vf.rt); const char *hint = "Enter = ändra fält Tab = byta fält F4 = mall" " ^F = bifoga fil F5 = validera ^X = rensa rad" " F9 = bokför Esc = avbryt"; for (;;) { int rr = tui_rt_run("Nytt verifikat", &vf.rt, hint); if (rr == -1) return 0; if (rr == -2) { vform_post(&vf, 1); continue; } if (rr == -3) { int64_t id = vform_post(&vf, 0); if (id > 0) return id; } } } 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; size_t n = 0; int fetch = 1, resort = 0; for (;;) { if (g_quit) break; if (fetch) { vlist_lines_free(items, n); vlist_free(v, n); items = NULL; n = 0; v = vlist_fetch(a, &n); if (!v) return; 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; } int start = 0; if (a->voucher_sel) { 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; 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 = v[cur].id; if (sel == -2) { if (lctx.resort) resort = 1; else fetch = 1; continue; } int64_t open_id = 0; int want_new = (sel == -4); if (sel >= 0) { if ((size_t)sel == n) want_new = 1; else open_id = v[sel].id; } else if (sel != -4) { break; } if (want_new) { open_id = vouchers_new(a); if (open_id <= 0) continue; a->voucher_sel = open_id; fetch = 1; } for (;;) { int64_t nv = 0; 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; fetch = 1; continue; } if (act == 1) fetch = 1; break; } } vlist_lines_free(items, n); vlist_free(v, n); }