aboutsummaryrefslogtreecommitdiff
path: root/clients/bokftui.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-18 21:07:38 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-18 21:07:38 +0200
commit49343ab72fa388aeba155c0fa605efdbfd8a345a (patch)
treebe6caf8a331179251aa47b365b837e0026e844c9 /clients/bokftui.c
parent0ec59a0dfce8d350f26783ccc2456e5cb68c37a5 (diff)
downloadbokf-49343ab72fa388aeba155c0fa605efdbfd8a345a.tar.gz
bokf-49343ab72fa388aeba155c0fa605efdbfd8a345a.zip
bokftui: Huvudbok/Verifikationslista views, Räkenskapsår screen, ^Enter/^Xv0.1.27
The report menu gains Kapitas-style Huvudbok and Verifikationslista tables. "Byt räkenskapsår" is now "Räkenskapsår": the list shows öppen/stängd and ^A closes or reopens the highlighted year (via the new fiscal_year.reopen). Menus remember the selection, Mallar opens straight into the list (Enter edit, ^N new, d arkivera), report tables get column rules and aligned sums, and text_view clears stale lines. Footer hints write control keys as ^N/^A/^C/^R/^X, ^Enter saves or posts a form and ^X clears the focused row (F9 still works as a fallback).
Diffstat (limited to 'clients/bokftui.c')
-rw-r--r--clients/bokftui.c514
1 files changed, 379 insertions, 135 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c
index 9ea6eb6..b2d8e42 100644
--- a/clients/bokftui.c
+++ b/clients/bokftui.c
@@ -18,11 +18,15 @@
#include "version.h"
#include "yyjson.h"
-/* universal "new/add" hotkey: Ctrl+N (0x0e), the de-facto Linux convention */
+/* universal "new/add" hotkey: ^N (0x0e), the de-facto Linux convention */
#define KEY_CTRL_N 0x0e
-/* Ctrl+C quits the application; Esc is navigation only */
+/* ^X clears the focused row in row editors */
+#define KEY_CTRL_X 0x18
+/* ^Enter saves/posts the form; sent via xterm modifyOtherKeys */
+#define KEY_CTRL_ENTER (KEY_MAX + 1)
+/* ^C quits the application; Esc is navigation only */
static int g_quit = 0;
-/* Ctrl+R re-execs the binary, keeping session, org, year and screen */
+/* ^R re-execs the binary, keeping session, org, year and screen */
static int g_reload = 0;
static char g_scene[32] = "dashboard";
static char g_reload_scene[32];
@@ -30,11 +34,11 @@ static char g_reload_scene[32];
static int ui_getch(void)
{
int ch = getch();
- if (ch == 3) { /* Ctrl+C */
+ if (ch == 3) { /* ^C */
g_quit = 1;
return 27; /* behave like "back" so screens unwind */
}
- if (ch == 18) { /* Ctrl+R */
+ if (ch == 18) { /* ^R */
g_reload = 1;
snprintf(g_reload_scene, sizeof g_reload_scene, "%s", g_scene);
return 27;
@@ -47,7 +51,7 @@ struct app {
char socket[256];
char session[128];
char username[64];
- char password[128]; /* typed or from BOKFD_PASSWORD, kept for Ctrl+R */
+ char password[128]; /* typed or from BOKFD_PASSWORD, kept for ^R */
int64_t org;
char org_name[128];
char role[32];
@@ -244,7 +248,7 @@ static int le_key(struct ledit *e, int ch)
le_backspace(e);
return 1;
}
- if (ch == 21) { /* Ctrl+U */
+ if (ch == 21) { /* ^U */
le_clear(e);
return 1;
}
@@ -560,8 +564,12 @@ static int text_view(const char *title, const char *text)
int view = LINES - 4;
for (;;) {
frame(title);
- for (int i = 0; i < view && top + i < n; i++)
- mvaddnstr(2 + i, 2, lines[top + i], COLS - 4);
+ for (int i = 0; i < view; i++) {
+ move(2 + i, 2);
+ clrtoeol();
+ if (top + i < n)
+ mvaddnstr(2 + i, 2, lines[top + i], COLS - 4);
+ }
hints("piltangenter/PgUp/PgDn rullar F5 = uppdatera q = tillbaka");
refresh();
int ch = ui_getch();
@@ -594,13 +602,15 @@ static int text_view(const char *title, const char *text)
}
static int menu(const char *title, const char *const *items, int n,
- int allow_new)
+ int allow_new, int *cursor)
{
- int sel = 0;
+ int sel = (cursor && *cursor >= 0 && *cursor < n) ? *cursor : 0;
int top = 0;
char gotobuf[12] = "";
int goto_active = 0;
for (;;) {
+ if (cursor)
+ *cursor = sel;
int view = (LINES - 4) / 2;
if (view < 1)
view = 1;
@@ -631,10 +641,10 @@ static int menu(const char *title, const char *const *items, int n,
}
hints(allow_new
? "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn,"
- " Home/End, Enter Ctrl+N = ny Esc/q = tillbaka"
- " Ctrl+C = avsluta"
+ " Home/End, Enter ^N = ny Esc/q = tillbaka"
+ " ^C = avsluta"
: "upp/ned, 1-9 = snabbval, g = gå till, PgUp/PgDn,"
- " Home/End, Enter Esc/q = tillbaka Ctrl+C = avsluta");
+ " Home/End, Enter Esc/q = tillbaka ^C = avsluta");
refresh();
int ch = ui_getch();
if (goto_active) {
@@ -673,8 +683,12 @@ static int menu(const char *title, const char *const *items, int n,
sel = 0;
else if (ch == KEY_END)
sel = n - 1;
- else if (ch >= '1' && ch <= '9' && ch - '1' < n)
- return ch - '1';
+ else if (ch >= '1' && ch <= '9' && ch - '1' < n) {
+ sel = ch - '1';
+ if (cursor)
+ *cursor = sel;
+ return sel;
+ }
else if (ch == 'g' || ch == 'G') {
goto_active = 1;
gotobuf[0] = '\0';
@@ -688,9 +702,14 @@ static int menu(const char *title, const char *const *items, int n,
}
static int select_list(const char *title, char **items, int n, int start,
- int allow_refresh, int *cursor, int allow_new)
+ int allow_refresh, int *cursor, int allow_new,
+ int allow_archive, int allow_toggle)
{
int sel = start;
+ if (sel < 0)
+ sel = 0;
+ if (n > 0 && sel >= n)
+ sel = n - 1;
int top = 0;
int view = LINES - 4;
char gotobuf[12] = "";
@@ -732,11 +751,13 @@ static int select_list(const char *title, char **items, int n, int start,
char hint[256];
snprintf(hint, sizeof hint,
"upp/ned, 1-9 = hoppa, g = gå till, PgUp/PgDn, Home/End,"
- " Enter%s%s Esc/q = tillbaka",
+ " Enter%s%s%s%s Esc/q = tillbaka",
allow_refresh ? " F5 = uppdatera" : "",
- allow_new ? " Ctrl+N = ny" : "");
+ allow_new ? " ^N = ny" : "",
+ allow_archive ? " d = arkivera" : "",
+ allow_toggle ? " ^A = öppna/stäng" : "");
snprintf(hint + strlen(hint), sizeof hint - strlen(hint),
- " Ctrl+C = avsluta");
+ " ^C = avsluta");
hints(hint);
}
refresh();
@@ -790,6 +811,10 @@ static int select_list(const char *title, char **items, int n, int start,
return -2;
else if (allow_new && ch == KEY_CTRL_N)
return -4;
+ else if (allow_archive && (ch == 'd' || ch == 'D') && n > 0)
+ return -6;
+ else if (allow_toggle && ch == 1 && n > 0)
+ return -7;
else if (ch == 27 || ch == 'q')
return g_quit ? -5 : -1;
}
@@ -849,6 +874,17 @@ static void kr_format(int64_t ore, char *buf, size_t n)
snprintf(buf, n, "%s%s,%02lld", neg ? "-" : "", out, v % 100);
}
+/* Whole kronor with space thousands, öre truncated like the blankett. */
+static void kr_whole(int64_t ore, char *buf, size_t n)
+{
+ char tmp[48];
+ kr_format((ore / 100) * 100, tmp, sizeof tmp);
+ char *comma = strchr(tmp, ',');
+ if (comma)
+ *comma = '\0';
+ snprintf(buf, n, "%s", tmp);
+}
+
static int parse_x_double(const char *s, double *out)
{
if (!s)
@@ -1092,7 +1128,7 @@ static int64_t fy_new_form(struct app *a)
attroff(A_REVERSE);
}
}
- hints("Tab = byta fält F5 = validera F9 = skapa Esc = avbryt");
+ hints("Tab = byta fält F5 = validera ^Enter = skapa Esc = avbryt");
if (caret_y >= 0)
move(caret_y, caret_x);
refresh();
@@ -1112,7 +1148,7 @@ static int64_t fy_new_form(struct app *a)
field_fresh = 1;
continue;
}
- if (ch == KEY_F(5) || ch == KEY_F(9)) {
+ if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) {
if (!label[0]) {
message("Räkenskapsår", "Etiketten får inte vara tom.");
continue;
@@ -1164,10 +1200,12 @@ static int64_t fy_new_form(struct app *a)
}
/* Pick which fiscal year the screens work in. Shown as its real range so
- broken fiscal years (not starting in January) are obvious. */
+ 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;
@@ -1188,6 +1226,7 @@ static void select_fiscal_year(struct app *a)
}
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;
@@ -1206,10 +1245,9 @@ static void select_fiscal_year(struct app *a)
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",
- strcmp(status ? status : "", "closed") == 0 ? "stängd"
- : "öppen");
+ snprintf(stbuf, sizeof stbuf, "%s", closed[i] ? "stängd" : "öppen");
pad_field(stbuf, sizeof stbuf, 6);
snprintf(line, sizeof line, "%-25s %s %s", ranges[i], stbuf,
labels[i]);
@@ -1221,14 +1259,18 @@ static void select_fiscal_year(struct app *a)
free(edate);
free(status);
}
- int sel = select_list("Välj räkenskapsår", lines, (int)n, sel_default,
- 0, NULL, 1);
+ int start = cursor >= 0 ? cursor : sel_default;
+ if (start >= (int)n)
+ start = (int)n - 1;
+ int sel = select_list("Räkenskapsår", lines, (int)n, start, 0,
+ &cursor, 1, 0, 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);
@@ -1236,8 +1278,41 @@ static void select_fiscal_year(struct app *a)
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 *ans = prompt(label, "n", 0);
+ 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))
+ 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;
@@ -1248,6 +1323,7 @@ static void select_fiscal_year(struct app *a)
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;
@@ -1380,7 +1456,7 @@ static char *file_browser(struct app *a, const char *start_dir)
}
char title[1200];
snprintf(title, sizeof title, "Välj fil — %.200s", dir);
- int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0);
+ int sel = select_list(title, items, (int)n + 1, 0, 1, NULL, 0, 0, 0);
for (size_t i = 0; i <= n; i++)
free(items[i]);
free(items);
@@ -1509,7 +1585,7 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new)
frame("Verifikat");
for (int i = 0; i < view && top + i < n; i++)
mvaddnstr(2 + i, 2, lines[top + i], COLS - 4);
- hints("upp/ned F5 = uppdatera Ctrl+N = nytt verifikat c ="
+ hints("upp/ned F5 = uppdatera ^N = nytt verifikat c ="
" rätta Esc/q = tillbaka");
refresh();
int ch = ui_getch();
@@ -1847,8 +1923,8 @@ static int64_t vouchers_new(struct app *a)
attroff(A_BOLD);
if (!balanced)
mvprintw(7 + nrows + 2, 2, "Differens: %s", diff);
- hints("F4 = mall Ctrl+F = bifoga fil Tab = byta fält F5 = validera"
- " F7 = rensa rad F9 = bokför Esc = avbryt");
+ hints("F4 = mall ^F = bifoga fil Tab = byta fält F5 = validera"
+ " ^X = rensa rad ^Enter = bokför Esc = avbryt");
if (caret_y >= 0)
move(caret_y, caret_x);
refresh();
@@ -1930,7 +2006,7 @@ static int64_t vouchers_new(struct app *a)
free(ser);
free(ds);
}
- int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0);
+ int tsel = select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, 0, 0);
if (tsel >= 0)
snprintf(tname, sizeof tname, "%s", names[tsel]);
for (size_t i = 0; i < ln; i++) {
@@ -2044,7 +2120,7 @@ static int64_t vouchers_new(struct app *a)
}
continue;
}
- if (ch == 6) { /* Ctrl+F: attach a file */
+ if (ch == 6) { /* ^F: attach a file */
char *path = file_browser(a, a->attachment_dir);
if (path) {
struct stat sb;
@@ -2098,7 +2174,7 @@ static int64_t vouchers_new(struct app *a)
}
continue;
}
- if (ch == KEY_F(7)) {
+ if (ch == KEY_CTRL_X) {
if (field >= 3) {
int ri = (field - 3) / 4;
if (ri < nrows) {
@@ -2112,7 +2188,7 @@ static int64_t vouchers_new(struct app *a)
}
/* build args for validate/post */
- int do_post = (ch == KEY_F(9));
+ int do_post = (ch == KEY_CTRL_ENTER || ch == KEY_F(9));
int do_check = (ch == KEY_F(5));
if (do_post || do_check) {
yyjson_mut_doc *d = yyjson_mut_doc_new(NULL);
@@ -2315,7 +2391,7 @@ static void vouchers_screen(struct app *a)
}
free(idstr);
free(resp);
- items[n] = xstrdup("+ Nytt verifikat (Ctrl+N)");
+ items[n] = xstrdup("+ Nytt verifikat (^N)");
}
size_t total = n + 1;
int start = 0;
@@ -2328,7 +2404,7 @@ static void vouchers_screen(struct app *a)
}
int cur = start;
int sel = select_list("Verifikat", items, (int)total, start, 1, &cur,
- 1);
+ 1, 0, 0);
if (cur >= 0 && (size_t)cur < n)
a->voucher_sel = ids[cur];
if (sel == -2) {
@@ -2553,6 +2629,22 @@ static void buf_line(struct buf *b, const char *fmt, ...)
buf_append(b, line, (size_t)n < sizeof line ? (size_t)n : sizeof line - 1);
}
+static void buf_rule(struct buf *b, int width)
+{
+ char line[128];
+ if (width > (int)sizeof line - 2)
+ width = (int)sizeof line - 2;
+ if (width < 1)
+ return;
+ memset(line, '-', (size_t)width);
+ line[width] = '\n';
+ buf_append(b, line, (size_t)width + 1);
+}
+
+/* prefix (8) + label (48) + amount columns */
+#define BS_RULE_W (8 + 48 + 4 * 14)
+#define IS_RULE_W (8 + 48 + 3 * 15)
+
static void fmt_head(struct buf *t, const struct app *a, const char *title,
yyjson_val *res)
{
@@ -2595,6 +2687,7 @@ static void bs_col_head(struct buf *t)
{
buf_line(t, " %5s %-48s %14s %14s %14s %14s\n", "", "", "Ing balans",
"Ing saldo", "Period", "Utg balans");
+ buf_rule(t, BS_RULE_W);
}
static int bs_rows(struct buf *t, yyjson_val *res, int lo, int hi,
@@ -2620,7 +2713,7 @@ static int bs_rows(struct buf *t, yyjson_val *res, int lo, int hi,
amt_col(a2, sizeof a2, 14, ib);
amt_col(a3, sizeof a3, 14, per);
amt_col(a4, sizeof a4, 14, ub);
- buf_line(t, "%-6s %s %s %s %s %s\n", acc, nm, a1, a2, a3, a4);
+ buf_line(t, " %5s %s %s %s %s %s\n", acc, nm, a1, a2, a3, a4);
}
sum->ib += ib;
sum->per += per;
@@ -2679,18 +2772,25 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res)
struct bs_sum anl = { 0, 0, 0 }, oms = { 0, 0, 0 };
struct bs_sum eget = { 0, 0, 0 }, skuld = { 0, 0, 0 };
buf_line(t, "TILLGÅNGAR\n");
- buf_line(t, "Anläggningstillgångar\n");
+ bs_col_head(t);
+ buf_line(t, "\nAnläggningstillgångar\n");
bs_rows(t, res, 1000, 1399, &anl);
bs_sum_line(t, "Summa anläggningstillgångar", &anl);
buf_line(t, "\nOmsättningstillgångar\n");
+ int printed = 0;
for (size_t i = 0; i < sizeof OMS / sizeof OMS[0]; i++) {
- if (i)
+ struct bs_sum probe = { 0, 0, 0 };
+ if (!bs_rows(NULL, res, OMS[i].lo, OMS[i].hi, &probe))
+ continue;
+ if (printed)
buf_line(t, "\n");
bs_group(t, res, OMS[i].title, OMS[i].sum, OMS[i].lo, OMS[i].hi,
&oms);
+ printed = 1;
}
bs_sum_line(t, "Summa omsättningstillgångar", &oms);
struct bs_sum as = { anl.ib + oms.ib, anl.per + oms.per, anl.ub + oms.ub };
+ buf_rule(t, BS_RULE_W);
bs_sum_line(t, "Summa tillgångar", &as);
buf_line(t, "\nEGET KAPITAL OCH SKULDER\n");
@@ -2710,15 +2810,18 @@ static void fmt_balans(struct buf *t, const struct app *a, yyjson_val *res)
2400, 2999, &skuld);
struct bs_sum es = { eget.ib + skuld.ib, eget.per + skuld.per,
eget.ub + skuld.ub };
+ buf_rule(t, BS_RULE_W);
bs_sum_line(t, "Summa eget kapital och skulder", &es);
yyjson_val *eq = yyjson_obj_get(res, "equity");
int64_t result = eq ? vint(eq, "result_ore") : 0;
char lbl[64], zero[48], a1[48];
- pad_label(lbl, sizeof lbl, "Beräknat resultat", 36);
+ pad_label(lbl, sizeof lbl, "Beräknat resultat", 48);
amt_col(zero, sizeof zero, 14, 0);
amt_col(a1, sizeof a1, 14, result);
- buf_line(t, "\n\n%-6s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1);
+ buf_line(t, "\n");
+ buf_rule(t, BS_RULE_W);
+ buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, zero, zero, a1, a1);
}
/* ---------------- resultaträkning ---------------- */
@@ -2841,11 +2944,13 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res,
buf_line(t, " %5s %-48s %15s %15s %15s\n", "",
gr->section, "Period", "Ackumulerat",
"Föreg. per.");
+ buf_rule(t, IS_RULE_W);
buf_line(t, "\n");
buf_line(t, "%s\n", gr->title);
}
sect = gr->section;
} else {
+ buf_line(t, "\n");
buf_line(t, "%s\n", gr->title);
}
int64_t sc = 0, sp = 0;
@@ -2903,10 +3008,14 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res,
}
int64_t ror = rev + exp, ror_pre = rev_pre + exp_pre;
int64_t after = ror + fin, after_pre = ror_pre + fin_pre;
- is_summary(t, "Rörelseresultat", ror, ror_pre, 1);
+ buf_line(t, "\n");
+ buf_rule(t, IS_RULE_W);
+ is_summary(t, "Rörelseresultat", ror, ror_pre, 0);
is_summary(t, "Resultat efter finansiella poster", after, after_pre, 1);
is_summary(t, "Resultat före skatt", after, after_pre, 1);
- is_summary(t, "Årets resultat", after, after_pre, 1);
+ buf_line(t, "\n");
+ buf_rule(t, IS_RULE_W);
+ is_summary(t, "Årets resultat", after, after_pre, 0);
if (has_booked) {
buf_line(t, "Bokfört resultat\n");
for (int k = 0; k < 2; k++) {
@@ -2930,10 +3039,14 @@ static void fmt_resultat(struct buf *t, const struct app *a, yyjson_val *res,
}
}
is_summary(t, "Summa", booked, booked_pre, 0);
+ buf_line(t, "\n");
+ buf_rule(t, IS_RULE_W);
is_summary(t, "Ej bokfört resultat", after + booked,
- after_pre + booked_pre, 2);
+ after_pre + booked_pre, 0);
} else {
- is_summary(t, "Ej bokfört resultat", after, after_pre, 2);
+ buf_line(t, "\n");
+ buf_rule(t, IS_RULE_W);
+ is_summary(t, "Ej bokfört resultat", after, after_pre, 0);
}
}
@@ -2945,6 +3058,7 @@ static void fmt_saldobalans(struct buf *t, const struct app *a,
fmt_head(t, a, "Saldobalans", res);
buf_line(t, " %5s %-48s %14s %14s %14s %14s\n", "Konto", "Benämning",
"Ingående", "Debet", "Kredit", "Utgående");
+ buf_rule(t, BS_RULE_W);
yyjson_val *arr = yyjson_obj_get(res, "accounts");
size_t n = yyjson_arr_size(arr);
for (size_t i = 0; i < n; i++) {
@@ -2965,7 +3079,9 @@ static void fmt_saldobalans(struct buf *t, const struct app *a,
amt_col(a2, sizeof a2, 14, vint(tot, "debit_ore"));
amt_col(a3, sizeof a3, 14, vint(tot, "credit_ore"));
amt_col(a4, sizeof a4, 14, vint(tot, "ub_ore"));
- buf_line(t, "\n %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4);
+ buf_line(t, "\n");
+ buf_rule(t, BS_RULE_W);
+ buf_line(t, " %5s %s %s %s %s %s\n", "", lbl, a1, a2, a3, a4);
}
/* ---------------- momsdeklaration ---------------- */
@@ -3056,10 +3172,9 @@ static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res)
break;
}
}
- char amt[32] = "";
+ char amt[48] = "";
if (ore)
- snprintf(amt, sizeof amt, "%lld",
- (long long)(ore / 100)); /* Kapitas truncates öre */
+ kr_whole(ore, amt, sizeof amt);
buf_line(t, "%s. %-66s %14s\n", vr->box, vr->label, amt);
}
int64_t ore49 = 0;
@@ -3071,9 +3186,11 @@ static void fmt_moms(struct buf *t, const struct app *a, yyjson_val *res)
}
}
buf_line(t, "\nG. %s\n", vat_section_title("G."));
- buf_line(t, "49. %-66s %14lld\n",
- "Moms att betala (+) eller att få tillbaka (-)",
- (long long)(ore49 / 100));
+ buf_rule(t, 87);
+ char amt49[48];
+ kr_whole(ore49, amt49, sizeof amt49);
+ buf_line(t, "49. %-66s %14s\n",
+ "Moms att betala (+) eller att få tillbaka (-)", amt49);
}
static int64_t prev_fy_id(struct app *a)
@@ -3105,22 +3222,124 @@ static int64_t prev_fy_id(struct app *a)
return best;
}
+/* ---------------- huvudbok ---------------- */
+
+static void fmt_huvudbok(struct buf *t, const struct app *a, yyjson_val *res)
+{
+ fmt_head(t, a, "Huvudbok", res);
+ yyjson_val *lv = yyjson_obj_get(res, "last_voucher");
+ if (lv)
+ buf_line(t, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"),
+ (long long)vint(lv, "number"));
+ buf_line(t, "%-8s %-58s %14s %14s %16s\n", "Konto", "", "Debet",
+ "Kredit", "Saldo");
+ buf_rule(t, 114);
+ yyjson_val *accs = yyjson_obj_get(res, "accounts");
+ size_t n = yyjson_arr_size(accs);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_val *ao = yyjson_arr_get(accs, i);
+ int64_t ib = vint(ao, "ib_ore");
+ int64_t d = vint(ao, "debit_ore");
+ int64_t c = vint(ao, "credit_ore");
+ buf_line(t, "\n%s %s\n\n", vstr(ao, "account"), vstr(ao, "name"));
+ char a1[48];
+ amt_col(a1, sizeof a1, 16, ib);
+ buf_line(t, " %-59s %16s\n", "Ingående balans:", a1);
+ buf_line(t, " %-59s %16s\n", "Ingående saldo:", a1);
+ buf_line(t, "\n");
+ yyjson_val *rows = yyjson_obj_get(ao, "rows");
+ size_t nr = yyjson_arr_size(rows);
+ for (size_t k = 0; k < nr; k++) {
+ yyjson_val *ro = yyjson_arr_get(rows, k);
+ char ver[32], nm[256], dd[48], cc[48], deb[48], kre[48], sal[48];
+ snprintf(ver, sizeof ver, "%s%lld", vstr(ro, "series"),
+ (long long)vint(ro, "number"));
+ const char *txt = vstr(ro, "row_description");
+ if (!*txt)
+ txt = vstr(ro, "description");
+ pad_label(nm, sizeof nm, txt, 40);
+ amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore"));
+ amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore"));
+ snprintf(deb, sizeof deb, "%s", vint(ro, "debit_ore") ? dd : "");
+ snprintf(kre, sizeof kre, "%s", vint(ro, "credit_ore") ? cc : "");
+ amt_col(sal, sizeof sal, 16, vint(ro, "saldo_ore"));
+ buf_line(t, " %-7s %-10s %s %14s %14s %16s\n", ver,
+ vstr(ro, "date"), nm, deb, kre, sal);
+ }
+ char s1[48], s2[48], s3[48];
+ amt_col(s1, sizeof s1, 14, d);
+ amt_col(s2, sizeof s2, 14, c);
+ amt_col(s3, sizeof s3, 16, d - c);
+ buf_line(t, " %-59s %14s %14s %16s\n", "Omslutning:", s1, s2,
+ s3);
+ amt_col(s3, sizeof s3, 16, vint(ao, "ub_ore"));
+ buf_line(t, " %-59s %14s %14s %16s\n", "Utgående saldo:", s1,
+ s2, s3);
+ }
+}
+
+/* ---------------- verifikationslista ---------------- */
+
+static void fmt_verifikationslista(struct buf *t, const struct app *a,
+ yyjson_val *res)
+{
+ fmt_head(t, a, "Verifikationslista", res);
+ yyjson_val *lv = yyjson_obj_get(res, "last_voucher");
+ if (lv)
+ buf_line(t, "Senaste ver.: %s%lld\n\n", vstr(lv, "series"),
+ (long long)vint(lv, "number"));
+ buf_line(t, "%-8s %-10s %-48s %14s %14s\n", "Ver.", "Datum/Konto",
+ "Benämning", "Debet", "Kredit");
+ buf_rule(t, 98);
+ yyjson_val *vs = yyjson_obj_get(res, "vouchers");
+ size_t n = yyjson_arr_size(vs);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_val *vo = yyjson_arr_get(vs, i);
+ char ver[32];
+ snprintf(ver, sizeof ver, "%s%lld", vstr(vo, "series"),
+ (long long)vint(vo, "number"));
+ buf_line(t, "%-8s %-10s %s\n", ver, vstr(vo, "date"),
+ vstr(vo, "description"));
+ yyjson_val *rows = yyjson_obj_get(vo, "rows");
+ size_t nr = yyjson_arr_size(rows);
+ for (size_t k = 0; k < nr; k++) {
+ yyjson_val *ro = yyjson_arr_get(rows, k);
+ char nm[256], dd[48], cc[48], deb[48], kre[48];
+ pad_label(nm, sizeof nm, vstr(ro, "name"), 48);
+ amt_col(dd, sizeof dd, 14, vint(ro, "debit_ore"));
+ amt_col(cc, sizeof cc, 14, vint(ro, "credit_ore"));
+ snprintf(deb, sizeof deb, "%s", vint(ro, "debit_ore") ? dd : "");
+ snprintf(kre, sizeof kre, "%s", vint(ro, "credit_ore") ? cc : "");
+ buf_line(t, " %-10s %s %14s %14s\n", vstr(ro, "account"),
+ nm, deb, kre);
+ }
+ }
+ yyjson_val *tot = yyjson_obj_get(res, "totals");
+ char s1[48], s2[48];
+ amt_col(s1, sizeof s1, 14, vint(tot, "debit_ore"));
+ amt_col(s2, sizeof s2, 14, vint(tot, "credit_ore"));
+ buf_line(t, "\n%-68s %14s %14s\n", "Omslutning:", s1, s2);
+}
+
static void reports_screen(struct app *a)
{
static const char *const report_items[] = {
"Saldobalans (trial balance)",
"Resultaträkning",
"Balansräkning",
+ "Huvudbok",
+ "Verifikationslista",
"Momsdeklaration",
"Kontolista (alla konton)",
};
+ static int last_sel = 0;
for (;;) {
if (g_quit)
return;
- int sel = menu("Rapporter", report_items, 5, 0);
+ int sel = menu("Rapporter", report_items, 7, 0, &last_sel);
if (sel < 0)
return;
- if (sel == 4) {
+ if (sel == 6) {
accounts_report(a);
continue;
}
@@ -3143,7 +3362,17 @@ static void reports_screen(struct app *a)
snprintf(args, sizeof args, "{\"fiscal_year\":%lld}",
(long long)a->fy);
break;
- case 3: {
+ case 3:
+ cmd = "report.general_ledger";
+ snprintf(args, sizeof args, "{\"fiscal_year\":%lld}",
+ (long long)a->fy);
+ break;
+ case 4:
+ cmd = "report.voucher_list";
+ snprintf(args, sizeof args, "{\"fiscal_year\":%lld}",
+ (long long)a->fy);
+ break;
+ case 5: {
cmd = "report.vat";
char *from = date_prompt("Från (YYYY-MM-DD): ", a->fy_start);
if (!from)
@@ -3196,6 +3425,10 @@ static void reports_screen(struct app *a)
yyjson_doc_free(pd);
} else if (sel == 2) {
fmt_balans(&out, a, res);
+ } else if (sel == 3) {
+ fmt_huvudbok(&out, a, res);
+ } else if (sel == 4) {
+ fmt_verifikationslista(&out, a, res);
} else {
fmt_moms(&out, a, res);
}
@@ -3250,7 +3483,7 @@ static void inbox_screen(struct app *a)
mvaddnstr(2 + (int)(i - (size_t)top), 2, items[i], COLS - 4);
if (n == 0)
mvaddstr(2, 2, "Inga obokförda underlag.");
- hints("a/Ctrl+N = lägg till fil PgUp/PgDn, Home/End rullar F5 ="
+ hints("a/^N = lägg till fil PgUp/PgDn, Home/End rullar F5 ="
" uppdatera Esc/q = tillbaka");
refresh();
int ch = ui_getch();
@@ -3324,10 +3557,11 @@ static void audit_screen(struct app *a)
{
static const char *const audit_items[] = { "Verifiera hashkedjan",
"Senaste händelser" };
+ static int last_sel = 0;
for (;;) {
if (g_quit)
return;
- int sel = menu("Revision", audit_items, 2, 0);
+ int sel = menu("Revision", audit_items, 2, 0, &last_sel);
if (sel < 0)
return;
if (sel == 0) {
@@ -3597,7 +3831,7 @@ static int template_form(struct app *a, const char *load_name)
"x = belopp i kronor. Positivt blir debet, negativt kredit.",
COLS - 4);
attroff(A_DIM);
- hints("Tab = byta fält F5 = validera F7 = rensa rad F9 = spara Esc = avbryt");
+ hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt");
if (caret_y >= 0)
move(caret_y, caret_x);
refresh();
@@ -3639,7 +3873,7 @@ static int template_form(struct app *a, const char *load_name)
}
continue;
}
- if (ch == KEY_F(7)) {
+ if (ch == KEY_CTRL_X) {
if (field >= 3) {
int ri = (field - 3) / 3;
if (ri < nrows) {
@@ -3653,7 +3887,7 @@ static int template_form(struct app *a, const char *load_name)
}
/* validation and save */
- if (ch == KEY_F(5) || ch == KEY_F(9)) {
+ if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) {
const char *problem = NULL;
char msg[256] = "";
if (!tname[0]) {
@@ -3768,10 +4002,12 @@ static int template_form(struct app *a, const char *load_name)
}
}
-static void template_archive_ui(struct app *a)
+static void template_archive(struct app *a, const char *name)
{
- char *name = prompt("Mall att arkivera: ", "", 0);
- if (!name || !*name)
+ char label[256];
+ snprintf(label, sizeof label, "Arkivera '%s'? (j/n): ", name);
+ char *ans = prompt(label, "n", 0);
+ 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);
@@ -3971,7 +4207,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold)
attroff(A_DIM);
}
}
- hints("Tab = byta fält F5 = validera F7 = rensa rad F9 = spara Esc = avbryt");
+ hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt");
if (caret_y >= 0)
move(caret_y, caret_x);
refresh();
@@ -4007,7 +4243,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold)
}
continue;
}
- if (ch == KEY_F(7)) {
+ if (ch == KEY_CTRL_X) {
int ri = field / 3;
if (ri < nrows) {
memset(&rows[ri], 0, sizeof rows[ri]);
@@ -4018,7 +4254,7 @@ static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold)
continue;
}
- if (ch == KEY_F(5) || ch == KEY_F(9)) {
+ if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) {
const char *problem = NULL;
char msg[256] = "";
int64_t sum = 0;
@@ -4227,7 +4463,7 @@ static void ib_screen(struct app *a)
attron(A_BOLD);
mvprintw(LINES - 4, 2, "Summa: %s", sumbuf);
attroff(A_BOLD);
- hints("e/Ctrl+N = redigera PgUp/PgDn, Home/End rullar F5 ="
+ hints("e/^N = redigera PgUp/PgDn, Home/End rullar F5 ="
" uppdatera Esc/q = tillbaka");
refresh();
int ch = ui_getch();
@@ -4353,9 +4589,9 @@ static void company_screen(struct app *a)
}
hints(owner
? "upp/ned Enter = ändra F5 = uppdatera"
- " Esc/q = tillbaka Ctrl+C = avsluta"
+ " Esc/q = tillbaka ^C = avsluta"
: "upp/ned F5 = uppdatera Esc/q = tillbaka"
- " Ctrl+C = avsluta (endast ägare kan ändra)");
+ " ^C = avsluta (endast ägare kan ändra)");
refresh();
int ch = ui_getch();
if (ch == KEY_UP && sel > 0)
@@ -4458,7 +4694,7 @@ static void settings_screen(struct app *a)
attroff(A_REVERSE);
}
hints("upp/ned Enter = ändra F5 = uppdatera Esc/q = tillbaka"
- " Ctrl+C = avsluta");
+ " ^C = avsluta");
refresh();
int ch = ui_getch();
if (ch == KEY_UP && sel > 0)
@@ -4516,81 +4752,76 @@ static void settings_screen(struct app *a)
static void templates_screen(struct app *a)
{
- static const char *const items[] = { "Mallar (lista/redigera)", "Ny mall",
- "Arkivera mall" };
+ int cursor = 0;
for (;;) {
if (g_quit)
return;
- int sel = menu("Mallar", items, 3, 1);
- if (sel == -4) {
- template_form(a, NULL);
- continue;
- }
- if (sel < 0)
+ 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;
- if (sel == 1) {
- template_form(a, NULL);
- continue;
}
- if (sel == 2) {
- template_archive_ui(a);
- continue;
+ 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 : "");
+ 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);
}
- for (;;) {
- 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");
- if (n == 0) {
- message("Mallar", "Inga mallar ännu.");
- free(resp);
- break;
- }
- char **names = xcalloc(n, sizeof(char *));
- char **lines = xcalloc(n, 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 : "");
- 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 s = select_list("Mallar", lines, (int)n, 0, 1, NULL, 0);
- if (s >= 0)
- template_form(a, names[s]);
- for (size_t i = 0; i < n; i++) {
+ names[n] = xstrdup("");
+ lines[n] = xstrdup("+ Ny mall (^N)");
+ int s = select_list("Mallar", lines, (int)n + 1, cursor, 1, &cursor, 1,
+ n > 0, 0);
+ if (s == -1) {
+ for (size_t i = 0; i <= n; i++) {
free(names[i]);
free(lines[i]);
}
free(names);
free(lines);
free(resp);
- if (s == -2)
- continue;
- break;
+ 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;
}
}
-/* Screen names for Ctrl+R restoration and --screen. Only the top-level
+/* Screen names for ^R restoration and --screen. Only the top-level
views; sub-screens unwind to their parent. */
static void open_scene(struct app *a, const char *scene)
{
@@ -4647,16 +4878,17 @@ static void do_reload(struct app *a, const char *self)
static const char *const MAIN_ITEMS[] = {
"Verifikat", "Underlag (inkorg)", "Ingående balans",
"Mallar", "Rapporter", "Revision",
- "Inställningar", "Företagsuppgifter", "Byt räkenskapsår",
+ "Inställningar", "Företagsuppgifter", "Räkenskapsår",
"Logga ut / avsluta",
};
static void dashboard(struct app *a)
{
+ static int last_sel = 0;
for (;;) {
if (g_reload)
return;
- int sel = menu("Bokf", MAIN_ITEMS, 10, 1);
+ int sel = menu("Bokf", MAIN_ITEMS, 10, 1, &last_sel);
snprintf(g_scene, sizeof g_scene, "%s", "dashboard");
if (g_quit || g_reload)
return;
@@ -4892,7 +5124,7 @@ static int login_screen(struct app *a)
if (field == 3)
attroff(A_REVERSE | A_BOLD);
}
- hints("Tab = byta fält/knapp Enter = logga in Ctrl+C = avsluta");
+ hints("Tab = byta fält/knapp Enter = logga in ^C = avsluta");
refresh();
if (field == 3) {
@@ -5012,7 +5244,7 @@ static int select_org(struct app *a)
free(role);
}
int sel = select_list("Välj organisation att representera", items, (int)n,
- 0, 0, NULL, 0);
+ 0, 0, NULL, 0, 0, 0);
for (size_t i = 0; i < n; i++)
free(items[i]);
free(items);
@@ -5038,6 +5270,12 @@ static void usage(FILE *f)
" --version\n");
}
+static void reset_modify_keys(void)
+{
+ putp("\033[>4m");
+ fflush(stdout);
+}
+
int main(int argc, char **argv)
{
struct app app;
@@ -5092,6 +5330,12 @@ int main(int argc, char **argv)
cbreak();
noecho();
keypad(stdscr, TRUE);
+ /* ^Enter has no control code; enable xterm modifyOtherKeys and bind the
+ sequence it produces. Terminals without it ignore the request. */
+ define_key("\033[27;5;13~", KEY_CTRL_ENTER);
+ putp("\033[>4;1m");
+ fflush(stdout);
+ atexit(reset_modify_keys);
curs_set(1);
const char *env_session = getenv("BOKFD_SESSION");