From 1816b5ae4068c725ec05780629d2cddc0d386c1a Mon Sep 17 00:00:00 2001 From: Jaromil Date: Tue, 2 Jun 2026 11:16:23 +0200 Subject: [PATCH 1/2] feat: configure personnel billing display --- AGENTS.md | 2 + README.md | 7 ++++ src/agiladmin/config.clj | 4 ++ src/agiladmin/view_person.clj | 35 ++++++++++++++--- test/agiladmin/config_test.clj | 20 +++++++++- test/agiladmin/view_person_test.clj | 61 ++++++++++++++++++++++++++++- 6 files changed, 121 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1126fbe..811b106 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,6 +48,8 @@ - `:source` - `:just-auth` - `:cache` enables runtime in-memory caches when `true`; the default is `false`. + - `:show-voluntary-hours` controls whether personnel monthly summaries mention voluntary hours; default `false`. + - `:vat-percentage` controls personnel VAT display; default `0`, which hides VAT text. - `:agiladmin :webserver` now separates internal bind settings from public URL settings: - `:host` and `:port` are Jetty bind values. - `:base-host` and `:base-path` are browser-facing URL parts. diff --git a/README.md b/README.md index 279da14..f7271cb 100644 --- a/README.md +++ b/README.md @@ -210,6 +210,10 @@ Example: appname: agiladmin agiladmin: + cache: false + show-voluntary-hours: false + vat-percentage: 0 + webserver: host: localhost port: 8000 @@ -239,6 +243,9 @@ Notes: - `budgets.ssh-key` is the private key path used for Git access; if it does not exist, Agiladmin generates a new keypair and exposes the public key in the `/config` page - project names are discovered from `*.yaml` files in `budgets.path`, using the part of the filename before the first `.` +- `cache` enables runtime in-memory caches when `true`; it defaults to `false` +- `show-voluntary-hours` controls whether personnel monthly summaries mention voluntary hours; it defaults to `false` +- `vat-percentage` controls personnel VAT display; it defaults to `0`, which hides the VAT sentence - `pocketbase` is optional only if you are using dev auth locally - `webserver.upload-max-size` is in bytes and defaults to `500000` - `webserver.base-path` is the browser-visible mount prefix; if you publish under a subpath such as `/agiladmin`, your reverse proxy must strip that prefix before forwarding to Jetty routes diff --git a/src/agiladmin/config.clj b/src/agiladmin/config.clj index 2e8412b..8f4eec2 100644 --- a/src/agiladmin/config.clj +++ b/src/agiladmin/config.clj @@ -35,6 +35,8 @@ :path s/Str} (s/optional-key :projects) [s/Str] (s/optional-key :cache) s/Bool + (s/optional-key :show-voluntary-hours) s/Bool + (s/optional-key :vat-percentage) s/Num (s/optional-key :webserver) {(s/optional-key :port) s/Num (s/optional-key :host) s/Str (s/optional-key :base-host) s/Str @@ -86,6 +88,8 @@ :ssh-key "id_rsa" :path "budgets/"} :cache false + :show-voluntary-hours false + :vat-percentage 0 :webserver {:base-host "" :base-path "/" diff --git a/src/agiladmin/view_person.clj b/src/agiladmin/view_person.clj index ca598aa..29e3b4e 100644 --- a/src/agiladmin/view_person.clj +++ b/src/agiladmin/view_person.clj @@ -73,6 +73,31 @@ tab/dataset to-table))) +(defn- show-voluntary-hours? + "Return true when personnel pages should mention voluntary hours." + [config] + (true? (get-in config [:agiladmin :show-voluntary-hours]))) + +(defn- voluntary-hours-text + [config hours] + (when (show-voluntary-hours? config) + (str " days, plus " hours " voluntary hours."))) + +(defn- vat-percentage + "Return the configured VAT percentage, defaulting to zero." + [config] + (or (get-in config [:agiladmin :vat-percentage]) 0)) + +(defn- vat-text + [config pay] + (let [percentage (vat-percentage config)] + (when (pos? percentage) + (str " (with " + percentage + "% VAT added is " + (util/round (+ pay (* pay (/ percentage 100)))) + ")")))) + (defn- load-person-page-data "Load the shared timesheet and project data needed by personnel pages." [config person year] @@ -113,9 +138,8 @@ " across " (keep #(when (= (:month %) (str year '- m)) (:days %)) - (:sheets timesheet)) - " days, plus " mvol - " voluntary hours." + (:sheets timesheet)) + (or (voluntary-hours-text config mvol) " days.") [:div {:class "month-detail overflow-x-auto"} (to-monthly-hours-table projects breakdown)]]])] [:div {:class "space-y-6"} @@ -271,9 +295,8 @@ (keep #(when (= (:month %) (str year '- m)) (:days %)) (:sheets timesheet)) - " days, plus " mvol - " voluntary hours." - " (with 21% VAT added is " (+ pay (* pay 0.21)) ")" + (or (voluntary-hours-text config mvol) " days.") + (vat-text config pay) [:div {:class "month-detail overflow-x-auto"} (to-monthly-bill-table projects breakdown)]]])])) (web/button-prev-year year person)] diff --git a/test/agiladmin/config_test.clj b/test/agiladmin/config_test.clj index b357887..33db42c 100644 --- a/test/agiladmin/config_test.clj +++ b/test/agiladmin/config_test.clj @@ -164,7 +164,9 @@ (get-in conf [:agiladmin :webserver :base-host]) => "" (get-in conf [:agiladmin :webserver :base-path]) => "/" (get-in conf [:agiladmin :webserver :upload-max-size]) => 500000 - (get-in conf [:agiladmin :cache]) => false)) + (get-in conf [:agiladmin :cache]) => false + (get-in conf [:agiladmin :show-voluntary-hours]) => false + (get-in conf [:agiladmin :vat-percentage]) => 0)) (fact "Application config loader preserves explicit webserver base values" (let [path "/tmp/agiladmin-webserver-explicit.yaml" @@ -202,6 +204,22 @@ (f/failed? conf) => false (get-in conf [:agiladmin :cache]) => true)) +(fact "Application config loader preserves personnel display settings" + (let [path "/tmp/agiladmin-personnel-display.yaml" + _ (spit path + (str "appname: agiladmin\n\n" + "agiladmin:\n" + " budgets:\n" + " git: ssh://git@example.org/admin-budgets\n" + " ssh-key: id_rsa\n" + " path: budgets/\n" + " show-voluntary-hours: true\n" + " vat-percentage: 21\n")) + conf (conf/load-config path conf/default-settings)] + (f/failed? conf) => false + (get-in conf [:agiladmin :show-voluntary-hours]) => true + (get-in conf [:agiladmin :vat-percentage]) => 21)) + (fact "Application config loader reports an explicit missing file" (let [conf (conf/load-config "/tmp/does-not-exist-agiladmin.yaml" conf/default-settings)] (f/failed? conf) => true diff --git a/test/agiladmin/view_person_test.clj b/test/agiladmin/view_person_test.clj index a5ca864..bb352ec 100644 --- a/test/agiladmin/view_person_test.clj +++ b/test/agiladmin/view_person_test.clj @@ -161,8 +161,29 @@ (:body response) => (contains "Yearly totals") (:body response) =not=> (contains "Total_billed") (:body response) =not=> (contains "Download yearly totals:") + (:body response) =not=> (contains "voluntary hours") (:body response) =not=> (contains "with 21% VAT added")))) +(fact "Manager personnel view shows voluntary hours when enabled" + (with-redefs [agiladmin.view-person/load-person-page-data + (fn [_ _ _] + {:ts-file "ignored.xlsx" + :timesheet {:sheets [{:month "2026-1" :days 20}]} + :projects {:CORE {:idx {:TASK-1 {:text "Task one"}}}} + :hours {:column-names [:month :project :task :tag :hours] + :rows [{:month "2026-1" + :project "CORE" + :task "TASK-1" + :tag "VOL" + :hours 3}]}})] + (let [response (view-person/list-person + {:agiladmin {:show-voluntary-hours true}} + {:role "manager" + :name "Manager User"} + "Manager User" + 2026)] + (:body response) => (contains "plus 3 voluntary hours.")))) + (fact "Personnel view keeps the upload form visible when timesheet loading fails" (with-redefs [agiladmin.view-person/load-person-page-data (fn [_ _ _] @@ -230,7 +251,45 @@ @cph-calls => 1 (:body response) => (contains "Download yearly totals:") (:body response) => (contains "2026-1") - (:body response) => (contains "2026-2"))))) + (:body response) => (contains "2026-2") + (:body response) =not=> (contains "VAT added") + (:body response) =not=> (contains "voluntary hours"))))) + +(fact "Admin personnel view shows configured VAT when enabled" + (with-redefs [agiladmin.view-person/load-person-page-data + (fn [_ _ _] + {:ts-file "ignored.xlsx" + :timesheet {:sheets [{:month "2026-1" :days 20}]} + :projects {:CORE {:idx {:TASK-1 {:text "Task one"}}}} + :hours {:column-names [:month :name :project :task :tag :hours] + :rows [{:month "2026-1" + :name "Admin User" + :project "CORE" + :task "TASK-1" + :tag "" + :hours 10}]}}) + agiladmin.core/derive-costs + (fn [_ _ _] + {:column-names [:month :name :project :task :tag :hours :cost] + :rows [{:month "2026-1" + :name "Admin User" + :project "CORE" + :task "TASK-1" + :tag "" + :hours 10 + :cost 1000}]}) + agiladmin.core/derive-cost-per-hour + (fn [dataset _ _] + (assoc dataset + :column-names [:month :name :project :task :tag :hours :cost :cph] + :rows (mapv #(assoc % :cph 100) (:rows dataset))))] + (let [response (view-person/list-person + {:agiladmin {:vat-percentage 21}} + {:role "admin" + :name "Admin User"} + "Admin User" + 2026)] + (:body response) => (contains "with 21% VAT added is 1210")))) (fact "Admin personnel view ignores xlsx files that do not match the timesheet naming pattern" (with-redefs [agiladmin.utils/now (fn [] {:year 2026}) From e66427fa96df2dbe8afa997a0e56f84eea7fcd14 Mon Sep 17 00:00:00 2001 From: Jaromil Date: Tue, 2 Jun 2026 11:28:55 +0200 Subject: [PATCH 2/2] refactor: rename voluntary hours config --- AGENTS.md | 2 +- README.md | 4 ++-- src/agiladmin/config.clj | 4 ++-- src/agiladmin/view_person.clj | 6 +++--- test/agiladmin/config_test.clj | 6 +++--- test/agiladmin/view_person_test.clj | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 811b106..8006ca5 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -48,7 +48,7 @@ - `:source` - `:just-auth` - `:cache` enables runtime in-memory caches when `true`; the default is `false`. - - `:show-voluntary-hours` controls whether personnel monthly summaries mention voluntary hours; default `false`. + - `:voluntary-hours` controls whether personnel monthly summaries mention voluntary hours; default `false`. - `:vat-percentage` controls personnel VAT display; default `0`, which hides VAT text. - `:agiladmin :webserver` now separates internal bind settings from public URL settings: - `:host` and `:port` are Jetty bind values. diff --git a/README.md b/README.md index f7271cb..f02a151 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ appname: agiladmin agiladmin: cache: false - show-voluntary-hours: false + voluntary-hours: false vat-percentage: 0 webserver: @@ -244,7 +244,7 @@ Notes: - `budgets.ssh-key` is the private key path used for Git access; if it does not exist, Agiladmin generates a new keypair and exposes the public key in the `/config` page - project names are discovered from `*.yaml` files in `budgets.path`, using the part of the filename before the first `.` - `cache` enables runtime in-memory caches when `true`; it defaults to `false` -- `show-voluntary-hours` controls whether personnel monthly summaries mention voluntary hours; it defaults to `false` +- `voluntary-hours` controls whether personnel monthly summaries mention voluntary hours; it defaults to `false` - `vat-percentage` controls personnel VAT display; it defaults to `0`, which hides the VAT sentence - `pocketbase` is optional only if you are using dev auth locally - `webserver.upload-max-size` is in bytes and defaults to `500000` diff --git a/src/agiladmin/config.clj b/src/agiladmin/config.clj index 8f4eec2..11b8019 100644 --- a/src/agiladmin/config.clj +++ b/src/agiladmin/config.clj @@ -35,7 +35,7 @@ :path s/Str} (s/optional-key :projects) [s/Str] (s/optional-key :cache) s/Bool - (s/optional-key :show-voluntary-hours) s/Bool + (s/optional-key :voluntary-hours) s/Bool (s/optional-key :vat-percentage) s/Num (s/optional-key :webserver) {(s/optional-key :port) s/Num (s/optional-key :host) s/Str @@ -88,7 +88,7 @@ :ssh-key "id_rsa" :path "budgets/"} :cache false - :show-voluntary-hours false + :voluntary-hours false :vat-percentage 0 :webserver {:base-host "" diff --git a/src/agiladmin/view_person.clj b/src/agiladmin/view_person.clj index 29e3b4e..054e546 100644 --- a/src/agiladmin/view_person.clj +++ b/src/agiladmin/view_person.clj @@ -73,14 +73,14 @@ tab/dataset to-table))) -(defn- show-voluntary-hours? +(defn- voluntary-hours? "Return true when personnel pages should mention voluntary hours." [config] - (true? (get-in config [:agiladmin :show-voluntary-hours]))) + (true? (get-in config [:agiladmin :voluntary-hours]))) (defn- voluntary-hours-text [config hours] - (when (show-voluntary-hours? config) + (when (voluntary-hours? config) (str " days, plus " hours " voluntary hours."))) (defn- vat-percentage diff --git a/test/agiladmin/config_test.clj b/test/agiladmin/config_test.clj index 33db42c..5a2d117 100644 --- a/test/agiladmin/config_test.clj +++ b/test/agiladmin/config_test.clj @@ -165,7 +165,7 @@ (get-in conf [:agiladmin :webserver :base-path]) => "/" (get-in conf [:agiladmin :webserver :upload-max-size]) => 500000 (get-in conf [:agiladmin :cache]) => false - (get-in conf [:agiladmin :show-voluntary-hours]) => false + (get-in conf [:agiladmin :voluntary-hours]) => false (get-in conf [:agiladmin :vat-percentage]) => 0)) (fact "Application config loader preserves explicit webserver base values" @@ -213,11 +213,11 @@ " git: ssh://git@example.org/admin-budgets\n" " ssh-key: id_rsa\n" " path: budgets/\n" - " show-voluntary-hours: true\n" + " voluntary-hours: true\n" " vat-percentage: 21\n")) conf (conf/load-config path conf/default-settings)] (f/failed? conf) => false - (get-in conf [:agiladmin :show-voluntary-hours]) => true + (get-in conf [:agiladmin :voluntary-hours]) => true (get-in conf [:agiladmin :vat-percentage]) => 21)) (fact "Application config loader reports an explicit missing file" diff --git a/test/agiladmin/view_person_test.clj b/test/agiladmin/view_person_test.clj index bb352ec..4f45457 100644 --- a/test/agiladmin/view_person_test.clj +++ b/test/agiladmin/view_person_test.clj @@ -177,7 +177,7 @@ :tag "VOL" :hours 3}]}})] (let [response (view-person/list-person - {:agiladmin {:show-voluntary-hours true}} + {:agiladmin {:voluntary-hours true}} {:role "manager" :name "Manager User"} "Manager User"