diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-19 22:33:33 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-19 22:33:33 +0200 |
| commit | e352cd34d30acc1955c62027c779ac98004ac201 (patch) | |
| tree | 8a94163b837fdc16e334c159d91a9f622d42763d /clients/bokftui.c | |
| parent | 84c4fdcf138ad6ef4f7edfea76f2318b7a7a5e3e (diff) | |
| download | bokf-e352cd34d30acc1955c62027c779ac98004ac201.tar.gz bokf-e352cd34d30acc1955c62027c779ac98004ac201.zip | |
tui: migrate the remaining screens to the widget layerv0.1.38
Diffstat (limited to 'clients/bokftui.c')
| -rw-r--r-- | clients/bokftui.c | 2744 |
1 files changed, 1146 insertions, 1598 deletions
diff --git a/clients/bokftui.c b/clients/bokftui.c index 0967f51..af6e284 100644 --- a/clients/bokftui.c +++ b/clients/bokftui.c @@ -806,6 +806,160 @@ done: free(resp); } +struct vdctx { + struct app *a; + int64_t id; + size_t natts; + const char *resp; + int corrected; + int want_refresh; + int posted_new; + int64_t posted_id; +}; + +static int vd_key(void *ud, int ch) +{ + struct vdctx *ctx = ud; + struct app *a = ctx->a; + if (ch == TUI_KEY_CTRL_N) { + int64_t nid = vouchers_new(a); + if (nid > 0) { + ctx->posted_id = nid; + ctx->posted_new = 1; + return TUI_HOOK_BACK; + } + return TUI_HOOK_STAY; + } + if (ch == 'c') { + char reasonbuf[512]; + char *reason = + tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ", + "", 0) + ? reasonbuf + : NULL; + if (reason && *reason) { + 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_int(d, o, "voucher", ctx->id); + yyjson_mut_obj_add_strcpy(d, o, "description", reason); + char *ref = util_random_id("tui-", 8); + yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); + char *cargs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *r = client_rpc(&a->conn, "voucher.correct", a->session, + a->org, cargs); + if (r && client_ok(r)) { + int64_t new_id = jint_val(r, "result.id", 0); + tui_message("Rättat", "Ändringsverifikat %lld skapat", + (long long)new_id); + ctx->corrected = 1; + } else { + show_error("Kunde inte rätta", r); + } + free(r); + free(cargs); + free(ref); + } + return TUI_HOOK_BACK; + } + if (ch == 6) { /* ^F: attach a file to this voucher */ + char *path = file_browser(a, a->attachment_dir); + if (path) { + struct stat sb; + if (stat(path, &sb) == 0 && + (long)sb.st_size > a->max_attachment_bytes) { + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", + (double)sb.st_size / (1024 * 1024), + a->max_attachment_bytes / (1024 * 1024)); + free(path); + } else { + char *b64 = read_file_b64(path); + if (!b64) { + tui_message("Fel", "Kunde inte läsa %s", path); + free(path); + } else { + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + 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, "filename", base); + yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); + yyjson_mut_obj_add_int(d, o, "voucher_id", ctx->id); + char *fargs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + free(b64); + free(path); + char *r = client_rpc(&a->conn, "attachment.put", a->session, + a->org, fargs); + if (r && client_ok(r)) + tui_message("Underlag", "Bifogat."); + else + show_error("Kunde inte bifoga", r); + free(r); + free(fargs); + ctx->want_refresh = 1; + return TUI_HOOK_BACK; + } + } + } + return TUI_HOOK_STAY; + } + if (ch == 'f' && ctx->natts > 0) { + char **alines = xcalloc(ctx->natts, sizeof(char *)); + int64_t *aids = xcalloc(ctx->natts, sizeof(int64_t)); + for (size_t i = 0; i < ctx->natts; i++) { + char path[64]; + snprintf(path, sizeof path, "result.attachments.%zu.id", i); + aids[i] = jint_val(ctx->resp, path, 0); + snprintf(path, sizeof path, "result.attachments.%zu.filename", i); + char *fn = jstr_dup(ctx->resp, path); + alines[i] = xstrdup(fn ? fn : "(namnlös)"); + free(fn); + } + int acursor = 0; + int asel = tui_select_list("Underlag", alines, (int)ctx->natts, 0, 0, + &acursor, 0, "ta bort", 0); + if (asel >= 0) { + attachment_download(a, aids[asel]); + } else if (asel == -6 && acursor >= 0 && + (size_t)acursor < ctx->natts) { + char q[320]; + snprintf(q, sizeof q, "Ta bort '%s' från verifikatet? (j/n): ", + alines[acursor]); + char ansbuf[512]; + char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) + ? ansbuf + : NULL; + if (ans && (ans[0] == 'j' || ans[0] == 'J')) { + char uargs[64]; + snprintf(uargs, sizeof uargs, "{\"id\":%lld,\"voucher_id\":%lld}", + (long long)aids[acursor], (long long)ctx->id); + char *r = client_rpc(&a->conn, "attachment.unlink", a->session, + a->org, uargs); + if (r && client_ok(r)) { + tui_message("Underlag", "Länken borttagen."); + ctx->want_refresh = 1; + for (size_t i = 0; i < ctx->natts; i++) + free(alines[i]); + free(alines); + free(aids); + return TUI_HOOK_REFRESH; + } + show_error("Kunde inte ta bort", r); + free(r); + } + } + for (size_t i = 0; i < ctx->natts; i++) + free(alines[i]); + free(alines); + free(aids); + return TUI_HOOK_STAY; + } + return TUI_HOOK_NONE; +} + static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) { if (out_new) @@ -882,181 +1036,26 @@ static int voucher_detail(struct app *a, int64_t id, int64_t *out_new) } buf_append(&text, "\0", 1); - int nlines = 0; - for (size_t i = 0; i < text.len && text.p[i]; i++) - if (text.p[i] == '\n') - nlines++; - char **lines = xcalloc(nlines + 2, sizeof(char *)); - int n = 0; - char *copy = xstrdup((char *)text.p); - for (char *p = copy;;) { - char *nl = strchr(p, '\n'); - if (nl) - *nl = '\0'; - lines[n++] = p; - if (!nl) - break; - p = nl + 1; - } - int top = 0, view = LINES - 4; - int want_refresh = 0, corrected = 0, stop = 0, posted_new = 0; - int64_t posted_id = 0; - while (!stop) { - char hint[256]; - snprintf(hint, sizeof hint, - "upp/ned F5 = uppdatera ^N = nytt verifikat c =" - " rätta ^F = bifoga%s Esc/q = tillbaka", - natts ? " f = underlag" : ""); - tui_frame("Verifikat"); - for (int i = 0; i < view && top + i < n; i++) - mvaddnstr(2 + i, 2, lines[top + i], COLS - 4); - tui_hints(hint); - refresh(); - int ch = ui_getch(); - if (ch == 'q' || ch == 27) { - stop = 1; - } else if (ch == KEY_F(5)) { - want_refresh = 1; - stop = 1; - } else if (ch == KEY_CTRL_N) { - int64_t nid = vouchers_new(a); - if (nid > 0) { - posted_id = nid; - posted_new = 1; - stop = 1; - } - } else if (ch == 'c') { - char reasonbuf[512]; char *reason = tui_prompt_into(reasonbuf, sizeof reasonbuf, "Beskriv rättelsen: ", "", 0) ? reasonbuf : NULL; - if (reason && *reason) { - 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_int(d, o, "voucher", id); - yyjson_mut_obj_add_strcpy(d, o, "description", reason); - char *ref = util_random_id("tui-", 8); - yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); - char *cargs = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *r = client_rpc(&a->conn, "voucher.correct", a->session, - a->org, cargs); - if (r && client_ok(r)) { - int64_t new_id = jint_val(r, "result.id", 0); - tui_message("Rättat", "Ändringsverifikat %lld skapat", - (long long)new_id); - corrected = 1; - } else { - show_error("Kunde inte rätta", r); - } - free(r); - free(cargs); - free(ref); - } - stop = 1; - } else if (ch == 6) { /* ^F: attach a file to this voucher */ - char *path = file_browser(a, a->attachment_dir); - if (path) { - struct stat sb; - if (stat(path, &sb) == 0 && - (long)sb.st_size > a->max_attachment_bytes) { - tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", - (double)sb.st_size / (1024 * 1024), - a->max_attachment_bytes / (1024 * 1024)); - free(path); - } else { - char *b64 = read_file_b64(path); - if (!b64) { - tui_message("Fel", "Kunde inte läsa %s", path); - free(path); - } else { - const char *base = strrchr(path, '/'); - base = base ? base + 1 : path; - 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, "filename", base); - yyjson_mut_obj_add_strcpy(d, o, "content_base64", - b64); - yyjson_mut_obj_add_int(d, o, "voucher_id", id); - char *fargs = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - free(b64); - free(path); - char *r = client_rpc(&a->conn, "attachment.put", - a->session, a->org, fargs); - if (r && client_ok(r)) - tui_message("Underlag", "Bifogat."); - else - show_error("Kunde inte bifoga", r); - free(r); - free(fargs); - want_refresh = 1; - stop = 1; - } - } - } - } else if (ch == 'f' && natts > 0) { - char **alines = xcalloc(natts, sizeof(char *)); - int64_t *aids = xcalloc(natts, sizeof(int64_t)); - for (size_t i = 0; i < natts; i++) { - char path[64]; - snprintf(path, sizeof path, "result.attachments.%zu.id", - i); - aids[i] = jint_val(resp, path, 0); - snprintf(path, sizeof path, - "result.attachments.%zu.filename", i); - char *fn = jstr_dup(resp, path); - alines[i] = xstrdup(fn ? fn : "(namnlös)"); - free(fn); - } - int acursor = 0; - int asel = tui_select_list("Underlag", alines, (int)natts, 0, 0, - &acursor, 0, "ta bort", 0); - if (asel >= 0) - attachment_download(a, aids[asel]); - else if (asel == -6 && acursor >= 0 && - (size_t)acursor < natts) { - char q[320]; - snprintf(q, sizeof q, - "Ta bort '%s' från verifikatet? (j/n): ", - alines[acursor]); - char ansbuf[512]; char *ans = tui_prompt_into(ansbuf, sizeof ansbuf, q, "n", 0) ? ansbuf : NULL; - if (ans && (ans[0] == 'j' || ans[0] == 'J')) { - char uargs[64]; - snprintf(uargs, sizeof uargs, - "{\"id\":%lld,\"voucher_id\":%lld}", - (long long)aids[acursor], (long long)id); - char *r = client_rpc(&a->conn, "attachment.unlink", - a->session, a->org, uargs); - if (r && client_ok(r)) { - tui_message("Underlag", "Länken borttagen."); - want_refresh = 1; - stop = 1; - } else { - show_error("Kunde inte ta bort", r); - } - free(r); - } - } - for (size_t i = 0; i < natts; i++) - free(alines[i]); - free(alines); - free(aids); - } else if (ch == KEY_DOWN && top + view < n) { - top++; - } else if (ch == KEY_UP && top > 0) { - top--; - } else if (ch == KEY_NPAGE) { - top += view; - } else if (ch == KEY_PPAGE) { - top -= view; - } - if (top + view > n) - top = n > view ? n - view : 0; - if (top < 0) - top = 0; - } - free(copy); - free(lines); + struct vdctx ctx; + memset(&ctx, 0, sizeof ctx); + ctx.a = a; + ctx.id = id; + ctx.natts = natts; + ctx.resp = resp; + char hint[256]; + snprintf(hint, sizeof hint, + "^N = nytt verifikat c = rätta ^F = bifoga%s", + natts ? " f = underlag" : ""); + int want_refresh = 0, corrected = 0; + int ret = tui_pager_hook("Verifikat", (const char *)text.p, hint, 0, + vd_key, &ctx); + if (ret == 1) + want_refresh = 1; + if (ctx.want_refresh) + want_refresh = 1; + corrected = ctx.corrected; + int posted_new = ctx.posted_new; + int64_t posted_id = ctx.posted_id; free(series); free(date); free(desc); @@ -1081,43 +1080,6 @@ struct tui_vrow { char text[64]; }; -static int row_has_content(const struct tui_vrow *r) -{ - return r->account[0] || r->debit[0] || r->credit[0] || r->text[0]; -} - -/* Keeps exactly one empty trailing row: appends one when the last row has - content, and collapses an empty row that is followed by another empty - row (the row the user just cleared). Adjusts *field so the selection - stays on the same logical cell. */ -static void normalize_rows(struct tui_vrow *rows, int *nrows, int *field) -{ - while (*nrows < 64 && row_has_content(&rows[*nrows - 1])) { - memset(&rows[*nrows], 0, sizeof rows[0]); - (*nrows)++; - } - int i = 0; - while (i < *nrows - 1) { - if (!row_has_content(&rows[i]) && !row_has_content(&rows[i + 1])) { - int fr = *field >= 3 ? (*field - 3) / 4 : -1; - if (fr > i) - *field -= 4; - memmove(&rows[i], &rows[i + 1], - (size_t)(*nrows - i - 1) * sizeof rows[0]); - memset(&rows[*nrows - 1], 0, sizeof rows[0]); - (*nrows)--; - continue; - } - i++; - } - if (*nrows < 1) { - *nrows = 1; - memset(&rows[0], 0, sizeof rows[0]); - if (*field >= 3) - *field = 3; - } -} - struct acct_cache { int loaded; int64_t org; @@ -1190,546 +1152,470 @@ static int acct_exists(struct app *a, const char *number) return 0; } -static int64_t vouchers_new(struct app *a) -{ - static struct tui_vrow rows[64]; - char date[16], desc[256], series[16], client_ref[64]; + +struct vform { + struct app *a; + struct tui_rt rt; + struct tui_vrow rows[64]; int64_t att_ids[32]; char att_names[32][80]; - int natt = 0; - curs_set(1); - memset(rows, 0, sizeof rows); - snprintf(date, sizeof date, "%s", - a->fy_start[0] ? a->fy_start : "2026-01-01"); - desc[0] = '\0'; - snprintf(series, sizeof series, "%s", a->default_series); - snprintf(client_ref, sizeof client_ref, "%s", util_random_id("tui-", 8)); - int nrows = 1; - int field = 0; - int field_fresh = 1; - size_t field_pos = (size_t)-1; - int nfields = 3 + 4 * nrows; - const int x_account = 2; - const int name_w = 24; - const int x_name = 9; - const int x_debit = 34; - const int x_credit = 46; - const int x_text = 58; - int text_w = COLS - x_text - 2; - if (text_w < 8) - text_w = 8; + int natt; + char date[16]; + char series[16]; + char desc[256]; + char client_ref[64]; +}; - for (;;) { - tui_frame("Nytt verifikat"); - int caret_y = -1, caret_x = -1; - attron(A_BOLD); - mvaddstr(2, 2, "Datum"); - mvaddstr(2, 22, "Serie"); - mvaddstr(4, 2, "Text"); - attroff(A_BOLD); - { - /* field order is 0 = date, 1 = series, 2 = text */ - const char *hvals[3] = { date, series, desc }; - int hx[3] = { 9, 29, 9 }; - int hy[3] = { 2, 2, 4 }; - int hw[3] = { 10, 8, COLS - 12 }; - for (int k = 0; k < 3; k++) { - int w = hw[k] > 1 ? hw[k] : 1; - char padded[512]; - snprintf(padded, sizeof padded, "%s", hvals[k]); - tui_pad_field(padded, sizeof padded, w); - if (field == k) { - attron(A_REVERSE); - caret_y = hy[k]; - size_t slen = strlen(hvals[k]); - size_t cp = (field_fresh || field_pos == (size_t)-1 || - field_pos > slen) - ? slen - : field_pos; - int cw = (int)tui_disp_width_n(hvals[k], cp); - caret_x = hx[k] + (cw > w ? w : cw); - } - mvaddstr(hy[k], hx[k], padded); - if (field == k) - attroff(A_REVERSE); - } - } - attron(A_BOLD); - mvaddstr(6, x_account, "Konto"); - mvaddstr(6, x_name, "Namn"); - mvaddstr(6, x_debit, "Debet"); - mvaddstr(6, x_credit, "Kredit"); - mvaddstr(6, x_text, "Text"); - attroff(A_BOLD); +static void vform_cell(void *ud, int row, int col, char **buf, size_t *cap) +{ + struct vform *vf = ud; + struct tui_vrow *r = &vf->rows[row]; + if (col == 0) { + *buf = r->account; + *cap = sizeof r->account; + } else if (col == 2) { + *buf = r->debit; + *cap = sizeof r->debit; + } else if (col == 3) { + *buf = r->credit; + *cap = sizeof r->credit; + } else { + *buf = r->text; + *cap = sizeof r->text; + } +} - int64_t sum_d = 0, sum_c = 0; - for (int i = 0; i < nrows; i++) { - int y = 7 + i; - int64_t v; - if (tui_parse_kr(rows[i].debit, &v) == 0) - sum_d += v; - if (tui_parse_kr(rows[i].credit, &v) == 0) - sum_c += v; - int sel_ci = -1; - if (field >= 3) { - int ri = (field - 3) / 4, ci = (field - 3) % 4; - if (ri == i) - sel_ci = ci; - } - const char *vals[4] = { rows[i].account, rows[i].debit, - rows[i].credit, rows[i].text }; - int xs[4] = { x_account, x_debit, x_credit, x_text }; - int ws[4] = { 6, 11, 11, text_w }; - for (int k = 0; k < 4; k++) { - int w = ws[k] > 1 ? ws[k] : 1; - char padded[512]; - snprintf(padded, sizeof padded, "%s", vals[k]); - tui_pad_field(padded, sizeof padded, w); - if (k == sel_ci) { - attron(A_REVERSE); - caret_y = y; - size_t slen = strlen(vals[k]); - size_t cp = (field_fresh || field_pos == (size_t)-1 || - field_pos > slen) - ? slen - : field_pos; - int cw = (int)tui_disp_width_n(vals[k], cp); - caret_x = xs[k] + (cw > w ? w : cw); - } - mvaddstr(y, xs[k], padded); - if (k == sel_ci) - attroff(A_REVERSE); - } - { - char nbuf[512]; - const char *nm = acct_name(a, rows[i].account); - snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - tui_pad_field(nbuf, sizeof nbuf, name_w); - attron(A_DIM); - mvaddstr(y, x_name, nbuf); - attroff(A_DIM); - } +static void vform_info(void *ud, int row, char *buf, size_t n) +{ + struct vform *vf = ud; + const char *nm = acct_name(vf->a, vf->rows[row].account); + snprintf(buf, n, "%s", nm ? nm : ""); +} + +static void vform_footer(void *ud, char *buf, size_t n) +{ + struct vform *vf = ud; + int64_t sum_d = 0, sum_c = 0; + for (int i = 0; i < vf->rt.nrows; i++) { + int64_t v; + if (tui_parse_kr(vf->rows[i].debit, &v) == 0) + sum_d += v; + if (tui_parse_kr(vf->rows[i].credit, &v) == 0) + sum_c += v; + } + char d1[32], c1[32], diff[32], line[1024]; + tui_kr_format(sum_d, d1, sizeof d1); + tui_kr_format(sum_c, c1, sizeof c1); + tui_kr_format(sum_d - sum_c, diff, sizeof diff); + int balanced = sum_d == sum_c; + struct buf t; + buf_init(&t); + if (vf->natt > 0) { + snprintf(line, sizeof line, "Bilagor: "); + for (int i = 0; i < vf->natt; i++) { + size_t used = strlen(line); + if (used + 1 >= sizeof line) + break; + snprintf(line + used, sizeof line - used, "%.79s%s", + vf->att_names[i], i + 1 < vf->natt ? ", " : ""); } - char d1[32], c1[32], diff[32]; - tui_kr_format(sum_d, d1, sizeof d1); - tui_kr_format(sum_c, c1, sizeof c1); - tui_kr_format(sum_d - sum_c, diff, sizeof diff); - int balanced = sum_d == sum_c; - move(7 + nrows, 2); - clrtoeol(); - if (natt > 0) { - char aline[4096] = "Bilagor: "; - for (int i = 0; i < natt; i++) { - size_t used = strlen(aline); - if (used + 1 >= sizeof aline) - break; - snprintf(aline + used, sizeof aline - used, "%.79s%s", - att_names[i], i + 1 < natt ? ", " : ""); + buf_append(&t, line, strlen(line)); + buf_append(&t, "\n", 1); + } + snprintf(line, sizeof line, "Summa debet %12s kredit %12s %s", d1, c1, + balanced ? "I BALANS" : "DIFF"); + buf_append(&t, line, strlen(line)); + if (!balanced) { + snprintf(line, sizeof line, "\nDifferens: %s", diff); + buf_append(&t, line, strlen(line)); + } + buf_append(&t, "\0", 1); + snprintf(buf, n, "%s", (char *)t.p); + buf_free(&t); +} + +static void vform_attach(struct vform *vf) +{ + struct app *a = vf->a; + char *path = file_browser(a, a->attachment_dir); + if (!path) + return; + struct stat sb; + if (stat(path, &sb) == 0 && + (long)sb.st_size > a->max_attachment_bytes) { + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", + (double)sb.st_size / (1024 * 1024), + a->max_attachment_bytes / (1024 * 1024)); + free(path); + return; + } + char *b64 = read_file_b64(path); + if (!b64) { + tui_message("Bilaga", "Kunde inte läsa %s", path); + free(path); + return; + } + if (vf->natt >= 32) { + tui_message("Bilaga", "Max 32 bilagor per verifikat."); + free(b64); + free(path); + return; + } + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + 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, "filename", base); + yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + size_t need = strlen(args) + 256; + char *raw = xmalloc(need); + snprintf(raw, need, + "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\"," + "\"session\":\"%s\",\"org\":%lld,\"args\":%s}", + a->session, (long long)a->org, args); + free(args); + char *r = NULL; + if (client_send_line(&a->conn, raw) == 0) + r = client_read_line(&a->conn); + free(raw); + if (r && client_ok(r)) { + vf->att_ids[vf->natt] = jint_val(r, "result.id", 0); + snprintf(vf->att_names[vf->natt], sizeof vf->att_names[vf->natt], + "%s", base); + vf->natt++; + } else { + show_error("Kunde inte bifoga filen", r); + } + free(r); + free(b64); + free(path); +} + +static void vform_template(struct vform *vf) +{ + struct app *a = vf->a; + char *lresp = client_rpc(&a->conn, "template.list", a->session, a->org, + "{\"active_only\":true}"); + if (!lresp || !client_ok(lresp)) { + show_error("Mallar", lresp); + free(lresp); + return; + } + size_t ln = jarr_size(lresp, "result.items"); + if (ln == 0) { + tui_message("Mallar", + "Inga mallar ännu. Skapa en under Mallar först."); + free(lresp); + return; + } + char **names = xcalloc(ln, sizeof(char *)); + char **lines = xcalloc(ln, sizeof(char *)); + for (size_t i = 0; i < ln; i++) { + char path[64], line[512]; + snprintf(path, sizeof path, "result.items.%zu.name", i); + char *nm = jstr_dup(lresp, path); + snprintf(path, sizeof path, "result.items.%zu.series", i); + char *ser = jstr_dup(lresp, path); + snprintf(path, sizeof path, "result.items.%zu.row_count", i); + int64_t rc = jint_val(lresp, path, 0); + snprintf(path, sizeof path, "result.items.%zu.description", i); + char *ds = jstr_dup(lresp, 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); + } + int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, + NULL, 0); + char tname[128] = ""; + if (tsel >= 0) + snprintf(tname, sizeof tname, "%s", names[tsel]); + for (size_t i = 0; i < ln; i++) { + free(names[i]); + free(lines[i]); + } + free(names); + free(lines); + free(lresp); + if (tsel < 0) + return; + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *to = yyjson_mut_obj(d); + yyjson_mut_doc_set_root(d, to); + yyjson_mut_obj_add_strcpy(d, to, "name", tname); + char *targs = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *resp = + client_rpc(&a->conn, "template.get", a->session, a->org, targs); + free(targs); + if (!resp || !client_ok(resp)) { + show_error("Mall", resp); + free(resp); + return; + } + char xsbuf[512]; + char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "", + 0) + ? xsbuf + : NULL; + double xv = 0; + if (xs && *xs && !parse_x_double(xs, &xv)) { + tui_message("Fel", "Ogiltigt belopp."); + free(resp); + return; + } + size_t n = jarr_size(resp, "result.rows"); + char **accts = xcalloc(n ? n : 1, sizeof(char *)); + char **forms = xcalloc(n ? n : 1, sizeof(char *)); + char **descs = xcalloc(n ? n : 1, sizeof(char *)); + struct template_row *in = xcalloc(n ? n : 1, sizeof *in); + for (size_t i = 0; i < n; i++) { + char path[64]; + snprintf(path, sizeof path, "result.rows.%zu.account", i); + accts[i] = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.rows.%zu.formula", i); + forms[i] = jstr_dup(resp, path); + snprintf(path, sizeof path, "result.rows.%zu.description", i); + descs[i] = jstr_dup(resp, path); + in[i].account = accts[i] ? accts[i] : ""; + in[i].formula = forms[i] ? forms[i] : ""; + in[i].description = descs[i] && *descs[i] ? descs[i] : NULL; + } + struct resolved_row *out = xcalloc(n ? n : 1, sizeof *out); + size_t on = 0; + char ferr[256] = ""; + if (formula_resolve_rows(in, n, xv, out, &on, ferr, sizeof ferr) != 0) { + tui_message("Mall", "%s", + ferr[0] ? ferr : "kunde inte lösa mallen"); + } else { + memset(vf->rows, 0, sizeof vf->rows); + int use = (int)(on > 64 ? 64 : on); + for (int i = 0; i < use; i++) { + snprintf(vf->rows[i].account, sizeof vf->rows[i].account, "%s", + out[i].account); + char tmp[32]; + if (out[i].debit_ore) { + tui_kr_format(out[i].debit_ore, tmp, sizeof tmp); + snprintf(vf->rows[i].debit, sizeof vf->rows[i].debit, "%s", + tmp); + } else { + tui_kr_format(out[i].credit_ore, tmp, sizeof tmp); + snprintf(vf->rows[i].credit, sizeof vf->rows[i].credit, "%s", + tmp); } - attron(A_DIM); - mvaddnstr(7 + nrows, 2, aline, COLS - 4); - attroff(A_DIM); + if (out[i].description[0]) + snprintf(vf->rows[i].text, sizeof vf->rows[i].text, "%s", + out[i].description); } - attron(A_BOLD); - mvprintw(7 + nrows + 1, 2, "Summa debet %12s kredit %12s ", d1, c1); - mvprintw(7 + nrows + 1, 58, "%s", balanced ? "I BALANS" : "DIFF"); - attroff(A_BOLD); - if (!balanced) - mvprintw(7 + nrows + 2, 2, "Differens: %s", diff); - tui_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(); + vf->rt.nrows = use > 0 ? use : 1; + char *tds = jstr_dup(resp, "result.description"); + char *tser = jstr_dup(resp, "result.series"); + if (!vf->desc[0] && tds && *tds) + replace_x_into(tds, xv, vf->desc, sizeof vf->desc); + if (tser && *tser) + snprintf(vf->series, sizeof vf->series, "%s", tser); + free(tds); + free(tser); + vf->rt.row = 0; + vf->rt.col = 0; + vf->rt.fresh = 1; + vf->rt.pos = (size_t)-1; + tui_rt_normalize(&vf->rt); + } + free(out); + for (size_t i = 0; i < n; i++) { + free(accts[i]); + free(forms[i]); + free(descs[i]); + } + free(accts); + free(forms); + free(descs); + free(in); + free(resp); +} - int ch = ui_getch(); - if (ch == 27) { - curs_set(0); - return 0; - } - if (ch == '\t') { - field = (field + 1) % nfields; - field_fresh = 1; - continue; - } - if (ch == KEY_BTAB) { - field = (field + nfields - 1) % nfields; - field_fresh = 1; +/* Returns the posted voucher id, or 0 to stay in the form. */ +static int64_t vform_post(struct vform *vf, int dry) +{ + struct app *a = vf->a; + 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, "date", vf->date); + yyjson_mut_obj_add_strcpy(d, o, "description", vf->desc); + yyjson_mut_obj_add_strcpy(d, o, "series", vf->series); + yyjson_mut_obj_add_strcpy(d, o, "client_ref", vf->client_ref); + yyjson_mut_val *arr = yyjson_mut_arr(d); + for (int i = 0; i < vf->rt.nrows; i++) { + if (!vf->rows[i].account[0]) continue; + int64_t dv = 0, cv = 0; + if (tui_parse_kr(vf->rows[i].debit, &dv) != 0 || + tui_parse_kr(vf->rows[i].credit, &cv) != 0) { + tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1); + yyjson_mut_doc_free(d); + return 0; } - if (ch == KEY_UP) { - if (field >= 3) { - int ci = (field - 3) % 4; - int ri = (field - 3) / 4; - if (ri > 0) { - field = 3 + (ri - 1) * 4 + ci; - field_fresh = 1; - } + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); + yyjson_mut_obj_add_strcpy(d, ro, "account", vf->rows[i].account); + yyjson_mut_obj_add_int(d, ro, "debit_ore", dv); + yyjson_mut_obj_add_int(d, ro, "credit_ore", cv); + if (vf->rows[i].text[0]) + yyjson_mut_obj_add_strcpy(d, ro, "description", + vf->rows[i].text); + } + yyjson_mut_obj_add_val(d, o, "rows", arr); + if (vf->natt > 0) { + yyjson_mut_val *aa = yyjson_mut_arr(d); + for (int i = 0; i < vf->natt; i++) + yyjson_mut_arr_add_int(d, aa, vf->att_ids[i]); + yyjson_mut_obj_add_val(d, o, "attachment_ids", aa); + } + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + if (dry) { + char *raw = xmalloc(strlen(args) + 256); + sprintf(raw, + "{\"v\":1,\"id\":\"tui\",\"cmd\":\"voucher.post\"," + "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true," + "\"args\":%s}", + a->session, (long long)a->org, args); + if (client_send_line(&a->conn, raw) == 0) { + char *r = client_read_line(&a->conn); + if (r && client_ok(r)) { + int64_t num = jint_val(r, "result.number", 0); + tui_message("Validering OK", + "Nummer %lld skulle tilldelas.", (long long)num); + } else { + show_error("Validering misslyckades", r); } - continue; + free(r); + } else { + tui_message("Fel", "Kunde inte nå servern"); } - if (ch == KEY_DOWN) { - if (field >= 3) { - int ci = (field - 3) % 4; - int ri = (field - 3) / 4; - if (ri + 1 < nrows) { - field = 3 + (ri + 1) * 4 + ci; - field_fresh = 1; - } - } - continue; + free(raw); + } else { + char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org, + args); + if (r && client_ok(r)) { + int64_t id = jint_val(r, "result.id", 0); + int64_t num = jint_val(r, "result.number", 0); + char *ser = jstr_dup(r, "result.series"); + int replayed = jbool_val(r, "result.replayed", 0); + tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "", + (long long)num, (long long)id, + replayed ? " — redan bokfört (idempotent)" : ""); + free(ser); + free(r); + free(args); + return id; } - if (ch == KEY_F(4)) { - char tname[128] = ""; - char *lresp = client_rpc(&a->conn, "template.list", a->session, - a->org, "{\"active_only\":true}"); - if (!lresp || !client_ok(lresp)) { - show_error("Mallar", lresp); - free(lresp); + show_error("Kunde inte bokföra", r); + free(r); + } + free(args); + return 0; +} + +static int vform_key(void *ud, int ch) +{ + struct vform *vf = ud; + if (ch == KEY_F(4)) { + vform_template(vf); + return TUI_HOOK_STAY; + } + if (ch == 6) { + vform_attach(vf); + return TUI_HOOK_STAY; + } + return TUI_HOOK_NONE; +} + +static int64_t vouchers_new(struct app *a) +{ + struct vform vf; + memset(&vf, 0, sizeof vf); + vf.a = a; + snprintf(vf.date, sizeof vf.date, "%s", + a->fy_start[0] ? a->fy_start : "2026-01-01"); + snprintf(vf.series, sizeof vf.series, "%s", a->default_series); + char *ref = util_random_id("tui-", 8); + snprintf(vf.client_ref, sizeof vf.client_ref, "%s", ref); + free(ref); + int text_w = COLS - 60; + if (text_w < 8) + text_w = 8; + struct tui_rt_col cols[5] = { + { "Konto", 2, 6, TUI_RT_EDIT }, + { "Namn", 9, 24, TUI_RT_INFO }, + { "Debet", 34, 11, TUI_RT_EDIT }, + { "Kredit", 46, 11, TUI_RT_EDIT }, + { "Text", 58, text_w, TUI_RT_EDIT }, + }; + tui_rt_init(&vf.rt, 1, 64, 5, cols, 7, vform_cell, vform_info, &vf); + tui_rt_set_footer(&vf.rt, vform_footer); + tui_rt_set_key(&vf.rt, vform_key); + struct tui_form_field ff[3]; + memset(ff, 0, sizeof ff); + ff[0].label = "Datum"; + ff[0].value = vf.date; + ff[0].cap = sizeof vf.date; + ff[0].kind = TUI_F_DATE; + ff[1].label = "Serie"; + ff[1].value = vf.series; + ff[1].cap = sizeof vf.series; + ff[1].kind = TUI_F_TEXT; + ff[2].label = "Text"; + ff[2].value = vf.desc; + ff[2].cap = sizeof vf.desc; + ff[2].kind = TUI_F_TEXT; + const char *hint = "F4 = mall ^F = bifoga fil Tab = byta fält F5 =" + " validera ^X = rensa rad ^Enter = bokför Esc =" + " avbryt"; + int stage = 0; + for (;;) { + if (stage == 0) { + int r = tui_form_run_hook("Nytt verifikat", ff, 3, 1, vform_key, + &vf); + if (r == TUI_FORM_BACK) + return 0; + if (r == TUI_FORM_REFRESH) { + vform_post(&vf, 1); continue; } - size_t ln = jarr_size(lresp, "result.items"); - if (ln == 0) { - tui_message("Mallar", - "Inga mallar ännu. Skapa en under Mallar först."); - free(lresp); + if (r == TUI_FORM_SUBMIT) { + int64_t id = vform_post(&vf, 0); + if (id > 0) + return id; continue; } - char **names = xcalloc(ln, sizeof(char *)); - char **lines = xcalloc(ln, sizeof(char *)); - for (size_t i = 0; i < ln; i++) { - char path[64], line[512]; - snprintf(path, sizeof path, "result.items.%zu.name", i); - char *nm = jstr_dup(lresp, path); - snprintf(path, sizeof path, "result.items.%zu.series", i); - char *ser = jstr_dup(lresp, path); - snprintf(path, sizeof path, "result.items.%zu.row_count", i); - int64_t rc = jint_val(lresp, path, 0); - snprintf(path, sizeof path, "result.items.%zu.description", - i); - char *ds = jstr_dup(lresp, 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); - } - int tsel = tui_select_list("Välj mall", lines, (int)ln, 0, 1, NULL, 0, NULL, 0); - if (tsel >= 0) - snprintf(tname, sizeof tname, "%s", names[tsel]); - for (size_t i = 0; i < ln; i++) { - free(names[i]); - free(lines[i]); - } - free(names); - free(lines); - free(lresp); - if (tsel < 0) + stage = 1; + } else { + int rr = tui_rt_run("Nytt verifikat", &vf.rt, hint); + if (rr == -1) { + stage = 0; continue; - { - char tbuf[128]; - snprintf(tbuf, sizeof tbuf, "%s", tname); - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *to = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, to); - yyjson_mut_obj_add_strcpy(d, to, "name", tbuf); - char *targs = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *resp = client_rpc(&a->conn, "template.get", a->session, - a->org, targs); - free(targs); - if (!resp || !client_ok(resp)) { - show_error("Mall", resp); - free(resp); - } else { - char xsbuf[512]; char *xs = tui_prompt_into(xsbuf, sizeof xsbuf, "Belopp (x, kr): ", "", 0) ? xsbuf : NULL; - double xv = 0; - if (xs && *xs && !parse_x_double(xs, &xv)) { - tui_message("Fel", "Ogiltigt belopp."); - free(resp); - continue; - } - size_t n = jarr_size(resp, "result.rows"); - char **accts = xcalloc(n ? n : 1, sizeof(char *)); - char **forms = xcalloc(n ? n : 1, sizeof(char *)); - char **descs = xcalloc(n ? n : 1, sizeof(char *)); - struct template_row *in = xcalloc(n ? n : 1, sizeof *in); - for (size_t i = 0; i < n; i++) { - char path[64]; - snprintf(path, sizeof path, "result.rows.%zu.account", - i); - accts[i] = jstr_dup(resp, path); - snprintf(path, sizeof path, "result.rows.%zu.formula", - i); - forms[i] = jstr_dup(resp, path); - snprintf(path, sizeof path, - "result.rows.%zu.description", i); - descs[i] = jstr_dup(resp, path); - in[i].account = accts[i] ? accts[i] : ""; - in[i].formula = forms[i] ? forms[i] : ""; - in[i].description = - descs[i] && *descs[i] ? descs[i] : NULL; - } - struct resolved_row *out = - xcalloc(n ? n : 1, sizeof *out); - size_t on = 0; - char ferr[256] = ""; - if (formula_resolve_rows(in, n, xv, out, &on, ferr, - sizeof ferr) != 0) { - tui_message("Mall", "%s", - ferr[0] ? ferr : "kunde inte lösa mallen"); - } else { - memset(rows, 0, sizeof rows); - int use = (int)(on > 64 ? 64 : on); - for (int i = 0; i < use; i++) { - snprintf(rows[i].account, sizeof rows[i].account, - "%s", out[i].account); - char tmp[32]; - if (out[i].debit_ore) { - tui_kr_format(out[i].debit_ore, tmp, sizeof tmp); - snprintf(rows[i].debit, sizeof rows[i].debit, - "%s", tmp); - } else { - tui_kr_format(out[i].credit_ore, tmp, - sizeof tmp); - snprintf(rows[i].credit, - sizeof rows[i].credit, "%s", tmp); - } - if (out[i].description[0]) - snprintf(rows[i].text, sizeof rows[i].text, - "%s", out[i].description); - } - nrows = use > 0 ? use : 1; - char *tds = jstr_dup(resp, "result.description"); - char *tser = jstr_dup(resp, "result.series"); - if (!desc[0] && tds && *tds) - replace_x_into(tds, xv, desc, sizeof desc); - if (tser && *tser) - snprintf(series, sizeof series, "%s", tser); - free(tds); - free(tser); - field = 3; - field_fresh = 1; - normalize_rows(rows, &nrows, &field); - nfields = 3 + 4 * nrows; - } - free(out); - for (size_t i = 0; i < n; i++) { - free(accts[i]); - free(forms[i]); - free(descs[i]); - } - free(accts); - free(forms); - free(descs); - free(in); - free(resp); - } - } - continue; - } - if (ch == 6) { /* ^F: attach a file */ - char *path = file_browser(a, a->attachment_dir); - if (path) { - struct stat sb; - if (stat(path, &sb) == 0 && - (long)sb.st_size > a->max_attachment_bytes) { - tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", - (double)sb.st_size / (1024 * 1024), - a->max_attachment_bytes / (1024 * 1024)); - free(path); - continue; - } - char *b64 = read_file_b64(path); - if (!b64) { - tui_message("Bilaga", "Kunde inte läsa %s", path); - } else if (natt >= 32) { - tui_message("Bilaga", "Max 32 bilagor per verifikat."); - } else { - const char *base = strrchr(path, '/'); - base = base ? base + 1 : path; - 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, "filename", base); - yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - size_t need = strlen(args) + 256; - char *raw = xmalloc(need); - snprintf(raw, need, - "{\"v\":1,\"id\":\"tui\",\"cmd\":" - "\"attachment.put\",\"session\":\"%s\"," - "\"org\":%lld,\"args\":%s}", - a->session, (long long)a->org, args); - free(args); - char *r = NULL; - if (client_send_line(&a->conn, raw) == 0) - r = client_read_line(&a->conn); - free(raw); - if (r && client_ok(r)) { - att_ids[natt] = jint_val(r, "result.id", 0); - snprintf(att_names[natt], sizeof att_names[natt], "%s", - base); - natt++; - } else { - show_error("Kunde inte bifoga filen", r); - } - free(r); - } - free(b64); - free(path); - } - continue; - } - if (ch == KEY_CTRL_X) { - if (field >= 3) { - int ri = (field - 3) / 4; - if (ri < nrows) { - memset(&rows[ri], 0, sizeof rows[ri]); - normalize_rows(rows, &nrows, &field); - nfields = 3 + 4 * nrows; - field_fresh = 1; - } - } - continue; - } - - /* build args for validate/post */ - 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); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - yyjson_mut_obj_add_strcpy(d, o, "date", date); - yyjson_mut_obj_add_strcpy(d, o, "description", desc); - yyjson_mut_obj_add_strcpy(d, o, "series", series); - yyjson_mut_obj_add_strcpy(d, o, "client_ref", client_ref); - yyjson_mut_val *arr = yyjson_mut_arr(d); - for (int i = 0; i < nrows; i++) { - if (!rows[i].account[0]) - continue; - int64_t dv = 0, cv = 0; - if (tui_parse_kr(rows[i].debit, &dv) != 0 || - tui_parse_kr(rows[i].credit, &cv) != 0) { - tui_message("Fel", "Ogiltigt belopp på rad %d", i + 1); - yyjson_mut_doc_free(d); - goto next_key; - } - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", rows[i].account); - yyjson_mut_obj_add_int(d, ro, "debit_ore", dv); - yyjson_mut_obj_add_int(d, ro, "credit_ore", cv); - if (rows[i].text[0]) - yyjson_mut_obj_add_strcpy(d, ro, "description", - rows[i].text); } - yyjson_mut_obj_add_val(d, o, "rows", arr); - if (natt > 0) { - yyjson_mut_val *aa = yyjson_mut_arr(d); - for (int i = 0; i < natt; i++) - yyjson_mut_arr_add_int(d, aa, att_ids[i]); - yyjson_mut_obj_add_val(d, o, "attachment_ids", aa); + if (rr == -2) { + vform_post(&vf, 1); + continue; } - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - if (do_check) { - char *raw = xmalloc(strlen(args) + 256); - sprintf(raw, - "{\"v\":1,\"id\":\"tui\",\"cmd\":\"voucher.post\"," - "\"session\":\"%s\",\"org\":%lld,\"dry_run\":true," - "\"args\":%s}", - a->session, (long long)a->org, args); - if (client_send_line(&a->conn, raw) == 0) { - char *r = client_read_line(&a->conn); - if (r && client_ok(r)) { - int64_t num = jint_val(r, "result.number", 0); - tui_message("Validering OK", - "Nummer %lld skulle tilldelas.", (long long)num); - } else { - show_error("Validering misslyckades", r); - } - free(r); - } else { - tui_message("Fel", "Kunde inte nå servern"); - } - free(raw); - } else { - char *r = client_rpc(&a->conn, "voucher.post", a->session, a->org, - args); - if (r && client_ok(r)) { - int64_t id = jint_val(r, "result.id", 0); - int64_t num = jint_val(r, "result.number", 0); - char *ser = jstr_dup(r, "result.series"); - int replayed = jbool_val(r, "result.replayed", 0); - tui_message("Bokfört", "%s%lld (id %lld)%s", ser ? ser : "", - (long long)num, (long long)id, - replayed ? " — redan bokfört (idempotent)" : ""); - free(ser); - free(r); - free(args); - curs_set(0); + if (rr == -3) { + int64_t id = vform_post(&vf, 0); + if (id > 0) return id; - } - show_error("Kunde inte bokföra", r); - free(r); } - free(args); - next_key: - continue; } - - /* text input into current field */ - char *buf = NULL; - size_t cap = 0; - if (field == 0) { - buf = date; - cap = sizeof date; - } else if (field == 1) { - buf = series; - cap = sizeof series; - } else if (field == 2) { - buf = desc; - cap = sizeof desc; - } else { - int ri = (field - 3) / 4, ci = (field - 3) % 4; - if (ri < nrows) { - switch (ci) { - case 0: - buf = rows[ri].account; - cap = sizeof rows[ri].account; - break; - case 1: - buf = rows[ri].debit; - cap = sizeof rows[ri].debit; - break; - case 2: - buf = rows[ri].credit; - cap = sizeof rows[ri].credit; - break; - default: - buf = rows[ri].text; - cap = sizeof rows[ri].text; - break; - } - } - } - if (!buf) - continue; - /* first keystroke in a freshly focused field replaces its content - (so a prefilled date can be typed over directly) */ - if (field == 0) - tui_date_field_edit(buf, cap, &field_pos, ch, &field_fresh); - else - tui_field_edit(buf, cap, &field_pos, ch, &field_fresh); - normalize_rows(rows, &nrows, &field); - nfields = 3 + 4 * nrows; } } @@ -3296,9 +3182,94 @@ static int64_t pick_voucher(struct app *a) return out; } +static void inbox_upload(struct app *a) +{ + char *path = file_browser(a, a->attachment_dir); + if (!path) + return; + struct stat sb; + if (stat(path, &sb) == 0 && (long)sb.st_size > a->max_attachment_bytes) { + tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", + (double)sb.st_size / (1024 * 1024), + a->max_attachment_bytes / (1024 * 1024)); + free(path); + return; + } + char *b64 = read_file_b64(path); + if (!b64) { + tui_message("Fel", "Kunde inte läsa %s", path); + free(path); + return; + } + const char *base = strrchr(path, '/'); + base = base ? base + 1 : path; + 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, "filename", base); + yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + free(b64); + size_t reqlen = strlen(args) + 256; + char *raw = xmalloc(reqlen); + snprintf(raw, reqlen, + "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\"," + "\"session\":\"%s\",\"org\":%lld,\"args\":%s}", + a->session, (long long)a->org, args); + free(args); + char *r = NULL; + if (client_send_line(&a->conn, raw) == 0) + r = client_read_line(&a->conn); + free(raw); + if (r && client_ok(r)) + tui_message("Underlag", "Sparat."); + else + show_error("Kunde inte spara", r); + free(r); + free(path); +} + +struct inboxctx { + struct app *a; + int64_t *ids; + size_t n; + int cursor; +}; + +static int inbox_key(void *ud, int ch) +{ + struct inboxctx *ctx = ud; + struct app *a = ctx->a; + int64_t sel_id = (ctx->cursor >= 0 && (size_t)ctx->cursor < ctx->n) + ? ctx->ids[ctx->cursor] + : 0; + if (ch == 'k' && sel_id) { + int64_t vid = pick_voucher(a); + if (vid) { + char kargs[64]; + snprintf(kargs, sizeof kargs, "{\"id\":%lld,\"voucher_id\":%lld}", + (long long)sel_id, (long long)vid); + char *r = client_rpc(&a->conn, "attachment.link", a->session, + a->org, kargs); + if (r && client_ok(r)) + tui_message("Underlag", "Kopplat till verifikatet."); + else + show_error("Kunde inte koppla", r); + free(r); + } + return TUI_HOOK_REFRESH; + } + if (ch == 'a' || ch == 'A') { + inbox_upload(a); + return TUI_HOOK_REFRESH; + } + return TUI_HOOK_NONE; +} + static void inbox_screen(struct app *a) { - int top = 0, sel = 0; + int cursor = 0; for (;;) { if (g_quit) return; @@ -3328,118 +3299,27 @@ static void inbox_screen(struct app *a) items[i] = xstrdup(line); free(fn); } - int view = LINES - 4; - if (n == 0) - sel = 0; - else if (sel >= (int)n) - sel = (int)n - 1; - if (sel < top) - top = sel; - if (sel >= top + view) - top = sel - view + 1; - if (top > (int)n - view) - top = (int)n - view; - if (top < 0) - top = 0; - tui_frame("Underlag (inkorg)"); - for (size_t i = (size_t)top; i < n && (int)(i - (size_t)top) < view; - i++) { - int y = 2 + (int)(i - (size_t)top); - if ((int)i == sel) - attron(A_REVERSE); - mvaddnstr(y, 2, items[i], COLS - 4); - if ((int)i == sel) - attroff(A_REVERSE); - } - if (n == 0) - mvaddstr(2, 2, "Inga obokförda underlag."); - tui_hints("Enter = hämta k = koppla till verifikat a/^N = lägg till" - " fil PgUp/PgDn, Home/End rullar F5 = uppdatera Esc/q =" - " tillbaka"); - refresh(); - int ch = ui_getch(); - int64_t sel_id = (sel >= 0 && (size_t)sel < n) ? ids[sel] : 0; + struct inboxctx ctx = { a, ids, n, cursor }; + int sel = tui_select_list_hook("Underlag (inkorg)", items, (int)n, + cursor, 1, &cursor, 1, NULL, 0, + inbox_key, &ctx); + int64_t sel_id = + (sel >= 0 && (size_t)sel < n) ? ids[sel] : 0; for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); free(resp); - if (ch == 27 || ch == 'q') + if (sel == -1) return; - if ((ch == '\n' || ch == '\r' || ch == KEY_ENTER) && sel_id) - attachment_download(a, sel_id); - else if (ch == KEY_DOWN && sel + 1 < (int)n) - sel++; - else if (ch == KEY_UP && sel > 0) - sel--; - else if (ch == KEY_NPAGE) - sel = sel + view < (int)n ? sel + view : (int)n - 1; - else if (ch == KEY_PPAGE) - sel = sel - view > 0 ? sel - view : 0; - else if (ch == KEY_HOME) - sel = 0; - else if (ch == KEY_END) - sel = n > 0 ? (int)n - 1 : 0; - else if (ch == 'k' && sel_id) { - int64_t vid = pick_voucher(a); - if (vid) { - char kargs[64]; - snprintf(kargs, sizeof kargs, - "{\"id\":%lld,\"voucher_id\":%lld}", - (long long)sel_id, (long long)vid); - char *r = client_rpc(&a->conn, "attachment.link", a->session, - a->org, kargs); - if (r && client_ok(r)) - tui_message("Underlag", "Kopplat till verifikatet."); - else - show_error("Kunde inte koppla", r); - free(r); - } - } else if (ch == 'a' || ch == KEY_CTRL_N) { - char *path = file_browser(a, a->attachment_dir); - if (!path) - continue; - struct stat sb; - if (stat(path, &sb) == 0 && - (long)sb.st_size > a->max_attachment_bytes) { - tui_message("Bilaga", "Filen är %.1f MB, max %ld MB.", - (double)sb.st_size / (1024 * 1024), - a->max_attachment_bytes / (1024 * 1024)); - free(path); - continue; - } - char *b64 = read_file_b64(path); - if (!b64) { - tui_message("Fel", "Kunde inte läsa %s", path); - continue; - } - const char *base = strrchr(path, '/'); - base = base ? base + 1 : path; - 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, "filename", base); - yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - free(b64); - size_t reqlen = strlen(args) + 256; - char *raw = xmalloc(reqlen); - snprintf(raw, reqlen, - "{\"v\":1,\"id\":\"tui\",\"cmd\":\"attachment.put\"," - "\"session\":\"%s\",\"org\":%lld,\"args\":%s}", - a->session, (long long)a->org, args); - free(args); - char *r = NULL; - if (client_send_line(&a->conn, raw) == 0) - r = client_read_line(&a->conn); - free(raw); - if (r && client_ok(r)) - tui_message("Underlag", "Sparat."); - else - show_error("Kunde inte spara", r); - free(r); + if (sel == -2) + continue; + if (sel == -4) { + inbox_upload(a); + continue; } + if (sel_id) + attachment_download(a, sel_id); } } @@ -3517,53 +3397,132 @@ struct tui_trow { char text[64]; }; -static int trow_has_content(const struct tui_trow *r) +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) { - return r->account[0] || r->formula[0] || r->text[0]; + 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 trow_normalize(struct tui_trow *rows, int *nrows, int *field) +static void tpl_info(void *ud, int row, char *buf, size_t n) { - while (*nrows < 64 && trow_has_content(&rows[*nrows - 1])) { - memset(&rows[*nrows], 0, sizeof rows[0]); - (*nrows)++; - } - int i = 0; - while (i < *nrows - 1) { - if (!trow_has_content(&rows[i]) && !trow_has_content(&rows[i + 1])) { - int fr = *field >= 3 ? (*field - 3) / 3 : -1; - if (fr > i) - *field -= 3; - memmove(&rows[i], &rows[i + 1], - (size_t)(*nrows - i - 1) * sizeof rows[0]); - memset(&rows[*nrows - 1], 0, sizeof rows[0]); - (*nrows)--; + 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; } - i++; + 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); } - if (*nrows < 1) { - *nrows = 1; - memset(&rows[0], 0, sizeof rows[0]); - if (*field >= 3) - *field = 3; + 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. name == NULL creates a new +/* 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) { - static struct tui_trow rows[64]; - char tname[128] = "", tdesc[256] = ""; - char series[16]; - snprintf(series, sizeof series, "%s", a->default_series); - int64_t tpl_id = 0; - curs_set(1); - memset(rows, 0, sizeof rows); - int nrows = 1; - + 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); @@ -3577,19 +3536,18 @@ static int template_form(struct app *a, const char *load_name) if (!resp || !client_ok(resp)) { show_error("Mall", resp); free(resp); - curs_set(0); return 0; } - tpl_id = jint_val(resp, "result.id", 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(tname, sizeof tname, "%s", nm); + snprintf(tf.name, sizeof tf.name, "%s", nm); if (ser && *ser) - snprintf(series, sizeof series, "%s", ser); + snprintf(tf.series, sizeof tf.series, "%s", ser); if (ds) - snprintf(tdesc, sizeof tdesc, "%s", ds); + snprintf(tf.desc, sizeof tf.desc, "%s", ds); free(nm); free(ser); free(ds); @@ -3604,292 +3562,77 @@ static int template_form(struct app *a, const char *load_name) char *form = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.description", i); char *rd = jstr_dup(resp, path); - snprintf(rows[i].account, sizeof rows[i].account, "%s", + snprintf(tf.rows[i].account, sizeof tf.rows[i].account, "%s", acc ? acc : ""); - snprintf(rows[i].formula, sizeof rows[i].formula, "%s", + snprintf(tf.rows[i].formula, sizeof tf.rows[i].formula, "%s", form ? form : ""); - snprintf(rows[i].text, sizeof rows[i].text, "%s", rd ? rd : ""); + snprintf(tf.rows[i].text, sizeof tf.rows[i].text, "%s", + rd ? rd : ""); free(acc); free(form); free(rd); } - nrows = n > 0 ? (int)n : 1; + tf.nrows = n > 0 ? (int)n : 1; free(resp); } - - int field = 0; - int field_fresh = 1; - size_t field_pos = (size_t)-1; - int nfields = 3 + 3 * nrows; - const int x_account = 2; - const int name_w = 24; - const int x_name = 9; - const int x_formula = 34; - const int form_w = 20; - const int x_text = 55; - int text_w = COLS - x_text - 2; + 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; + const char *title = load_name ? "Redigera mall" : "Ny mall"; + const char *hint = "Tab = byta fält F5 = validera ^X = rensa rad" + " ^Enter = spara Esc = avbryt"; + int stage = 0; for (;;) { - tui_frame(load_name ? "Redigera mall" : "Ny mall"); - int caret_y = -1, caret_x = -1; - attron(A_BOLD); - mvaddstr(2, 2, "Namn"); - mvaddstr(2, 46, "Serie"); - mvaddstr(4, 2, "Text"); - attroff(A_BOLD); - { - const char *hvals[3] = { tname, series, tdesc }; - int hx[3] = { 9, 53, 9 }; - int hy[3] = { 2, 2, 4 }; - int hw[3] = { 34, 8, COLS - 12 }; - for (int k = 0; k < 3; k++) { - int w = hw[k] > 1 ? hw[k] : 1; - char padded[512]; - snprintf(padded, sizeof padded, "%s", hvals[k]); - tui_pad_field(padded, sizeof padded, w); - if (field == k) { - attron(A_REVERSE); - caret_y = hy[k]; - size_t slen = strlen(hvals[k]); - size_t cp = (field_fresh || field_pos == (size_t)-1 || - field_pos > slen) - ? slen - : field_pos; - int cw = (int)tui_disp_width_n(hvals[k], cp); - caret_x = hx[k] + (cw > w ? w : cw); - } - mvaddstr(hy[k], hx[k], padded); - if (field == k) - attroff(A_REVERSE); - } - } - attron(A_BOLD); - mvaddstr(6, x_account, "Konto"); - mvaddstr(6, x_name, "Namn"); - mvaddstr(6, x_formula, "Formel"); - mvaddstr(6, x_text, "Radtext"); - attroff(A_BOLD); - - for (int i = 0; i < nrows; i++) { - int y = 7 + i; - int sel_ci = -1; - if (field >= 3) { - int ri = (field - 3) / 3, ci = (field - 3) % 3; - if (ri == i) - sel_ci = ci; - } - const char *vals[3] = { rows[i].account, rows[i].formula, - rows[i].text }; - int xs[3] = { x_account, x_formula, x_text }; - int ws[3] = { 6, form_w, text_w }; - for (int k = 0; k < 3; k++) { - int w = ws[k] > 1 ? ws[k] : 1; - char padded[512]; - snprintf(padded, sizeof padded, "%s", vals[k]); - tui_pad_field(padded, sizeof padded, w); - if (k == sel_ci) { - attron(A_REVERSE); - caret_y = y; - size_t slen = strlen(vals[k]); - size_t cp = (field_fresh || field_pos == (size_t)-1 || - field_pos > slen) - ? slen - : field_pos; - int cw = (int)tui_disp_width_n(vals[k], cp); - caret_x = xs[k] + (cw > w ? w : cw); - } - mvaddstr(y, xs[k], padded); - if (k == sel_ci) - attroff(A_REVERSE); - } - { - char nbuf[512]; - const char *nm = acct_name(a, rows[i].account); - snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - tui_pad_field(nbuf, sizeof nbuf, name_w); - attron(A_DIM); - mvaddstr(y, x_name, nbuf); - attroff(A_DIM); - } - } - move(7 + nrows, 2); - clrtoeol(); - attron(A_DIM); - mvaddnstr(7 + nrows, 2, - "x = belopp i kronor. Positivt blir debet, negativt kredit.", - COLS - 4); - attroff(A_DIM); - tui_hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); - if (caret_y >= 0) - move(caret_y, caret_x); - refresh(); - - int ch = ui_getch(); - if (ch == 27) { - curs_set(0); - return 0; - } - if (ch == ' ') { - field = (field + 1) % nfields; - field_fresh = 1; - continue; - } - if (ch == KEY_BTAB) { - field = (field + nfields - 1) % nfields; - field_fresh = 1; - continue; - } - if (ch == KEY_UP) { - if (field >= 3) { - int ci = (field - 3) % 3; - int ri = (field - 3) / 3; - if (ri > 0) { - field = 3 + (ri - 1) * 3 + ci; - field_fresh = 1; - } - } - continue; - } - if (ch == KEY_DOWN) { - if (field >= 3) { - int ci = (field - 3) % 3; - int ri = (field - 3) / 3; - if (ri + 1 < nrows) { - field = 3 + (ri + 1) * 3 + ci; - field_fresh = 1; - } - } - continue; - } - if (ch == KEY_CTRL_X) { - if (field >= 3) { - int ri = (field - 3) / 3; - if (ri < nrows) { - memset(&rows[ri], 0, sizeof rows[ri]); - trow_normalize(rows, &nrows, &field); - nfields = 3 + 3 * nrows; - field_fresh = 1; - } - } - continue; - } - - /* validation and save */ - if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { - const char *problem = NULL; - char msg[256] = ""; - if (!tname[0]) { - problem = "Namn saknas."; - } - int used = 0; - for (int i = 0; i < nrows && !problem; i++) { - if (!rows[i].account[0]) - continue; - if (!acct_exists(a, rows[i].account)) { - snprintf(msg, sizeof msg, - "Konto %.15s finns inte (rad %d).", - rows[i].account, i + 1); - problem = msg; - break; - } - if (!rows[i].formula[0] || !formula_valid(rows[i].formula)) { - snprintf(msg, sizeof msg, - "Ogiltig formel på rad %d: '%.60s'.", i + 1, - 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); + if (stage == 0) { + int r = tui_form_run(title, ff, 3, 1); + if (r == TUI_FORM_BACK) + return 0; + if (r == TUI_FORM_REFRESH) { + tpl_save(&tf, 1); 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); - if (tpl_id) - yyjson_mut_obj_add_int(d, o, "id", tpl_id); - yyjson_mut_obj_add_strcpy(d, o, "name", tname); - yyjson_mut_obj_add_strcpy(d, o, "series", series); - yyjson_mut_obj_add_strcpy(d, o, "description", tdesc); - yyjson_mut_val *arr = yyjson_mut_arr(d); - for (int i = 0; i < nrows; i++) { - if (!rows[i].account[0]) - continue; - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", rows[i].account); - yyjson_mut_obj_add_strcpy(d, ro, "formula", rows[i].formula); - if (rows[i].text[0]) - yyjson_mut_obj_add_strcpy(d, ro, "description", - 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 = tpl_id ? "template.update" : "template.create"; - char *resp = ch == KEY_F(5) - ? rpc_dry(a, cmd, args) - : client_rpc(&a->conn, cmd, a->session, a->org, - args); - free(args); - if (resp && client_ok(resp)) { - if (ch == KEY_F(5)) { - tui_message("Validering OK", - "Mallen är korrekt (%d rader).", used); - } else { - tui_message("Mall", "Mallen '%s' sparad.", tname); - free(resp); - curs_set(0); + if (r == TUI_FORM_SUBMIT) { + if (tpl_save(&tf, 0)) return 1; - } - } else { - show_error("Kunde inte spara mallen", resp); + continue; } - free(resp); - continue; - } - - char *buf = NULL; - size_t cap = 0; - if (field == 0) { - buf = tname; - cap = sizeof tname; - } else if (field == 1) { - buf = series; - cap = sizeof series; - } else if (field == 2) { - buf = tdesc; - cap = sizeof tdesc; + stage = 1; } else { - int ri = (field - 3) / 3, ci = (field - 3) % 3; - if (ri < nrows) { - switch (ci) { - case 0: - buf = rows[ri].account; - cap = sizeof rows[ri].account; - break; - case 1: - buf = rows[ri].formula; - cap = sizeof rows[ri].formula; - break; - default: - buf = rows[ri].text; - cap = sizeof rows[ri].text; - break; - } + int rr = tui_rt_run(title, &tf.rt, hint); + if (rr == -1) { + stage = 0; + continue; + } + if (rr == -2) { + tpl_save(&tf, 1); + continue; } + if (rr == -3 && tpl_save(&tf, 0)) + return 1; } - if (!buf) - continue; - tui_field_edit(buf, cap, &field_pos, ch, &field_fresh); - trow_normalize(rows, &nrows, &field); - nfields = 3 + 3 * nrows; } } @@ -3926,39 +3669,6 @@ struct tui_ibrow { char text[64]; }; -static int ibrow_has_content(const struct tui_ibrow *r) -{ - return r->account[0] || r->amount[0] || r->text[0]; -} - -static void ibrow_normalize(struct tui_ibrow *rows, int *nrows, int *field) -{ - while (*nrows < 64 && ibrow_has_content(&rows[*nrows - 1])) { - memset(&rows[*nrows], 0, sizeof rows[0]); - (*nrows)++; - } - int i = 0; - while (i < *nrows - 1) { - if (!ibrow_has_content(&rows[i]) && !ibrow_has_content(&rows[i + 1])) { - int fr = *field / 3; - if (fr > i) - *field -= 3; - memmove(&rows[i], &rows[i + 1], - (size_t)(*nrows - i - 1) * sizeof rows[0]); - memset(&rows[*nrows - 1], 0, sizeof rows[0]); - (*nrows)--; - continue; - } - i++; - } - if (*nrows < 1) { - *nrows = 1; - memset(&rows[0], 0, sizeof rows[0]); - if (*field >= 3) - *field = 0; - } -} - /* Loads the net IB per account for the active fiscal year. */ /* Effective opening balances of the selected year: the year's IB vouchers plus all earlier movements, exactly what the reports show. */ @@ -4013,378 +3723,271 @@ static int64_t ib_old_lookup(char **accs, int64_t *amts, int n, return 0; } +struct ibform { + struct app *a; + struct tui_rt rt; + struct tui_ibrow rows[64]; + char **old_acc; + int64_t *old_amt; + int nold; +}; + +static void ib_cell(void *ud, int row, int col, char **buf, size_t *cap) +{ + struct ibform *ib = ud; + struct tui_ibrow *r = &ib->rows[row]; + if (col == 0) { + *buf = r->account; + *cap = sizeof r->account; + } else if (col == 2) { + *buf = r->amount; + *cap = sizeof r->amount; + } else { + *buf = r->text; + *cap = sizeof r->text; + } +} + +static void ib_info(void *ud, int row, char *buf, size_t n) +{ + struct ibform *ib = ud; + const char *nm = acct_name(ib->a, ib->rows[row].account); + snprintf(buf, n, "%s", nm ? nm : ""); +} + +static void ib_footer(void *ud, char *buf, size_t n) +{ + (void)ud; + snprintf(buf, n, "Positiva belopp = debet, negativa = kredit. Summan ska" + " bli noll."); +} + +static const char *ib_check(struct ibform *ib, int *used_out, char *msg, + size_t msglen) +{ + int64_t sum = 0; + int used = 0; + for (int i = 0; i < ib->rt.nrows; i++) { + if (!ib->rows[i].account[0]) + continue; + if (!acct_exists(ib->a, ib->rows[i].account)) { + snprintf(msg, msglen, "Konto %.15s finns inte (rad %d).", + ib->rows[i].account, i + 1); + return msg; + } + double kr = 0; + if (ib->rows[i].amount[0] && + !parse_x_double(ib->rows[i].amount, &kr)) { + snprintf(msg, msglen, "Ogiltigt belopp på rad %d: '%.20s'.", i + 1, + ib->rows[i].amount); + return msg; + } + sum += (int64_t)llround(kr * 100.0); + used++; + } + if (used == 0) + return "Ange minst ett konto."; + if (sum != 0) { + snprintf(msg, msglen, "Summan måste bli noll (nu %lld öre).", + (long long)sum); + return msg; + } + *used_out = used; + return NULL; +} + +/* Posts the delta against the existing IB so nothing is ever edited. */ +static int ib_post_delta(struct ibform *ib) +{ + struct app *a = ib->a; + 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, "date", a->fy_start); + yyjson_mut_obj_add_strcpy(d, o, "description", "Ingående balans"); + yyjson_mut_obj_add_strcpy(d, o, "series", "IB"); + char *ref = util_random_id("tui-", 8); + yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); + free(ref); + yyjson_mut_val *arr = yyjson_mut_arr(d); + int posted = 0; + for (int i = 0; i < ib->rt.nrows; i++) { + if (!ib->rows[i].account[0]) + continue; + double kr = 0; + if (ib->rows[i].amount[0] && + !parse_x_double(ib->rows[i].amount, &kr)) + kr = 0; + int64_t new_amt = (int64_t)llround(kr * 100.0); + int64_t prev_amt = + ib_old_lookup(ib->old_acc, ib->old_amt, ib->nold, + ib->rows[i].account); + int64_t delta = new_amt - prev_amt; + if (delta == 0) + continue; + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); + yyjson_mut_obj_add_strcpy(d, ro, "account", ib->rows[i].account); + yyjson_mut_obj_add_int(d, ro, "debit_ore", delta > 0 ? delta : 0); + yyjson_mut_obj_add_int(d, ro, "credit_ore", delta < 0 ? -delta : 0); + if (ib->rows[i].text[0]) + yyjson_mut_obj_add_strcpy(d, ro, "description", + ib->rows[i].text); + posted++; + } + for (int i = 0; i < ib->nold; i++) { + int still = 0; + for (int j = 0; j < ib->rt.nrows; j++) + if (strcmp(ib->rows[j].account, ib->old_acc[i]) == 0) { + still = 1; + break; + } + if (!still && ib->old_amt[i] != 0) { + yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); + yyjson_mut_obj_add_strcpy(d, ro, "account", ib->old_acc[i]); + yyjson_mut_obj_add_int(d, ro, "debit_ore", + ib->old_amt[i] < 0 ? -ib->old_amt[i] : 0); + yyjson_mut_obj_add_int(d, ro, "credit_ore", + ib->old_amt[i] > 0 ? ib->old_amt[i] : 0); + posted++; + } + } + if (posted == 0) { + yyjson_mut_doc_free(d); + tui_message("Ingående balans", "Ingen ändring."); + return 0; + } + yyjson_mut_obj_add_val(d, o, "rows", arr); + char *args = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + char *resp = + client_rpc(&a->conn, "voucher.post", a->session, a->org, args); + free(args); + if (resp && client_ok(resp)) { + int64_t num = jint_val(resp, "result.number", 0); + tui_message("Ingående balans", + "Sparat som IB %lld (%d justerade rader).", + (long long)num, posted); + free(resp); + return 1; + } + show_error("Kunde inte spara ingående balans", resp); + free(resp); + return 0; +} + static int ib_form(struct app *a, char **old_acc, int64_t *old_amt, int nold) { - static struct tui_ibrow rows[64]; + struct ibform ib; + memset(&ib, 0, sizeof ib); + ib.a = a; + ib.old_acc = old_acc; + ib.old_amt = old_amt; + ib.nold = nold; if (nold > 63) { tui_message("Ingående balans", "För många konton (%d) för att redigera här.", nold); return 0; } - memset(rows, 0, sizeof rows); for (int i = 0; i < nold && i < 63; i++) { - snprintf(rows[i].account, sizeof rows[i].account, "%s", old_acc[i]); - tui_kr_format(old_amt[i], rows[i].amount, sizeof rows[i].amount); + snprintf(ib.rows[i].account, sizeof ib.rows[i].account, "%s", + old_acc[i]); + tui_kr_format(old_amt[i], ib.rows[i].amount, + sizeof ib.rows[i].amount); } - int nrows = nold > 0 ? nold : 1; - int field = 0; - int field_fresh = 1; - size_t field_pos = (size_t)-1; - int nfields = 3 * nrows; - const int x_account = 2; - const int name_w = 24; - const int x_name = 9; - const int x_amount = 34; - const int amount_w = 14; - const int x_text = 49; - int text_w = COLS - x_text - 2; + int text_w = COLS - 51; if (text_w < 6) text_w = 6; - curs_set(1); + struct tui_rt_col cols[4] = { + { "Konto", 2, 6, TUI_RT_EDIT }, + { "Namn", 9, 24, TUI_RT_INFO }, + { "Belopp", 34, 14, TUI_RT_EDIT }, + { "Text", 49, text_w, TUI_RT_EDIT }, + }; + tui_rt_init(&ib.rt, nold > 0 ? nold : 1, 64, 4, cols, 5, ib_cell, ib_info, + &ib); + tui_rt_set_footer(&ib.rt, ib_footer); for (;;) { - tui_frame("Ingående balans"); - int caret_y = -1, caret_x = -1; - attron(A_BOLD); - mvaddstr(2, 2, - "Positiva belopp = debet, negativa = kredit. Summan ska bli" - " noll."); - attroff(A_BOLD); - attron(A_BOLD); - mvaddstr(4, x_account, "Konto"); - mvaddstr(4, x_name, "Namn"); - mvaddstr(4, x_amount, "Belopp"); - mvaddstr(4, x_text, "Text"); - attroff(A_BOLD); - for (int i = 0; i < nrows; i++) { - int y = 5 + i; - int sel_ci = -1; - if (field >= 0 && field < nfields) { - int ri = field / 3, ci = field % 3; - if (ri == i) - sel_ci = ci; - } - const char *vals[3] = { rows[i].account, rows[i].amount, - rows[i].text }; - int xs[3] = { x_account, x_amount, x_text }; - int ws[3] = { 6, amount_w, text_w }; - for (int k = 0; k < 3; k++) { - int w = ws[k] > 1 ? ws[k] : 1; - char padded[512]; - snprintf(padded, sizeof padded, "%s", vals[k]); - tui_pad_field(padded, sizeof padded, w); - if (k == sel_ci) { - attron(A_REVERSE); - caret_y = y; - size_t slen = strlen(vals[k]); - size_t cp = (field_fresh || field_pos == (size_t)-1 || - field_pos > slen) - ? slen - : field_pos; - int cw = (int)tui_disp_width_n(vals[k], cp); - caret_x = xs[k] + (cw > w ? w : cw); - } - mvaddstr(y, xs[k], padded); - if (k == sel_ci) - attroff(A_REVERSE); - } - { - char nbuf[512]; - const char *nm = acct_name(a, rows[i].account); - snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); - tui_pad_field(nbuf, sizeof nbuf, name_w); - attron(A_DIM); - mvaddstr(y, x_name, nbuf); - attroff(A_DIM); - } - } - tui_hints("Tab = byta fält F5 = validera ^X = rensa rad ^Enter = spara Esc = avbryt"); - if (caret_y >= 0) - move(caret_y, caret_x); - refresh(); - - int ch = ui_getch(); - if (ch == 27) { - curs_set(0); + int rr = tui_rt_run("Ingående balans", &ib.rt, + "Tab = byta fält F5 = validera ^X = rensa rad" + " ^Enter = spara Esc = avbryt"); + if (rr == -1) return 0; - } - if (ch == '\t') { - field = (field + 1) % nfields; - field_fresh = 1; + char msg[256]; + int used = 0; + const char *problem = ib_check(&ib, &used, msg, sizeof msg); + if (problem) { + tui_message("Ingående balans", "%s", problem); continue; } - if (ch == KEY_BTAB) { - field = (field + nfields - 1) % nfields; - field_fresh = 1; + if (rr == -2) { + tui_message("Validering OK", "%d konton, summan är noll.", used); continue; } - if (ch == KEY_UP) { - int ci = field % 3, ri = field / 3; - if (ri > 0) { - field = (ri - 1) * 3 + ci; - field_fresh = 1; - } - continue; - } - if (ch == KEY_DOWN) { - int ci = field % 3, ri = field / 3; - if (ri + 1 < nrows) { - field = (ri + 1) * 3 + ci; - field_fresh = 1; - } - continue; - } - if (ch == KEY_CTRL_X) { - int ri = field / 3; - if (ri < nrows) { - memset(&rows[ri], 0, sizeof rows[ri]); - ibrow_normalize(rows, &nrows, &field); - nfields = 3 * nrows; - field_fresh = 1; - } - continue; - } - - if (ch == KEY_F(5) || ch == KEY_CTRL_ENTER || ch == KEY_F(9)) { - const char *problem = NULL; - char msg[256] = ""; - int64_t sum = 0; - int used = 0; - for (int i = 0; i < nrows && !problem; i++) { - if (!rows[i].account[0]) - continue; - if (!acct_exists(a, rows[i].account)) { - snprintf(msg, sizeof msg, - "Konto %.15s finns inte (rad %d).", - rows[i].account, i + 1); - problem = msg; - break; - } - double kr = 0; - if (rows[i].amount[0] && !parse_x_double(rows[i].amount, &kr)) { - snprintf(msg, sizeof msg, - "Ogiltigt belopp på rad %d: '%.20s'.", i + 1, - rows[i].amount); - problem = msg; - break; - } - sum += (int64_t)llround(kr * 100.0); - used++; - } - if (!problem && used == 0) - problem = "Ange minst ett konto."; - if (!problem && sum != 0) { - snprintf(msg, sizeof msg, - "Summan måste bli noll (nu %lld öre).", - (long long)sum); - problem = msg; - } - if (problem) { - tui_message("Ingående balans", "%s", problem); - continue; - } - - if (ch == KEY_F(5)) { - tui_message("Validering OK", "%d konton, summan är noll.", used); - continue; - } + if (ib_post_delta(&ib)) + return 1; + } +} - /* delta against the existing IB so nothing is ever edited */ - yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); - yyjson_mut_val *o = yyjson_mut_obj(d); - yyjson_mut_doc_set_root(d, o); - char datebuf[16]; - snprintf(datebuf, sizeof datebuf, "%s", a->fy_start); - yyjson_mut_obj_add_strcpy(d, o, "date", datebuf); - yyjson_mut_obj_add_strcpy(d, o, "description", - "Ingående balans"); - yyjson_mut_obj_add_strcpy(d, o, "series", "IB"); - char *ref = util_random_id("tui-", 8); - yyjson_mut_obj_add_strcpy(d, o, "client_ref", ref); - free(ref); - yyjson_mut_val *arr = yyjson_mut_arr(d); - int posted = 0; - for (int i = 0; i < nrows; i++) { - if (!rows[i].account[0]) - continue; - double kr = 0; - if (rows[i].amount[0] && !parse_x_double(rows[i].amount, &kr)) - kr = 0; - int64_t new_amt = (int64_t)llround(kr * 100.0); - int64_t prev_amt = ib_old_lookup(old_acc, old_amt, nold, - rows[i].account); - int64_t delta = new_amt - prev_amt; - if (delta == 0) - continue; - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", rows[i].account); - yyjson_mut_obj_add_int(d, ro, "debit_ore", - delta > 0 ? delta : 0); - yyjson_mut_obj_add_int(d, ro, "credit_ore", - delta < 0 ? -delta : 0); - if (rows[i].text[0]) - yyjson_mut_obj_add_strcpy(d, ro, "description", - rows[i].text); - posted++; - } - /* accounts that were in the old IB but removed now */ - for (int i = 0; i < nold; i++) { - int still = 0; - for (int j = 0; j < nrows; j++) - if (strcmp(rows[j].account, old_acc[i]) == 0) { - still = 1; - break; - } - if (!still && old_amt[i] != 0) { - yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); - yyjson_mut_obj_add_strcpy(d, ro, "account", old_acc[i]); - yyjson_mut_obj_add_int(d, ro, "debit_ore", - old_amt[i] < 0 ? -old_amt[i] : 0); - yyjson_mut_obj_add_int(d, ro, "credit_ore", - old_amt[i] > 0 ? old_amt[i] : 0); - posted++; - } - } - if (posted == 0) { - yyjson_mut_doc_free(d); - tui_message("Ingående balans", "Ingen ändring."); - continue; - } - yyjson_mut_obj_add_val(d, o, "rows", arr); - char *args = yyjson_mut_write(d, 0, NULL); - yyjson_mut_doc_free(d); - char *resp = - client_rpc(&a->conn, "voucher.post", a->session, a->org, args); - free(args); - if (resp && client_ok(resp)) { - int64_t num = jint_val(resp, "result.number", 0); - tui_message("Ingående balans", - "Sparat som IB %lld (%d justerade rader).", - (long long)num, posted); - free(resp); - curs_set(0); - return 1; - } - show_error("Kunde inte spara ingående balans", resp); - free(resp); - continue; - } +struct ibpager { + struct app *a; + char **accs; + int64_t *amts; + int n; +}; - char *buf = NULL; - size_t cap = 0; - int ri = field / 3, ci = field % 3; - if (ri < nrows) { - switch (ci) { - case 0: - buf = rows[ri].account; - cap = sizeof rows[ri].account; - break; - case 1: - buf = rows[ri].amount; - cap = sizeof rows[ri].amount; - break; - default: - buf = rows[ri].text; - cap = sizeof rows[ri].text; - break; - } - } - if (!buf) - continue; - tui_field_edit(buf, cap, &field_pos, ch, &field_fresh); - ibrow_normalize(rows, &nrows, &field); - nfields = 3 * nrows; +static int ib_view_key(void *ud, int ch) +{ + struct ibpager *p = ud; + if (ch == 'e' || ch == 'E' || ch == TUI_KEY_CTRL_N) { + if (ib_form(p->a, p->accs, p->amts, p->n)) + return TUI_HOOK_REFRESH; + return TUI_HOOK_STAY; } + return TUI_HOOK_NONE; } +/* The list is a read-only table with no selection: tui_pager shows the + columns and the sum as plain text, and the e/^N keys run the editor. */ static void ib_screen(struct app *a) { - char **accs = NULL; - int64_t *amts = NULL; - int n = 0; - int reload = 1; - int top = 0; for (;;) { - if (reload) { - for (int i = 0; i < n; i++) - free(accs[i]); - free(accs); - free(amts); - accs = NULL; - amts = NULL; - n = 0; - top = 0; - reload = 0; - if (ib_load(a, &accs, &amts, &n) != 0) - return; - } - int view = LINES - 10; - if (view < 1) - view = 1; - if (top > n - view) - top = n - view; - if (top < 0) - top = 0; - tui_frame("Ingående balans"); - attron(A_BOLD); - mvprintw(2, 2, "Räkenskapsår %s ingående balans (effektiv)", + char **accs = NULL; + int64_t *amts = NULL; + int n = 0; + if (ib_load(a, &accs, &amts, &n) != 0) + return; + struct buf t; + buf_init(&t); + buf_line(&t, "Räkenskapsår %s ingående balans (effektiv)\n\n", a->fy_label); - mvaddstr(4, 2, "Konto"); - mvaddstr(4, 9, "Namn"); - mvaddstr(4, 62, "Belopp"); - attroff(A_BOLD); + buf_line(&t, "%-7s%-53s%s\n\n", "Konto", "Namn", "Belopp"); int64_t sum = 0; - for (int i = 0; i < n; i++) + for (int i = 0; i < n; i++) { sum += amts[i]; - if (n == 0) { - mvaddstr(6, 2, "Ingen ingående balans registrerad ännu."); - } - for (int i = 0; i < view && top + i < n; i++) { - int idx = top + i; - char amount[32]; - tui_kr_format(amts[idx], amount, sizeof amount); - const char *nm = acct_name(a, accs[idx]); - char nbuf[128]; + char amount[32], nbuf[128]; + tui_kr_format(amts[i], amount, sizeof amount); + const char *nm = acct_name(a, accs[i]); snprintf(nbuf, sizeof nbuf, "%s", nm ? nm : ""); tui_pad_field(nbuf, sizeof nbuf, 50); - mvprintw(6 + i, 2, "%-6s %s %14s", accs[idx], nbuf, amount); + buf_line(&t, "%-6s %s %14s\n", accs[i], nbuf, amount); } + if (n == 0) + buf_line(&t, "Ingen ingående balans registrerad ännu.\n"); char sumbuf[32]; tui_kr_format(sum, sumbuf, sizeof sumbuf); - attron(A_BOLD); - mvprintw(LINES - 4, 2, "Summa: %s", sumbuf); - attroff(A_BOLD); - tui_hints("e/^N = redigera PgUp/PgDn, Home/End rullar F5 =" - " uppdatera Esc/q = tillbaka"); - refresh(); - int ch = ui_getch(); - if (ch == 'e' || ch == 'E' || ch == KEY_CTRL_N) { - if (ib_form(a, accs, amts, n)) - reload = 1; - continue; - } - if (ch == KEY_DOWN && top + view < n) - top++; - else if (ch == KEY_UP && top > 0) - top--; - else if (ch == KEY_NPAGE) - top += view; - else if (ch == KEY_PPAGE) - top -= view; - else if (ch == KEY_HOME) - top = 0; - else if (ch == KEY_END) - top = n > view ? n - view : 0; - else if (ch == KEY_F(5)) { - reload = 1; - continue; - } else if (ch == 27 || ch == 'q') { - for (int i = 0; i < n; i++) - free(accs[i]); - free(accs); - free(amts); + buf_line(&t, "\nSumma: %s\n", sumbuf); + buf_append(&t, "\0", 1); + struct ibpager p = { a, accs, amts, n }; + int ret = tui_pager_hook("Ingående balans", (const char *)t.p, + "e/^N = redigera", 0, ib_view_key, &p); + buf_free(&t); + for (int i = 0; i < n; i++) + free(accs[i]); + free(accs); + free(amts); + if (ret == 0) return; - } } } @@ -5950,94 +5553,36 @@ static int login_screen(struct app *a) const char *env_pass = getenv("BOKFD_PASSWORD"); snprintf(user, sizeof user, "%s", a->username); snprintf(pass, sizeof pass, "%s", env_pass ? env_pass : ""); - int field = 3; /* 0 server, 1 user, 2 password, 3 = Logga in button */ + struct tui_form_field ff[4]; + memset(ff, 0, sizeof ff); + ff[0].label = "Server"; + ff[0].value = socket_path; + ff[0].cap = sizeof socket_path; + ff[0].kind = TUI_F_TEXT; + ff[1].label = "Användare"; + ff[1].value = user; + ff[1].cap = sizeof user; + ff[1].kind = TUI_F_TEXT; + ff[2].label = "Lösenord"; + ff[2].value = pass; + ff[2].cap = sizeof pass; + ff[2].kind = TUI_F_TEXT; + ff[2].mask = 1; + ff[3].label = ""; + ff[3].value = "Logga in"; + ff[3].kind = TUI_F_ACTION; for (;;) { - tui_frame("bokf — inloggning"); - attron(A_BOLD); - mvaddstr(4, 4, "Server"); - mvaddstr(6, 4, "Användare"); - mvaddstr(8, 4, "Lösenord"); - attroff(A_BOLD); - mvaddstr(4, 16, socket_path); - mvaddstr(6, 16, user); - { - char stars[128]; - size_t n = strlen(pass); - if (n > sizeof stars - 1) - n = sizeof stars - 1; - memset(stars, '*', n); - stars[n] = '\0'; - mvaddstr(8, 16, stars); - } - { - const char *btn = " Logga in "; - int bw = tui_disp_width(btn); - int bx = (COLS - bw) / 2; - if (bx < 2) - bx = 2; - if (field == 3) { - attron(A_REVERSE | A_BOLD); - curs_set(0); + int r = tui_form_run("bokf — inloggning", ff, 4, 1); + if (r == TUI_FORM_BACK) { + if (g_quit) { + endwin(); + return -1; } - mvaddstr(11, bx, btn); - if (field == 3) - attroff(A_REVERSE | A_BOLD); - } - tui_hints("Tab = byta fält/knapp Enter = logga in ^C = avsluta"); - refresh(); - - if (field == 3) { - int ch = ui_getch(); - if (ch == 27) { - if (g_quit) { - endwin(); - return -1; - } - continue; /* Esc never exits here */ - } - if (ch == '\t') { - field = 0; - curs_set(1); - continue; - } - if (ch == KEY_BTAB) { - field = 2; - curs_set(1); - continue; - } - if (ch != '\n' && ch != '\r' && ch != KEY_ENTER && ch != ' ') - continue; - /* fall through to the login attempt */ - } else { - int y = 4 + field * 2; - int rc = tui_edit_field(y, 16, field == 0 ? socket_path - : (field == 1 ? user : pass), - field == 0 ? sizeof socket_path - : (field == 1 ? sizeof user - : sizeof pass), - field == 2); - if (rc == 0) { - if (g_quit) { - endwin(); - return -1; - } - continue; - } - if (rc == 2) { - field = (field + 1) % 4; - continue; - } - if (rc == 3) { - field = (field + 3) % 4; - continue; - } - if (field < 2) { - field++; - continue; - } - /* field 2: Enter submits */ + continue; /* Esc never exits here */ } + if (r != TUI_FORM_SUBMIT && r != 3) + continue; /* attempt login */ snprintf(a->socket, sizeof a->socket, "%s", socket_path); @@ -6212,9 +5757,12 @@ int main(int argc, char **argv) app.username[0] && env_pass && *env_pass ? 1 : 0); for (int i = 0; i < 60 && !app.session[0]; i++) { if (i > 0) { - mvprintw(LINES / 2, 2, "Återansluter till servern... (%ds)", - i); - refresh(); + char msg[128]; + snprintf(msg, sizeof msg, + "Återansluter till servern... (%ds)", i); + tui_set_status(msg, NULL); + tui_frame("bokf — ansluter"); + tui_redraw(); sleep(1); } if (session_arg && *session_arg) { |
