#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 tui_trow { char account[16]; char formula[64]; char text[64]; }; struct tplform { struct app *a; struct tui_rt rt; struct tui_trow rows[64]; int nrows; char name[128]; char desc[256]; char series[16]; int64_t id; }; static void tpl_cell(void *ud, int row, int col, char **buf, size_t *cap) { struct tplform *tf = ud; struct tui_trow *r = &tf->rows[row]; if (col == 0) { *buf = r->account; *cap = sizeof r->account; } else if (col == 2) { *buf = r->formula; *cap = sizeof r->formula; } else { *buf = r->text; *cap = sizeof r->text; } } static void tpl_info(void *ud, int row, char *buf, size_t n) { struct tplform *tf = ud; const char *nm = acct_name(tf->a, tf->rows[row].account); snprintf(buf, n, "%s", nm ? nm : ""); } static void tpl_footer(void *ud, char *buf, size_t n) { (void)ud; snprintf(buf, n, "x = belopp i kronor. Positivt blir debet, negativt" " kredit."); } /* F5 dry-runs; Ctrl+Enter creates/updates. Returns 1 when saved. */ static int tpl_save(struct tplform *tf, int dry) { const char *problem = NULL; char msg[256] = ""; if (!tf->name[0]) problem = "Namn saknas."; int used = 0; for (int i = 0; i < tf->rt.nrows && !problem; i++) { if (!tf->rows[i].account[0]) continue; if (!acct_exists(tf->a, tf->rows[i].account)) { snprintf(msg, sizeof msg, "Konto %.15s finns inte (rad %d).", tf->rows[i].account, i + 1); problem = msg; break; } if (!tf->rows[i].formula[0] || !formula_valid(tf->rows[i].formula)) { snprintf(msg, sizeof msg, "Ogiltig formel på rad %d: '%.60s'.", i + 1, tf->rows[i].formula); problem = msg; break; } used++; } if (!problem && used == 0) problem = "Mallen behöver minst en rad."; if (problem) { tui_message("Mall", "%s", problem); return 0; } yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); yyjson_mut_val *o = yyjson_mut_obj(d); yyjson_mut_doc_set_root(d, o); if (tf->id) yyjson_mut_obj_add_int(d, o, "id", tf->id); yyjson_mut_obj_add_strcpy(d, o, "name", tf->name); yyjson_mut_obj_add_strcpy(d, o, "series", tf->series); yyjson_mut_obj_add_strcpy(d, o, "description", tf->desc); yyjson_mut_val *arr = yyjson_mut_arr(d); for (int i = 0; i < tf->rt.nrows; i++) { if (!tf->rows[i].account[0]) continue; yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); yyjson_mut_obj_add_strcpy(d, ro, "account", tf->rows[i].account); yyjson_mut_obj_add_strcpy(d, ro, "formula", tf->rows[i].formula); if (tf->rows[i].text[0]) yyjson_mut_obj_add_strcpy(d, ro, "description", tf->rows[i].text); } yyjson_mut_obj_add_val(d, o, "rows", arr); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); const char *cmd = tf->id ? "template.update" : "template.create"; char *resp = dry ? rpc_dry(tf->a, cmd, args) : client_rpc(&tf->a->conn, cmd, tf->a->session, tf->a->org, args); free(args); if (resp && client_ok(resp)) { if (dry) { tui_message("Validering OK", "Mallen är korrekt (%d rader).", used); } else { tui_message("Mall", "Mallen '%s' sparad.", tf->name); free(resp); return 1; } } else { show_error("Kunde inte spara mallen", resp); } free(resp); return 0; } /* Template editor: a form, not a wizard. load_name == NULL creates a new template, otherwise the named template is loaded and saved via template.update. */ static int template_form(struct app *a, const char *load_name) { struct tplform tf; memset(&tf, 0, sizeof tf); tf.a = a; snprintf(tf.series, sizeof tf.series, "%s", a->default_series); if (load_name && *load_name) { 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, "name", load_name); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = client_rpc(&a->conn, "template.get", a->session, a->org, args); free(args); if (!resp || !client_ok(resp)) { show_error("Mall", resp); free(resp); return 0; } tf.id = jint_val(resp, "result.id", 0); char *nm = jstr_dup(resp, "result.name"); char *ser = jstr_dup(resp, "result.series"); char *ds = jstr_dup(resp, "result.description"); if (nm) snprintf(tf.name, sizeof tf.name, "%s", nm); if (ser && *ser) snprintf(tf.series, sizeof tf.series, "%s", ser); if (ds) snprintf(tf.desc, sizeof tf.desc, "%s", ds); free(nm); free(ser); free(ds); size_t n = jarr_size(resp, "result.rows"); if (n > 64) n = 64; for (size_t i = 0; i < n; 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.formula", i); char *form = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.description", i); char *rd = jstr_dup(resp, path); snprintf(tf.rows[i].account, sizeof tf.rows[i].account, "%s", acc ? acc : ""); snprintf(tf.rows[i].formula, sizeof tf.rows[i].formula, "%s", form ? form : ""); snprintf(tf.rows[i].text, sizeof tf.rows[i].text, "%s", rd ? rd : ""); free(acc); free(form); free(rd); } tf.nrows = n > 0 ? (int)n : 1; free(resp); } int text_w = COLS - 57; if (text_w < 8) text_w = 8; struct tui_rt_col cols[4] = { { "Konto", 2, 6, TUI_RT_EDIT }, { "Namn", 9, 24, TUI_RT_INFO }, { "Formel", 34, 20, TUI_RT_EDIT }, { "Radtext", 55, text_w, TUI_RT_EDIT }, }; tui_rt_init(&tf.rt, tf.nrows > 0 ? tf.nrows : 1, 64, 4, cols, 7, tpl_cell, tpl_info, &tf); tui_rt_set_footer(&tf.rt, tpl_footer); struct tui_form_field ff[3]; memset(ff, 0, sizeof ff); ff[0].label = "Namn"; ff[0].value = tf.name; ff[0].cap = sizeof tf.name; ff[0].kind = TUI_F_TEXT; ff[1].label = "Serie"; ff[1].value = tf.series; ff[1].cap = sizeof tf.series; ff[1].kind = TUI_F_TEXT; ff[2].label = "Text"; ff[2].value = tf.desc; ff[2].cap = sizeof tf.desc; ff[2].kind = TUI_F_TEXT; tui_rt_set_fields(&tf.rt, ff, 3); const char *title = load_name ? "Redigera mall" : "Ny mall"; const char *hint = "Enter = ändra fält Tab = byta fält F5 = validera" " ^X = rensa rad ^Enter = spara Esc = avbryt"; for (;;) { int rr = tui_rt_run(title, &tf.rt, hint); if (rr == -1) return 0; if (rr == -2) { tpl_save(&tf, 1); continue; } if (rr == -3 && tpl_save(&tf, 0)) return 1; } } static void template_archive(struct app *a, const char *name) { char label[256]; snprintf(label, sizeof label, "Arkivera '%s'? (j/n): ", name); char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, label, "n", 0) ? ansbuf : NULL; if (!ans || (ans[0] != 'j' && ans[0] != 'J')) return; 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, "name", name); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *resp = client_rpc(&a->conn, "template.archive", a->session, a->org, args); free(args); if (resp && client_ok(resp)) tui_message("Mall", "Mallen '%s' arkiverad.", name); else show_error("Kunde inte arkivera", resp); free(resp); } void templates_screen(struct app *a) { int cursor = 0; for (;;) { if (g_quit) return; char *resp = client_rpc(&a->conn, "template.list", a->session, a->org, "{\"active_only\":true}"); if (!resp || !client_ok(resp)) { show_error("Mallar", resp); free(resp); return; } size_t n = jarr_size(resp, "result.items"); char **names = xcalloc(n + 1, sizeof(char *)); char **lines = xcalloc(n + 1, sizeof(char *)); for (size_t i = 0; i < n; i++) { char path[64], line[512]; snprintf(path, sizeof path, "result.items.%zu.name", i); char *nm = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.series", i); char *ser = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.row_count", i); int64_t rc = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.description", i); char *ds = jstr_dup(resp, 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); } names[n] = xstrdup(""); lines[n] = xstrdup("+ Ny mall (^N)"); int s = tui_select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1, n > 0 ? "arkivera" : NULL, 0); if (s == -1) { for (size_t i = 0; i <= n; i++) { free(names[i]); free(lines[i]); } free(names); free(lines); free(resp); return; } if (s == -6) { if (cursor >= 0 && (size_t)cursor < n) template_archive(a, names[cursor]); } else if (s == -4 || (s >= 0 && (size_t)s == n)) { template_form(a, NULL); } else if (s >= 0) { template_form(a, names[s]); } for (size_t i = 0; i <= n; i++) { free(names[i]); free(lines[i]); } free(names); free(lines); free(resp); if (s == -5) return; } }