Last active
June 29, 2017 19:23
-
-
Save spaced/f5662b605c6d42643e3e692c0b76685a to your computer and use it in GitHub Desktop.
TogglReport
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/local/bin/amm | |
/** | |
* A ammonite script http://www.lihaoyi.com/Ammonite/#Ammonite-Shell | |
* For report toggl datas | |
* HowTo: | |
* - install ammonite: sudo curl -L -o /usr/local/bin/amm https://git.io/vHaMa && sudo chmod +x /usr/local/bin/amm | |
* - update script with Toggl-apikey and | |
* - chmod +x togglReport.sc | |
* - ./togglReport.sc | |
* | |
* Available subcommands: | |
reportYear | |
--year Int (default 2017) | |
reportMonth | |
--month Int (default current month) | |
--year Int (default current year) | |
reportMonthForExcel | |
--month Int (default current month) | |
--year Int (default current year) | |
*/ | |
import ammonite.ops._ | |
import scala.collection.JavaConverters._ | |
import java.time._ | |
import $ivy.`org.scalaj::scalaj-http:2.3.0`, scalaj.http._ | |
import $ivy.`net.sf.biweekly:biweekly:0.6.1`,biweekly._ | |
/** | |
* Settings | |
*/ | |
val TogglApiToken = "YourTogglAPIToken" | |
val WorkspaceId = "YOURWORKSPACEID" | |
val WorkPerWeekInHour = 42 | |
val WorkPercentage = 0.8 | |
val AnnualLeave: List[(LocalDate, LocalDate)] = List( | |
(LocalDate.of(2017,1,24), LocalDate.of(2017,1,24)), | |
(LocalDate.of(2017,4,18), LocalDate.of(2017,4,28)) | |
) | |
val WorkPerWeekInMS = WorkPerWeekInHour * 60 * 60 * 1000 * WorkPercentage | |
val WorkPerDayInMS: Int = (WorkPerWeekInMS / 5).toInt | |
object DateExtension { | |
object Interval { | |
def apply( t : (LocalDate, LocalDate)):Interval = Interval(t._1,t._2) | |
} | |
case class Interval(from: LocalDate, to: LocalDate) { | |
def contains(date: LocalDate): Boolean = from == date || to == date || (from.isBefore(date) && to.isAfter(date)) | |
} | |
} | |
def fetchFreeDays:List[java.time.LocalDate] = { | |
//gesetzliche feiertage stadt zug | |
val resp = Http("https://fcal.ch/privat/fcal_holidays.ics?hl=de&klasse=3&geo=2871").asString | |
val ical = Biweekly.parse(resp.body).first() | |
ical.getEvents.asScala.toList.map(_.getDateStart().getValue.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()) | |
} | |
def fetchTogglEntries(since: LocalDate) :Map[java.time.LocalDate,Int] = { | |
val dateFormatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd") | |
def fetchDatas(aggr: List[upickle.Js.Value],page: Int): List[upickle.Js.Value] = { | |
val resp = Http("https://toggl.com/reports/api/v2/details") | |
.auth(TogglApiToken, "api_token") | |
.param("workspace_id", WorkspaceId) | |
.param("since", since.format(dateFormatter)) | |
.param("user_agent", "api_test") | |
.param("page", page.toString) | |
.asString | |
val parsed = upickle.json.read(resp.body).asInstanceOf[upickle.Js.Obj] | |
val datas:List[upickle.Js.Value] = parsed("data").arr.toList | |
val newDatas:List[upickle.Js.Value] = aggr ++ datas | |
if (parsed("total_count").num.toInt == newDatas.size) | |
newDatas | |
else | |
fetchDatas(newDatas, page +1) | |
} | |
val tupleList: Seq[(String,Int)] = fetchDatas(Nil,1).map( d => (d("start").str,d("dur").num.toInt)) | |
val gPerDay = tupleList | |
.groupBy( t => LocalDate.from(format.DateTimeFormatter.ISO_OFFSET_DATE_TIME.parse(t._1))) | |
.mapValues( _.map(_._2).sum) | |
gPerDay | |
} | |
val oldestOnTop = Ordering.fromLessThan[LocalDate](_ isBefore _) | |
def formatHH(tInMilli: Int):BigDecimal = { | |
val hhUnrounded = BigDecimal(tInMilli)/1000/60/60 | |
hhUnrounded.setScale(2, BigDecimal.RoundingMode.HALF_EVEN) | |
} | |
object Report { | |
case class DayReport(date: LocalDate, expected: Int, worked: Int, totalDiff:Int) { | |
def diff:Int = worked - expected | |
} | |
} | |
val upDownFn = (dr: Report.DayReport) => s"${dr.date}\texpected: ${formatHH(dr.expected)}\tworked: ${formatHH(dr.worked)}\tdiff: ${formatHH(dr.diff)}\taggr: ${formatHH(dr.totalDiff)}" | |
def reportFor(date: LocalDate, dayReportFormatter: Report.DayReport => String = upDownFn): Unit = { | |
val togglEntries = fetchTogglEntries(date) | |
val freeDays = fetchFreeDays | |
val today = LocalDate.now() | |
def calcDay(reports: List[Report.DayReport], date: LocalDate): List[Report.DayReport] = { | |
val expected = date.getDayOfWeek match { | |
case DayOfWeek.SATURDAY => 0 | |
case DayOfWeek.SUNDAY => 0 | |
case _ if freeDays.contains(date) || AnnualLeave.exists( DateExtension.Interval(_).contains(date) ) => 0 | |
case _ => WorkPerDayInMS | |
} | |
val worked = togglEntries.getOrElse(date, 0) | |
val dayReport = Report.DayReport(date,expected, worked, reports.headOption.map(_.totalDiff).getOrElse(0) + worked - expected ) | |
if (date == today) { | |
dayReport :: reports | |
} else { | |
calcDay(dayReport :: reports, date.plusDays(1)) | |
} | |
} | |
calcDay(Nil, date).reverse.foreach{ dr => | |
System.out.println(dayReportFormatter(dr)) | |
} | |
} | |
@main | |
def reportYear(year: Int = LocalDate.now().getYear) = | |
reportFor(LocalDate.of(year, 1, 1)) | |
@main | |
def reportMonth(month: Int = LocalDate.now().getMonthValue, year: Int = LocalDate.now().getYear) = | |
reportFor(LocalDate.of(year, month, 1)) | |
@main | |
def reportMonthForExcel(month: Int = LocalDate.now().getMonthValue, year: Int = LocalDate.now().getYear) = | |
reportFor(LocalDate.of(year, month, 1), (dr: Report.DayReport) => formatHH(dr.worked).toString()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment