#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" 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[] = { "Verifikat", "Underlag (inkorg)", "Bankavstämning", "Mallar", "Fakturor", "Lönekörningar", "Rapporter", "Bokslut", "Bolaget", "System", "Ingående balans", "Räkenskapsår", "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 0: open_scene(a, "vouchers"); break; case 1: open_scene(a, "inbox"); break; case 2: open_scene(a, "bank"); break; case 3: open_scene(a, "templates"); break; case 4: open_scene(a, "invoices"); break; case 5: open_scene(a, "payroll"); break; case 6: open_scene(a, "reports"); break; case 7: open_scene(a, "bokslut"); break; case 8: open_scene(a, "company"); break; case 9: open_scene(a, "system"); break; case 10: open_scene(a, "ib"); break; case 11: select_fiscal_year(a); break; case 12: return; default: break; } } }