aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_templates.c
diff options
context:
space:
mode:
Diffstat (limited to 'clients/screens_templates.c')
-rw-r--r--clients/screens_templates.c343
1 files changed, 343 insertions, 0 deletions
diff --git a/clients/screens_templates.c b/clients/screens_templates.c
new file mode 100644
index 0000000..b2cd7ab
--- /dev/null
+++ b/clients/screens_templates.c
@@ -0,0 +1,343 @@
+#include <dirent.h>
+#include <errno.h>
+#include <locale.h>
+#include <math.h>
+#include <ncursesw/ncurses.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#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;
+ }
+}