Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
- `:source`
- `:just-auth`
- `:cache` enables runtime in-memory caches when `true`; the default is `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.
- `:base-host` and `:base-path` are browser-facing URL parts.
Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,10 @@ Example:
appname: agiladmin

agiladmin:
cache: false
voluntary-hours: false
vat-percentage: 0

webserver:
host: localhost
port: 8000
Expand Down Expand Up @@ -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`
- `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
Expand Down
4 changes: 4 additions & 0 deletions src/agiladmin/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
:path s/Str}
(s/optional-key :projects) [s/Str]
(s/optional-key :cache) 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
(s/optional-key :base-host) s/Str
Expand Down Expand Up @@ -86,6 +88,8 @@
:ssh-key "id_rsa"
:path "budgets/"}
:cache false
:voluntary-hours false
:vat-percentage 0
:webserver
{:base-host ""
:base-path "/"
Expand Down
35 changes: 29 additions & 6 deletions src/agiladmin/view_person.clj
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,31 @@
tab/dataset
to-table)))

(defn- voluntary-hours?
"Return true when personnel pages should mention voluntary hours."
[config]
(true? (get-in config [:agiladmin :voluntary-hours])))

(defn- voluntary-hours-text
[config hours]
(when (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]
Expand Down Expand Up @@ -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"}
Expand Down Expand Up @@ -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)]
Expand Down
20 changes: 19 additions & 1 deletion test/agiladmin/config_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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 :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"
Expand Down Expand Up @@ -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"
" voluntary-hours: true\n"
" vat-percentage: 21\n"))
conf (conf/load-config path conf/default-settings)]
(f/failed? conf) => false
(get-in conf [:agiladmin :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
Expand Down
61 changes: 60 additions & 1 deletion test/agiladmin/view_person_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -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 {: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 [_ _ _]
Expand Down Expand Up @@ -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})
Expand Down