aboutsummaryrefslogtreecommitdiff
path: root/src/commands.c
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-19 21:10:54 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-19 21:10:54 +0200
commitbeb52c4a2ca23f29dc992e145810a47241c7d43b (patch)
treeb8fff8dce1cb01d42a1783040d7270558831bd2e /src/commands.c
parent77c298b3b64f2493e509734df0084068ad24441f (diff)
downloadbokf-beb52c4a2ca23f29dc992e145810a47241c7d43b.tar.gz
bokf-beb52c4a2ca23f29dc992e145810a47241c7d43b.zip
årsredovisning: Information om året per räkenskapsår (schema v6)v0.1.34
Diffstat (limited to 'src/commands.c')
-rw-r--r--src/commands.c336
1 files changed, 305 insertions, 31 deletions
diff --git a/src/commands.c b/src/commands.c
index 62de0be..448d03e 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -517,12 +517,17 @@ static yyjson_mut_val *org_json(struct req *r, sqlite3_stmt *st)
sq(sqlite3_column_text(st, 12)));
yyjson_mut_obj_add_strcpy(r->rdoc, o, "created_at",
sq(sqlite3_column_text(st, 13)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "description",
+ sq(sqlite3_column_text(st, 14)));
+ yyjson_mut_obj_add_int(r->rdoc, o, "shares",
+ sqlite3_column_int64(st, 15));
return o;
}
#define ORG_COLUMNS \
"id,name,org_nr,vat_nr,address,postal_code,city,country,email," \
- "phone,fiscal_year_start_month,moms_period,framework,created_at"
+ "phone,fiscal_year_start_month,moms_period,framework,created_at," \
+ "description,shares"
static yyjson_mut_val *h_org_create(struct req *r)
{
@@ -689,15 +694,21 @@ static yyjson_mut_val *h_org_update(struct req *r)
const char *phone = arg_str(r->args, "phone");
const char *moms = arg_str(r->args, "moms_period");
const char *framework = arg_str(r->args, "framework");
+ const char *description = arg_str(r->args, "description");
int64_t fy_month = 0;
arg_int(r->args, "fiscal_year_start_month", &fy_month);
+ int64_t shares = -1;
+ arg_int(r->args, "shares", &shares);
if (!name && !org_nr && !vat_nr && !address && !postal && !city &&
- !country && !email && !phone && !moms && !framework && fy_month == 0)
+ !country && !email && !phone && !moms && !framework && fy_month == 0 &&
+ !description && shares < 0)
return fail(r, "INVALID_ARGS", "nothing to update");
if (name && !*name)
return fail(r, "INVALID_ARGS", "name cannot be empty");
if (fy_month && (fy_month < 1 || fy_month > 12))
return fail(r, "INVALID_ARGS", "fiscal_year_start_month must be 1-12");
+ if (shares < -1)
+ return fail(r, "INVALID_ARGS", "shares must be >= 0");
if (moms && strcmp(moms, "month") != 0 && strcmp(moms, "quarter") != 0 &&
strcmp(moms, "year") != 0)
return fail(r, "INVALID_ARGS", "moms_period must be month, quarter or year");
@@ -705,15 +716,16 @@ static yyjson_mut_val *h_org_update(struct req *r)
strcmp(framework, "K3") != 0)
return fail(r, "INVALID_ARGS", "framework must be K2 or K3");
- static const char *const keys[9] = { "name", "org_nr", "vat_nr",
- "address", "postal_code", "city",
- "country", "email", "phone" };
- const char *texts[9] = { name, org_nr, vat_nr, address, postal,
- city, country, email, phone };
+ static const char *const keys[10] = { "name", "org_nr", "vat_nr",
+ "address", "postal_code", "city",
+ "country", "email", "phone",
+ "description" };
+ const char *texts[10] = { name, org_nr, vat_nr, address, postal,
+ city, country, email, phone, description };
if (r->dry_run) {
yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
- for (int i = 0; i < 9; i++)
+ for (int i = 0; i < 10; i++)
if (texts[i])
yyjson_mut_obj_add_strcpy(r->rdoc, o, keys[i], texts[i]);
if (fy_month)
@@ -723,6 +735,8 @@ static yyjson_mut_val *h_org_update(struct req *r)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "moms_period", moms);
if (framework)
yyjson_mut_obj_add_strcpy(r->rdoc, o, "framework", framework);
+ if (shares >= 0)
+ yyjson_mut_obj_add_int(r->rdoc, o, "shares", shares);
yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
return o;
}
@@ -735,30 +749,33 @@ static yyjson_mut_val *h_org_update(struct req *r)
" vat_nr=COALESCE(?4,vat_nr), address=COALESCE(?5,address),"
" postal_code=COALESCE(?6,postal_code), city=COALESCE(?7,city),"
" country=COALESCE(?8,country), email=COALESCE(?9,email),"
- " phone=COALESCE(?10,phone),"
- " fiscal_year_start_month=CASE WHEN ?11=0"
- " THEN fiscal_year_start_month ELSE ?11 END,"
- " moms_period=COALESCE(?12,moms_period),"
- " framework=COALESCE(?13,framework)"
+ " phone=COALESCE(?10,phone), description=COALESCE(?11,"
+ " description),"
+ " fiscal_year_start_month=CASE WHEN ?12=0"
+ " THEN fiscal_year_start_month ELSE ?12 END,"
+ " moms_period=COALESCE(?13,moms_period),"
+ " framework=COALESCE(?14,framework),"
+ " shares=CASE WHEN ?15<0 THEN shares ELSE ?15 END"
" WHERE id=?1",
-1, &st, NULL) != SQLITE_OK)
return fail(r, "INTERNAL", "database error");
sqlite3_bind_int64(st, 1, r->org_id);
- for (int i = 0; i < 9; i++) {
+ for (int i = 0; i < 10; i++) {
if (texts[i])
sqlite3_bind_text(st, i + 2, texts[i], -1, SQLITE_TRANSIENT);
else
sqlite3_bind_null(st, i + 2);
}
- sqlite3_bind_int64(st, 11, fy_month);
+ sqlite3_bind_int64(st, 12, fy_month);
if (moms)
- sqlite3_bind_text(st, 12, moms, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 13, moms, -1, SQLITE_TRANSIENT);
else
- sqlite3_bind_null(st, 12);
+ sqlite3_bind_null(st, 13);
if (framework)
- sqlite3_bind_text(st, 13, framework, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 14, framework, -1, SQLITE_TRANSIENT);
else
- sqlite3_bind_null(st, 13);
+ sqlite3_bind_null(st, 14);
+ sqlite3_bind_int64(st, 15, shares);
int rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE)
@@ -953,6 +970,182 @@ static yyjson_mut_val *h_org_member_remove(struct req *r)
}
/* ------------------------------------------------------------------ */
+/* board members (årsredovisning signatures) */
+/* ------------------------------------------------------------------ */
+
+static yyjson_mut_val *board_json(struct req *r, sqlite3_stmt *st)
+{
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", sqlite3_column_int64(st, 0));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name",
+ sq(sqlite3_column_text(st, 1)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "title",
+ sq(sqlite3_column_text(st, 2)));
+ return o;
+}
+
+static yyjson_mut_val *h_board_list(struct req *r)
+{
+ yyjson_mut_val *items = yyjson_mut_arr(r->rdoc);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "SELECT id,name,title FROM board_members"
+ " WHERE org_id=?1 ORDER BY id",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ while (sqlite3_step(st) == SQLITE_ROW)
+ yyjson_mut_arr_add_val(items, board_json(r, st));
+ sqlite3_finalize(st);
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_val(r->rdoc, o, "items", items);
+ return o;
+}
+
+static yyjson_mut_val *board_get(struct req *r, int64_t id)
+{
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "SELECT id,name,title FROM board_members"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, id);
+ yyjson_mut_val *o = NULL;
+ if (sqlite3_step(st) == SQLITE_ROW)
+ o = board_json(r, st);
+ sqlite3_finalize(st);
+ if (!o)
+ return fail(r, "NOT_FOUND", "board member not found");
+ return o;
+}
+
+static yyjson_mut_val *h_board_add(struct req *r)
+{
+ const char *name = arg_str(r->args, "name");
+ const char *title = arg_str(r->args, "title");
+ if (!name || !*name)
+ return fail(r, "INVALID_ARGS", "name is required");
+ if (!title || !*title)
+ title = "Styrelseledamot";
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", title);
+ return o;
+ }
+ char ts[32];
+ util_iso8601(util_now(), ts, sizeof ts);
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "INSERT INTO board_members(org_id,name,title,"
+ "created_at) VALUES(?1,?2,?3,?4)",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, name, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 3, title, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 4, ts, -1, SQLITE_TRANSIENT);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ int64_t id = db_last_id(r->db);
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "board.add", reqjson, "OK", NULL);
+ free(reqjson);
+ return board_get(r, id);
+}
+
+static yyjson_mut_val *h_board_update(struct req *r)
+{
+ int64_t id = 0;
+ if (!arg_int(r->args, "id", &id) || id <= 0)
+ return fail(r, "INVALID_ARGS", "id is required");
+ const char *name = arg_str(r->args, "name");
+ const char *title = arg_str(r->args, "title");
+ if (!name && !title)
+ return fail(r, "INVALID_ARGS", "nothing to update");
+ if (name && !*name)
+ return fail(r, "INVALID_ARGS", "name cannot be empty");
+ if (title && !*title)
+ return fail(r, "INVALID_ARGS", "title cannot be empty");
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
+ if (name)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "name", name);
+ if (title)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "title", title);
+ return o;
+ }
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "UPDATE board_members SET name=COALESCE(?3,name),"
+ " title=COALESCE(?4,title)"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, id);
+ if (name)
+ sqlite3_bind_text(st, 3, name, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 3);
+ if (title)
+ sqlite3_bind_text(st, 4, title, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 4);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ if (sqlite3_changes(r->db) == 0)
+ return fail(r, "NOT_FOUND", "board member not found");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "board.update", reqjson, "OK", NULL);
+ free(reqjson);
+ return board_get(r, id);
+}
+
+static yyjson_mut_val *h_board_remove(struct req *r)
+{
+ int64_t id = 0;
+ if (!arg_int(r->args, "id", &id) || id <= 0)
+ return fail(r, "INVALID_ARGS", "id is required");
+ if (r->dry_run) {
+ yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
+ yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
+ yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
+ return o;
+ }
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(r->db,
+ "DELETE FROM board_members"
+ " WHERE org_id=?1 AND id=?2",
+ -1, &st, NULL) != SQLITE_OK)
+ return fail(r, "INTERNAL", "database error");
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_int64(st, 2, id);
+ int rc = sqlite3_step(st);
+ sqlite3_finalize(st);
+ if (rc != SQLITE_DONE)
+ return fail(r, "INTERNAL", sqlite3_errmsg(r->db));
+ if (sqlite3_changes(r->db) == 0)
+ return fail(r, "NOT_FOUND", "board member not found");
+ char *reqjson = audit_args_json(r->args);
+ audit_append(r->db, r->org_id, r->sess->user_id, r->sess->token_id,
+ "board.remove", reqjson, "OK", NULL);
+ free(reqjson);
+ return yyjson_mut_obj(r->rdoc);
+}
+
+/* ------------------------------------------------------------------ */
/* users and tokens */
/* ------------------------------------------------------------------ */
@@ -1698,10 +1891,22 @@ static yyjson_mut_val *fy_json(struct req *r, sqlite3_stmt *st)
sq(sqlite3_column_text(st, 5)));
yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore",
sqlite3_column_int64(st, 6));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "events",
+ sq(sqlite3_column_text(st, 7)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "agm_date",
+ sq(sqlite3_column_text(st, 8)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "dividend_date",
+ sq(sqlite3_column_text(st, 9)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "employees",
+ sq(sqlite3_column_text(st, 10)));
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes",
+ sq(sqlite3_column_text(st, 11)));
return o;
}
-#define FY_COLUMNS "id,label,start_date,end_date,status,locked_until,dividend_ore"
+#define FY_COLUMNS \
+ "id,label,start_date,end_date,status,locked_until,dividend_ore," \
+ "events,agm_date,dividend_date,employees,notes"
static yyjson_mut_val *h_fiscal_year_list(struct req *r)
{
@@ -1779,20 +1984,46 @@ static yyjson_mut_val *h_fiscal_year_open(struct req *r)
return fail(r, "INVALID_ARGS", "start_date must be before end_date");
if (fy_exists_in_range(r->db, r->org_id, start, end))
return fail(r, "CONFLICT", "fiscal year overlaps an existing one");
+ /* "Information om året" carries over from the latest earlier year, so
+ only what changes needs to be edited. Dates and the dividend are
+ year-specific and start empty. */
+ char prev_events[600] = "", prev_emp[64] = "", prev_notes[600] = "";
+ sqlite3_stmt *st = NULL;
+ if (sqlite3_prepare_v2(
+ r->db,
+ "SELECT events,employees,notes FROM fiscal_years"
+ " WHERE org_id=?1 AND end_date < ?2"
+ " ORDER BY end_date DESC LIMIT 1",
+ -1, &st, NULL) == SQLITE_OK) {
+ sqlite3_bind_int64(st, 1, r->org_id);
+ sqlite3_bind_text(st, 2, start, -1, SQLITE_TRANSIENT);
+ if (sqlite3_step(st) == SQLITE_ROW) {
+ snprintf(prev_events, sizeof prev_events, "%s",
+ (const char *)sqlite3_column_text(st, 0));
+ snprintf(prev_emp, sizeof prev_emp, "%s",
+ (const char *)sqlite3_column_text(st, 1));
+ snprintf(prev_notes, sizeof prev_notes, "%s",
+ (const char *)sqlite3_column_text(st, 2));
+ }
+ sqlite3_finalize(st);
+ }
char ts[32];
util_iso8601(util_now(), ts, sizeof ts);
- sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(
r->db,
"INSERT INTO fiscal_years(org_id,label,start_date,end_date,"
- "created_at) VALUES(?1,?2,?3,?4,?5)",
+ "events,employees,notes,created_at)"
+ " VALUES(?1,?2,?3,?4,?5,?6,?7,?8)",
-1, &st, NULL) != SQLITE_OK)
return fail(r, "INTERNAL", "database error");
sqlite3_bind_int64(st, 1, r->org_id);
sqlite3_bind_text(st, 2, label, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 3, start, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(st, 4, end, -1, SQLITE_TRANSIENT);
- sqlite3_bind_text(st, 5, ts, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 5, prev_events, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 6, prev_emp, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 7, prev_notes, -1, SQLITE_TRANSIENT);
+ sqlite3_bind_text(st, 8, ts, -1, SQLITE_TRANSIENT);
int rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE)
@@ -1880,17 +2111,31 @@ static yyjson_mut_val *h_fiscal_year_reopen(struct req *r)
return yyjson_mut_obj(r->rdoc);
}
-/* Fiscal-year metadata that is not a posting: today only the board's
- proposed dividend, kept for the årsredovisning draft. */
+/* Fiscal-year metadata that is not a posting: the board's proposed
+ dividend and the year's material events, kept for the årsredovisning
+ draft. Both fields are optional; at least one must be given. */
static yyjson_mut_val *h_fiscal_year_update(struct req *r)
{
int64_t id = 0, dividend = 0;
if (!arg_int(r->args, "id", &id) || id <= 0)
return fail(r, "INVALID_ARGS", "id is required");
- if (!arg_int(r->args, "dividend_ore", &dividend))
- return fail(r, "INVALID_ARGS", "dividend_ore is required");
- if (dividend < 0)
+ int have_div = arg_int(r->args, "dividend_ore", &dividend);
+ const char *events = arg_str(r->args, "events");
+ const char *agm = arg_str(r->args, "agm_date");
+ const char *pay = arg_str(r->args, "dividend_date");
+ const char *employees = arg_str(r->args, "employees");
+ const char *notes = arg_str(r->args, "notes");
+ if (!have_div && !events && !agm && !pay && !employees && !notes)
+ return fail(r, "INVALID_ARGS", "nothing to update");
+ if (have_div && dividend < 0)
return fail(r, "INVALID_ARGS", "dividend_ore must be >= 0");
+ if (agm && *agm && !util_parse_iso_date(agm))
+ return fail(r, "INVALID_ARGS", "agm_date must be YYYY-MM-DD");
+ if (pay && *pay && !util_parse_iso_date(pay))
+ return fail(r, "INVALID_ARGS", "dividend_date must be YYYY-MM-DD");
+ static const char *const skeys[4] = { "events", "agm_date",
+ "dividend_date", "employees" };
+ const char *svals[4] = { events, agm, pay, employees };
sqlite3_stmt *st = NULL;
if (sqlite3_prepare_v2(r->db,
"SELECT count(*) FROM fiscal_years"
@@ -1909,17 +2154,40 @@ static yyjson_mut_val *h_fiscal_year_update(struct req *r)
yyjson_mut_val *o = yyjson_mut_obj(r->rdoc);
yyjson_mut_obj_add_bool(r->rdoc, o, "dry_run", true);
yyjson_mut_obj_add_int(r->rdoc, o, "id", id);
- yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend);
+ if (have_div)
+ yyjson_mut_obj_add_int(r->rdoc, o, "dividend_ore", dividend);
+ for (int i = 0; i < 4; i++)
+ if (svals[i])
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, skeys[i], svals[i]);
+ if (notes)
+ yyjson_mut_obj_add_strcpy(r->rdoc, o, "notes", notes);
return o;
}
if (sqlite3_prepare_v2(r->db,
- "UPDATE fiscal_years SET dividend_ore=?3"
+ "UPDATE fiscal_years SET"
+ " dividend_ore=CASE WHEN ?3<0 THEN dividend_ore"
+ " ELSE ?3 END,"
+ " events=COALESCE(?4,events),"
+ " agm_date=COALESCE(?5,agm_date),"
+ " dividend_date=COALESCE(?6,dividend_date),"
+ " employees=COALESCE(?7,employees),"
+ " notes=COALESCE(?8,notes)"
" WHERE org_id=?1 AND id=?2",
-1, &st, NULL) != SQLITE_OK)
return fail(r, "INTERNAL", "database error");
sqlite3_bind_int64(st, 1, r->org_id);
sqlite3_bind_int64(st, 2, id);
- sqlite3_bind_int64(st, 3, dividend);
+ sqlite3_bind_int64(st, 3, have_div ? dividend : -1);
+ for (int i = 0; i < 4; i++) {
+ if (svals[i])
+ sqlite3_bind_text(st, i + 4, svals[i], -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, i + 4);
+ }
+ if (notes)
+ sqlite3_bind_text(st, 8, notes, -1, SQLITE_TRANSIENT);
+ else
+ sqlite3_bind_null(st, 8);
int rc = sqlite3_step(st);
sqlite3_finalize(st);
if (rc != SQLITE_DONE)
@@ -4148,6 +4416,12 @@ const struct command g_commands[] = {
h_org_member_set_role },
{ "org.member_remove", "Remove a member from the org", PERM_OWNER, 1, 1, 1,
h_org_member_remove },
+ { "board.list", "List board members", PERM_READ, 1, 0, 0, h_board_list },
+ { "board.add", "Add a board member", PERM_OWNER, 1, 1, 1, h_board_add },
+ { "board.update", "Update a board member", PERM_OWNER, 1, 1, 1,
+ h_board_update },
+ { "board.remove", "Remove a board member", PERM_OWNER, 1, 1, 1,
+ h_board_remove },
{ "user.create", "Create a user (system admin)", PERM_ADMIN, 0, 1, 1,
h_user_create },
{ "user.list", "List users (system admin)", PERM_ADMIN, 0, 0, 0,