aboutsummaryrefslogtreecommitdiff
path: root/clients/screens_invoices.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-22 12:43:52 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-22 12:43:52 +0200
commit07f5b140922d13519ad7a0d1dad67a9b403be9f1 (patch)
treeb6dde2ed6adb90c6423a5e585b693b908637e5ee /clients/screens_invoices.c
parent760cb25bd220718e37d8b95016f30cdf8ef6acdc (diff)
downloadbokf-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.c465
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;
}