Add billing component skeleton.

[?]
Feb 1, 2021, 4:37 AM
ANDJ6GEY2IRDNKPVXESYEZKU24BAXFB5PPSZFIJRMBGL57A622FQC

Dependencies

  • [2] RV7ZIULZ Update overview to have access to the real project detail capability.
  • [3] NAFJ6RB3 Minor module reorg.
  • [4] Z5KNL332 Add skeleton of project overview HTML.
  • [5] PPW6ROC5 Render project data
  • [6] GLQSD33Y Use mock capability for overview init.
  • [7] B4MTB6UO Persist project across pages.
  • [8] QH4UB73N Format with purty.
  • [9] U7YAT2ZK Add error reporting to signup form.
  • [10] QAC2QJ32 Add project overview page to client.
  • [11] 7TQPQW3N Begin adding parsing for project detail.
  • [*] RSF6UAJK Break out api module for timeline.
  • [*] EA5BFM5G Split Login component into its own module.

Change contents

  • file addition: Billing.purs (----------)
    [13.1]
    module Aftok.Api.Billing where
    import Prelude
    import Control.Monad.Except.Trans (ExceptT, runExceptT)
    -- import Control.Monad.Except.Trans (ExceptT, runExceptT, except, withExceptT)
    -- import Control.Monad.Error.Class (throwError)
    -- import Data.Argonaut.Core (Json)
    import Data.Argonaut.Decode (class DecodeJson, decodeJson, (.:))
    import Data.DateTime (DateTime)
    -- import Data.DateTime.Instant (Instant, toDateTime)
    import Data.Either (Either(..), note)
    -- import Data.Foldable (class Foldable, foldr, foldl, foldMapDefaultR)
    -- import Data.Functor.Compose (Compose(..))
    -- import Data.Map as M
    import Data.Maybe (Maybe)
    import Data.Newtype (class Newtype, unwrap)
    -- import Data.Ratio (Ratio, (%))
    -- import Data.Time.Duration (Hours(..), Days(..))
    import Data.Tuple (Tuple(..))
    -- import Data.Traversable (class Traversable, traverse)
    import Data.UUID (UUID, parseUUID)
    -- import Effect (Effect)
    import Effect.Aff (Aff)
    -- import Effect.Class as EC
    -- import Foreign.Object (Object)
    -- import Affjax (get)
    -- import Affjax.ResponseFormat as RF
    import Aftok.Types
    ( ProjectId )
    import Aftok.Api.Types
    (APIError(..))
    -- import Aftok.Api.Json
    -- ( Decode
    -- , decompose
    -- , parseDatedResponse
    -- , parseDatedResponseMay
    -- )
    newtype BillableId
    = BillableId UUID
    derive instance billableIdEq :: Eq BillableId
    derive instance billableIdOrd :: Ord BillableId
    derive instance billableIdNewtype :: Newtype BillableId _
    instance billableIdDecodeJson :: DecodeJson BillableId where
    decodeJson json = do
    uuidStr <- decodeJson json
    BillableId <$> (note "Failed to decode billable UUID" $ parseUUID uuidStr)
    newtype Billable' t = Billable
    {
    }
    type Billable = Billable' DateTime
    newtype PaymentRequestId
    = PaymentRequestId UUID
    derive instance paymentRequestIdEq :: Eq PaymentRequestId
    derive instance paymentRequestIdOrd :: Ord PaymentRequestId
    derive instance paymentRequestIdNewtype :: Newtype PaymentRequestId _
    instance paymentRequestIdDecodeJson :: DecodeJson PaymentRequestId where
    decodeJson json = do
    uuidStr <- decodeJson json
    PaymentRequestId <$> (note "Failed to decode paymentRequest UUID" $ parseUUID uuidStr)
    newtype PaymentRequest' t = PaymentRequest
    {
    }
    type PaymentRequest = PaymentRequest' DateTime
    createBillable :: ProjectId -> Billable -> Aff (Either APIError BillableId)
    createBillable pid bid = pure $ Left Forbidden
    listProjectBillables :: ProjectId -> Aff (Either APIError (Array (Tuple BillableId Billable)))
    listProjectBillables pid = pure $ Left Forbidden
    listUnpaidPaymentRequests :: BillableId -> Aff (Either APIError (Array (Tuple PaymentRequestId PaymentRequest)))
    listUnpaidPaymentRequests billId = pure $ Left Forbidden
  • file addition: Billing.purs (----------)
    [14.1]
    module Aftok.Billing where
    import Prelude
    import Control.Monad.Trans.Class (lift)
    -- import Data.DateTime (DateTime, date)
    import Data.Either (Either(..))
    import Data.Foldable (all)
    import Data.Maybe (Maybe(..), isNothing)
    -- import Data.Unfoldable as U
    import Data.Newtype (unwrap)
    import Data.Symbol (SProxy(..))
    import Data.Traversable (traverse)
    import Data.Tuple (Tuple)
    import Effect.Aff (Aff)
    -- import Effect.Class (liftEffect)
    -- import Effect.Now (nowDateTime)
    import Halogen as H
    import Halogen.HTML.Core (ClassName(..))
    import Halogen.HTML as HH
    import Halogen.HTML.Properties as P
    import Aftok.ProjectList as ProjectList
    import Aftok.Types (System, ProjectId)
    import Aftok.Api.Types (APIError(..))
    import Aftok.Api.Project (Project)
    import Aftok.Api.Billing
    ( BillableId
    , Billable
    , PaymentRequestId
    , PaymentRequest
    , createBillable
    , listProjectBillables
    , listUnpaidPaymentRequests
    )
    type BillingInput
    = Maybe Project
    type BillingState
    = { selectedProject :: Maybe Project
    , billables :: Array (Tuple BillableId Billable)
    , selectedBillable :: Maybe (Tuple BillableId Billable)
    , paymentRequests :: Array (Tuple PaymentRequestId PaymentRequest)
    }
    data BillingAction
    = Initialize
    | ProjectSelected Project
    type Slot id
    = forall query. H.Slot query ProjectList.Event id
    type Slots
    = ( projectList :: ProjectList.Slot Unit
    )
    _projectList = SProxy :: SProxy "projectList"
    type Capability (m :: Type -> Type)
    = { createBillable :: ProjectId -> Billable -> m (Either APIError BillableId)
    , listProjectBillables :: ProjectId -> m (Either APIError (Array (Tuple BillableId Billable)))
    , listUnpaidPaymentRequests :: BillableId -> m (Either APIError (Array (Tuple PaymentRequestId PaymentRequest)))
    }
    component ::
    forall query m.
    Monad m =>
    System m ->
    Capability m ->
    ProjectList.Capability m ->
    H.Component HH.HTML query BillingInput ProjectList.Event m
    component system caps pcaps =
    H.mkComponent
    { initialState
    , render
    , eval:
    H.mkEval
    $ H.defaultEval
    { handleAction = eval
    , initialize = Just Initialize
    }
    }
    where
    initialState :: BillingInput -> BillingState
    initialState input =
    { selectedProject: input
    , billables: []
    , selectedBillable: Nothing
    , paymentRequests: []
    }
    render :: BillingState -> H.ComponentHTML BillingAction Slots m
    render st =
    HH.section
    [ P.classes (ClassName <$> [ "section-border", "border-primary" ]) ]
    [ HH.div
    [ P.classes (ClassName <$> [ "container", "pt-6" ]) ]
    [ HH.h1
    [ P.classes (ClassName <$> [ "mb-0", "font-weight-bold", "text-center" ]) ]
    [ HH.text "Billing" ]
    , HH.p
    [ P.classes (ClassName <$> [ "col-md-5", "text-muted", "text-center", "mx-auto" ]) ]
    [ HH.text "Your project's payment requests &amp; payments" ]
    , HH.div_
    [ HH.slot
    _projectList
    unit
    (ProjectList.component system pcaps)
    st.selectedProject
    (Just <<< (\(ProjectList.ProjectChange p) -> ProjectSelected p))
    ]
    , HH.div
    [ P.classes (ClassName <$> if isNothing st.selectedProject then [ "collapse" ] else []) ]
    [ billingDetail st ]
    ]
    ]
    billingDetail :: BillingState -> H.ComponentHTML BillingAction Slots m
    billingDetail st = do
    HH.div [] []
    eval :: BillingAction -> H.HalogenM BillingState BillingAction Slots ProjectList.Event m Unit
    eval action = do
    case action of
    Initialize -> do
    currentProject <- H.gets (_.selectedProject)
    billables <- lift $ traverse (caps.listProjectBillables <<< (_.projectId) <<< unwrap) currentProject
    case billables of
    Nothing -> pure unit
    Just (Left err) -> lift $ system.error (show err)
    Just (Right b) -> H.modify_ (_ { billables = b })
    ProjectSelected p -> do
    currentProject <- H.gets (_.selectedProject)
    when (all (\p' -> (unwrap p').projectId /= (unwrap p).projectId) currentProject)
    $ do
    H.raise (ProjectList.ProjectChange p)
    H.modify_ (_ { selectedProject = Just p })
    apiCapability :: Capability Aff
    apiCapability =
    { createBillable: createBillable
    , listProjectBillables: listProjectBillables
    , listUnpaidPaymentRequests: listUnpaidPaymentRequests
    }
    mockCapability :: Capability Aff
    mockCapability =
    { createBillable: \_ _ -> pure $ Left Forbidden
    , listProjectBillables: \_ -> pure $ Left Forbidden
    , listUnpaidPaymentRequests: \_ -> pure $ Left Forbidden
    }
  • edit in client/src/Aftok/Overview.purs at line 4
    [3.137][3.137:270]()
    -- import Control.Alt ((<|>))
    -- import Control.Monad.Rec.Class (forever)
    -- import Control.Monad.State (State, put, get, evalState)
  • edit in client/src/Aftok/Overview.purs at line 5
    [3.44][3.313:373](),[3.313][3.313:373](),[3.373][3.698:701](),[3.701][3.377:414](),[3.377][3.377:414](),[3.452][3.452:513]()
    -- import Control.Monad.Maybe.Trans (MaybeT(..), runMaybeT)
    --
    -- import Data.Array (reverse, cons)
    -- import Data.Date.Component (Year(..), Month(..), Day(..))
  • edit in client/src/Aftok/Overview.purs at line 9
    [2.36][3.648:679](),[3.648][3.648:679]()
    -- import Data.Enum (fromEnum)
  • edit in client/src/Aftok/Overview.purs at line 11
    [3.31][3.651:686]()
    -- import Data.Formatters.DateTime
  • edit in client/src/Aftok/Overview.purs at line 18
    [3.159][3.950:1073](),[3.950][3.950:1073]()
    -- import Data.Tuple (Tuple(..))
    -- import Data.Unfoldable as U
    -- -- import Text.Format as F -- (format, zeroFill, width)
  • edit in client/src/Aftok/Overview.purs at line 19
    [3.187][3.1074:1102](),[3.1074][3.1074:1102]()
    -- import Effect.Aff as Aff
  • edit in client/src/Aftok/Overview.purs at line 21
    [3.221][3.1162:1224](),[3.1162][3.1162:1224]()
    -- import Effect.Exception (error)
    -- import Effect.Now (now)
  • edit in client/src/Aftok/Overview.purs at line 23
    [3.1245][3.1245:1346]()
    -- import Halogen.Query.EventSource (EventSource)
    -- import Halogen.Query.EventSource as EventSource
  • edit in client/src/Aftok/Overview.purs at line 25
    [3.1414][3.1414:1483]()
    -- import Halogen.HTML.CSS as CSS
    -- import Halogen.HTML.Events as E
  • edit in client/src/Aftok/Overview.purs at line 26
    [3.1520][3.1520:1722](),[3.1723][3.1723:1758](),[3.1758][3.760:811](),[3.811][3.1811:1829](),[3.1811][3.1811:1829](),[3.1829][3.812:854](),[3.854][3.1873:1892](),[3.1873][3.1873:1892](),[3.1892][3.855:872](),[3.872][3.1910:1984](),[3.1910][3.1910:1984]()
    -- import CSS (backgroundColor, clear, clearBoth, border, rgb, solid, borderRadius, marginLeft)
    -- import CSS.Display (display, flex)
    -- import CSS.Geometry (width, height)
    -- import CSS.Size (px, pct)
    -- import Aftok.Api.Overview as TL
    -- import Aftok.Api.Overview
    -- ( OverviewError,
    -- Event(..),
    -- Interval(..),
    -- TimeInterval,
    -- KeyedEvent,
    -- TimeSpan,
    -- start, end, interval,
    -- event, eventTime, keyedEvent
    -- )