diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-22 12:43:52 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-22 12:43:52 +0200 |
| commit | 07f5b140922d13519ad7a0d1dad67a9b403be9f1 (patch) | |
| tree | b6dde2ed6adb90c6423a5e585b693b908637e5ee /clients/screens_invoices.c | |
| parent | 760cb25bd220718e37d8b95016f30cdf8ef6acdc (diff) | |
| download | bokf-07f5b140922d13519ad7a0d1dad67a9b403be9f1.tar.gz bokf-07f5b140922d13519ad7a0d1dad67a9b403be9f1.zip | |
tui: kundutkast, <UTKAST>, explicit Spara och F2-åtgärder
Diffstat (limited to 'clients/screens_invoices.c')
| -rw-r--r-- | clients/screens_invoices.c | 465 |
1 files changed, 371 insertions, 94 deletions
diff --git a/clients/screens_invoices.c b/clients/screens_invoices.c index 352c184..712a8a8 100644 --- a/clients/screens_invoices.c +++ b/clients/screens_invoices.c @@ -14,6 +14,7 @@ #include <unistd.h> #include "client.h" +#include "drafts.h" #include "tui.h" #include "formula.h" #include "util.h" @@ -219,12 +220,128 @@ static int customer_save(struct app *a, int64_t id, const char *name, return ok; } -static void customer_form(struct app *a, int64_t id) +/* Drafts for the customer register: new entities exist only as drafts + until Spara writes them (docs/TUI-GUIDELINES.md "Interaction model"). */ + +static struct drafts g_drafts; +static int g_drafts_ready; + +static struct drafts *kdrafts(void) +{ + if (!g_drafts_ready) { + drafts_init(&g_drafts); + g_drafts_ready = 1; + } + return &g_drafts; +} + +struct cform_ctx { + struct app *a; + int64_t id; + const char *draft_id; + int deleted; + char name[256]; + char address[512]; + char postal[32]; + char city[128]; + char vat[64]; + char email[256]; + char your_ref[128]; + char payment[24]; + char notes[2048]; +}; + +static char *customer_fields_json(const struct cform_ctx *c) +{ + yyjson_mut_doc *d = yyjson_mut_doc_new(NULL); + yyjson_mut_val *o = yyjson_mut_obj(d); + + yyjson_mut_doc_set_root(d, o); + yyjson_mut_obj_add_strcpy(d, o, "name", c->name); + yyjson_mut_obj_add_strcpy(d, o, "address", c->address); + yyjson_mut_obj_add_strcpy(d, o, "postal_code", c->postal); + yyjson_mut_obj_add_strcpy(d, o, "city", c->city); + yyjson_mut_obj_add_strcpy(d, o, "vat_nr", c->vat); + yyjson_mut_obj_add_strcpy(d, o, "email", c->email); + yyjson_mut_obj_add_strcpy(d, o, "your_ref", c->your_ref); + yyjson_mut_obj_add_int(d, o, "payment_days", + strtoll(c->payment, NULL, 10)); + yyjson_mut_obj_add_strcpy(d, o, "notes", c->notes); + char *s = yyjson_mut_write(d, 0, NULL); + yyjson_mut_doc_free(d); + return s; +} + +static void customer_load_json(const char *json, struct cform_ctx *c) { - char name[256] = "", address[512] = "", postal[32] = "", city[128] = ""; - char vat[64] = "", email[256] = "", your_ref[128] = ""; - char payment[16] = "30", notes[2048] = ""; - if (id > 0) { + yyjson_doc *doc = yyjson_read(json, strlen(json), 0); + + if (!doc) + return; + yyjson_val *o = yyjson_doc_get_root(doc); + const struct { + const char *key; + char *dst; + size_t cap; + } fields[] = { + { "name", c->name, sizeof c->name }, + { "address", c->address, sizeof c->address }, + { "postal_code", c->postal, sizeof c->postal }, + { "city", c->city, sizeof c->city }, + { "vat_nr", c->vat, sizeof c->vat }, + { "email", c->email, sizeof c->email }, + { "your_ref", c->your_ref, sizeof c->your_ref }, + { "notes", c->notes, sizeof c->notes }, + }; + for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) + snprintf(fields[i].dst, fields[i].cap, "%s", + vstr(o, fields[i].key)); + int64_t days = vint(o, "payment_days"); + if (days > 0) + snprintf(c->payment, sizeof c->payment, "%lld", (long long)days); + yyjson_doc_free(doc); +} + +static int cform_key(void *ud, int ch) +{ + struct cform_ctx *c = ud; + + if (ch != KEY_F(2)) + return TUI_HOOK_NONE; + int have = drafts_have(kdrafts(), c->a->org, "customer", c->draft_id); + struct tui_action acts[] = { + { "customer.save", "Spara", 0, 1, NULL }, + { "customer.draft.delete", "Radera utkast", 0, have, "inget utkast" }, + }; + static int afocus; + int r = tui_action_menu("Åtgärder", acts, 2, &afocus); + if (r == 0) + return TUI_HOOK_SUBMIT; + if (r == 1) { + if (tui_confirm("Kund", "Radera utkastet?")) { + drafts_del(kdrafts(), c->a->org, "customer", c->draft_id); + c->deleted = 1; + return TUI_HOOK_BACK; + } + return TUI_HOOK_STAY; + } + return TUI_HOOK_STAY; +} + +static void customer_form(struct app *a, int64_t id, const char *draft_id) +{ + struct cform_ctx c; + + memset(&c, 0, sizeof c); + c.a = a; + c.id = id; + c.draft_id = draft_id; + snprintf(c.payment, sizeof c.payment, "30"); + + const char *fields = drafts_get(kdrafts(), a->org, "customer", draft_id); + if (fields) { + customer_load_json(fields, &c); + } else if (id > 0) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); char *resp = @@ -234,43 +351,46 @@ static void customer_form(struct app *a, int64_t id) free(resp); return; } - struct { + const struct { const char *key; char *dst; size_t cap; - } fields[] = { - { "name", name, sizeof name }, - { "address", address, sizeof address }, - { "postal_code", postal, sizeof postal }, - { "city", city, sizeof city }, - { "vat_nr", vat, sizeof vat }, - { "email", email, sizeof email }, - { "your_ref", your_ref, sizeof your_ref }, - { "notes", notes, sizeof notes }, + } f[] = { + { "name", c.name, sizeof c.name }, + { "address", c.address, sizeof c.address }, + { "postal_code", c.postal, sizeof c.postal }, + { "city", c.city, sizeof c.city }, + { "vat_nr", c.vat, sizeof c.vat }, + { "email", c.email, sizeof c.email }, + { "your_ref", c.your_ref, sizeof c.your_ref }, + { "notes", c.notes, sizeof c.notes }, }; - for (size_t i = 0; i < sizeof fields / sizeof fields[0]; i++) { - char path[64], *v; - snprintf(path, sizeof path, "result.%s", fields[i].key); + for (size_t i = 0; i < sizeof f / sizeof f[0]; i++) { + char path[64]; + char *v; + snprintf(path, sizeof path, "result.%s", f[i].key); v = jstr_dup(resp, path); if (v) { - snprintf(fields[i].dst, fields[i].cap, "%s", v); + snprintf(f[i].dst, f[i].cap, "%s", v); free(v); } } - snprintf(payment, sizeof payment, "%lld", + snprintf(c.payment, sizeof c.payment, "%lld", (long long)jint_val(resp, "result.payment_days", 30)); free(resp); } + const char *labels[] = { "Namn", "Adress", "Postnummer", "Ort", "Momsreg.nr", "E-post", "Er referens", "Betalningsdagar", "Anteckningar" }; - char *vals[] = { name, address, postal, city, vat, - email, your_ref, payment, notes }; - size_t caps[] = { sizeof name, sizeof address, sizeof postal, - sizeof city, sizeof vat, sizeof email, - sizeof your_ref, sizeof payment, sizeof notes }; + char *vals[] = { c.name, c.address, c.postal, c.city, + c.vat, c.email, c.your_ref, c.payment, + c.notes }; + size_t caps[] = { sizeof c.name, sizeof c.address, sizeof c.postal, + sizeof c.city, sizeof c.vat, sizeof c.email, + sizeof c.your_ref, sizeof c.payment, sizeof c.notes }; struct tui_form_field ff[10]; memset(ff, 0, sizeof ff); for (int i = 0; i < 9; i++) { @@ -282,22 +402,47 @@ static void customer_form(struct app *a, int64_t id) ff[9].label = ""; ff[9].value = "Spara"; ff[9].kind = TUI_F_ACTION; + static int sel = 0; + tui_form_hint_extra("F2 = åtgärder"); for (;;) { - int r = tui_form_run(id > 0 ? "Kund" : "Ny kund", ff, 10, 1, &sel); + char title[64]; + snprintf(title, sizeof title, "%s%s", id > 0 ? "Kund" : "Ny kund", + drafts_have(kdrafts(), a->org, "customer", draft_id) + ? " <UTKAST>" + : ""); + int r = tui_form_run_hook(title, ff, 10, 1, cform_key, &c, &sel); + if (c.deleted) + break; + if (r >= 0) { + char *json = customer_fields_json(&c); + if (json) { + drafts_put(kdrafts(), a->org, "customer", draft_id, json); + free(json); + } + } if (r == TUI_FORM_BACK) - return; + break; if (r == TUI_FORM_REFRESH) { - customer_save(a, id, name, address, postal, city, vat, email, - your_ref, payment, notes, 1); + customer_save(a, id, c.name, c.address, c.postal, c.city, c.vat, + c.email, c.your_ref, c.payment, c.notes, 1); continue; } if (r == TUI_FORM_SUBMIT || r == 9) { - if (customer_save(a, id, name, address, postal, city, vat, email, - your_ref, payment, notes, 0)) - return; + if (customer_save(a, id, c.name, c.address, c.postal, c.city, + c.vat, c.email, c.your_ref, c.payment, c.notes, + 0)) { + drafts_del(kdrafts(), a->org, "customer", draft_id); + break; + } + char *json = customer_fields_json(&c); + if (json) { + drafts_put(kdrafts(), a->org, "customer", draft_id, json); + free(json); + } } } + tui_form_hint_extra(NULL); } static void customer_archive_toggle(struct app *a, int64_t id, const char *name, @@ -319,59 +464,170 @@ static void customer_archive_toggle(struct app *a, int64_t id, const char *name, free(r); } +struct clist { + struct app *a; + int *cur; + char **items; + char **names; + char **dkeys; + int64_t *ids; + unsigned char *active; + size_t nserver; + size_t nrows; /* server rows + temp drafts; the add row is at nrows */ +}; + +static void clist_clear(struct clist *l) +{ + if (l->items) { + for (size_t i = 0; i <= l->nrows; i++) { + free(l->items[i]); + free(l->names[i]); + free(l->dkeys[i]); + } + } + free(l->items); + free(l->names); + free(l->dkeys); + free(l->ids); + free(l->active); + l->items = NULL; + l->names = NULL; + l->dkeys = NULL; + l->ids = NULL; + l->active = NULL; + l->nserver = l->nrows = 0; +} + +static int clist_is_server_id(const struct clist *l, const char *id) +{ + long long v = strtoll(id, NULL, 10); + + if (v <= 0) + return 0; + for (size_t i = 0; i < l->nserver; i++) + if (l->ids[i] == v) + return 1; + return 0; +} + +static void clist_collect_temp(void *ud, const char *id, const char *fields) +{ + struct clist *l = ud; + char nbuf[256], line[768]; + const char *name = "(namnlös)"; + yyjson_doc *doc; + + if (clist_is_server_id(l, id)) + return; + doc = yyjson_read(fields, strlen(fields), 0); + if (doc) { + yyjson_val *o = yyjson_doc_get_root(doc); + const char *n = vstr(o, "name"); + if (*n) + name = n; + size_t i = l->nrows; + l->names[i] = xstrdup(name); + snprintf(nbuf, sizeof nbuf, "<UTKAST> %s", name); + snprintf(line, sizeof line, "%s", nbuf); + l->items[i] = xstrdup(line); + l->dkeys[i] = xstrdup(id); + l->ids[i] = 0; + l->active[i] = 1; + l->nrows++; + yyjson_doc_free(doc); + return; + } + size_t i = l->nrows; + l->names[i] = xstrdup(name); + snprintf(line, sizeof line, "<UTKAST> %s", name); + l->items[i] = xstrdup(line); + l->dkeys[i] = xstrdup(id); + l->ids[i] = 0; + l->active[i] = 1; + l->nrows++; +} + +static int clist_key(void *ud, int ch) +{ + struct clist *l = ud; + static int afocus; + + if (ch != KEY_F(2) || !l->cur) + return TUI_HOOK_NONE; + int i = *l->cur; + if (i < 0 || (size_t)i >= l->nrows) + return TUI_HOOK_STAY; + struct tui_action acts[] = { + { "customer.open", "Öppna", 0, 1, NULL }, + { "customer.draft.delete", "Radera utkast", 0, l->dkeys[i] != NULL, + "inget utkast" }, + { "customer.archive", l->active[i] ? "Arkivera" : "Återaktivera", 0, + l->ids[i] != 0, "gäller inte utkast" }, + }; + int r = tui_action_menu("Åtgärder", acts, 3, &afocus); + if (r == 0) { + char idbuf[32]; + const char *dk = l->dkeys[i]; + if (!dk) { + snprintf(idbuf, sizeof idbuf, "%lld", (long long)l->ids[i]); + dk = idbuf; + } + customer_form(l->a, l->ids[i], dk); + return TUI_HOOK_REFRESH; + } + if (r == 1) { + if (tui_confirm("Kund", "Radera utkastet?")) + drafts_del(kdrafts(), l->a->org, "customer", l->dkeys[i]); + return TUI_HOOK_REFRESH; + } + if (r == 2) { + customer_archive_toggle(l->a, l->ids[i], l->names[i], + l->active[i] != 0); + return TUI_HOOK_REFRESH; + } + return TUI_HOOK_STAY; +} + void customers_screen(struct app *a) { - char **items = NULL; - char **names = NULL; - int64_t *ids = NULL; - unsigned char *active = NULL; - size_t n = 0; + struct clist l; + memset(&l, 0, sizeof l); + l.a = a; + int cur = 0; + l.cur = &cur; int fetch = 1; for (;;) { if (g_quit) { - for (size_t i = 0; i < n; i++) { - free(items[i]); - free(names[i]); - } - free(items); - free(names); - free(ids); - free(active); + clist_clear(&l); return; } if (fetch) { - for (size_t i = 0; i < n; i++) { - free(items[i]); - free(names[i]); - } - free(items); - free(names); - free(ids); - free(active); - items = NULL; - names = NULL; - ids = NULL; - active = NULL; - n = 0; + clist_clear(&l); fetch = 0; char *resp = client_rpc(&a->conn, "customer.list", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Kunder", resp); free(resp); + clist_clear(&l); return; } - n = jarr_size(resp, "result.items"); - items = xcalloc(n + 1, sizeof(char *)); - names = xcalloc(n + 1, sizeof(char *)); - ids = xcalloc(n ? n : 1, sizeof(int64_t)); - active = xcalloc(n + 1, 1); + size_t n = jarr_size(resp, "result.items"); + size_t ndraft = drafts_count(kdrafts(), a->org, "customer"); + size_t cap = n + ndraft + 2; + l.items = xcalloc(cap, sizeof(char *)); + l.names = xcalloc(cap, sizeof(char *)); + l.dkeys = xcalloc(cap, sizeof(char *)); + l.ids = xcalloc(cap, sizeof(int64_t)); + l.active = xcalloc(cap, 1); + l.nserver = n; + l.nrows = n; for (size_t i = 0; i < n; i++) { - char path[64], line[768]; + char path[64], line[768], idbuf[32]; snprintf(path, sizeof path, "result.items.%zu.id", i); - ids[i] = jint_val(resp, path, 0); + l.ids[i] = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.active", i); - active[i] = (unsigned char)jbool_val(resp, path, 1); + l.active[i] = (unsigned char)jbool_val(resp, path, 1); snprintf(path, sizeof path, "result.items.%zu.name", i); char *name = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.postal_code", i); @@ -390,60 +646,81 @@ void customers_screen(struct app *a) tui_pad_field(pbuf, sizeof pbuf, 30); snprintf(vbuf, sizeof vbuf, "%s", vat ? vat : ""); tui_pad_field(vbuf, sizeof vbuf, 18); - snprintf(line, sizeof line, "%s %s %s %3lld dagar %s", nbuf, - pbuf, vbuf, (long long)days, - active[i] ? "" : "(arkiverad)"); - items[i] = xstrdup(line); - names[i] = xstrdup(name ? name : ""); + snprintf(idbuf, sizeof idbuf, "%lld", (long long)l.ids[i]); + int isdraft = + drafts_have(kdrafts(), a->org, "customer", idbuf); + if (isdraft) + l.dkeys[i] = xstrdup(idbuf); + snprintf(line, sizeof line, "%s%s %s %s %3lld dagar %s", + isdraft ? "<UTKAST> " : "", nbuf, pbuf, vbuf, + (long long)days, + l.active[i] ? "" : "(arkiverad)"); + l.items[i] = xstrdup(line); + l.names[i] = xstrdup(name ? name : ""); free(name); free(postal); free(city); free(vat); } free(resp); - items[n] = xstrdup("+ Ny kund (^N)"); + drafts_foreach(kdrafts(), a->org, "customer", clist_collect_temp, + &l); + l.items[l.nrows] = xstrdup("+ Ny kund (^N)"); + l.names[l.nrows] = xstrdup(""); + l.ids[l.nrows] = 0; + l.active[l.nrows] = 1; } int start = 0; if (a->customer_sel) { - for (size_t i = 0; i < n; i++) - if (ids[i] == a->customer_sel) { + for (size_t i = 0; i < l.nserver; i++) + if (l.ids[i] == a->customer_sel) { start = (int)i; break; } } - int cur = start; - int sel = tui_select_list("Kunder", items, (int)n + 1, start, 1, &cur, - 1, "arkivera/återaktivera", 0); - if (cur >= 0 && (size_t)cur < n) - a->customer_sel = ids[cur]; + cur = start; + tui_list_hint_extra("F2 = åtgärder"); + int sel = tui_select_list_hook("Kunder", l.items, + (int)l.nrows + 1, start, 1, &cur, 1, + "arkivera/återaktivera", 0, clist_key, + &l); + tui_list_hint_extra(NULL); + if (cur >= 0 && (size_t)cur < l.nserver) + a->customer_sel = l.ids[cur]; if (sel == -1) { - for (size_t i = 0; i < n; i++) { - free(items[i]); - free(names[i]); - } - free(items); - free(names); - free(ids); - free(active); + clist_clear(&l); return; } if (sel == -2) { fetch = 1; continue; } - if (sel == -6 && cur >= 0 && (size_t)cur < n) { - customer_archive_toggle(a, ids[cur], names[cur], - active[cur] != 0); + if (sel == -6 && cur >= 0 && (size_t)cur < l.nrows) { + if (l.ids[cur] == 0) { + tui_message("Kund", "Utkast raderas med F2."); + } else { + customer_archive_toggle(a, l.ids[cur], l.names[cur], + l.active[cur] != 0); + } fetch = 1; continue; } - if (sel == -4 || (sel >= 0 && (size_t)sel == n)) { - customer_form(a, 0); + if (sel == -4 || (cur >= 0 && (size_t)cur == l.nrows)) { + char *tid = drafts_new_id(); + drafts_put(kdrafts(), a->org, "customer", tid, "{}"); + customer_form(a, 0, tid); + free(tid); fetch = 1; continue; } - if (sel >= 0 && (size_t)sel < n) { - customer_form(a, ids[sel]); + if (sel >= 0 && (size_t)sel < l.nrows) { + char idbuf[32]; + const char *dk = l.dkeys[sel]; + if (!dk) { + snprintf(idbuf, sizeof idbuf, "%lld", (long long)l.ids[sel]); + dk = idbuf; + } + customer_form(a, l.ids[sel], dk); fetch = 1; continue; } |
