Bash script to create a billing report task in kanbanflow
Needs jq >= 1.5
curl -s https://gist.githubusercontent.com/jroehl/8ba7eb7280278518a52889613dcc02d3/raw/create-billing-report.sh | bash -s "--month=1 --year=2020"
Needs jq >= 1.5
curl -s https://gist.githubusercontent.com/jroehl/8ba7eb7280278518a52889613dcc02d3/raw/create-billing-report.sh | bash -s "--month=1 --year=2020"
#!/usr/bin/env bash | |
# exit when any command fails | |
set -e | |
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" | |
[[ -f "${DIR}/.env" ]] && source "${DIR}/.env" | |
# ENV variables | |
# NEW_TASK_COLUMN_ID | |
# NEW_TASK_SWIMLANE_ID | |
# NEW_TASK_TARGET_ID | |
# API_TOKEN | |
readonly BASE_URL="https://kanbanflow.com/api/v1" | |
readonly TASK_PATH="tasks" | |
readonly BOARD_PATH="board" | |
readonly AUTH_HEADER="Authorization: Basic $(echo "apiToken:${API_TOKEN}" | base64)" | |
function first_day() { | |
local month=${1} | |
local date_type="+%F" | |
if [ ${month} -gt 1 ]; then | |
echo $(date --utc -d "$((month - 1))/1 month" "${date_type}") | |
else | |
echo $(date --utc -d "${month}/1 month - 1 month" "${date_type}") | |
fi | |
} | |
function last_day() { | |
local month=${1} | |
local year=${2} | |
local date_type="+%F" | |
echo $(date -d "${year}/${month}/1 + 1 month - 1 day" "${date_type}") | |
} | |
function fetch_tasks_until() { | |
local until="${1}" | |
local swimlane_index="${2:-0}" | |
tasks=$( | |
curl -X GET -H "${AUTH_HEADER}" -s \ | |
"${BASE_URL}/${TASK_PATH}?swimlaneIndex=${swimlane_index}&columnName=Done&limit=100&startGroupingDate=${until}" | |
) | |
echo "${tasks}" | |
} | |
function iterate_swimlanes() { | |
local start="${1}" | |
board=$( | |
curl -X GET -H "${AUTH_HEADER}" -s \ | |
"${BASE_URL}/${BOARD_PATH}" | |
) | |
results="[]" | |
swimlanes=($(echo "${board}" | jq -r '.swimlanes[] | @base64')) | |
for i in ${!swimlanes[@]}; do | |
tasks=$(fetch_tasks_until ${start} ${i}) | |
results="$(echo "$tasks" | jq --argjson r "${results}" -r '$r + .[].tasks')" | |
done | |
echo $results | |
} | |
function get_time_spent() { | |
local delta="${1}" | |
((h = ${delta} / 3600)) | |
((m = (${delta} % 3600) / 60)) | |
((s = ${delta} % 60)) | |
printf "%02d hours %02d minutes %02d seconds\n" $h $m $s | |
} | |
function get_customer_name() { | |
local arg=${1} | |
if [[ "$arg" =~ (^cx:([^,]*)) ]]; then | |
echo ${BASH_REMATCH[2]} | |
else | |
echo "N/A" | |
fi | |
} | |
function iterate_tasks() { | |
local tasks="${1}" | |
declare -a billable_tasks | |
for row in $(echo "${1}" | jq -r '.[] | @base64'); do | |
_jq() { | |
echo ${row} | base64 --decode | jq -r "${1}" | |
} | |
local time_spent=$(get_time_spent $(_jq '.totalSecondsSpent')) | |
local time_spent_seconds=$(_jq '.totalSecondsSpent') | |
local task_id=$(_jq '._id') | |
local name=$(_jq '.name') | |
local labels=$(_jq '.labels | map(select(.name | contains("billable") | not)) | [.[].name] | join(", ")') | |
local grouping_date=$(_jq '.groupingDate') | |
json=" | |
{ | |
\"name\": \"${name}\", | |
\"timeSpent\": \"${time_spent}\", | |
\"timeSpentSeconds\": ${time_spent_seconds}, | |
\"labels\": \"${labels}\", | |
\"customer\": \"$(get_customer_name "${labels}")\", | |
\"taskId\": \"${task_id}\", | |
\"groupingDate\": \"${grouping_date}\" | |
} | |
" | |
billable_tasks+=("${json}") | |
[ -z ${DRY_RUN+x} ] && add_billed_label "${task_id}" | |
done | |
echo ${billable_tasks[@]} | jq -s '.' | |
} | |
function add_billed_label() { | |
local task_id="${1}" | |
curl -X POST -H "${AUTH_HEADER}" -H "Content-type: application/json" -s \ | |
"${BASE_URL}/${TASK_PATH}/${task_id}/labels" -d '{ "name": "billed" }' >/dev/null | |
} | |
function add_billing_task() { | |
local name="${1}" | |
local description="${2}" | |
local sub_tasks="${3}" | |
json_body=$( | |
echo "{ | |
\"name\": \"${name}\", | |
\"columnId\": \"${NEW_TASK_COLUMN_ID}\", | |
\"swimlaneId\": \"${NEW_TASK_SWIMLANE_ID}\", | |
\"position\": \"top\", | |
\"color\": \"yellow\", | |
\"dates\": [{ | |
\"dueTimestamp\": \"$(date +%FT%TZ)\", | |
\"targetColumnId\": \"${NEW_TASK_TARGET_ID}\" | |
}], | |
\"labels\": [{ \"name\": \"finance\" }], | |
\"description\": ${description}, | |
\"subTasks\": ${sub_tasks} | |
}" | |
) | |
task_id=$( | |
curl -X POST -H "${AUTH_HEADER}" -H "Content-type: application/json" -s \ | |
"${BASE_URL}/${TASK_PATH}" -d "${json_body}" | jq -r '.taskId' | |
) | |
echo "Task created (https://kanbanflow.com/t/${task_id})" | |
} | |
function get_sub_tasks() { | |
local result="${1}" | |
grouped=$(echo "$result" | jq -r 'group_by(.customer) | map({"customer": .[0].customer, timeSpentSeconds: map(.timeSpentSeconds) | add})') | |
declare -a sub_tasks | |
for row in $(echo "${grouped}" | jq -r '.[] | @base64'); do | |
_jq() { | |
echo ${row} | base64 --decode | jq -r "${1}" | |
} | |
local time_spent=$(get_time_spent $(_jq '.timeSpentSeconds')) | |
local customer=$(_jq '.customer') | |
json=" | |
{ | |
\"customer\": \"${customer}\", | |
\"timeSpent\": \"${time_spent}\" | |
} | |
" | |
sub_tasks+=("${json}") | |
done | |
echo ${sub_tasks[@]} | jq -s -a '[.[] | { name: "Bill \(.customer?) (\(.timeSpent))" }]' | |
} | |
function main() { | |
for i in "$@"; do | |
case $i in | |
-m=* | --month=*) | |
MONTH="${i#*=}" | |
shift # past argument=value | |
;; | |
-y=* | --year=*) | |
YEAR="${i#*=}" | |
shift # past argument=value | |
;; | |
--dry-run) | |
DRY_RUN=YES | |
shift # past argument with no value | |
;; | |
*) | |
# unknown option | |
;; | |
esac | |
done | |
MONTH=${MONTH:-$(date +%m)} | |
YEAR=${YEAR:-$(date +%Y)} | |
first=$(first_day ${MONTH} ${YEAR}) | |
last=$(last_day ${MONTH} ${YEAR}) | |
echo "Fetching time entries starting ${first} until ${last}" | |
first_epoch=$(date -d ${first} +%s) | |
last_epoch=$(date -d ${last} +%s) | |
tasks=$(iterate_swimlanes "${last}") | |
# filter tasks having label "billable" and not "billed" | |
billable_tasks=$( | |
echo "${tasks}" | jq --argjson f ${first_epoch} --argjson t ${last_epoch} ' | |
def billable(a): [.labels?[].name?] | contains(["billable"]); | |
def notbilled(a): [.labels?[].name?] | contains(["billed"]) | not; | |
def inrange(a): (.groupingDate | strptime("%Y-%m-%d") | mktime) | . >= $f and . <= $t; | |
[ | |
.[] | select(.labels? and billable(.) and notbilled(.) and inrange(.)) | |
] | |
' | |
) | |
result_json=$(iterate_tasks "${billable_tasks}") | |
description="*Billing entries*" | |
description+=$( | |
echo $result_json | jq -r 'group_by(.customer) | map( | |
"\n- *\(.[0].customer)*\n\(map(. | " - \(.groupingDate) _\(.timeSpent)_\n \(.name?) \n(\(.labels?))") | join("\n"))" | |
) | join("\n")' | |
) | |
escaped_sub_tasks=$(get_sub_tasks "${result_json}") | |
escaped_description=$(echo "$description" | jq -a -R -s '.') | |
if [ -z ${DRY_RUN+x} ]; then | |
add_billing_task "Rechnungen schreiben ${first} bis ${last}" "${escaped_description}" "${escaped_sub_tasks}" | |
else | |
echo "Running in dry-run mode" | |
echo ">> Sanitized result" | |
echo $result_json | jq | |
echo ">> Subtasks" | |
echo $escaped_sub_tasks | jq | |
echo ">> Description" | |
echo $description | |
fi | |
} | |
main "$@" |