diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..6d50ab2 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +export HEROKU_APP=agilogy-time-tracking diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31d0c88..c9130b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -27,7 +27,7 @@ tasks.withType { }) } application { - mainClass.set("com.agilogy.timetracking.app.ConsoleAppKt") + mainClass.set("com.agilogy.timetracking.app.AppKt") } dependencies { @@ -36,7 +36,6 @@ dependencies { implementation(ktorServerNetty) implementation(project(":db")) implementation(project(":domain")) - implementation(project(":console")) implementation(project(":postgresdb")) implementation(project(":httpapi")) implementation(suspendApp) diff --git a/components/console/build.gradle.kts b/components/console/build.gradle.kts deleted file mode 100644 index 4a2c3cb..0000000 --- a/components/console/build.gradle.kts +++ /dev/null @@ -1,3 +0,0 @@ -dependencies { - implementation(project(":domain")) -} diff --git a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/ArgsParser.kt b/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/ArgsParser.kt deleted file mode 100644 index 34415ab..0000000 --- a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/ArgsParser.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.agilogy.timetracking.driveradapters.console - -import arrow.core.raise.Raise -import com.agilogy.timetracking.domain.DeveloperName -import com.agilogy.timetracking.domain.ProjectName -import java.time.Instant -import java.time.YearMonth -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import java.util.* - -data class ArgsParseError(val message: String) - -object ArgsParser { - - context(Raise) - fun parse(args: List): Command { - fun arg(index: Int): String = if (index < args.size) args[index] else "" - - return if (args.isEmpty()) Help - else if (arg(0) == "report") when (args.size - 1) { - 0 -> GlobalReport(YearMonth.now()) - 1 -> GlobalReport(parseMonth(arg(1))) - 2 -> DeveloperReport(parseMonth(arg(1)), DeveloperName(arg(2))) - else -> raise(ArgsParseError("Invalid number of arguments for command ${arg(0)}")) - } else if (arg(0) == "list") { - ListTimeEntries(parseMonth(arg(1)), args.getOrElse(2) { null }?.let { DeveloperName(it) }) - } else if (arg(0) == "add") { - val zoneId = parseZoneId(arg(5)) - AddTimeEntry( - DeveloperName(arg(1)), - ProjectName(arg(2)), - parseInstant(arg(3), zoneId)..parseInstant(arg(4), zoneId), - zoneId, - ) - } else raise(ArgsParseError("Unknown command ${arg(0)}")) - } - - context(Raise) - private fun parseMonth(value: String): YearMonth = parse("month", value) { YearMonth.parse(it) } - - context(Raise) - private fun parseInstant(value: String, zoneId: ZoneId): Instant = parse("instant", value) { - val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm", Locale.ENGLISH).withZone(zoneId) - ZonedDateTime.parse(value, formatter).toInstant() - } - - context(Raise) - private fun parseZoneId(value: String): ZoneId = parse("zoneid", value) { ZoneId.of(it) } - - context(Raise) - private fun parse(type: String, value: String, parse: (String) -> A): A = - runCatching { parse(value) }.getOrElse { raise(ArgsParseError("Invalid $type $value")) } - - fun help(): String = - """ - Usage: timetracking [options] - - Commands: - add Adds a new time entry - developer: developer name - project: project name - start: start time in the format yyyy-MM-dd HH:mm - end: end time in the format yyyy-MM-dd HH:mm or just HH:mm - zoneId: Timezone identifier like Europe/Monaco - - Example: - add pepe teto "2023-01-10 08:00" "2023-01-10 17:00" Europe/Madrid - - list [] Show the global time tracking report for the given month - month: month in the format yyyy-MM, defaults to current month - report Show the time tracking report for the given developer and month - month: month in the format yyyy-MM - developer: developer name - - """.trimIndent() -} diff --git a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/Command.kt b/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/Command.kt deleted file mode 100644 index f7b0ab5..0000000 --- a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/Command.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.agilogy.timetracking.driveradapters.console - -import com.agilogy.timetracking.domain.DeveloperName -import com.agilogy.timetracking.domain.ProjectName -import java.time.Instant -import java.time.YearMonth -import java.time.ZoneId - -sealed interface Command - -object Help : Command -data class GlobalReport(val yearMonth: YearMonth) : Command -data class DeveloperReport(val yearMonth: YearMonth, val developer: DeveloperName) : Command -data class ListTimeEntries(val yearMonth: YearMonth, val developer: DeveloperName?) : Command -data class AddTimeEntry( - val developer: DeveloperName, - val project: ProjectName, - val range: ClosedRange, - val zoneId: ZoneId, -) : Command diff --git a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/Console.kt b/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/Console.kt deleted file mode 100644 index 0918eed..0000000 --- a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/Console.kt +++ /dev/null @@ -1,70 +0,0 @@ -package com.agilogy.timetracking.driveradapters.console - -import arrow.core.Tuple5 -import com.agilogy.timetracking.domain.DeveloperName -import com.agilogy.timetracking.domain.Hours -import com.agilogy.timetracking.domain.ProjectName -import java.time.LocalDate -import java.time.LocalTime -import java.time.ZoneId -import kotlin.math.max - -class Console { - - fun printHelp(message: String): Unit = println(message) - - fun print(report: Map, Hours>) { - val table = table( - report.map { (dp, hours) -> listOf(dp.first.name, dp.second.name, hours.value.toString()) }, - "Developer", - "Project", - "Hours", - ) - println(table) - } - - fun print(report: List>) { - val table = table( - report.map { (date, project, hours) -> listOf(date.toString(), project.name, hours.value.toString()) }, - "Date", - "Project", - "Hours", - ) - println(table) - } - - fun table(data: List>, vararg columns: String): String { - val columnLengths = columns.mapIndexed { i, header -> - max(data.maxOfOrNull { it[i].length } ?: 0, header.length) - } - val separators = columnLengths.map { "-" * it } - return listOf(columns.toList(), separators, * data.toTypedArray()).joinToString(separator = "\n") { row -> - row.zip(columnLengths).joinToString(" ") { (value, length) -> value.padEnd(length) } - } - } - - private operator fun String.times(n: Int): String = repeat(n) - - fun printTimeEntries( - listTimeEntries: List, ZoneId>>, - ): Unit = println( - table( - listTimeEntries.map { (developer, project, date, range, zoneId) -> - listOf( - developer.name, - project.name, - date.toString(), - range.start.toString(), - range.endInclusive.toString(), - zoneId.id, - ) - }, - "Developer", - "Project", - "Date", - "Start", - "End", - "ZoneId", - ), - ) -} diff --git a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/ConsoleAdapter.kt b/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/ConsoleAdapter.kt deleted file mode 100644 index 1f37c5b..0000000 --- a/components/console/src/main/kotlin/com/agilogy/timetracking/driveradapters/console/ConsoleAdapter.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.agilogy.timetracking.driveradapters.console - -import arrow.core.raise.effect -import arrow.core.raise.fold -import com.agilogy.timetracking.domain.TimeTrackingApp -import com.agilogy.timetracking.domain.toInstantRange -import com.agilogy.timetracking.domain.toLocalDateRange -import java.util.regex.Pattern - -class ConsoleAdapter( - private val timeTrackingApp: TimeTrackingApp, - private val console: Console = Console(), -) { - - suspend fun mainLoop() { - print("timeTrackingApp >> ") - var args = readln().splitPreservingQuotedStrings() - - while (args != listOf("exit")) { - effect { runCommand(ArgsParser.parse(args)) }.fold( - { println(it.message); runCommand(Help) }, - { println("Command executed successfully") }, - ) - print("timeTrackingApp >> ") - args = readln().splitPreservingQuotedStrings() - } - } - - private fun String.splitPreservingQuotedStrings(): List { - val words = ArrayList() - val pattern = Pattern.compile("\"([^\"]*)\"|(\\S+)") - - val matcher = pattern.matcher(this) - while (matcher.find()) { - if (matcher.group(1) != null) { - // Quoted string found, add it as a whole - words.add(matcher.group(1)) - } else { - // Non-quoted word found, split it using whitespaces - val splitWords = matcher.group(2).split("\\s+").toTypedArray() - for (splitWord in splitWords) { - if (splitWord.isNotEmpty()) { - words.add(splitWord) - } - } - } - } - - return words - } - private suspend fun runCommand(cmd: Command) { - when (cmd) { - is GlobalReport -> { - val report = timeTrackingApp.getDeveloperHours(cmd.yearMonth.toInstantRange()) - console.print(report) - } - - is DeveloperReport -> { - val report = - timeTrackingApp.getDeveloperHoursByProjectAndDate(cmd.developer, cmd.yearMonth.toLocalDateRange()) - console.print(report) - } - - Help -> console.printHelp(ArgsParser.help()) - is ListTimeEntries -> - console.printTimeEntries( - timeTrackingApp.listTimeEntries(cmd.yearMonth.toLocalDateRange(), cmd.developer), - ) - - is AddTimeEntry -> - timeTrackingApp.saveTimeEntries(cmd.developer, listOf(Triple(cmd.project, cmd.range, cmd.zoneId))) - } - } -} diff --git a/docs/Requirements.md b/docs/Requirements.md index 362ab6a..66279c6 100644 --- a/docs/Requirements.md +++ b/docs/Requirements.md @@ -30,7 +30,7 @@ - [x] The application is deployed in Heroku - [x] The application is usable via an HTTP API in Json -- [ ] Remove the example console application +- [x] Remove the example console application - [ ] The application runs the migrations on each deployment / start up ### Inform time entries