summaryrefslogtreecommitdiff
path: root/clients/screens_payroll.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-21 11:45:54 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-21 11:45:54 +0200
commite1dd7112e0346914710566660625da0f6b866e8a (patch)
treec4b0d5c7f6f62c1c4c38245e7c001b3fd4b03e74 /clients/screens_payroll.c
parent182f0a592b6fe97fc1ff87add227f9d52a4ef902 (diff)
downloadbokf-0.1.55.tar.gz
bokf-0.1.55.zip
tui: payroll screens, Företag/System menu and settings splitv0.1.55
Diffstat (limited to 'clients/screens_payroll.c')
-rw-r--r--clients/screens_payroll.c1260
1 files changed, 1260 insertions, 0 deletions
diff --git a/clients/screens_payroll.c b/clients/screens_payroll.c
new file mode 100644
index 0000000..9bfaf38
--- /dev/null
+++ b/clients/screens_payroll.c
@@ -0,0 +1,1260 @@
+#include <dirent.h>
+#include <errno.h>
+#include <locale.h>
+#include <math.h>
+#include <ncursesw/ncurses.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "client.h"
+#include "tui.h"
+#include "formula.h"
+#include "util.h"
+#include "version.h"
+#include "yyjson.h"
+#include "ui.h"
+
+/* ------------------------------------------------------------------ */
+/* lön: Lönekörningar, Anställda, Skattetabeller */
+/* ------------------------------------------------------------------ */
+
+static void payroll_today(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';
+}
+
+static int payroll_can_write(struct app *a)
+{
+ return strcmp(a->role, "owner") == 0 ||
+ strcmp(a->role, "bookkeeper") == 0;
+}
+
+static int payroll_is_owner(struct app *a)
+{
+ return strcmp(a->role, "owner") == 0;
+}
+
+static int payroll_digits(const char *s)
+{
+ if (!s || !*s)
+ return 0;
+ for (const char *p = s; *p; p++)
+ if (*p < '0' || *p > '9')
+ return 0;
+ return 1;
+}
+
+static int payroll_parse_int(const char *s, int lo, int hi, int *out)
+{
+ if (!s || !*s)
+ return -1;
+ char *end = NULL;
+ long v = strtol(s, &end, 10);
+ if (!end || *end || v < lo || v > hi)
+ return -1;
+ *out = (int)v;
+ return 0;
+}
+
+static const char *payroll_status_sv(const char *s)
+{
+ return s && strcmp(s, "paid") == 0 ? "betald" : "bokförd";
+}
+
+static void payroll_voucher_ver(struct app *a, int64_t vid, char *buf, size_t n)
+{
+ buf[0] = '\0';
+ if (vid <= 0)
+ return;
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)vid);
+ char *resp = client_rpc(&a->conn, "voucher.get", a->session, a->org, args);
+ if (resp && client_ok(resp)) {
+ char *ser = jstr_dup(resp, "result.series");
+ snprintf(buf, n, "%s%lld", ser ? ser : "",
+ (long long)jint_val(resp, "result.number", 0));
+ free(ser);
+ }
+ free(resp);
+}
+
+static void payroll_copy(char *dst, size_t n, const char *resp,
+ const char *path)
+{
+ char *v = jstr_dup(resp, path);
+ snprintf(dst, n, "%s", v ? v : "");
+ free(v);
+}
+
+static int payroll_period_valid(const char *p)
+{
+ if (!p || strlen(p) != 7 || p[4] != '-')
+ return 0;
+ for (int i = 0; i < 7; i++) {
+ if (i == 4)
+ continue;
+ if (p[i] < '0' || p[i] > '9')
+ return 0;
+ }
+ int m = (p[5] - '0') * 10 + (p[6] - '0');
+ return m >= 1 && m <= 12;
+}
+
+/* ------------------------------------------------------------------ */
+/* Anställda (employee register) */
+/* ------------------------------------------------------------------ */
+
+struct employee_form {
+ int64_t id;
+ int is_new;
+ char name[200];
+ char personal_no[16];
+ char address[512];
+ char postal_code[32];
+ char city[128];
+ char bank_account[64];
+ char email[256];
+ char salary_account[16];
+ char tax_table[8];
+ char tax_column[8];
+ int64_t monthly_salary_ore;
+};
+
+static char *employee_form_args(const struct employee_form *f, char *err,
+ size_t errn)
+{
+ if (!f->name[0]) {
+ snprintf(err, errn, "Namn saknas.");
+ return NULL;
+ }
+ if (f->is_new && !f->personal_no[0]) {
+ snprintf(err, errn, "Personnummer saknas.");
+ return NULL;
+ }
+ int table = 0, column = 0;
+ if (payroll_parse_int(f->tax_table, 29, 42, &table) != 0) {
+ snprintf(err, errn, "Skattetabell måste vara 29–42.");
+ return NULL;
+ }
+ if (payroll_parse_int(f->tax_column, 1, 6, &column) != 0) {
+ snprintf(err, errn, "Kolumn måste vara 1–6.");
+ return NULL;
+ }
+ if (f->monthly_salary_ore < 0) {
+ snprintf(err, errn, "Månadslönen kan inte vara negativ.");
+ return NULL;
+ }
+ if (f->salary_account[0] && !payroll_digits(f->salary_account)) {
+ snprintf(err, errn, "Lönekontot får bara innehålla siffror.");
+ 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);
+ if (!f->is_new)
+ yyjson_mut_obj_add_int(d, o, "id", f->id);
+ yyjson_mut_obj_add_strcpy(d, o, "name", f->name);
+ if (f->personal_no[0])
+ yyjson_mut_obj_add_strcpy(d, o, "personal_no", f->personal_no);
+ yyjson_mut_obj_add_strcpy(d, o, "address", f->address);
+ yyjson_mut_obj_add_strcpy(d, o, "postal_code", f->postal_code);
+ yyjson_mut_obj_add_strcpy(d, o, "city", f->city);
+ yyjson_mut_obj_add_strcpy(d, o, "bank_account", f->bank_account);
+ yyjson_mut_obj_add_strcpy(d, o, "email", f->email);
+ if (f->salary_account[0])
+ yyjson_mut_obj_add_strcpy(d, o, "salary_account", f->salary_account);
+ yyjson_mut_obj_add_int(d, o, "monthly_salary_ore", f->monthly_salary_ore);
+ yyjson_mut_obj_add_int(d, o, "tax_table", table);
+ yyjson_mut_obj_add_int(d, o, "tax_column", column);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ return args;
+}
+
+static void employee_form(struct app *a, int64_t id)
+{
+ static int sel = 0;
+ struct employee_form f;
+ memset(&f, 0, sizeof f);
+ f.id = id;
+ f.is_new = id <= 0;
+ snprintf(f.tax_table, sizeof f.tax_table, "30");
+ snprintf(f.tax_column, sizeof f.tax_column, "1");
+ if (!f.is_new) {
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)id);
+ char *resp =
+ client_rpc(&a->conn, "employee.get", a->session, a->org, args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Anställd", resp);
+ free(resp);
+ return;
+ }
+ payroll_copy(f.name, sizeof f.name, resp, "result.name");
+ payroll_copy(f.personal_no, sizeof f.personal_no, resp,
+ "result.personal_no");
+ payroll_copy(f.address, sizeof f.address, resp, "result.address");
+ payroll_copy(f.postal_code, sizeof f.postal_code, resp,
+ "result.postal_code");
+ payroll_copy(f.city, sizeof f.city, resp, "result.city");
+ payroll_copy(f.bank_account, sizeof f.bank_account, resp,
+ "result.bank_account");
+ payroll_copy(f.email, sizeof f.email, resp, "result.email");
+ payroll_copy(f.salary_account, sizeof f.salary_account, resp,
+ "result.salary_account");
+ snprintf(f.tax_table, sizeof f.tax_table, "%lld",
+ (long long)jint_val(resp, "result.tax_table", 30));
+ snprintf(f.tax_column, sizeof f.tax_column, "%lld",
+ (long long)jint_val(resp, "result.tax_column", 1));
+ f.monthly_salary_ore =
+ jint_val(resp, "result.monthly_salary_ore", 0);
+ f.personal_no[0] = '\0';
+ free(resp);
+ }
+ for (;;) {
+ if (g_quit)
+ return;
+ struct tui_form_field ff[11];
+ memset(ff, 0, sizeof ff);
+ int i = 0;
+ ff[i].label = "Namn";
+ ff[i].value = f.name;
+ ff[i].cap = sizeof f.name;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = f.is_new ? "Personnummer"
+ : "Personnummer (tomt = oförändrat)";
+ ff[i].value = f.personal_no;
+ ff[i].cap = sizeof f.personal_no;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Adress";
+ ff[i].value = f.address;
+ ff[i].cap = sizeof f.address;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Postnummer";
+ ff[i].value = f.postal_code;
+ ff[i].cap = sizeof f.postal_code;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Ort";
+ ff[i].value = f.city;
+ ff[i].cap = sizeof f.city;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Bankkonto (nettolön)";
+ ff[i].value = f.bank_account;
+ ff[i].cap = sizeof f.bank_account;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "E-post (lönebesked)";
+ ff[i].value = f.email;
+ ff[i].cap = sizeof f.email;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Lönekonto";
+ ff[i].value = f.salary_account;
+ ff[i].cap = sizeof f.salary_account;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Månadslön (kr)";
+ ff[i].kind = TUI_F_AMOUNT;
+ ff[i].ore = &f.monthly_salary_ore;
+ i++;
+ ff[i].label = "Skattetabell (29–42)";
+ ff[i].value = f.tax_table;
+ ff[i].cap = sizeof f.tax_table;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ ff[i].label = "Kolumn (1–6)";
+ ff[i].value = f.tax_column;
+ ff[i].cap = sizeof f.tax_column;
+ ff[i].kind = TUI_F_TEXT;
+ i++;
+ const char *title = f.is_new ? "Ny anställd" : "Anställd";
+ int r = tui_form_run(title, ff, i, 1, &sel);
+ if (r == TUI_FORM_BACK)
+ return;
+ if (r != TUI_FORM_REFRESH && r != TUI_FORM_SUBMIT)
+ continue;
+ char err[256];
+ char *args = employee_form_args(&f, err, sizeof err);
+ if (!args) {
+ tui_message(title, "%s", err);
+ continue;
+ }
+ const char *cmd = f.is_new ? "employee.create" : "employee.update";
+ char *resp = r == TUI_FORM_REFRESH ? rpc_dry(a, cmd, args)
+ : client_rpc(&a->conn, cmd,
+ a->session, a->org,
+ args);
+ free(args);
+ if (r == TUI_FORM_REFRESH) {
+ if (resp && client_ok(resp))
+ tui_message(title, "Validering OK.");
+ else
+ show_error(title, resp);
+ free(resp);
+ continue;
+ }
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunde inte spara", resp);
+ free(resp);
+ continue;
+ }
+ free(resp);
+ tui_message(title, f.is_new ? "Anställd skapad." : "Sparad.");
+ return;
+ }
+}
+
+static void employee_archive(struct app *a, int64_t id, const char *name,
+ int active)
+{
+ if (!tui_confirm("Anställd", active ? "Arkivera %s?" : "Återaktivera %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, "employee.archive", a->session, a->org, args);
+ if (r && client_ok(r))
+ tui_message("Anställd", active ? "Arkiverad." : "Återaktiverad.");
+ else
+ show_error("Anställd", r);
+ free(r);
+}
+
+void employees_screen(struct app *a)
+{
+ for (;;) {
+ if (g_quit)
+ return;
+ char *resp = client_rpc(&a->conn, "employee.list", a->session, a->org,
+ "{}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Anställda", resp);
+ free(resp);
+ return;
+ }
+ size_t n = jarr_size(resp, "result.items");
+ char **lines = xcalloc(n + 1, sizeof(char *));
+ char **names = xcalloc(n + 1, sizeof(char *));
+ int64_t *ids = xcalloc(n + 1, sizeof(int64_t));
+ unsigned char *active = xcalloc(n + 1, 1);
+ for (size_t i = 0; i < n; i++) {
+ char path[64];
+ 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.personal_no", i);
+ char *pn = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.monthly_salary_ore",
+ i);
+ int64_t salary = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.tax_table", i);
+ int64_t table = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.tax_column", i);
+ int64_t column = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.email", i);
+ char *email = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.active", i);
+ active[i] = (unsigned char)jbool_val(resp, path, 1);
+ char nbuf[224], pbuf[24], abuf[32], tbuf[16], ebuf[256];
+ snprintf(nbuf, sizeof nbuf, "%s", names[i] ? names[i] : "");
+ tui_pad_field(nbuf, sizeof nbuf, 24);
+ snprintf(pbuf, sizeof pbuf, "%s", pn ? pn : "");
+ tui_pad_field(pbuf, sizeof pbuf, 15);
+ tui_amt_col(abuf, sizeof abuf, 12, salary);
+ snprintf(tbuf, sizeof tbuf, "%lld/%lld", (long long)table,
+ (long long)column);
+ snprintf(ebuf, sizeof ebuf, "%s", email ? email : "");
+ char line[600];
+ snprintf(line, sizeof line, "%s %s %s %s %s%s", nbuf, pbuf, abuf,
+ tbuf, ebuf, active[i] ? "" : " (arkiverad)");
+ lines[i] = xstrdup(line);
+ free(pn);
+ free(email);
+ }
+ free(resp);
+ size_t start = 0;
+ for (size_t i = 0; i < n; i++)
+ if (ids[i] == a->employee_sel) {
+ start = i;
+ break;
+ }
+ lines[n] = xstrdup("+ Ny anställd (^N)");
+ int cur = (int)start;
+ int sel = tui_select_list("Anställda", lines, (int)n + 1, cur, 1,
+ &cur, 1, "arkivera/återaktivera", 0);
+ if (cur >= 0 && (size_t)cur < n)
+ a->employee_sel = ids[cur];
+ if (sel == -1) {
+ for (size_t i = 0; i <= n; i++) {
+ free(lines[i]);
+ free(names[i]);
+ }
+ free(lines);
+ free(names);
+ free(ids);
+ free(active);
+ return;
+ }
+ if (sel == -6 && cur >= 0 && (size_t)cur < n) {
+ employee_archive(a, ids[cur], names[cur], active[cur]);
+ } else if (sel == -4 || (sel >= 0 && (size_t)sel == n)) {
+ employee_form(a, 0);
+ } else if (sel >= 0 && (size_t)sel < n) {
+ employee_form(a, ids[sel]);
+ }
+ for (size_t i = 0; i <= n; i++) {
+ free(lines[i]);
+ free(names[i]);
+ }
+ free(lines);
+ free(names);
+ free(ids);
+ free(active);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Skattetabeller */
+/* ------------------------------------------------------------------ */
+
+static void tax_tables_fetch(struct app *a, int year)
+{
+ if (!tui_confirm("Skattetabeller",
+ "Hämta och ersätta skattetabellerna för %d från"
+ " Skatteverket?",
+ year))
+ return;
+ char args[48];
+ snprintf(args, sizeof args, "{\"year\":%d}", year);
+ char *resp = client_rpc(&a->conn, "payroll.tax_tables_fetch", a->session,
+ a->org, args);
+ if (resp && client_ok(resp)) {
+ tui_message("Skattetabeller", "%lld rader för %lld sparade.",
+ (long long)jint_val(resp, "result.rows", 0),
+ (long long)jint_val(resp, "result.year", 0));
+ } else {
+ show_error("Kunde inte hämta skattetabellerna", resp);
+ }
+ free(resp);
+}
+
+static void tax_tables_import(struct app *a, int year)
+{
+ char yearbuf[8];
+ snprintf(yearbuf, sizeof yearbuf, "%d", year);
+ char *yb = tui_prompt_into(yearbuf, sizeof yearbuf,
+ "Inkomstår för filen: ", yearbuf, 0)
+ ? yearbuf
+ : NULL;
+ if (!yb || !*yb)
+ return;
+ char *end = NULL;
+ long y = strtol(yb, &end, 10);
+ if (!end || *end || y < 2000 || y > 9999) {
+ tui_message("Skattetabeller", "Ogiltigt inkomstår.");
+ return;
+ }
+ char *path = file_browser(a, a->attachment_dir);
+ if (!path)
+ return;
+ char *b64 = read_file_b64(path);
+ if (!b64) {
+ tui_message("Skattetabeller", "Kunde inte läsa %s", path);
+ free(path);
+ return;
+ }
+ free(path);
+ 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, "year", y);
+ yyjson_mut_obj_add_strcpy(d, o, "content_base64", b64);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ free(b64);
+ if (!args)
+ return;
+ char *resp = client_rpc(&a->conn, "payroll.tax_tables_import", a->session,
+ a->org, args);
+ free(args);
+ if (resp && client_ok(resp)) {
+ tui_message("Skattetabeller", "%lld rader för %lld importerade.",
+ (long long)jint_val(resp, "result.rows", 0),
+ (long long)jint_val(resp, "result.year", 0));
+ } else {
+ show_error("Kunde inte importera skattetabellerna", resp);
+ }
+ free(resp);
+}
+
+void payroll_tax_tables_screen(struct app *a)
+{
+ static int sel = 4;
+ int owner = payroll_is_owner(a);
+ const char *reason = owner ? NULL : "kräver ägarbehörighet";
+ for (;;) {
+ if (g_quit)
+ return;
+ char *resp = client_rpc(&a->conn, "payroll.tax_tables_status",
+ a->session, a->org, "{}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Skattetabeller", resp);
+ free(resp);
+ return;
+ }
+ int current_year = (int)jint_val(resp, "result.current_year", 0);
+ int stale = jbool_val(resp, "result.stale", 1);
+ char years[256] = "inga";
+ size_t ny = jarr_size(resp, "result.stored_years");
+ if (ny > 0) {
+ years[0] = '\0';
+ size_t off = 0;
+ for (size_t i = 0; i < ny; i++) {
+ char path[48];
+ snprintf(path, sizeof path, "result.stored_years.%zu", i);
+ int64_t y = jint_val(resp, path, 0);
+ size_t left = sizeof years - off;
+ int w = snprintf(years + off, left, "%s%lld", i ? ", " : "",
+ (long long)y);
+ if (w < 0 || (size_t)w >= left)
+ break;
+ off += (size_t)w;
+ }
+ }
+ char current[8];
+ snprintf(current, sizeof current, "%d", current_year);
+ char fetched[64] = "—";
+ char *fa = jstr_dup(resp, "result.fetched_at");
+ if (fa && *fa)
+ snprintf(fetched, sizeof fetched, "%s", fa);
+ free(fa);
+ char url[1024] = "—";
+ char *su = jstr_dup(resp, "result.source_url");
+ if (su && *su)
+ snprintf(url, sizeof url, "%s", su);
+ free(su);
+ char status[160];
+ if (stale)
+ snprintf(status, sizeof status,
+ "Status: skattetabeller för %d saknas", current_year);
+ else
+ snprintf(status, sizeof status,
+ "Status: skattetabeller för %d finns", current_year);
+ free(resp);
+ struct tui_form_field ff[4];
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Aktuellt inkomstår";
+ ff[0].value = current;
+ ff[0].cap = sizeof current;
+ ff[0].kind = TUI_F_TEXT;
+ ff[1].label = "Hämtade år";
+ ff[1].value = years;
+ ff[1].cap = sizeof years;
+ ff[1].kind = TUI_F_TEXT;
+ ff[2].label = "Senast hämtad";
+ ff[2].value = fetched;
+ ff[2].cap = sizeof fetched;
+ ff[2].kind = TUI_F_TEXT;
+ ff[3].label = "Källa";
+ ff[3].value = url;
+ ff[3].cap = sizeof url;
+ ff[3].kind = TUI_F_TEXT;
+ struct tui_form_action acts[3];
+ acts[0].label = status;
+ acts[0].enabled = -1;
+ acts[0].disabled_reason = NULL;
+ acts[1].label = "Hämta från Skatteverket";
+ acts[1].enabled = owner ? 1 : 0;
+ acts[1].disabled_reason = reason;
+ acts[2].label = "Importera tabellfil (offline)…";
+ acts[2].enabled = owner ? 1 : 0;
+ acts[2].disabled_reason = reason;
+ int r = tui_form_run_actions(
+ "Skattetabeller", ff, 4, 0, acts, 3,
+ "upp/ned/Tab = flytta Enter = utför F5 = uppdatera"
+ " Esc/q = tillbaka ^C = avsluta",
+ &sel);
+ if (r == TUI_FORM_ACTION + 1)
+ tax_tables_fetch(a, current_year);
+ else if (r == TUI_FORM_ACTION + 2)
+ tax_tables_import(a, current_year);
+ else if (r == TUI_FORM_BACK)
+ return;
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* Lönekörningar */
+/* ------------------------------------------------------------------ */
+
+struct payroll_run {
+ struct app *a;
+ int64_t id;
+ char period[16];
+ char pay_date[16];
+ char status[16];
+ int64_t gross_ore;
+ int64_t tax_ore;
+ int64_t avgifter_ore;
+ int64_t net_ore;
+ int lines;
+};
+
+static void payroll_run_defaults(struct app *a, char *period, size_t pn,
+ char *pay_date, size_t dn)
+{
+ char today[16];
+ payroll_today(today, sizeof today);
+ if (a->fy_start[0] && a->fy_end[0] &&
+ strcmp(today, a->fy_start) >= 0 && strcmp(today, a->fy_end) <= 0) {
+ snprintf(period, pn, "%.7s", today);
+ snprintf(pay_date, dn, "%s", today);
+ return;
+ }
+ if (a->fy_start[0] && strlen(a->fy_start) >= 7) {
+ snprintf(period, pn, "%.7s", a->fy_start);
+ snprintf(pay_date, dn, "%s", a->fy_start);
+ return;
+ }
+ snprintf(period, pn, "%.7s", today);
+ snprintf(pay_date, dn, "%s", today);
+}
+
+static int payroll_run_load(struct payroll_run *r)
+{
+ char args[64];
+ snprintf(args, sizeof args, "{\"id\":%lld}", (long long)r->id);
+ char *resp =
+ client_rpc(&r->a->conn, "payroll.run_get", r->a->session, r->a->org,
+ args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Lönekörning", resp);
+ free(resp);
+ return -1;
+ }
+ payroll_copy(r->period, sizeof r->period, resp, "result.period");
+ payroll_copy(r->pay_date, sizeof r->pay_date, resp, "result.pay_date");
+ payroll_copy(r->status, sizeof r->status, resp, "result.status");
+ r->gross_ore = jint_val(resp, "result.gross_ore", 0);
+ r->tax_ore = jint_val(resp, "result.tax_ore", 0);
+ r->avgifter_ore = jint_val(resp, "result.avgifter_ore", 0);
+ r->net_ore = jint_val(resp, "result.net_ore", 0);
+ r->lines = (int)jarr_size(resp, "result.lines");
+ free(resp);
+ return 0;
+}
+
+static void payroll_fmt_lines(struct buf *t, yyjson_val *items)
+{
+ buf_line(t, "%-22s %-6s %13s %13s %13s %13s\n", "Namn", "Tabell",
+ "Brutto", "Skatt", "Avgifter", "Netto");
+ size_t n = yyjson_arr_size(items);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_val *it = yyjson_arr_get(items, i);
+ char name[224], tab[16], g[32], tx[32], av[32], net[32];
+ snprintf(name, sizeof name, "%s", vstr(it, "name"));
+ tui_pad_field(name, sizeof name, 22);
+ snprintf(tab, sizeof tab, "%lld/%lld",
+ (long long)vint(it, "tax_table"),
+ (long long)vint(it, "tax_column"));
+ tui_pad_field(tab, sizeof tab, 6);
+ tui_amt_col(g, sizeof g, 13, vint(it, "gross_ore"));
+ tui_amt_col(tx, sizeof tx, 13, vint(it, "tax_ore"));
+ tui_amt_col(av, sizeof av, 13, vint(it, "avgifter_ore"));
+ tui_amt_col(net, sizeof net, 13, vint(it, "net_ore"));
+ buf_line(t, "%s %s %s %s %s %s\n", name, tab, g, tx, av, net);
+ }
+}
+
+static void payroll_fmt_total_line(struct buf *t, int64_t gross, int64_t tax,
+ int64_t avgifter, int64_t net)
+{
+ char name[32] = "Summa", g[32], tx[32], av[32], nb[32];
+ tui_pad_field(name, sizeof name, 22);
+ tui_amt_col(g, sizeof g, 13, gross);
+ tui_amt_col(tx, sizeof tx, 13, tax);
+ tui_amt_col(av, sizeof av, 13, avgifter);
+ tui_amt_col(nb, sizeof nb, 13, net);
+ buf_rule(t, 80);
+ buf_line(t, MK_DIM "%s %-6s %s %s %s %s\n", name, "", g, tx, av, nb);
+}
+
+static void payroll_fmt_totals(struct buf *t, yyjson_val *totals)
+{
+ if (!totals)
+ return;
+ payroll_fmt_total_line(t, vint(totals, "gross_ore"),
+ vint(totals, "tax_ore"),
+ vint(totals, "avgifter_ore"),
+ vint(totals, "net_ore"));
+}
+
+static void payroll_run_preview(struct payroll_run *r)
+{
+ if (!payroll_period_valid(r->period)) {
+ tui_message("Lönekörning", "Perioden måste vara ÅÅÅÅ-MM.");
+ return;
+ }
+ char *args = NULL;
+ const char *cmd;
+ if (r->id > 0) {
+ char idargs[64];
+ snprintf(idargs, sizeof idargs, "{\"id\":%lld}", (long long)r->id);
+ args = xstrdup(idargs);
+ cmd = "payroll.run_get";
+ } 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, "period", r->period);
+ args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ cmd = "payroll.run_preview";
+ }
+ if (!args)
+ return;
+ char *resp =
+ client_rpc(&r->a->conn, cmd, r->a->session, r->a->org, args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Lönekörning", resp);
+ free(resp);
+ return;
+ }
+ yyjson_doc *pd = parse(resp);
+ yyjson_val *res = pd ? jget(yyjson_doc_get_root(pd), "result") : NULL;
+ struct buf t;
+ buf_init(&t);
+ if (!res) {
+ buf_line(&t, "%s\n", resp);
+ } else {
+ if (r->id > 0) {
+ buf_line(&t, MK_HEAD "Lönekörning %s — %s\n", r->period,
+ payroll_status_sv(r->status));
+ buf_line(&t, MK_DIM "Betalningsdatum %s\n", r->pay_date);
+ } else {
+ buf_line(&t, MK_HEAD
+ "Lönekörning %s — förhandsvisning (tabellår %lld)\n",
+ r->period, (long long)vint(res, "tax_year"));
+ }
+ buf_line(&t, "\n");
+ payroll_fmt_lines(&t, yyjson_obj_get(res, "items"));
+ if (r->id > 0)
+ payroll_fmt_total_line(&t, r->gross_ore, r->tax_ore,
+ r->avgifter_ore, r->net_ore);
+ else
+ payroll_fmt_totals(&t, yyjson_obj_get(res, "totals"));
+ yyjson_val *accts = yyjson_obj_get(res, "accounts");
+ if (accts && yyjson_is_obj(accts)) {
+ int64_t rate = vint(res, "avgift_rate_bp");
+ buf_line(&t, "\n" MK_DIM
+ "Konton: skatt %s, avgifter %s (%.2f %%), avräkning %s,"
+ " netto %s, skattekonto %s.\n",
+ vstr(accts, "tax"), vstr(accts, "avgift"),
+ (double)rate / 100.0, vstr(accts, "liability"),
+ vstr(accts, "net"), vstr(accts, "payment"));
+ }
+ }
+ buf_append(&t, "", 1);
+ char title[64];
+ snprintf(title, sizeof title, "Lönekörning %s", r->period);
+ tui_pager(title, (const char *)t.p, NULL);
+ buf_free(&t);
+ yyjson_doc_free(pd);
+ free(resp);
+}
+
+static void payroll_run_post(struct payroll_run *r)
+{
+ if (!payroll_period_valid(r->period)) {
+ tui_message("Lönekörning", "Perioden måste vara ÅÅÅÅ-MM.");
+ return;
+ }
+ if (!util_parse_iso_date(r->pay_date)) {
+ tui_message("Lönekörning", "Betalningsdatum måste vara ÅÅÅÅ-MM-DD.");
+ return;
+ }
+ if (!tui_confirm("Lönekörning",
+ "Bokför lönekörningen %s med betaldatum %s?",
+ r->period, r->pay_date))
+ return;
+ 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, "period", r->period);
+ yyjson_mut_obj_add_strcpy(d, o, "pay_date", r->pay_date);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ if (!args)
+ return;
+ char *resp = client_rpc(&r->a->conn, "payroll.run_post", r->a->session,
+ r->a->org, args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunde inte bokföra lönekörningen", resp);
+ free(resp);
+ return;
+ }
+ r->id = jint_val(resp, "result.id", 0);
+ char ver[32];
+ payroll_voucher_ver(r->a, jint_val(resp, "result.voucher_id", 0), ver,
+ sizeof ver);
+ free(resp);
+ char msg[96];
+ if (ver[0])
+ snprintf(msg, sizeof msg, "Lönekörningen är bokförd (%s).", ver);
+ else
+ snprintf(msg, sizeof msg, "Lönekörningen är bokförd.");
+ tui_message("Lönekörning", "%s", msg);
+}
+
+static void payroll_run_payslip(struct payroll_run *r)
+{
+ char idargs[64];
+ snprintf(idargs, sizeof idargs, "{\"id\":%lld}", (long long)r->id);
+ char *resp = client_rpc(&r->a->conn, "payroll.run_get", r->a->session,
+ r->a->org, idargs);
+ if (!resp || !client_ok(resp)) {
+ show_error("Lönebesked", resp);
+ free(resp);
+ return;
+ }
+ yyjson_doc *d = parse(resp);
+ yyjson_val *lines =
+ d ? jget(yyjson_doc_get_root(d), "result.lines") : NULL;
+ size_t n = yyjson_arr_size(lines);
+ int64_t employee_id = 0;
+ if (n == 1) {
+ employee_id = vint(yyjson_arr_get(lines, 0), "employee_id");
+ } else if (n > 1) {
+ char **names = xcalloc(n, sizeof(char *));
+ int64_t *ids = xcalloc(n, sizeof(int64_t));
+ for (size_t i = 0; i < n; i++) {
+ yyjson_val *it = yyjson_arr_get(lines, i);
+ names[i] = xstrdup(vstr(it, "name"));
+ ids[i] = vint(it, "employee_id");
+ }
+ int sel = tui_choice_prompt("Välj anställd: ",
+ (const char *const *)names, (int)n, 0);
+ if (sel >= 0)
+ employee_id = ids[sel];
+ for (size_t i = 0; i < n; i++)
+ free(names[i]);
+ free(names);
+ free(ids);
+ }
+ free(resp);
+ yyjson_doc_free(d);
+ if (n == 0) {
+ tui_message("Lönebesked", "Körningen har inga anställda.");
+ return;
+ }
+ if (employee_id <= 0)
+ return;
+ yyjson_mut_doc *ad = yyjson_mut_doc_new(NULL);
+ yyjson_mut_val *o = yyjson_mut_obj(ad);
+ yyjson_mut_doc_set_root(ad, o);
+ yyjson_mut_obj_add_int(ad, o, "run_id", r->id);
+ yyjson_mut_obj_add_int(ad, o, "employee_id", employee_id);
+ char *args = yyjson_mut_write(ad, 0, NULL);
+ yyjson_mut_doc_free(ad);
+ if (!args)
+ return;
+ resp = client_rpc(&r->a->conn, "payroll.payslip", r->a->session, r->a->org,
+ args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunde inte hämta lönebeskedet", resp);
+ free(resp);
+ return;
+ }
+ char *b64 = jstr_dup(resp, "result.content_base64");
+ char *fname = jstr_dup(resp, "result.filename");
+ free(resp);
+ if (b64 && fname)
+ pdf_save_and_open(b64, fname, "Lönebesked");
+ free(b64);
+ free(fname);
+}
+
+static void payroll_run_agi(struct payroll_run *r)
+{
+ if (!payroll_is_owner(r->a)) {
+ tui_message("AGI-underlag", "Kräver ägarbehörighet.");
+ return;
+ }
+ 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, "period", r->period);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *resp =
+ client_rpc(&r->a->conn, "payroll.agi", r->a->session, r->a->org, args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("AGI-underlag", resp);
+ free(resp);
+ return;
+ }
+ yyjson_doc *pd = parse(resp);
+ yyjson_val *res = pd ? jget(yyjson_doc_get_root(pd), "result") : NULL;
+ struct buf t;
+ buf_init(&t);
+ if (!res) {
+ buf_line(&t, "%s\n", resp);
+ } else {
+ buf_line(&t, MK_HEAD "Arbetsgivardeklaration (AGI) %s\n",
+ vstr(res, "period"));
+ buf_line(&t, MK_DIM "Betalningsdatum %s\n", vstr(res, "pay_date"));
+ buf_line(&t, "\n");
+ buf_line(&t, "%-22s %-15s %-6s %13s %13s %13s %13s\n", "Namn",
+ "Personnummer", "Tabell", "Brutto", "Skatt", "Avgifter",
+ "Netto");
+ yyjson_val *items = yyjson_obj_get(res, "items");
+ size_t n = yyjson_arr_size(items);
+ for (size_t i = 0; i < n; i++) {
+ yyjson_val *it = yyjson_arr_get(items, i);
+ char name[224], pn[24], tab[16], g[32], tx[32], av[32], net[32];
+ snprintf(name, sizeof name, "%s", vstr(it, "name"));
+ tui_pad_field(name, sizeof name, 22);
+ snprintf(pn, sizeof pn, "%s", vstr(it, "personal_no"));
+ tui_pad_field(pn, sizeof pn, 15);
+ snprintf(tab, sizeof tab, "%lld/%lld",
+ (long long)vint(it, "tax_table"),
+ (long long)vint(it, "tax_column"));
+ tui_pad_field(tab, sizeof tab, 6);
+ tui_amt_col(g, sizeof g, 13, vint(it, "gross_ore"));
+ tui_amt_col(tx, sizeof tx, 13, vint(it, "tax_ore"));
+ tui_amt_col(av, sizeof av, 13, vint(it, "avgifter_ore"));
+ tui_amt_col(net, sizeof net, 13, vint(it, "net_ore"));
+ buf_line(&t, "%s %s %s %s %s %s %s\n", name, pn, tab, g, tx, av,
+ net);
+ }
+ payroll_fmt_totals(&t, yyjson_obj_get(res, "totals"));
+ buf_line(&t, "\n" MK_DIM
+ "Underlaget fylls i manuellt på skatteverket.se.\n");
+ }
+ buf_append(&t, "", 1);
+ tui_pager("AGI-underlag", (const char *)t.p, NULL);
+ buf_free(&t);
+ yyjson_doc_free(pd);
+ free(resp);
+}
+
+static void payroll_run_pay_tax(struct payroll_run *r)
+{
+ int64_t total = r->tax_ore + r->avgifter_ore;
+ if (total <= 0) {
+ tui_message("Betala skatt & avgifter", "Inget att betala.");
+ return;
+ }
+ char tb[32], sb[32], ab[32];
+ tui_kr_format(total, tb, sizeof tb);
+ tui_kr_format(r->tax_ore, sb, sizeof sb);
+ tui_kr_format(r->avgifter_ore, ab, sizeof ab);
+ if (!tui_confirm("Betala skatt & avgifter",
+ "Betala %s för %s (skatt %s + avgifter %s)?", tb,
+ r->period, sb, ab))
+ return;
+ char date[16];
+ payroll_today(date, sizeof date);
+ if (!tui_date_prompt_into(date, sizeof date, "Betalningsdatum: ", date))
+ return;
+ 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, "run_id", r->id);
+ yyjson_mut_obj_add_strcpy(d, o, "date", date);
+ char *args = yyjson_mut_write(d, 0, NULL);
+ yyjson_mut_doc_free(d);
+ char *resp = client_rpc(&r->a->conn, "payroll.pay_tax", r->a->session,
+ r->a->org, args);
+ free(args);
+ if (!resp || !client_ok(resp)) {
+ show_error("Kunde inte betala", resp);
+ free(resp);
+ return;
+ }
+ char ver[32];
+ payroll_voucher_ver(r->a, jint_val(resp, "result.payment_voucher_id", 0),
+ ver, sizeof ver);
+ free(resp);
+ if (ver[0])
+ tui_message("Betala skatt & avgifter", "Betalningen bokförd (%s).",
+ ver);
+ else
+ tui_message("Betala skatt & avgifter", "Betalningen bokförd.");
+}
+
+static int payroll_salary_count(struct app *a)
+{
+ char *resp = client_rpc(&a->conn, "employee.list", a->session, a->org,
+ "{\"active_only\":true}");
+ if (!resp || !client_ok(resp)) {
+ free(resp);
+ return 0;
+ }
+ size_t n = jarr_size(resp, "result.items");
+ int count = 0;
+ for (size_t i = 0; i < n; i++) {
+ char path[64];
+ snprintf(path, sizeof path, "result.items.%zu.monthly_salary_ore", i);
+ if (jint_val(resp, path, 0) > 0)
+ count++;
+ }
+ free(resp);
+ return count;
+}
+
+static int64_t payroll_run_screen(struct app *a, int64_t id)
+{
+ static int focus = 0;
+ struct payroll_run r;
+ memset(&r, 0, sizeof r);
+ r.a = a;
+ r.id = id;
+ payroll_run_defaults(a, r.period, sizeof r.period, r.pay_date,
+ sizeof r.pay_date);
+ int can_write = payroll_can_write(a);
+ int owner = payroll_is_owner(a);
+ for (;;) {
+ if (g_quit)
+ return r.id;
+ int posted = r.id > 0;
+ int paid = 0;
+ if (posted) {
+ if (payroll_run_load(&r) != 0)
+ return r.id;
+ paid = strcmp(r.status, "paid") == 0;
+ }
+ char status[160];
+ if (!posted)
+ snprintf(status, sizeof status, "Status: inte bokförd");
+ else if (paid)
+ snprintf(status, sizeof status, "Status: betald");
+ else
+ snprintf(status, sizeof status, "Status: bokförd %s", r.pay_date);
+ char totals[192];
+ if (posted) {
+ char g[32], tx[32], av[32], net[32];
+ tui_kr_format(r.gross_ore, g, sizeof g);
+ tui_kr_format(r.tax_ore, tx, sizeof tx);
+ tui_kr_format(r.avgifter_ore, av, sizeof av);
+ tui_kr_format(r.net_ore, net, sizeof net);
+ snprintf(totals, sizeof totals,
+ "Brutto %s Skatt %s Avgifter %s Netto %s", g, tx,
+ av, net);
+ } else {
+ int count = payroll_salary_count(a);
+ snprintf(totals, sizeof totals, "%d anställd%s med månadslön",
+ count, count == 1 ? "" : "a");
+ }
+ const char *post_reason = NULL;
+ if (!can_write)
+ post_reason = "du har inte behörighet";
+ else if (posted)
+ post_reason = "körningen är redan bokförd";
+ const char *payslip_reason =
+ posted ? NULL : "körningen är inte bokförd än";
+ const char *agi_reason = NULL;
+ if (!owner)
+ agi_reason = "kräver ägarbehörighet";
+ else if (!posted)
+ agi_reason = "körningen är inte bokförd än";
+ const char *pay_reason = NULL;
+ if (!can_write)
+ pay_reason = "du har inte behörighet";
+ else if (!posted)
+ pay_reason = "körningen är inte bokförd än";
+ else if (paid)
+ pay_reason = "redan betald";
+ else if (r.tax_ore + r.avgifter_ore <= 0)
+ pay_reason = "inget att betala";
+ struct tui_form_action acts[7] = {
+ { status, -1, NULL },
+ { totals, -1, NULL },
+ { "Förhandsvisa (F5)", 1, NULL },
+ { "Bokför körning (^Enter)", post_reason ? 0 : 1, post_reason },
+ { "Lönebesked (PDF)", payslip_reason ? 0 : 1, payslip_reason },
+ { "AGI-underlag", agi_reason ? 0 : 1, agi_reason },
+ { "Betala skatt & avgifter", pay_reason ? 0 : 1, pay_reason },
+ };
+ struct tui_form_field ff[2];
+ memset(ff, 0, sizeof ff);
+ ff[0].label = "Period (ÅÅÅÅ-MM)";
+ ff[0].value = r.period;
+ ff[0].cap = sizeof r.period;
+ ff[0].kind = TUI_F_TEXT;
+ ff[1].label = "Betalningsdatum";
+ ff[1].value = r.pay_date;
+ ff[1].cap = sizeof r.pay_date;
+ ff[1].kind = TUI_F_DATE;
+ char title[64];
+ if (posted)
+ snprintf(title, sizeof title, "Lönekörning %s", r.period);
+ else
+ snprintf(title, sizeof title, "Ny lönekörning");
+ int editable = can_write && !posted;
+ tui_form_hint("upp/ned/Tab = flytta Enter = ändra/utför"
+ " F5 = förhandsvisa ^Enter = bokför"
+ " Esc/q = tillbaka ^C = avsluta");
+ int ret = tui_form_run_actions(title, ff, 2, editable, acts, 7, NULL,
+ &focus);
+ if (ret == TUI_FORM_BACK)
+ return r.id;
+ if (ret == TUI_FORM_REFRESH || ret == TUI_FORM_ACTION + 2)
+ payroll_run_preview(&r);
+ else if ((ret == TUI_FORM_SUBMIT || ret == TUI_FORM_ACTION + 3) &&
+ !posted)
+ payroll_run_post(&r);
+ else if (ret == TUI_FORM_ACTION + 4)
+ payroll_run_payslip(&r);
+ else if (ret == TUI_FORM_ACTION + 5)
+ payroll_run_agi(&r);
+ else if (ret == TUI_FORM_ACTION + 6)
+ payroll_run_pay_tax(&r);
+ }
+ return r.id;
+}
+
+void payroll_screen(struct app *a)
+{
+ int64_t keep_id = a->payroll_sel;
+ for (;;) {
+ if (g_quit)
+ return;
+ char *sresp = client_rpc(&a->conn, "payroll.tax_tables_status",
+ a->session, a->org, "{}");
+ int stale = 0;
+ int year = 0;
+ if (sresp && client_ok(sresp)) {
+ stale = jbool_val(sresp, "result.stale", 0);
+ year = (int)jint_val(sresp, "result.current_year", 0);
+ }
+ free(sresp);
+ char *resp = client_rpc(&a->conn, "payroll.run_list", a->session,
+ a->org, "{\"limit\":200}");
+ if (!resp || !client_ok(resp)) {
+ show_error("Lönekörningar", resp);
+ free(resp);
+ return;
+ }
+ size_t n = jarr_size(resp, "result.items");
+ char **lines = 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];
+ snprintf(path, sizeof path, "result.items.%zu.id", i);
+ ids[i] = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.period", i);
+ char *period = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.pay_date", i);
+ char *pay_date = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.status", i);
+ char *status = jstr_dup(resp, path);
+ snprintf(path, sizeof path, "result.items.%zu.gross_ore", i);
+ int64_t gross = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.net_ore", i);
+ int64_t net = jint_val(resp, path, 0);
+ snprintf(path, sizeof path, "result.items.%zu.line_count", i);
+ int64_t lc = jint_val(resp, path, 0);
+ char sbuf[16], gbuf[32], nbuf[32], line[256];
+ snprintf(sbuf, sizeof sbuf, "%s",
+ payroll_status_sv(status ? status : "posted"));
+ tui_pad_field(sbuf, sizeof sbuf, 7);
+ tui_amt_col(gbuf, sizeof gbuf, 13, gross);
+ tui_amt_col(nbuf, sizeof nbuf, 13, net);
+ snprintf(line, sizeof line, "%s %s %s %s %s %lld anst.",
+ period ? period : "", pay_date ? pay_date : "", sbuf,
+ gbuf, nbuf, (long long)lc);
+ lines[i] = xstrdup(line);
+ free(period);
+ free(pay_date);
+ free(status);
+ }
+ free(resp);
+ size_t start = 0;
+ for (size_t i = 0; i < n; i++)
+ if (ids[i] == keep_id) {
+ start = i;
+ break;
+ }
+ lines[n] = xstrdup("+ Ny lönekörning (^N)");
+ char title[160];
+ if (stale)
+ snprintf(title, sizeof title,
+ "Lönekörningar — skattetabeller för %d saknas (se"
+ " Skattetabeller)",
+ year);
+ else
+ snprintf(title, sizeof title, "Lönekörningar");
+ int cur = (int)start;
+ int sel = tui_select_list(title, lines, (int)n + 1, cur, 1, &cur, 1,
+ NULL, 0);
+ if (cur >= 0 && (size_t)cur < n)
+ a->payroll_sel = ids[cur];
+ if (sel == -1) {
+ for (size_t i = 0; i <= n; i++)
+ free(lines[i]);
+ free(lines);
+ free(ids);
+ return;
+ }
+ if (sel == -4 || (sel >= 0 && (size_t)sel == n)) {
+ if (payroll_salary_count(a) == 0) {
+ tui_message("Lönekörningar",
+ "Inga aktiva anställda med månadslön. Lägg upp"
+ " dem under Register → Anställda.");
+ } else {
+ int64_t rid = payroll_run_screen(a, 0);
+ if (rid > 0) {
+ keep_id = rid;
+ a->payroll_sel = rid;
+ }
+ }
+ for (size_t i = 0; i <= n; i++)
+ free(lines[i]);
+ free(lines);
+ free(ids);
+ continue;
+ }
+ if (sel >= 0 && (size_t)sel < n) {
+ int64_t rid = payroll_run_screen(a, ids[sel]);
+ if (rid > 0) {
+ keep_id = rid;
+ a->payroll_sel = rid;
+ }
+ }
+ for (size_t i = 0; i <= n; i++)
+ free(lines[i]);
+ free(lines);
+ free(ids);
+ }
+}