aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_dashboard.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 22:00:01 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 22:00:01 +0200
commit10a535ace098144980d0ffd1593f6384c39cc221 (patch)
tree53a7c6c5026d479c4b6d5fd4e5b341474cd190fa /clients/screens_dashboard.c
parent3a5a6d3b04dc4e17a42225afb243eecb3502ead6 (diff)
downloadbokf-10a535ace098144980d0ffd1593f6384c39cc221.tar.gz
bokf-10a535ace098144980d0ffd1593f6384c39cc221.zip
tui: split bokftui into screen files
Diffstat (limited to 'clients/screens_dashboard.c')
-rw-r--r--clients/screens_dashboard.c354
1 files changed, 354 insertions, 0 deletions
diff --git a/clients/screens_dashboard.c b/clients/screens_dashboard.c
new file mode 100644
index 0000000..0c133b2
--- /dev/null
+++ b/clients/screens_dashboard.c
@@ -0,0 +1,354 @@
+#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"
+
+void update_status(struct app *a)
+{
+ char st[512], right[160];
+ snprintf(st, sizeof st, "%s | %s - %s | %s | %s", a->org_name,
+ a->fy_start, a->fy_end, a->role, a->username);
+ if (g_server_version[0])
+ snprintf(right, sizeof right, "tui %s srv %s", BOKF_VERSION,
+ g_server_version);
+ else
+ snprintf(right, sizeof right, "tui %s", BOKF_VERSION);
+ tui_set_status(st, right);
+}
+
+/* Create a new fiscal year. Returns the new id (>0) or 0. */
+static int64_t fy_new_form(struct app *a)
+{
+ static int sel = 0;
+ char label[32] = "", start[16] = "", end[16] = "";
+ char *resp =
+ client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}");
+ if (resp && client_ok(resp)) {
+ size_t n = jarr_size(resp, "result.items");
+ char max_end[16] = "";
+ for (size_t i = 0; i < n; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.items.%zu.end_date", i);
+ char *e = jstr_dup(resp, path);
+ if (e && strcmp(e, max_end) > 0)
+ snprintf(max_end, sizeof max_end, "%s", e);
+ free(e);
+ }
+ if (max_end[0])
+ util_date_add_days(max_end, 1, start, sizeof start);
+ }
+ free(resp);
+ if (!start[0]) {
+ time_t t = time(NULL);
+ struct tm tm;
+ gmtime_r(&t, &tm);
+ int y = tm.tm_year + 1900;
+ if (y < 1900 || y > 2200)
+ y = 2026;
+ snprintf(start, sizeof start, "%04d-01-01", y);
+ }
+ util_date_add_months(start, 12, end, sizeof end);
+ util_date_add_days(end, -1, end, sizeof end);
+ int sy = 0, sm = 0, sd = 0, ey = 0, em = 0, ed = 0;
+ util_date_parse(start, &sy, &sm, &sd);
+ util_date_parse(end, &ey, &em, &ed);
+ if (sm == 1 && sd == 1)
+ snprintf(label, sizeof label, "%d", ey);
+ else
+ snprintf(label, sizeof label, "%d/%d", sy, ey);
+
+ struct tui_form_field ff[3];
+ for (;;) {
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Etikett";
+ ff[0].value = label;
+ ff[0].cap = sizeof label;
+ ff[0].kind = TUI_F_TEXT;
+ ff[1].label = "Startdatum";
+ ff[1].value = start;
+ ff[1].cap = sizeof start;
+ ff[1].kind = TUI_F_DATE;
+ ff[2].label = "Slutdatum";
+ ff[2].value = end;
+ ff[2].cap = sizeof end;
+ ff[2].kind = TUI_F_DATE;
+ int r = tui_form_run("Nytt räkenskapsår", ff, 3, 1, &sel);
+ if (r == TUI_FORM_BACK)
+ return 0;
+ if (r != TUI_FORM_REFRESH && r != TUI_FORM_SUBMIT)
+ continue; /* a field was edited; keep the form up */
+ if (!label[0]) {
+ tui_message("Räkenskapsår", "Etiketten får inte vara tom.");
+ continue;
+ }
+ if (!util_parse_iso_date(start) || !util_parse_iso_date(end)) {
+ tui_message("Räkenskapsår",
+ "Start- och slutdatum måste vara ÅÅÅÅ-MM-DD.");
+ continue;
+ }
+ if (strcmp(start, end) >= 0) {
+ tui_message("Räkenskapsår",
+ "Startdatum måste ligga före slutdatum.");
+ continue;
+ }
+ if (r == TUI_FORM_REFRESH) {
+ tui_message("Validering OK", "%s %s - %s", label, start, end);
+ continue;
+ }
+ 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, "label", label);
+ yyjson_mut_obj_add_strcpy(d, o, "start_date", start);
+ yyjson_mut_obj_add_strcpy(d, o, "end_date", end);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *oresp = client_rpc(&a->conn, "fiscal_year.open", a->session,
+ a->org, args);
+ free(args);
+ if (oresp && client_ok(oresp)) {
+ int64_t id = jint_val(oresp, "result.id", 0);
+ tui_message("Räkenskapsår", "%s skapat.", label);
+ free(oresp);
+ return id;
+ }
+ show_error("Kunde inte skapa räkenskapsåret", oresp);
+ free(oresp);
+ }
+}
+
+/* Pick which fiscal year the screens work in. Shown as its real range so
+ broken fiscal years (not starting in January) are obvious. Ctrl+A closes
+ or reopens the highlighted year. */
+static void select_fiscal_year(struct app *a)
+{
+ int64_t prefer = a->fy;
+ int cursor = -1;
+ for (;;) {
+ if (g_quit)
+ return;
+ char *resp =
+ client_rpc(&a->conn, "fiscal_year.list", a->session, a->org, "{}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Räkenskapsår", resp);
+ free(resp);
+ return;
+ }
+ size_t n = jarr_size(resp, "result.items");
+ if (n == 0) {
+ free(resp);
+ int64_t id = fy_new_form(a);
+ if (id > 0)
+ prefer = id;
+ continue;
+ }
+ char **lines = xcalloc(n, sizeof(char *));
+ int64_t *ids = xcalloc(n, sizeof(int64_t));
+ int *closed = xcalloc(n, sizeof(int));
+ char (*ranges)[40] = xcalloc(n, sizeof *ranges);
+ char (*labels)[64] = xcalloc(n, sizeof *labels);
+ int sel_default = 0;
+ 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.label", i);
+ char *label = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.start_date", i);
+ char *sdate = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.end_date", i);
+ char *edate = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.status", i);
+ char *status = jstr_dup(resp, path);
+ snprintf(ranges[i], sizeof ranges[i], "%s - %s",
+ sdate ? sdate : "?", edate ? edate : "?");
+ snprintf(labels[i], sizeof labels[i], "%s", label ? label : "");
+ closed[i] = strcmp(status ? status : "", "closed") == 0;
+ char line[256], stbuf[16];
+ snprintf(stbuf, sizeof stbuf, "%s", closed[i] ? "stängd" : "öppen");
+ tui_pad_field(stbuf, sizeof stbuf, 6);
+ snprintf(line, sizeof line, "%-25s %s %s", ranges[i], stbuf,
+ labels[i]);
+ lines[i] = xstrdup(line);
+ if (ids[i] == prefer)
+ sel_default = (int)i;
+ free(label);
+ free(sdate);
+ free(edate);
+ free(status);
+ }
+ int start = cursor >= 0 ? cursor : sel_default;
+ if (start >= (int)n)
+ start = (int)n - 1;
+ int sel = tui_select_list("Räkenskapsår", lines, (int)n, start, 0,
+ &cursor, 1, NULL, 1);
+ for (size_t i = 0; i < n; i++)
+ free(lines[i]);
+ free(lines);
+ free(resp);
+ if (sel == -4) {
+ free(ids);
+ free(closed);
+ free(ranges);
+ free(labels);
+ int64_t id = fy_new_form(a);
+ if (id > 0)
+ prefer = id;
+ continue;
+ }
+ if (sel == -7) {
+ if (cursor >= 0 && (size_t)cursor < n) {
+ char label[192];
+ snprintf(label, sizeof label, "%s räkenskapsåret %s? (j/n): ",
+ closed[cursor] ? "Återöppna" : "Avsluta",
+ labels[cursor]);
+ char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, label, "n", 0) ? ansbuf : NULL;
+ if (ans && (ans[0] == 'j' || ans[0] == 'J')) {
+ char args[64];
+ snprintf(args, sizeof args,
+ "{\"id\":%lld,\"confirm\":true}",
+ (long long)ids[cursor]);
+ char *r = client_rpc(&a->conn,
+ closed[cursor]
+ ? "fiscal_year.reopen"
+ : "fiscal_year.close",
+ a->session, a->org, args);
+ if (r && client_ok(r))
+ tui_message("Räkenskapsår", "Räkenskapsåret %s är %s.",
+ labels[cursor],
+ closed[cursor] ? "återöppnat" : "avslutat");
+ else
+ show_error("Kunde inte ändra status", r);
+ free(r);
+ }
+ }
+ free(ids);
+ free(closed);
+ free(ranges);
+ free(labels);
+ continue;
+ }
+ if (sel < 0) {
+ free(ids);
+ free(closed);
+ free(ranges);
+ free(labels);
+ return;
+ }
+ a->fy = ids[sel];
+ snprintf(a->fy_label, sizeof a->fy_label, "%s", labels[sel]);
+ /* range is "start - end" */
+ snprintf(a->fy_start, sizeof a->fy_start, "%.10s", ranges[sel]);
+ snprintf(a->fy_end, sizeof a->fy_end, "%s", ranges[sel] + 13);
+ free(ids);
+ free(closed);
+ free(ranges);
+ free(labels);
+ a->voucher_sel = 0;
+ update_status(a);
+ return;
+ }
+}
+static const char *const MAIN_ITEMS[] = {
+ MK_SECTION "Bokföring",
+ "Verifikat", "Underlag (inkorg)", "Bankavstämning",
+ MK_SECTION "Fakturering",
+ "Fakturor",
+ MK_SECTION "Rapporter & bokslut",
+ "Rapporter", "Bokslut",
+ MK_SECTION "Register",
+ "Mallar", "Kunder", "Momsregler", "Företagsuppgifter", "Inställningar",
+ MK_SECTION "Räkenskapsår",
+ "Ingående balans", "Räkenskapsår",
+ MK_SECTION "Övrigt",
+ "Revision", "Logga ut / avsluta",
+};
+
+void dashboard(struct app *a)
+{
+ static int last_sel = 0;
+ for (;;) {
+ if (g_reload)
+ return;
+ int sel = tui_menu("Bokf", MAIN_ITEMS,
+ (int)(sizeof MAIN_ITEMS / sizeof MAIN_ITEMS[0]), 1,
+ &last_sel);
+ snprintf(g_scene, sizeof g_scene, "%s", "dashboard");
+ if (g_quit || g_reload)
+ return;
+ if (sel == -4) {
+ vouchers_new(a);
+ continue;
+ }
+ if (sel == -5)
+ return;
+ if (sel < 0)
+ continue; /* Esc/back at the top level never exits */
+ switch (sel) {
+ case 1:
+ open_scene(a, "vouchers");
+ break;
+ case 2:
+ open_scene(a, "inbox");
+ break;
+ case 3:
+ open_scene(a, "bank");
+ break;
+ case 5:
+ open_scene(a, "invoices");
+ break;
+ case 7:
+ open_scene(a, "reports");
+ break;
+ case 8:
+ open_scene(a, "bokslut");
+ break;
+ case 10:
+ open_scene(a, "templates");
+ break;
+ case 11:
+ open_scene(a, "customers");
+ break;
+ case 12:
+ open_scene(a, "rules");
+ break;
+ case 13:
+ open_scene(a, "company");
+ break;
+ case 14:
+ open_scene(a, "settings");
+ break;
+ case 16:
+ open_scene(a, "ib");
+ break;
+ case 17:
+ select_fiscal_year(a);
+ break;
+ case 19:
+ open_scene(a, "audit");
+ break;
+ case 20:
+ return;
+ default:
+ break; /* section header */
+ }
+ }
+}