#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" struct company_field { const char *key; const char *label; int numeric; /* 0 = text, 1 = month (1-12), 2 = unsigned integer */ }; static const struct company_field COMPANY_FIELDS[] = { { "name", "Företagsnamn", 0 }, { "org_nr", "Organisationsnummer", 0 }, { "vat_nr", "Momsregistreringsnummer", 0 }, { "address", "Adress", 0 }, { "postal_code", "Postnummer", 0 }, { "city", "Ort", 0 }, { "country", "Land", 0 }, { "email", "E-post", 0 }, { "phone", "Telefon", 0 }, { "description", "Verksamhetsbeskrivning", 0 }, { "shares", "Antal aktier", 2 }, { "moms_period", "Momsperiod (month/quarter/year)", 0 }, { "framework", "Regelverk (K2/K3)", 0 }, { "fiscal_year_start_month", "Startmånad räkenskapsår (1-12)", 1 }, }; static int company_field_valid(const struct company_field *f, const char *v) { if (f->numeric) { char *end = NULL; long n = strtol(v, &end, 10); if (!end || *end != '\0') return 0; return f->numeric == 2 ? n >= 0 : (n >= 1 && n <= 12); } if (strcmp(f->key, "moms_period") == 0) return strcmp(v, "month") == 0 || strcmp(v, "quarter") == 0 || strcmp(v, "year") == 0; if (strcmp(f->key, "framework") == 0) return strcmp(v, "K2") == 0 || strcmp(v, "K3") == 0; return 1; } /* Board members shown in the årsredovisning (name + title). */ static void board_screen(struct app *a) { int owner = a->role[0] && strcmp(a->role, "owner") == 0; int cursor = 0; for (;;) { if (g_quit) return; char *resp = client_rpc(&a->conn, "board.list", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Styrelseledamöter", resp); free(resp); return; } size_t n = jarr_size(resp, "result.items"); int rows = (int)n + (owner ? 1 : 0); char **lines = xcalloc(rows ? rows : 1, sizeof(char *)); char **names = xcalloc(n + 1, sizeof(char *)); char **titles = xcalloc(n + 1, sizeof(char *)); int64_t *ids = xcalloc(n + 1, sizeof(int64_t)); for (size_t i = 0; i < n; i++) { char path[64], line[512]; snprintf(path, sizeof path, "result.items.%zu.id", i); ids[i] = jint_val(resp, path, 0); snprintf(path, sizeof path, "result.items.%zu.name", i); names[i] = jstr_dup(resp, path); snprintf(path, sizeof path, "result.items.%zu.title", i); titles[i] = jstr_dup(resp, path); snprintf(line, sizeof line, "%s %s", titles[i] ? titles[i] : "", names[i] ? names[i] : ""); lines[i] = xstrdup(line); } if (owner) lines[n] = xstrdup("+ Ny ledamot (^N)"); int s = tui_select_list("Styrelseledamöter", lines, rows, cursor, 1, &cursor, owner ? 1 : 0, owner && n > 0 ? "ta bort" : NULL, 0); if (s == -6 && cursor >= 0 && (size_t)cursor < n) { char q[320]; snprintf(q, sizeof q, "Ta bort '%s'? (j/n): ", lines[cursor]); 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 args[64]; snprintf(args, sizeof args, "{\"id\":%lld}", (long long)ids[cursor]); char *r = client_rpc(&a->conn, "board.remove", a->session, a->org, args); if (r && client_ok(r)) { tui_message("Styrelseledamöter", "Borttagen."); } else { show_error("Kunde inte ta bort", r); } free(r); } } else if (s == -4 || (s >= 0 && owner && (size_t)s == n)) { char namebuf[512] = "", titlebuf[512] = ""; int ok_name = tui_prompt_into(namebuf, sizeof namebuf, "Namn: ", "", 0); int ok_title = ok_name && namebuf[0] ? tui_prompt_into(titlebuf, sizeof titlebuf, "Titel: ", "Styrelseledamot", 0) : 0; if (ok_name && namebuf[0] && ok_title && titlebuf[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); yyjson_mut_obj_add_strcpy(d, o, "name", namebuf); yyjson_mut_obj_add_strcpy(d, o, "title", titlebuf); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *r = client_rpc(&a->conn, "board.add", a->session, a->org, args); free(args); if (r && client_ok(r)) tui_message("Styrelseledamöter", "Tillagd."); else show_error("Kunde inte lägga till", r); free(r); } } else if (s >= 0 && (size_t)s < n) { if (!owner) { tui_message("Styrelseledamöter", "Endast ägare kan ändra styrelseledamöterna."); } else { char namebuf[512] = "", titlebuf[512] = ""; int ok_name = tui_prompt_into(namebuf, sizeof namebuf, "Namn: ", names[s] ? names[s] : "", 0); int ok_title = ok_name && namebuf[0] ? tui_prompt_into( titlebuf, sizeof titlebuf, "Titel: ", titles[s] ? titles[s] : "", 0) : 0; if (ok_name && namebuf[0] && ok_title && titlebuf[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); yyjson_mut_obj_add_int(d, o, "id", ids[s]); yyjson_mut_obj_add_strcpy(d, o, "name", namebuf); yyjson_mut_obj_add_strcpy(d, o, "title", titlebuf); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *r = client_rpc(&a->conn, "board.update", a->session, a->org, args); free(args); if (r && client_ok(r)) tui_message("Styrelseledamöter", "Sparad."); else show_error("Kunde inte spara", r); free(r); } } } int back = s == -1; for (int i = 0; i < rows; i++) free(lines[i]); free(lines); for (size_t i = 0; i < n; i++) { free(names[i]); free(titles[i]); } free(names); free(titles); free(ids); free(resp); if (back) return; } } void company_screen(struct app *a) { static int sel = 0; const int nf = (int)(sizeof COMPANY_FIELDS / sizeof COMPANY_FIELDS[0]); const int ninv = 2; static const char *const invkeys[2] = { "invoice_bankgiro", "invoice_our_ref" }; static const char *const invlabels[2] = { "Bankgiro", "Vår referens (faktura)" }; int owner = a->role[0] && strcmp(a->role, "owner") == 0; for (;;) { if (g_quit) return; char *resp = client_rpc(&a->conn, "org.get", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Företagsuppgifter", resp); free(resp); return; } char *vals[20]; struct tui_form_field ff[20]; memset(ff, 0, sizeof ff); for (int i = 0; i < nf; i++) { char path[64]; vals[i] = xcalloc(512, 1); snprintf(path, sizeof path, "result.%s", COMPANY_FIELDS[i].key); if (COMPANY_FIELDS[i].numeric) { snprintf(vals[i], 512, "%lld", (long long)jint_val(resp, path, 0)); } else { char *v = jstr_dup(resp, path); snprintf(vals[i], 512, "%s", v ? v : ""); free(v); } ff[i].label = COMPANY_FIELDS[i].label; ff[i].value = vals[i]; ff[i].cap = 512; ff[i].kind = TUI_F_TEXT; } free(resp); char *sresp = client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); for (int i = 0; i < ninv; i++) { char path[64]; int idx = nf + i; vals[idx] = xcalloc(512, 1); snprintf(path, sizeof path, "result.%s", invkeys[i]); char *v = sresp ? jstr_dup(sresp, path) : NULL; snprintf(vals[idx], 512, "%s", v ? v : ""); free(v); ff[idx].label = invlabels[i]; ff[idx].value = vals[idx]; ff[idx].cap = 512; ff[idx].kind = TUI_F_TEXT; } free(sresp); ff[nf + ninv].label = "Styrelseledamöter"; ff[nf + ninv].kind = TUI_F_ACTION; int r = tui_form_run("Företagsuppgifter", ff, nf + ninv + 1, owner, &sel); if (r >= 0 && r < nf) { if (!company_field_valid(&COMPANY_FIELDS[r], vals[r])) { tui_message("Ogiltigt värde", "%s", COMPANY_FIELDS[r].label); } else { 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 (COMPANY_FIELDS[r].numeric) yyjson_mut_obj_add_int(d, o, COMPANY_FIELDS[r].key, strtoll(vals[r], NULL, 10)); else yyjson_mut_obj_add_strcpy(d, o, COMPANY_FIELDS[r].key, vals[r]); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *rr = client_rpc(&a->conn, "org.update", a->session, a->org, args); free(args); if (!rr || !client_ok(rr)) show_error("Kunde inte spara", rr); free(rr); } } else if (r >= nf && r < nf + ninv) { 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, "key", invkeys[r - nf]); yyjson_mut_obj_add_strcpy(d, o, "value", vals[r]); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *rr = client_rpc(&a->conn, "settings.set", a->session, a->org, args); free(args); if (!rr || !client_ok(rr)) show_error("Kunde inte spara", rr); free(rr); } if (r == nf + ninv) board_screen(a); for (int i = 0; i < nf + ninv; i++) free(vals[i]); if (r == TUI_FORM_BACK) return; } } static const char *const SETTING_KEYS[] = { "default_series", "attachment_dir", "invoice_receivable_account", "invoice_revenue_account", "smtp_host", "smtp_port", "smtp_user", "smtp_from", "smtp_reply_to", "smtp_security", "smtp_password", }; static const char *const SETTING_LABELS[] = { "Standardserie", "Bilagornas mapp", "Fordringskonto", "Intäktskonto", "SMTP-server", "SMTP-port", "SMTP-användare", "SMTP-avsändare", "SMTP-svarsadress", "SMTP-säkerhet", "SMTP-lösenord", }; static const char *const SECURITY_CHOICES[] = { "starttls", "tls", "plain" }; void settings_screen(struct app *a) { static int sel = 0; const int nset = (int)(sizeof SETTING_KEYS / sizeof SETTING_KEYS[0]); for (;;) { if (g_quit) return; char *resp = client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error("Inställningar", resp); free(resp); return; } int pw_set = jbool_val(resp, "result.smtp_password_set", 0); char pwstatus[192]; snprintf(pwstatus, sizeof pwstatus, "SMTP-lösenord (lämna tomt för oförändrat): %s", pw_set ? "satt" : "inte satt"); char *vals[16]; struct tui_form_field ff[16]; memset(ff, 0, sizeof ff); int sec_choice = 0; for (int i = 0; i < nset; i++) { char path[64], *v; vals[i] = xcalloc(512, 1); snprintf(path, sizeof path, "result.%s", SETTING_KEYS[i]); v = jstr_dup(resp, path); snprintf(vals[i], 512, "%s", v ? v : ""); free(v); const char *key = SETTING_KEYS[i]; ff[i].label = SETTING_LABELS[i]; ff[i].value = vals[i]; ff[i].cap = 512; ff[i].kind = TUI_F_TEXT; if (strcmp(key, "smtp_security") == 0) { ff[i].kind = TUI_F_CHOICE; ff[i].choices = SECURITY_CHOICES; ff[i].nchoices = 3; ff[i].choice = &sec_choice; for (int k = 0; k < 3; k++) if (strcmp(vals[i], SECURITY_CHOICES[k]) == 0) sec_choice = k; } else if (strcmp(key, "smtp_password") == 0) { ff[i].mask = 1; } } free(resp); struct tui_form_action acts[1]; memset(acts, 0, sizeof acts); acts[0].label = pwstatus; acts[0].enabled = -1; /* dim status row, not selectable */ int r = tui_form_run_actions( "Inställningar", ff, nset, 1, acts, 1, "upp/ned/Home/End Enter = ändra F5 = uppdatera" " Esc/q = tillbaka ^C = avsluta", &sel); if (r >= 0 && r < nset) { const char *key = SETTING_KEYS[r]; const char *val = vals[r]; if (strcmp(key, "smtp_security") == 0) val = SECURITY_CHOICES[sec_choice]; if (strcmp(key, "smtp_password") == 0 && !*vals[r]) { /* empty means "keep the stored secret" */ } else { 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, "key", key); yyjson_mut_obj_add_strcpy(d, o, "value", val); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); char *rr = client_rpc(&a->conn, "settings.set", a->session, a->org, args); free(args); if (rr && client_ok(rr)) { snprintf(vals[r], 512, "%s", val); if (strcmp(key, "default_series") == 0) snprintf(a->default_series, sizeof a->default_series, "%s", val); else if (strcmp(key, "attachment_dir") == 0) snprintf(a->attachment_dir, sizeof a->attachment_dir, "%s", val); } else { show_error("Kunde inte spara inställningen", rr); } free(rr); } } for (int i = 0; i < nset; i++) free(vals[i]); if (r == TUI_FORM_BACK) return; } }