packages feed

asana-1.0.1.0: library/Asana/Api/Task.hs

module Asana.Api.Task
  ( Task(..)
  , Membership(..)
  , TaskStatusFilter(..)
  , ResourceSubtype(..)
  , PostTask(..)
  , getTask
  , getProjectTasks
  , getProjectTasksCompletedSince
  , postTask
  , addTag
  , putCompleted
  , taskUrl
  , extractNumberField
  , extractEnumField
  ) where

import Asana.Api.Prelude

import Asana.Api.CustomField
import Asana.Api.Gid
import Asana.Api.Named
import Asana.Api.Request
import Asana.Api.Tag
import Data.Aeson
import Data.Aeson.Casing (aesonPrefix, snakeCase)
import Data.HashMap.Strict (HashMap)
import qualified Data.Text as T
import Data.Time (UTCTime, getCurrentTime)
import Data.Time.ISO8601 (formatISO8601)

data Membership = Membership
  { mProject :: Named
  , mSection :: Maybe Named
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON Membership where
  parseJSON = genericParseJSON $ aesonPrefix snakeCase

data ResourceSubtype = DefaultTask | Milestone | Section
  deriving stock (Eq, Generic, Show)

instance FromJSON ResourceSubtype where
  parseJSON =
    genericParseJSON $ defaultOptions { constructorTagModifier = snakeCase }

data Task = Task
  { tAssignee :: Maybe Named
  , tName :: Text
  , tCompleted :: Bool
  , tCompletedAt :: Maybe UTCTime
  , tCreatedAt :: UTCTime
  , tCustomFields :: CustomFields
  , tMemberships :: [Membership]
  , tGid :: Gid
  , tResourceSubtype :: ResourceSubtype
  , tNotes :: Text
  , tProjects :: [AsanaReference]
  , tTags :: [Tag]
  }
  deriving stock (Eq, Generic, Show)

instance FromJSON Task where
  parseJSON = genericParseJSON $ aesonPrefix snakeCase

-- | Return all details for a task by id
getTask
  :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)
  => Gid
  -> m Task
getTask taskId = getSingle $ "/tasks/" <> T.unpack (gidToText taskId)

data PostTask = PostTask
  { ptProjects :: [Gid]
  , ptCustomFields :: HashMap Gid Text
  , ptName :: Text
  , ptNotes :: Text
  , ptParent :: Maybe Gid
  }
  deriving stock Generic

instance FromJSON PostTask where
  parseJSON = genericParseJSON $ aesonPrefix snakeCase

instance ToJSON PostTask where
  toJSON = genericToJSON $ aesonPrefix snakeCase
  toEncoding = genericToEncoding $ aesonPrefix snakeCase

-- | Create a new 'Task'
postTask
  :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)
  => PostTask
  -> m (Result Task)
postTask body = fmap adData . fromJSON <$> post "/tasks" (ApiData body)

-- | Return compact task details for a project
--
-- Iterating ourselves and returning @['Task']@ is a better interface but
-- precludes us logging things each time we request an element. So we return
-- @'Named'@ for now and let the caller use @'getTask'@ themselves.
--
getProjectTasks
  :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)
  => Gid
  -> TaskStatusFilter
  -> m [Named]
getProjectTasks projectId taskStatusFilter = do
  now <- liftIO getCurrentTime
  getAllParams
    (T.unpack $ "/projects/" <> gidToText projectId <> "/tasks")
    (completedSince now)

 where
  completedSince now = case taskStatusFilter of
    AllTasks -> []
    IncompletedTasks -> [("completed_since", formatISO8601 now)]

data TaskStatusFilter = IncompletedTasks | AllTasks

getProjectTasksCompletedSince
  :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)
  => Gid
  -> UTCTime
  -> m [Named]
getProjectTasksCompletedSince projectId since = getAllParams
  (T.unpack $ "/projects/" <> gidToText projectId <> "/tasks")
  [("completed_since", formatISO8601 since)]

addTag
  :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)
  => Gid
  -> Gid -- ^ Tag
  -> m ()
addTag task tag =
  void $ post ("/tasks/" <> T.unpack (gidToText task) <> "/addTag") $ ApiData
    (object ["tag" .= tag])

putCompleted
  :: (MonadUnliftIO m, MonadLogger m, MonadReader env m, HasAsanaAccessKey env)
  => Gid
  -> Bool
  -> m ()
putCompleted taskId completed =
  void $ put ("/tasks/" <> T.unpack (gidToText taskId)) $ ApiData
    (object ["completed" .= completed])

taskUrl :: Task -> Text
taskUrl Task {..} = "https://app.asana.com/0/0/" <> gidToText tGid <> "/f"

extractNumberField :: Text -> Task -> Maybe CustomField
extractNumberField fieldName Task {..} =
  listToMaybe $ flip mapMaybe (getCustomFields tCustomFields) $ \case
    customField@(CustomNumber _ t _) -> customField <$ guard (t == fieldName)
    _ -> Nothing

extractEnumField :: Text -> Task -> Maybe CustomField
extractEnumField fieldName Task {..} =
  listToMaybe $ flip mapMaybe (getCustomFields tCustomFields) $ \case
    customField@(CustomEnum _ t _ _) ->
      if t == fieldName then Just customField else Nothing
    _ -> Nothing