#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" 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; }; static int vd_key(void *ud, int ch) { struct vdctx *ctx = ud; struct app *a = ctx->a; 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_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; } static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) { if (out_new) *out_new = 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)); int hdr = snprintf( line, sizeof line, MK_HEAD " %-6s %-34s D %12s K %12s\n", "Konto", "Benämning", "Debet", "Kredit"); buf_append(&text, line, (size_t)hdr); size_t nrows = jarr_size(resp, "result.rows"); 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.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); char d[32], c[32]; tui_kr_format(debit, d, sizeof d); tui_kr_format(credit, c, sizeof c); char acol[32], ncol[256]; snprintf(acol, sizeof acol, "%s", acc ? acc : ""); tui_pad_field(acol, sizeof acol, 6); snprintf(ncol, sizeof ncol, "%s", name ? name : ""); tui_pad_field(ncol, sizeof ncol, 34); snprintf(line, sizeof line, " %s %s D %12s K %12s\n", acol, ncol, d, c); buf_append(&text, line, strlen(line)); free(acc); free(name); } 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; char hint[256]; snprintf(hint, sizeof hint, "^N = nytt verifikat c = rätta ^F = bifoga%s", natts ? " f = underlag" : ""); int want_refresh = 0, corrected = 0; int ret = tui_pager_hook("Verifikat", (const char *)text.p, hint, 0, 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; 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); 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) { 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); 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); const char *hint = "Enter = ändra fält Tab = byta fält F4 = mall" " ^F = bifoga fil F5 = validera ^X = rensa rad" " ^Enter = 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; } } } void vouchers_screen(struct app *a) { char **items = NULL; int64_t *ids = NULL; size_t n = 0; int fetch = 1; for (;;) { if (g_quit) { for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); return; } if (fetch) { for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); items = NULL; ids = NULL; n = 0; fetch = 0; char largs[96]; snprintf(largs, sizeof largs, "{\"limit\":200,\"fiscal_year\":%lld}", (long long)a->fy); char *resp = client_rpc(&a->conn, "voucher.list", a->session, a->org, largs); if (!resp || !client_ok(resp)) { show_error("Verifikat", resp); free(resp); return; } n = jarr_size(resp, "result.items"); items = xcalloc(n + 1, sizeof(char *)); ids = xcalloc(n ? n : 1, sizeof(int64_t)); char **idstr = xcalloc(n ? n : 1, sizeof(char *)); int idw = 4; for (size_t i = 0; i < n; i++) { char path[64]; 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 *ser = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.number", i); int64_t number = jint_val(resp, path, 0); char idbuf[32]; snprintf(idbuf, sizeof idbuf, "%s%lld", ser ? ser : "", (long long)number); idstr[i] = xstrdup(idbuf); int l = (int)strlen(idbuf); if (l > idw) idw = l; free(ser); } for (size_t i = 0; i < n; i++) { char path[64], line[512]; 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(path, sizeof path, "result.items.%zu.attachment_count", i); int atts = (int)jint_val(resp, path, 0); snprintf(line, sizeof line, " %c %-*s %s %s", atts ? 'x' : ' ', idw, idstr[i], date ? date : "", desc ? desc : ""); items[i] = xstrdup(line); free(date); free(desc); free(idstr[i]); } free(idstr); free(resp); items[n] = xstrdup("+ Nytt verifikat (^N)"); } size_t total = n + 1; int start = 0; if (a->voucher_sel) { for (size_t i = 0; i < n; i++) if (ids[i] == a->voucher_sel) { start = (int)i; break; } } int cur = start; int sel = tui_select_list("Verifikat", items, (int)total, start, 1, &cur, 1, NULL, 0); if (cur >= 0 && (size_t)cur < n) a->voucher_sel = ids[cur]; if (sel == -2) { 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 if ((size_t)sel < n) open_id = ids[sel]; } else if (sel != -4) { for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); return; } 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 act = voucher_detail(a, open_id, &nv); if (act == 2) { open_id = nv; a->voucher_sel = nv; fetch = 1; continue; } if (act == 1) fetch = 1; break; } } }