#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; } } struct setting_field { const char *key; const char *label; const char *const *choices; int nchoices; int mask; }; static void setting_save(struct app *a, const struct setting_field *f, const char *value, const char *title) { if (f->mask && !*value) return; /* empty secret means keep the stored one */ 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", f->key); yyjson_mut_obj_add_strcpy(d, o, "value", value); char *args = yyjson_mut_write(d, 0, NULL); yyjson_mut_doc_free(d); if (!args) return; char *rr = client_rpc(&a->conn, "settings.set", a->session, a->org, args); free(args); if (rr && client_ok(rr)) { if (strcmp(f->key, "default_series") == 0) snprintf(a->default_series, sizeof a->default_series, "%s", value); else if (strcmp(f->key, "attachment_dir") == 0) snprintf(a->attachment_dir, sizeof a->attachment_dir, "%s", value); } else { show_error(title, rr); } free(rr); } static void settings_form(struct app *a, const char *title, const struct setting_field *fields, int n, int can_edit, int show_password_status, int *focus) { for (;;) { if (g_quit) return; char *resp = client_rpc(&a->conn, "settings.get", a->session, a->org, "{}"); if (!resp || !client_ok(resp)) { show_error(title, resp); free(resp); return; } char *vals[8]; int choice[8]; struct tui_form_field ff[8]; memset(ff, 0, sizeof ff); for (int i = 0; i < n; i++) { char path[64]; vals[i] = xcalloc(512, 1); snprintf(path, sizeof path, "result.%s", fields[i].key); char *v = jstr_dup(resp, path); snprintf(vals[i], 512, "%s", v ? v : ""); free(v); ff[i].label = fields[i].label; ff[i].value = vals[i]; ff[i].cap = 512; ff[i].kind = TUI_F_TEXT; ff[i].mask = fields[i].mask; choice[i] = 0; if (fields[i].choices) { ff[i].kind = TUI_F_CHOICE; ff[i].choices = fields[i].choices; ff[i].nchoices = fields[i].nchoices; ff[i].choice = &choice[i]; for (int k = 0; k < fields[i].nchoices; k++) if (strcmp(vals[i], fields[i].choices[k]) == 0) choice[i] = k; } } char pwstatus[192]; struct tui_form_action acts[1]; memset(acts, 0, sizeof acts); if (show_password_status) { int pw_set = jbool_val(resp, "result.smtp_password_set", 0); snprintf(pwstatus, sizeof pwstatus, "SMTP-lösenord (lämna tomt för oförändrat): %s", pw_set ? "satt" : "inte satt"); acts[0].label = pwstatus; acts[0].enabled = -1; } free(resp); tui_form_hint("upp/ned/Home/End Enter = ändra F5 = uppdatera" " Esc/q = tillbaka ^C = avsluta"); int r = tui_form_run_actions(title, ff, n, can_edit, acts, show_password_status ? 1 : 0, NULL, focus); if (r >= 0 && r < n) { const char *val = vals[r]; if (fields[r].choices) val = fields[r].choices[choice[r]]; setting_save(a, &fields[r], val, title); } for (int i = 0; i < n; i++) free(vals[i]); if (r == TUI_FORM_BACK) return; } } static const char *const SECURITY_CHOICES[] = { "starttls", "tls", "plain" }; static const struct setting_field INVOICE_SETTINGS[] = { { "default_series", "Standardserie", NULL, 0, 0 }, { "invoice_receivable_account", "Fordringskonto", NULL, 0, 0 }, { "invoice_revenue_account", "Intäktskonto", NULL, 0, 0 }, { "invoice_bankgiro", "Bankgiro", NULL, 0, 0 }, { "invoice_our_ref", "Vår referens (faktura)", NULL, 0, 0 }, }; static const struct setting_field SMTP_SETTINGS[] = { { "smtp_host", "SMTP-server", NULL, 0, 0 }, { "smtp_port", "SMTP-port", NULL, 0, 0 }, { "smtp_user", "SMTP-användare", NULL, 0, 0 }, { "smtp_from", "SMTP-avsändare", NULL, 0, 0 }, { "smtp_reply_to", "SMTP-svarsadress", NULL, 0, 0 }, { "smtp_security", "SMTP-säkerhet", SECURITY_CHOICES, 3, 0 }, { "smtp_password", "SMTP-lösenord", NULL, 0, 1 }, }; static int settings_can_write(struct app *a) { return a->role[0] && (strcmp(a->role, "owner") == 0 || strcmp(a->role, "bookkeeper") == 0); } static void invoice_settings_screen(struct app *a) { static int sel = 0; settings_form(a, "Fakturauppgifter", INVOICE_SETTINGS, (int)(sizeof INVOICE_SETTINGS / sizeof INVOICE_SETTINGS[0]), settings_can_write(a), 0, &sel); } static void smtp_settings_screen(struct app *a) { static int sel = 0; settings_form(a, "E-post (SMTP)", SMTP_SETTINGS, (int)(sizeof SMTP_SETTINGS / sizeof SMTP_SETTINGS[0]), settings_can_write(a), 1, &sel); } static const char *const SYSTEM_ITEMS[] = { "Skattetabeller", "Revision", }; void system_screen(struct app *a) { static int sel = 0; for (;;) { if (g_quit) return; int r = tui_menu("System", SYSTEM_ITEMS, (int)(sizeof SYSTEM_ITEMS / sizeof SYSTEM_ITEMS[0]), 0, &sel); if (r < 0) return; if (r == 0) payroll_tax_tables_screen(a); else if (r == 1) audit_screen(a); } } static void company_data_screen(struct app *a) { static int sel = 0; const int nf = (int)(sizeof COMPANY_FIELDS / sizeof COMPANY_FIELDS[0]); 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); int r = tui_form_run("Företagsuppgifter", ff, nf, 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); } } for (int i = 0; i < nf; i++) free(vals[i]); if (r == TUI_FORM_BACK) return; } } static const char *const COMPANY_ITEMS[] = { "Företagsuppgifter", "Fakturauppgifter", "E-post (SMTP)", "Styrelseledamöter", MK_SECTION "Register", "Anställda", "Kunder", "Momsregler", }; void company_screen(struct app *a) { static int sel = 0; for (;;) { if (g_quit) return; int r = tui_menu("Bolaget", COMPANY_ITEMS, (int)(sizeof COMPANY_ITEMS / sizeof COMPANY_ITEMS[0]), 0, &sel); if (r < 0) return; switch (r) { case 0: company_data_screen(a); break; case 1: invoice_settings_screen(a); break; case 2: smtp_settings_screen(a); break; case 3: board_screen(a); break; case 5: employees_screen(a); break; case 6: customers_screen(a); break; case 7: rules_screen(a); break; default: break; } } }