#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "client.h" #include "tui.h" #include "formula.h" #include "util.h" #include "version.h" #include "yyjson.h" #include "ui.h" /* ------------------------------------------------------------------ */ /* invoicing: Fakturor, Kunder */ /* ------------------------------------------------------------------ */ static void today_iso(char *buf, size_t n) { time_t now = time(NULL); struct tm tm; localtime_r(&now, &tm); char tmp[64]; snprintf(tmp, sizeof tmp, "%04d-%02d-%02d", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday); size_t len = strlen(tmp); if (len >= n) len = n ? n - 1 : 0; memcpy(buf, tmp, len); if (n) buf[len] = '\0'; } /* Directory for generated invoice PDFs. */ static void invoice_cache_dir(char *buf, size_t n) { const char *cache = getenv("XDG_CACHE_HOME"); if (cache && *cache) { snprintf(buf, n, "%s/bokf", cache); return; } snprintf(buf, n, "/tmp"); } /* Keep a customer-supplied name from escaping the directory. */ static void safe_fname(const char *in, char *out, size_t n) { size_t o = 0; for (const char *p = in ? in : ""; *p && o + 1 < n; p++) { unsigned char c = (unsigned char)*p; if (c < 32 || c == '/') c = '-'; out[o++] = (char)c; } out[o] = '\0'; } static int write_pdf_b64(const char *b64, const char *path) { unsigned char *data = NULL; size_t len = 0; if (!b64 || util_b64_decode(b64, strlen(b64), &data, &len) != 0) return -1; FILE *fp = fopen(path, "wb"); if (!fp) { free(data); return -1; } size_t wrote = fwrite(data, 1, len, fp); fclose(fp); free(data); return wrote == len ? 0 : -1; } /* Open a saved PDF with xdg-open when a desktop session is present; otherwise (or on fork failure) show where it was saved. */ static void open_pdf(const char *path) { const char *display = getenv("DISPLAY"); const char *wayland = getenv("WAYLAND_DISPLAY"); if ((!display || !*display) && (!wayland || !*wayland)) { tui_message("Faktura PDF", "Sparad: %s", path); return; } pid_t pid = fork(); if (pid == 0) { pid_t gc = fork(); if (gc == 0) { setsid(); execlp("xdg-open", "xdg-open", path, (char *)NULL); _exit(127); } _exit(gc < 0 ? 1 : 0); } if (pid > 0) waitpid(pid, NULL, 0); else tui_message("Faktura PDF", "Sparad: %s", path); } static void save_and_open_pdf(const char *b64, const char *filename) { char dir[512], path[700], safe[600]; invoice_cache_dir(dir, sizeof dir); safe_fname(filename, safe, sizeof safe); if (snprintf(path, sizeof path, "%s/%s", dir, safe) >= (int)sizeof path) { tui_message("Faktura PDF", "Sökvägen blir för lång."); return; } config_mkdirs(path); if (write_pdf_b64(b64, path) != 0) { tui_message("Faktura PDF", "Kunde inte spara %s", path); return; } open_pdf(path); } static const char *invoice_status_sv(const char *s) { if (s && strcmp(s, "credited") == 0) return "krediterad"; return "utfärdad"; } static void qty_sv(int64_t milli, char *buf, size_t n) { int64_t whole = milli / 1000; int64_t frac = milli % 1000; char tmp[64]; if (frac == 0) { snprintf(tmp, sizeof tmp, "%lld", (long long)whole); } else { if (frac < 0) frac = -frac; char fr[16]; snprintf(fr, sizeof fr, "%03lld", (long long)frac); while (fr[0] && fr[strlen(fr) - 1] == '0') fr[strlen(fr) - 1] = '\0'; snprintf(tmp, sizeof tmp, "%lld,%s", (long long)whole, fr); } size_t len = strlen(tmp); if (len >= n) len = n ? n - 1 : 0; memcpy(buf, tmp, len); if (n) buf[len] = '\0'; } static const char *vat_label(const char *code) { if (!code) return ""; if (strcmp(code, "25") == 0) return "25%"; if (strcmp(code, "12") == 0) return "12%"; if (strcmp(code, "6") == 0) return "6%"; if (strcmp(code, "0") == 0) return "0%"; if (strcmp(code, "rc") == 0) return "Omvänd moms"; if (strcmp(code, "eu") == 0) return "EU"; return code; } static const char *vat_code_of(const char *s) { if (!s || !*s) return "25"; if (strcmp(s, "25") == 0 || strcmp(s, "25%") == 0) return "25"; if (strcmp(s, "12") == 0 || strcmp(s, "12%") == 0) return "12"; if (strcmp(s, "6") == 0 || strcmp(s, "6%") == 0) return "6"; if (strcmp(s, "0") == 0 || strcmp(s, "0%") == 0) return "0"; if (strcasecmp(s, "rc") == 0 || strcasecmp(s, "Omvänd moms") == 0) return "rc"; if (strcasecmp(s, "eu") == 0 || strcasecmp(s, "EU") == 0) return "eu"; return NULL; } static const char *unit_code_of(const char *s) { if (!s || !*s) return "st"; if (strcasecmp(s, "st") == 0) return "st"; if (strcasecmp(s, "tim") == 0) return "tim"; if (strcasecmp(s, "dag") == 0) return "dag"; if (strcasecmp(s, "km") == 0) return "km"; return NULL; } static int unit_index(const char *s) { static const char *const opts[] = { "st", "tim", "dag", "km" }; if (s && *s) { for (int i = 0; i < 4; i++) if (strcmp(s, opts[i]) == 0) return i; } return 0; } static int qty_is_valid(const char *s) { if (!s || !*s) return 0; int sep = 0, frac = 0, intd = 0, nonzero = 0; for (const char *p = s; *p; p++) { if (*p == ',' || *p == '.') { if (sep) return 0; sep = 1; continue; } if (*p < '0' || *p > '9') return 0; if (*p != '0') nonzero = 1; if (sep) { frac++; if (frac > 3) return 0; } else if (++intd > 15) { return 0; } } if (!intd || (sep && frac == 0)) return 0; return nonzero; } /* ---------------------------- Kunder ------------------------------ */ static int customer_save(struct app *a, int64_t id, const char *name, const char *address, const char *postal, const char *city, const char *vat, const char *email, const char *your_ref, const char *payment, const char *notes, int dry) { if (!name || !*name) { tui_message("Kund", "Namn krävs."); return 0; } if (!payment || !*payment) { tui_message("Kund", "Betalningsdagar krävs."); return 0; } for (const char *p = payment; *p; p++) { if (*p < '0' || *p > '9') { tui_message("Kund", "Betalningsdagar ska vara ett heltal."); 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 (id > 0) yyjson_mut_obj_add_int(d, o, "id", id); yyjson_mut_obj_add_strcpy(d, o, "name", name); yyjson_mut_obj_add_strcpy(d, o, "address", address); yyjson_mut_obj_add_strcpy(d, o, "postal_code", postal); yyjson_mut_obj_add_strcpy(d, o, "city", city); yyjson_mut_obj_add_strcpy(d, o, "vat_nr", vat); yyjson_mut_obj_add_strcpy(d, o, "email", email); yyjson_mut_obj_add_strcpy(d, o, "your_ref", your_ref); yyjson_mut_obj_add_int(d, o, "payment_days", strtoll(payment, NULL, 10)); yyjson_mut_obj_add_strcpy(d, o, "notes", notes); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); const char *cmd = id > 0 ? "customer.update" : "customer.create"; char *r = dry ? rpc_dry(a, cmd, args) : client_rpc(&a->conn, cmd, a->session, a->org, args); free(args); int ok = r && client_ok(r); if (ok) tui_message("Kund", dry ? "Uppgifterna är giltiga." : "Sparat."); else show_error(dry ? "Kunden kunde inte valideras" : "Kunde inte spara", r); free(r); return ok; } static void customer_form(struct app *a, int64_t id) { 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) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); char *resp = client_rpc(&a->conn, "customer.get", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Kund", resp); free(resp); return; } 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 }, }; 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); v = jstr_dup(resp, path); if (v) { snprintf(fields[i].dst, fields[i].cap, "%s", v); free(v); } } snprintf(payment, sizeof 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 }; struct tui_form_field ff[10]; memset(ff, 0, sizeof ff); for (int i = 0; i < 9; i++) { ff[i].label = labels[i]; ff[i].value = vals[i]; ff[i].cap = caps[i]; ff[i].kind = TUI_F_TEXT; } ff[9].label = ""; ff[9].value = "Spara"; ff[9].kind = TUI_F_ACTION; static int sel = 0; for (;;) { int r = tui_form_run(id > 0 ? "Kund" : "Ny kund", ff, 10, 1, &sel); if (r == TUI_FORM_BACK) return; if (r == TUI_FORM_REFRESH) { customer_save(a, id, name, address, postal, city, vat, email, your_ref, payment, 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; } } } static void customer_archive_toggle(struct app *a, int64_t id, const char *name, int active) { if (!tui_confirm("Kund", active ? "Arkivera kund %s?" : "Återaktivera kund %s?", name ? name : "")) return; char args[128]; snprintf(args, sizeof args, "{\"id\":%lld,\"active\":%s}", (long long)id, active ? "false" : "true"); char *r = client_rpc(&a->conn, "customer.archive", a->session, a->org, args); if (r && client_ok(r)) tui_message("Kund", active ? "Arkiverad." : "Återaktiverad."); else show_error("Kund", r); free(r); } void customers_screen(struct app *a) { char **items = NULL; char **names = NULL; int64_t *ids = NULL; unsigned char *active = NULL; size_t n = 0; 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); 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; 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); 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); for (size_t i = 0; i < n; i++) { char path[64], line[768]; snprintf(path, sizeof path, "result.items.%zu.id", i); 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); 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); char *postal = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.city", i); char *city = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.vat_nr", i); char *vat = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.payment_days", i); int64_t days = jint_val(resp, path, 0); char nbuf[256], pbuf[256], vbuf[80]; snprintf(nbuf, sizeof nbuf, "%s", name ? name : ""); tui_pad_field(nbuf, sizeof nbuf, 30); snprintf(pbuf, sizeof pbuf, "%s %s", postal ? postal : "", city ? city : ""); 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 : ""); free(name); free(postal); free(city); free(vat); } free(resp); items[n] = xstrdup("+ Ny kund (^N)"); } int start = 0; if (a->customer_sel) { for (size_t i = 0; i < n; i++) if (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]; 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); 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); fetch = 1; continue; } if (sel == -4 || (sel >= 0 && (size_t)sel == n)) { customer_form(a, 0); fetch = 1; continue; } if (sel >= 0 && (size_t)sel < n) { customer_form(a, ids[sel]); fetch = 1; continue; } } } /* --------------------------- Fakturor ----------------------------- */ struct invctx { struct app *a; int64_t id; int64_t number; int64_t customer_id; char customer[256]; }; static void inv_detail_pdf(struct invctx *ctx) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ctx->id); char *resp = client_rpc(&ctx->a->conn, "invoice.pdf", ctx->a->session, ctx->a->org, args); if (!resp || !client_ok(resp)) { show_error("Kunde inte hämta PDF", resp); free(resp); return; } char *b64 = jstr_dup(resp, "result.content_base64"); if (b64) { char fname[800]; snprintf(fname, sizeof fname, "Faktura %lld %s.pdf", (long long)ctx->number, ctx->customer); save_and_open_pdf(b64, fname); free(b64); } free(resp); } static void inv_detail_send(struct invctx *ctx) { struct app *a = ctx->a; char email[256] = ""; if (ctx->customer_id > 0) { char cargs[64]; snprintf(cargs, sizeof cargs, "{\"id\":%lld}", (long long)ctx->customer_id); char *c = client_rpc(&a->conn, "customer.get", a->session, a->org, cargs); if (c && client_ok(c)) { char *e = jstr_dup(c, "result.email"); if (e) { snprintf(email, sizeof email, "%s", e); free(e); } } free(c); } if (!email[0]) { tui_message("Skicka faktura", "Kunden har ingen e-postadress."); return; } if (!tui_confirm("Skicka faktura", "Skicka faktura %lld till %s?", (long long)ctx->number, email)) return; char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ctx->id); char *r = client_rpc(&a->conn, "invoice.send", a->session, a->org, args); if (r && client_ok(r)) { char *to = jstr_dup(r, "result.sent_to"); tui_message("Skickat", "Faktura %lld skickad till %s.", (long long)ctx->number, to && *to ? to : email); free(to); } else { show_error("Skicka faktura", r); } free(r); } static int inv_key(void *ud, int ch) { struct invctx *ctx = ud; if (ch == 'p') { inv_detail_pdf(ctx); return TUI_HOOK_STAY; } if (ch == 's') { inv_detail_send(ctx); return TUI_HOOK_STAY; } return TUI_HOOK_NONE; } static void invoices_detail(struct app *a, int64_t id) { char args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id); for (;;) { char *resp = client_rpc(&a->conn, "invoice.get", a->session, a->org, args); if (!resp || !client_ok(resp)) { show_error("Faktura", resp); free(resp); return; } int64_t number = jint_val(resp, "result.number", 0); int64_t customer_id = jint_val(resp, "result.customer_id", 0); int64_t net = jint_val(resp, "result.net_ore", 0); int64_t vat = jint_val(resp, "result.vat_ore", 0); int64_t total = jint_val(resp, "result.total_ore", 0); char *cust = jstr_dup(resp, "result.customer_name"); char *status = jstr_dup(resp, "result.status"); char *date = jstr_dup(resp, "result.invoice_date"); char *due = jstr_dup(resp, "result.due_date"); char *delivery = jstr_dup(resp, "result.delivery_date"); char *your_ref = jstr_dup(resp, "result.your_ref"); char *our_ref = jstr_dup(resp, "result.our_ref"); char *notes = jstr_dup(resp, "result.notes"); char *ocr = jstr_dup(resp, "result.ocr"); char *sent_at = jstr_dup(resp, "result.last_sent_at"); char *sent_to = jstr_dup(resp, "result.last_sent_to"); struct buf t; buf_init(&t); buf_line(&t, MK_HEAD "Faktura %lld %s\n", (long long)number, invoice_status_sv(status)); buf_line(&t, MK_DIM "%s\n\n", cust ? cust : ""); buf_line(&t, "Fakturadatum: %s\n", date ? date : ""); buf_line(&t, "Förfallodatum: %s\n", due ? due : ""); if (delivery && *delivery) buf_line(&t, "Leveransdatum: %s\n", delivery); if (your_ref && *your_ref) buf_line(&t, "Er referens: %s\n", your_ref); if (our_ref && *our_ref) buf_line(&t, "Vår referens: %s\n", our_ref); if (ocr && *ocr) buf_line(&t, "OCR: %s\n", ocr); if (sent_at && *sent_at) buf_line(&t, "Skickad: %s%s%s\n", sent_at, sent_to && *sent_to ? " till " : "", sent_to && *sent_to ? sent_to : ""); if (notes && *notes) buf_line(&t, "\nFritext:\n%s\n", notes); buf_line(&t, "\n"); buf_line(&t, MK_STICKY MK_HEAD "%-34s %8s %-6s %12s %-13s %-18s %12s\n", "Beskrivning", "Antal", "Enhet", "À-pris", "Moms", "Anm", "Belopp"); size_t nrows = jarr_size(resp, "result.rows"); for (size_t i = 0; i < nrows; i++) { char path[64]; snprintf(path, sizeof path, "result.rows.%zu.description", i); char *desc = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.quantity_milli", i); int64_t qm = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.rows.%zu.unit", i); char *unit = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.unit_price_ore", i); int64_t price = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.rows.%zu.vat_code", i); char *vat_code = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.note", i); char *note = jstr_dup(resp, path); snprintf(path, sizeof path, "result.rows.%zu.amount_ore", i); int64_t amount = jint_val(resp, path, 0); char dbuf[256], qbuf[24], qcol[24], ubuf[24], pcol[40], vbuf[24], nbuf[128], acol[40]; snprintf(dbuf, sizeof dbuf, "%s", desc ? desc : ""); tui_pad_field(dbuf, sizeof dbuf, 34); qty_sv(qm, qbuf, sizeof qbuf); tui_pad_hdr(qcol, sizeof qcol, 8, qbuf); snprintf(ubuf, sizeof ubuf, "%s", unit ? unit : ""); tui_pad_field(ubuf, sizeof ubuf, 6); tui_amt_col(pcol, sizeof pcol, 12, price); snprintf(vbuf, sizeof vbuf, "%s", vat_label(vat_code)); tui_pad_field(vbuf, sizeof vbuf, 13); snprintf(nbuf, sizeof nbuf, "%s", note ? note : ""); tui_pad_field(nbuf, sizeof nbuf, 18); tui_amt_col(acol, sizeof acol, 12, amount); buf_line(&t, "%s %s %s %s %s %s %s\n", dbuf, qcol, ubuf, pcol, vbuf, nbuf, acol); free(desc); free(unit); free(vat_code); free(note); } char netc[40], vatc[40], totc[40]; tui_amt_col(netc, sizeof netc, 14, net); tui_amt_col(vatc, sizeof vatc, 14, vat); tui_amt_col(totc, sizeof totc, 14, total); buf_line(&t, "\n%46s %14s\n", "Netto", netc); buf_line(&t, "%46s %14s\n", "Moms", vatc); buf_line(&t, MK_HEAD "%46s %14s\n", "Att betala", totc); buf_append(&t, "\0", 1); struct invctx ctx; memset(&ctx, 0, sizeof ctx); ctx.a = a; ctx.id = id; ctx.number = number; ctx.customer_id = customer_id; snprintf(ctx.customer, sizeof ctx.customer, "%s", cust ? cust : ""); int ret = tui_pager_hook("Faktura", (const char *)t.p, "p = visa PDF s = skicka", 0, inv_key, &ctx); free(cust); free(status); free(date); free(due); free(delivery); free(your_ref); free(our_ref); free(notes); free(ocr); free(sent_at); free(sent_to); buf_free(&t); free(resp); if (ret != 1) return; } } #define IFORM_ROWS 64 struct irow { char description[128]; char quantity[16]; char unit[8]; char price[24]; char vat[16]; char note[64]; }; struct iform { struct app *a; struct tui_rt rt; struct irow rows[IFORM_ROWS]; char date[16]; char due[16]; char delivery[16]; char your_ref[64]; char our_ref[64]; char notes[256]; char auto_due[16]; char auto_ref[64]; size_t ncust; int64_t *cust_ids; int *cust_days; char **cust_names; char **cust_emails; char **cust_refs; int cust_sel; int issued; }; static void iform_customers_free(struct iform *f) { for (size_t i = 0; i < f->ncust; i++) { free(f->cust_names[i]); free(f->cust_emails[i]); free(f->cust_refs[i]); } free(f->cust_names); free(f->cust_emails); free(f->cust_refs); free(f->cust_ids); free(f->cust_days); f->cust_names = NULL; f->cust_emails = NULL; f->cust_refs = NULL; f->cust_ids = NULL; f->cust_days = NULL; f->ncust = 0; } static int iform_load_customers(struct iform *f) { char *resp = client_rpc(&f->a->conn, "customer.list", f->a->session, f->a->org, "{\"active_only\":true}"); if (!resp || !client_ok(resp)) { show_error("Kunder", resp); free(resp); return -1; } size_t n = jarr_size(resp, "result.items"); f->cust_ids = xcalloc(n ? n : 1, sizeof(int64_t)); f->cust_days = xcalloc(n ? n : 1, sizeof(int)); f->cust_names = xcalloc(n ? n : 1, sizeof(char *)); f->cust_emails = xcalloc(n ? n : 1, sizeof(char *)); f->cust_refs = xcalloc(n ? n : 1, sizeof(char *)); for (size_t i = 0; i < n; i++) { char path[64]; snprintf(path, sizeof path, "result.items.%zu.id", i); f->cust_ids[i] = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.payment_days", i); f->cust_days[i] = (int)jint_val(resp, path, 30); snprintf(path, sizeof path, "result.items.%zu.name", i); char *nm = jstr_dup(resp, path); f->cust_names[i] = nm ? nm : xstrdup(""); snprintf(path, sizeof path, "result.items.%zu.email", i); char *em = jstr_dup(resp, path); f->cust_emails[i] = em ? em : xstrdup(""); snprintf(path, sizeof path, "result.items.%zu.your_ref", i); char *rf = jstr_dup(resp, path); f->cust_refs[i] = rf ? rf : xstrdup(""); } f->ncust = n; free(resp); return 0; } /* Re-apply the customer-derived defaults only while the user has not changed them (they still equal what the previous customer produced). */ static void iform_customer_changed(struct iform *f) { if (f->ncust == 0 || f->cust_sel < 0 || (size_t)f->cust_sel >= f->ncust) return; char today[16], newdue[16]; today_iso(today, sizeof today); util_date_add_days(today, f->cust_days[f->cust_sel], newdue, sizeof newdue); if (!f->due[0] || strcmp(f->due, f->auto_due) == 0) snprintf(f->due, sizeof f->due, "%s", newdue); snprintf(f->auto_due, sizeof f->auto_due, "%s", newdue); const char *ref = f->cust_refs[f->cust_sel]; if (!f->your_ref[0] || strcmp(f->your_ref, f->auto_ref) == 0) snprintf(f->your_ref, sizeof f->your_ref, "%s", ref ? ref : ""); snprintf(f->auto_ref, sizeof f->auto_ref, "%s", ref ? ref : ""); } static void iform_cell(void *ud, int row, int col, char **buf, size_t *cap) { struct iform *f = ud; struct irow *r = &f->rows[row]; if (col == 0) { *buf = r->description; *cap = sizeof r->description; } else if (col == 1) { *buf = r->quantity; *cap = sizeof r->quantity; } else if (col == 2) { *buf = r->unit; *cap = sizeof r->unit; } else if (col == 3) { *buf = r->price; *cap = sizeof r->price; } else if (col == 4) { *buf = r->vat; *cap = sizeof r->vat; } else { *buf = r->note; *cap = sizeof r->note; } } static const char *const UNIT_CHOICES[] = { "st", "tim", "dag", "km" }; static const char *const VAT_LABELS[] = { "25%", "12%", "6%", "0%", "Omvänd moms", "EU" }; static const char *const VAT_CODES[] = { "25", "12", "6", "0", "rc", "eu" }; static int vat_index(const char *s) { const char *code = vat_code_of(s); if (!code) return 0; for (int i = 0; i < 6; i++) if (strcmp(code, VAT_CODES[i]) == 0) return i; return 0; } static int iform_key(void *ud, int ch) { struct iform *f = ud; struct tui_rt *rt = &f->rt; if (ch != '\n' && ch != '\r' && ch != KEY_ENTER) return TUI_HOOK_NONE; if (rt->focus == 0) { if (f->ncust == 0) { tui_message("Ny faktura", "Inga kunder. Skapa en under Register " "→ Kunder först."); return TUI_HOOK_STAY; } int c = tui_choice_prompt("Kund: ", (const char *const *)f->cust_names, (int)f->ncust, f->cust_sel); if (c >= 0) { int changed = c != f->cust_sel; f->cust_sel = c; if (changed) iform_customer_changed(f); tui_rt_normalize(rt); rt->focus = tui_rt_focus_step(0, rt->nfields, &rt->row, &rt->col, rt->nrows, rt->neditable, 1); } return TUI_HOOK_STAY; } if (rt->focus < 0 && rt->row >= 0 && rt->row < IFORM_ROWS) { struct irow *r = &f->rows[rt->row]; if (rt->col == 2) { int c = tui_choice_prompt("Enhet: ", UNIT_CHOICES, 4, unit_index(r->unit)); if (c >= 0) snprintf(r->unit, sizeof r->unit, "%s", UNIT_CHOICES[c]); return TUI_HOOK_STAY; } if (rt->col == 4) { int c = tui_choice_prompt("Moms: ", VAT_LABELS, 6, vat_index(r->vat)); if (c >= 0) snprintf(r->vat, sizeof r->vat, "%s", VAT_LABELS[c]); return TUI_HOOK_STAY; } } return TUI_HOOK_NONE; } static char *iform_draft_args(struct iform *f, char *err, size_t errn) { if (f->ncust == 0 || f->cust_sel < 0 || (size_t)f->cust_sel >= f->ncust) { snprintf(err, errn, "Välj en kund."); return NULL; } if (!util_parse_iso_date(f->date)) { snprintf(err, errn, "Ogiltigt fakturadatum (ÅÅÅÅ-MM-DD)."); return NULL; } if (!util_parse_iso_date(f->due)) { snprintf(err, errn, "Ogiltigt förfallodatum (ÅÅÅÅ-MM-DD)."); return NULL; } if (f->delivery[0] && !util_parse_iso_date(f->delivery)) { snprintf(err, errn, "Ogiltigt leveransdatum (ÅÅÅÅ-MM-DD)."); return NULL; } 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, "customer_id", f->cust_ids[f->cust_sel]); yyjson_mut_obj_add_strcpy(d, o, "invoice_date", f->date); yyjson_mut_obj_add_strcpy(d, o, "due_date", f->due); yyjson_mut_obj_add_strcpy(d, o, "delivery_date", f->delivery); yyjson_mut_obj_add_strcpy(d, o, "your_ref", f->your_ref); yyjson_mut_obj_add_strcpy(d, o, "our_ref", f->our_ref); yyjson_mut_obj_add_strcpy(d, o, "notes", f->notes); yyjson_mut_val *arr = yyjson_mut_arr(d); int used = 0; for (int i = 0; i < f->rt.nrows; i++) { struct irow *r = &f->rows[i]; if (!r->description[0] && !r->quantity[0] && !r->unit[0] && !r->price[0] && !r->vat[0] && !r->note[0]) continue; if (!r->description[0]) { snprintf(err, errn, "Rad %d: beskrivning saknas.", i + 1); yyjson_mut_doc_free(d); return NULL; } const char *qty = r->quantity[0] ? r->quantity : "1"; if (!qty_is_valid(qty)) { snprintf(err, errn, "Rad %d: antal måste vara ett positivt tal med högst " "3 decimaler.", i + 1); yyjson_mut_doc_free(d); return NULL; } int64_t price = 0; if (tui_parse_kr(r->price, &price) != 0) { snprintf(err, errn, "Rad %d: ogiltigt à-pris.", i + 1); yyjson_mut_doc_free(d); return NULL; } const char *unit = unit_code_of(r->unit); if (!unit) { snprintf(err, errn, "Rad %d: ogiltig enhet (st, tim, dag, km).", i + 1); yyjson_mut_doc_free(d); return NULL; } const char *vat = vat_code_of(r->vat); if (!vat) { snprintf(err, errn, "Rad %d: ogiltig momskod.", i + 1); yyjson_mut_doc_free(d); return NULL; } yyjson_mut_val *ro = yyjson_mut_arr_add_obj(d, arr); yyjson_mut_obj_add_strcpy(d, ro, "description", r->description); yyjson_mut_obj_add_strcpy(d, ro, "quantity", qty); yyjson_mut_obj_add_strcpy(d, ro, "unit", unit); yyjson_mut_obj_add_int(d, ro, "unit_price_ore", price); yyjson_mut_obj_add_strcpy(d, ro, "note", r->note); yyjson_mut_obj_add_strcpy(d, ro, "vat_code", vat); used++; } if (used == 0) { snprintf(err, errn, "Minst en rad krävs."); yyjson_mut_doc_free(d); return NULL; } yyjson_mut_obj_add_val(d, o, "rows", arr); char *out = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); return out; } static void iform_preview(struct iform *f) { char err[256]; char *args = iform_draft_args(f, err, sizeof err); if (!args) { tui_message("Faktura", "%s", err); return; } char *resp = client_rpc(&f->a->conn, "invoice.preview", f->a->session, f->a->org, args); free(args); if (!resp || !client_ok(resp)) { show_error("Faktura", resp); free(resp); return; } char *b64 = jstr_dup(resp, "result.content_base64"); if (b64) { save_and_open_pdf(b64, "Faktura förhandsvisning.pdf"); free(b64); } free(resp); } static int64_t iform_issue(struct iform *f) { if (f->issued) return 0; char err[256]; char *args = iform_draft_args(f, err, sizeof err); if (!args) { tui_message("Faktura", "%s", err); return 0; } char *resp = client_rpc(&f->a->conn, "invoice.issue", f->a->session, f->a->org, args); free(args); if (!resp || !client_ok(resp)) { show_error("Faktura", resp); free(resp); return 0; } int64_t id = jint_val(resp, "result.id", 0); int64_t number = jint_val(resp, "result.number", 0); free(resp); if (id <= 0) return 0; f->issued = 1; const char *email = f->cust_emails[f->cust_sel]; if (email && *email) { if (tui_confirm("Skicka faktura", "Skicka faktura %lld till %s?", (long long)number, email)) { char args2[64]; snprintf(args2, sizeof args2, "{\"id\":%lld}", (long long)id); char *r = client_rpc(&f->a->conn, "invoice.send", f->a->session, f->a->org, args2); if (r && client_ok(r)) { char *to = jstr_dup(r, "result.sent_to"); tui_message("Skickat", "Faktura %lld skickad till %s.", (long long)number, to && *to ? to : email); free(to); } else { show_error("Skicka faktura", r); } free(r); } } else { tui_message("Faktura", "Faktura %lld skapad.", (long long)number); } return id; } static int64_t invoices_new(struct app *a) { struct iform f; memset(&f, 0, sizeof f); f.a = a; today_iso(f.date, sizeof f.date); snprintf(f.delivery, sizeof f.delivery, "%s", f.date); snprintf(f.due, sizeof f.due, "%s", f.date); snprintf(f.auto_due, sizeof f.auto_due, "%s", f.due); if (iform_load_customers(&f) != 0) { iform_customers_free(&f); return 0; } char *resp = client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); if (resp && client_ok(resp)) { char *v = jstr_dup(resp, "result.invoice_our_ref"); if (v) { snprintf(f.our_ref, sizeof f.our_ref, "%s", v); free(v); } } free(resp); if (f.ncust > 0) { f.cust_sel = 0; iform_customer_changed(&f); } struct tui_rt_col cols[6] = { { "Beskrivning", 2, 34, TUI_RT_EDIT }, { "Antal", 37, 8, TUI_RT_EDIT }, { "Enhet", 46, 6, TUI_RT_EDIT }, { "À-pris", 53, 14, TUI_RT_EDIT }, { "Moms", 68, 14, TUI_RT_EDIT }, { "Anm", 83, 32, TUI_RT_EDIT }, }; tui_rt_init(&f.rt, 1, IFORM_ROWS, 6, cols, 0, iform_cell, NULL, &f); tui_rt_set_key(&f.rt, iform_key); struct tui_form_field ff[7]; memset(ff, 0, sizeof ff); ff[0].label = "Kund"; ff[0].kind = TUI_F_CHOICE; ff[0].choices = (const char *const *)f.cust_names; ff[0].nchoices = (int)f.ncust; ff[0].choice = &f.cust_sel; ff[1].label = "Fakturadatum"; ff[1].value = f.date; ff[1].cap = sizeof f.date; ff[1].kind = TUI_F_DATE; ff[2].label = "Förfallodatum"; ff[2].value = f.due; ff[2].cap = sizeof f.due; ff[2].kind = TUI_F_DATE; ff[3].label = "Leveransdatum"; ff[3].value = f.delivery; ff[3].cap = sizeof f.delivery; ff[3].kind = TUI_F_DATE; ff[4].label = "Er referens"; ff[4].value = f.your_ref; ff[4].cap = sizeof f.your_ref; ff[4].kind = TUI_F_TEXT; ff[5].label = "Vår referens"; ff[5].value = f.our_ref; ff[5].cap = sizeof f.our_ref; ff[5].kind = TUI_F_TEXT; ff[6].label = "Fritext"; ff[6].value = f.notes; ff[6].cap = sizeof f.notes; ff[6].kind = TUI_F_TEXT; tui_rt_set_fields(&f.rt, ff, 7); const char *hint = "Enter = välj/ändra Tab = byta fält F5 = förhandsvisa" " ^Enter = utfärda Esc = avbryt"; int64_t out = 0; for (;;) { int rr = tui_rt_run("Ny faktura", &f.rt, hint); if (rr == -1) break; if (rr == -2) { iform_preview(&f); continue; } if (rr == -3) { out = iform_issue(&f); if (out > 0) break; } } iform_customers_free(&f); return out; } static int invoices_key(void *ud, int ch) { struct app *a = ud; if (ch != 'n') return TUI_HOOK_NONE; char *resp = client_rpc(&a->conn, "invoice.sequence_get", a->session, a->org, "{}"); char def[16] = "1"; if (resp && client_ok(resp)) snprintf(def, sizeof def, "%lld", (long long)jint_val(resp, "result.next_number", 1)); free(resp); char buf[16] = ""; if (!tui_prompt_into(buf, sizeof buf, "Nästa fakturanummer", def, 0)) return TUI_HOOK_STAY; char args[64]; snprintf(args, sizeof args, "{\"next_number\":%s}", buf); resp = client_rpc(&a->conn, "invoice.sequence_set", a->session, a->org, args); if (resp && client_ok(resp)) tui_message("Fakturor", "Nästa fakturanummer: %s.", buf); else show_error("Fakturor", resp); free(resp); return TUI_HOOK_REFRESH; } void invoices_screen(struct app *a) { char **items = NULL; int64_t *ids = NULL; size_t n = 0; int fetch = 1; for (;;) { if (g_quit) { for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); return; } if (fetch) { for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); items = NULL; ids = NULL; n = 0; fetch = 0; char *resp = client_rpc(&a->conn, "invoice.list", a->session, a->org, "{\"limit\":200}"); if (!resp || !client_ok(resp)) { show_error("Fakturor", resp); free(resp); return; } n = jarr_size(resp, "result.items"); items = xcalloc(n + 1, sizeof(char *)); ids = xcalloc(n ? n : 1, sizeof(int64_t)); for (size_t i = 0; i < n; i++) { char path[64], line[768]; snprintf(path, sizeof path, "result.items.%zu.id", i); ids[i] = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.number", i); int64_t number = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.invoice_date", i); char *date = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.customer_name", i); char *cust = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.total_ore", i); int64_t total = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.status", i); char *status = jstr_dup(resp, path); char nbuf[32], cbuf[256], abuf[40]; snprintf(nbuf, sizeof nbuf, "Faktura %lld", (long long)number); snprintf(cbuf, sizeof cbuf, "%s", cust ? cust : ""); tui_pad_field(cbuf, sizeof cbuf, 28); tui_amt_col(abuf, sizeof abuf, 14, total); snprintf(line, sizeof line, "%s %s %s %s %s", nbuf, date ? date : "", cbuf, abuf, invoice_status_sv(status)); items[i] = xstrdup(line); free(date); free(cust); free(status); } free(resp); items[n] = xstrdup("+ Ny faktura (^N)"); } int start = 0; if (a->invoice_sel) { for (size_t i = 0; i < n; i++) if (ids[i] == a->invoice_sel) { start = (int)i; break; } } int cur = start; int sel = tui_select_list_hook("Fakturor", items, (int)n + 1, start, 1, &cur, 1, NULL, 0, invoices_key, a); if (cur >= 0 && (size_t)cur < n) a->invoice_sel = ids[cur]; if (sel == -1) { for (size_t i = 0; i < n; i++) free(items[i]); free(items); free(ids); return; } if (sel == -2) { fetch = 1; continue; } if (sel == -4 || (sel >= 0 && (size_t)sel == n)) { int64_t nid = invoices_new(a); if (nid > 0) a->invoice_sel = nid; fetch = 1; continue; } if (sel >= 0 && (size_t)sel < n) { invoices_detail(a, ids[sel]); continue; } } }