diff --git a/modules/common/src/main/scala/docspell/common/Timestamp.scala b/modules/common/src/main/scala/docspell/common/Timestamp.scala index bac4bb8f..1f970d41 100644 --- a/modules/common/src/main/scala/docspell/common/Timestamp.scala +++ b/modules/common/src/main/scala/docspell/common/Timestamp.scala @@ -4,6 +4,7 @@ import java.time.{Instant, LocalDate, ZoneId} import cats.effect.Sync import io.circe.{Decoder, Encoder} +import java.time.temporal.ChronoUnit import java.time.LocalDateTime import java.time.ZonedDateTime @@ -59,6 +60,9 @@ object Timestamp { def atUtc(ldt: LocalDateTime): Timestamp = from(ldt.atZone(UTC)) + def daysBetween(ts0: Timestamp, ts1: Timestamp): Long = + ChronoUnit.DAYS.between(ts0.toUtcDate, ts1.toUtcDate) + implicit val encodeTimestamp: Encoder[Timestamp] = BaseJsonCodecs.encodeInstantEpoch.contramap(_.value) diff --git a/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala b/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala index d00d07a2..de50600e 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/MailContext.scala @@ -11,6 +11,7 @@ case class MailContext( items: List[MailContext.ItemData], more: Boolean, account: AccountId, + username: String, itemUri: Option[LenientUri] ) @@ -20,12 +21,14 @@ object MailContext { items: Vector[QItem.ListItem], max: Int, account: AccountId, - itemBaseUri: Option[LenientUri] + itemBaseUri: Option[LenientUri], + now: Timestamp ): MailContext = MailContext( - items.take(max - 1).map(ItemData.apply).toList.sortBy(_.dueDate), + items.take(max - 1).map(ItemData(now)).toList.sortBy(_.dueDate), items.sizeCompare(max) >= 0, account, + account.user.id.capitalize, itemBaseUri ) @@ -34,13 +37,22 @@ object MailContext { name: String, date: Timestamp, dueDate: Option[Timestamp], - source: String + source: String, + overDue: Boolean, + dueIn: Option[String] ) object ItemData { - def apply(i: QItem.ListItem): ItemData = - ItemData(i.id, i.name, i.date, i.dueDate, i.source) + def apply(now: Timestamp)(i: QItem.ListItem): ItemData = { + val dueIn = i.dueDate.map(dt => Timestamp.daysBetween(now, dt)) + val dueInLabel = dueIn.map { + case 0 => "**today**" + case 1 => "**tomorrow**" + case n => s"in $n days" + } + ItemData(i.id, i.name, i.date, i.dueDate, i.source, dueIn.exists(_ < 0), dueInLabel) + } implicit def yamusca: ValueConverter[ItemData] = ValueConverter.deriveConverter[ItemData] diff --git a/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala b/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala index 691867c0..ed167bec 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/MailTemplate.scala @@ -5,24 +5,26 @@ import yamusca.implicits._ object MailTemplate { val text = mustache""" -## Hello {{{ account.user }}}, +## Hello {{{ username }}}, -this is Docspell informing you about due items coming up. +this is Docspell informing you about your next due items coming up. {{#itemUri}} {{#items}} -- [{{name}}]({{itemUri}}/{{id}}), due on *{{dueDate}}* +- {{#overDue}}**(OVERDUE)** {{/overDue}}[{{name}}]({{itemUri}}/{{id}}), + due {{dueIn}} on *{{dueDate}}* (received on {{date}} via {{source}}) {{/items}} {{/itemUri}} {{^itemUri}} {{#items}} -- *{{name}}*, due on *{{dueDate}}* +- {{#overDue}}**(OVERDUE)** {{/overDue}}*{{name}}*, + due {{dueIn}} on *{{dueDate}}* (received on {{date}} via {{source}}) {{/items}} {{/itemUri}} {{#more}} -- (There are more due items, left out for brevity) +- … more have been left out for brevity {{/more}} diff --git a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala index 9b493654..bca4a017 100644 --- a/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala +++ b/modules/joex/src/main/scala/docspell/joex/notify/NotifyDueItemsTask.scala @@ -16,7 +16,7 @@ import docspell.joex.notify.MailContext import docspell.joex.notify.MailTemplate object NotifyDueItemsTask { - val maxItems: Long = 5 + val maxItems: Long = 7 type Args = NotifyDueItemsArgs def apply[F[_]: Sync](emil: Emil[F]): Task[F, Args, Unit] = @@ -83,21 +83,23 @@ object NotifyDueItemsTask { cfg: RUserEmail, args: Args, items: Vector[QItem.ListItem] - ): F[Mail[F]] = Sync[F].delay { - val templateCtx = - MailContext.from(items, maxItems.toInt, args.account, args.itemDetailUrl) - val md = MailTemplate.render(templateCtx) - val recp = args.recipients - .map(MailAddress.parse) - .map { - case Right(ma) => ma - case Left(err) => throw new Exception(s"Unable to parse recipient address: $err") - } - MailBuilder.build( - From(cfg.mailFrom), - Tos(recp), - Subject("Next due items"), - MarkdownBody[F](md) - ) - } + ): F[Mail[F]] = + Timestamp.current[F].map { now => + val templateCtx = + MailContext.from(items, maxItems.toInt, args.account, args.itemDetailUrl, now) + val md = MailTemplate.render(templateCtx) + val recp = args.recipients + .map(MailAddress.parse) + .map { + case Right(ma) => ma + case Left(err) => + throw new Exception(s"Unable to parse recipient address: $err") + } + MailBuilder.build( + From(cfg.mailFrom), + Tos(recp), + Subject("Next due items"), + MarkdownBody[F](md) + ) + } }