feat: chore creation form with one-off schedule and assignee picker
- Replaced CNewChore placeholder with a full creation form - Form includes: chore name (text input), date (date picker for one-off), assignee (dropdown with Anyone + household members) - CNewChore action shows choresViewWithForm (list + form inline) - CCreateChore processes form data, parses date to ScheduleOneOff, parses assignee to ChoreAssignee, calls createChore - Cancel button (CRefreshChores) returns to list-only view - parseSchedule: reads YYYY-MM-DD date string into ScheduleOneOff - parseAssignee: parses 'anyone' or 'user:<id>' into ChoreAssignee
This commit is contained in:
+98
-4
@@ -7,13 +7,16 @@
|
|||||||
{-# LANGUAGE TypeApplications #-}
|
{-# LANGUAGE TypeApplications #-}
|
||||||
{-# LANGUAGE TypeOperators #-}
|
{-# LANGUAGE TypeOperators #-}
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
{-# OPTIONS_GHC -Wno-unused-imports -Wno-unused-do-bind -Wno-name-shadowing -Wno-redundant-constraints -Wno-redundant-constraints #-}
|
{-# OPTIONS_GHC -Wno-unused-imports -Wno-unused-do-bind -Wno-name-shadowing -Wno-redundant-constraints #-}
|
||||||
|
|
||||||
module Sis.Page.Chores (page) where
|
module Sis.Page.Chores (page) where
|
||||||
|
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Data.Time (getCurrentTime)
|
import Data.Text.Read qualified as TR
|
||||||
|
import Data.Time (Day)
|
||||||
|
import Data.Time qualified as Time
|
||||||
import Effectful
|
import Effectful
|
||||||
|
|
||||||
import Sis.Database
|
import Sis.Database
|
||||||
@@ -21,8 +24,10 @@ import Sis.Route
|
|||||||
import Sis.Style (colorGreen, colorRed, colorYellow, nbBadgeClass, nbBoxClass, nbButtonDefaultClass, nbContainerClass, nbHeadingClass, nbInputClass, nbLabelClass, nbListItemClass)
|
import Sis.Style (colorGreen, colorRed, colorYellow, nbBadgeClass, nbBoxClass, nbButtonDefaultClass, nbContainerClass, nbHeadingClass, nbInputClass, nbLabelClass, nbListItemClass)
|
||||||
import Sis.Types
|
import Sis.Types
|
||||||
import Sis.View.Layout
|
import Sis.View.Layout
|
||||||
|
import Text.Read (readMaybe)
|
||||||
import Web.Hyperbole
|
import Web.Hyperbole
|
||||||
import Web.Hyperbole.Effect.Session
|
import Web.Hyperbole.Effect.Session
|
||||||
|
import Web.Hyperbole.HyperView.Forms
|
||||||
import Web.Hyperbole.Page
|
import Web.Hyperbole.Page
|
||||||
|
|
||||||
data ChoresPage = ChoresPage
|
data ChoresPage = ChoresPage
|
||||||
@@ -34,6 +39,7 @@ instance (DB :> es, IOE :> es) => HyperView ChoresPage es where
|
|||||||
= CRefreshChores
|
= CRefreshChores
|
||||||
| CDeleteChore ChoreId
|
| CDeleteChore ChoreId
|
||||||
| CNewChore
|
| CNewChore
|
||||||
|
| CCreateChore
|
||||||
deriving stock (Generic)
|
deriving stock (Generic)
|
||||||
deriving anyclass (ViewAction)
|
deriving anyclass (ViewAction)
|
||||||
|
|
||||||
@@ -52,8 +58,38 @@ instance (DB :> es, IOE :> es) => HyperView ChoresPage es where
|
|||||||
deleteChore (unChoreId cid)
|
deleteChore (unChoreId cid)
|
||||||
update CRefreshChores
|
update CRefreshChores
|
||||||
update CNewChore = do
|
update CNewChore = do
|
||||||
-- TODO: show chore creation form
|
mUser <- lookupSession @UserSession
|
||||||
pure (el "New chore form coming soon")
|
case mUser of
|
||||||
|
Nothing -> pure (el "Not authenticated")
|
||||||
|
Just us -> do
|
||||||
|
hhs <- getUserHouseholds (UserId (usUserId us))
|
||||||
|
case hhs of
|
||||||
|
[] -> pure $ hyper ChoresPage $ el "No households"
|
||||||
|
(h : _) -> do
|
||||||
|
let hid = unHouseholdId (householdId h)
|
||||||
|
chores' <- getChores hid
|
||||||
|
mems <- getMembers hid
|
||||||
|
pure $ hyper ChoresPage $ choresViewWithForm chores' mems
|
||||||
|
update CCreateChore = do
|
||||||
|
form <- formData @ChoreFormData
|
||||||
|
mUser <- lookupSession @UserSession
|
||||||
|
case mUser of
|
||||||
|
Nothing -> pure (el "Not authenticated")
|
||||||
|
Just us -> do
|
||||||
|
hhs <- getUserHouseholds (UserId (usUserId us))
|
||||||
|
case hhs of
|
||||||
|
[] -> pure $ hyper ChoresPage $ el "No households"
|
||||||
|
(h : _) -> do
|
||||||
|
let hid = unHouseholdId (householdId h)
|
||||||
|
let schedule = parseSchedule (cfdStartDate form)
|
||||||
|
assignee = parseAssignee (cfdAssignee form)
|
||||||
|
_ <- createChore hid (cfdName form) assignee schedule (cfdNotify form)
|
||||||
|
chores' <- getChores hid
|
||||||
|
pure $ hyper ChoresPage $ choresView chores'
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Views
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
choresView :: [Chore] -> View ChoresPage ()
|
choresView :: [Chore] -> View ChoresPage ()
|
||||||
choresView chores' = do
|
choresView chores' = do
|
||||||
@@ -65,6 +101,39 @@ choresView chores' = do
|
|||||||
then el @ att "style" "opacity:0.5" $ text "No chores yet. Create one to get started!"
|
then el @ att "style" "opacity:0.5" $ text "No chores yet. Create one to get started!"
|
||||||
else el @ att "class" nbBoxClass $ mapM_ choreRow chores'
|
else el @ att "class" nbBoxClass $ mapM_ choreRow chores'
|
||||||
|
|
||||||
|
-- | Chores list with the creation form shown
|
||||||
|
choresViewWithForm :: [Chore] -> [Membership] -> View ChoresPage ()
|
||||||
|
choresViewWithForm chores' mems = do
|
||||||
|
el @ att "class" nbContainerClass @ att "style" "max-width:960px;margin:0 auto" $ do
|
||||||
|
el @ att "style" "display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem" $ do
|
||||||
|
el @ att "class" nbHeadingClass $ text "Chores"
|
||||||
|
button CRefreshChores @ att "class" nbButtonDefaultClass $ text "Cancel"
|
||||||
|
-- New chore form
|
||||||
|
el @ att "class" nbBoxClass @ att "style" "padding:1.5rem;margin-bottom:1.5rem" $ do
|
||||||
|
el @ att "class" nbHeadingClass $ text "New Chore"
|
||||||
|
form CCreateChore $ do
|
||||||
|
el @ att "class" nbLabelClass $ text "Chore Name"
|
||||||
|
tag "input" @ att "type" "text" . att "name" "cfdName" . att "class" nbInputClass @ att "style" "width:100%;margin-bottom:1rem" $ none
|
||||||
|
el @ att "class" nbLabelClass $ text "Date (YYYY-MM-DD)"
|
||||||
|
tag "input" @ att "type" "date" . att "name" "cfdStartDate" . att "class" nbInputClass @ att "style" "width:100%;margin-bottom:1rem" $ none
|
||||||
|
tag "input" @ att "type" "hidden" . att "name" "cfdScheduleType" . att "value" "one_off" $ none
|
||||||
|
tag "input" @ att "type" "hidden" . att "name" "cfdPeriod" . att "value" "daily" $ none
|
||||||
|
tag "input" @ att "type" "hidden" . att "name" "cfdNotify" . att "value" "false" $ none
|
||||||
|
el @ att "class" nbLabelClass $ text "Assigned To"
|
||||||
|
tag "select" @ att "name" "cfdAssignee" . att "class" nbInputClass @ att "style" "width:100%;margin-bottom:1rem" $ do
|
||||||
|
tag "option" @ att "value" "anyone" $ text "Anyone"
|
||||||
|
mapM_ memberOption mems
|
||||||
|
submit (text "Create Chore") @ att "class" nbButtonDefaultClass @ att "style" "width:100%"
|
||||||
|
-- Chores list below the form
|
||||||
|
if null chores'
|
||||||
|
then el @ att "style" "opacity:0.5" $ text "No chores yet."
|
||||||
|
else el @ att "class" nbBoxClass $ mapM_ choreRow chores'
|
||||||
|
|
||||||
|
memberOption :: Membership -> View ctx ()
|
||||||
|
memberOption m = do
|
||||||
|
let val = "user:" <> T.pack (show (unUserId (membershipUserId m)))
|
||||||
|
tag "option" @ att "value" val $ text (membershipDisplayName m)
|
||||||
|
|
||||||
choreRow :: Chore -> View ChoresPage ()
|
choreRow :: Chore -> View ChoresPage ()
|
||||||
choreRow c = do
|
choreRow c = do
|
||||||
el @ att "class" nbListItemClass @ att "style" "display:flex;justify-content:space-between;align-items:center;padding:0.5rem" $ do
|
el @ att "class" nbListItemClass @ att "style" "display:flex;justify-content:space-between;align-items:center;padding:0.5rem" $ do
|
||||||
@@ -76,6 +145,10 @@ choreRow c = do
|
|||||||
button CNewChore @ att "class" nbButtonDefaultClass @ att "style" "font-size:0.8rem;padding:0.25rem 0.5rem" $ text "Edit"
|
button CNewChore @ att "class" nbButtonDefaultClass @ att "style" "font-size:0.8rem;padding:0.25rem 0.5rem" $ text "Edit"
|
||||||
button (CDeleteChore (choreId c)) @ att "class" nbButtonDefaultClass @ att "style" "font-size:0.8rem;padding:0.25rem 0.5rem;background:var(--nb-red)" $ text "Delete"
|
button (CDeleteChore (choreId c)) @ att "class" nbButtonDefaultClass @ att "style" "font-size:0.8rem;padding:0.25rem 0.5rem;background:var(--nb-red)" $ text "Delete"
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Helpers
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
scheduleBadge :: Schedule -> Text
|
scheduleBadge :: Schedule -> Text
|
||||||
scheduleBadge ScheduleSometime{} = "sometime"
|
scheduleBadge ScheduleSometime{} = "sometime"
|
||||||
scheduleBadge ScheduleOneOff{} = "one-off"
|
scheduleBadge ScheduleOneOff{} = "one-off"
|
||||||
@@ -89,6 +162,27 @@ scheduleLabel (ScheduleRecurring p _ mt _ _) =
|
|||||||
timePart = maybe "" (" at " <>) mt
|
timePart = maybe "" (" at " <>) mt
|
||||||
in "Recurs " <> pText <> timePart
|
in "Recurs " <> pText <> timePart
|
||||||
|
|
||||||
|
-- | Parse a date string (YYYY-MM-DD) into a ScheduleOneOff. Falls back to today.
|
||||||
|
parseSchedule :: Text -> Schedule
|
||||||
|
parseSchedule t = case readMaybe (T.unpack t) of
|
||||||
|
Just d -> ScheduleOneOff d Nothing
|
||||||
|
Nothing -> ScheduleSometime
|
||||||
|
|
||||||
|
-- | Parse an assignee value ("anyone" or "user:<id>")
|
||||||
|
parseAssignee :: Text -> ChoreAssignee
|
||||||
|
parseAssignee "anyone" = AssigneeAnyone
|
||||||
|
parseAssignee t
|
||||||
|
| "user:" `T.isPrefixOf` t =
|
||||||
|
let uidText = T.drop 5 t
|
||||||
|
in case TR.decimal uidText of
|
||||||
|
Right (uid, _) -> AssigneeUser (UserId uid)
|
||||||
|
Left _ -> AssigneeAnyone
|
||||||
|
| otherwise = AssigneeAnyone
|
||||||
|
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
-- Page
|
||||||
|
----------------------------------------------------------------------
|
||||||
|
|
||||||
page :: (Hyperbole :> es, DB :> es, IOE :> es) => Page es '[ChoresPage]
|
page :: (Hyperbole :> es, DB :> es, IOE :> es) => Page es '[ChoresPage]
|
||||||
page = do
|
page = do
|
||||||
mSession <- lookupSession @UserSession
|
mSession <- lookupSession @UserSession
|
||||||
|
|||||||
Reference in New Issue
Block a user