|
|
|
@ -18,7 +18,6 @@ import Data.Time.Calendar
|
|
|
|
|
import System.FilePath (takeFileName, (</>))
|
|
|
|
|
import System.IO.Storage (putValue, getValue)
|
|
|
|
|
import Text.Hamlet hiding (hamletFile)
|
|
|
|
|
import Text.ParserCombinators.Parsec -- hiding (string)
|
|
|
|
|
import Text.Printf
|
|
|
|
|
import Text.RegexPR
|
|
|
|
|
import Yesod.Form
|
|
|
|
@ -56,9 +55,9 @@ getRootR = redirect RedirectTemporary defaultroute where defaultroute = Register
|
|
|
|
|
-- | The main journal view, with accounts sidebar.
|
|
|
|
|
getJournalR :: Handler RepHtml
|
|
|
|
|
getJournalR = do
|
|
|
|
|
vd@VD{opts=opts,fspec=fspec,j=j} <- getViewData
|
|
|
|
|
let sidecontent = balanceReportAsHtml opts vd $ balanceReport opts fspec j
|
|
|
|
|
maincontent = journalReportAsHtml opts vd $ journalReport opts fspec j
|
|
|
|
|
vd@VD{opts=opts,m=m,j=j} <- getViewData
|
|
|
|
|
let sidecontent = balanceReportAsHtml opts vd{q=""} $ balanceReport opts nullfilterspec j
|
|
|
|
|
maincontent = journalReportAsHtml opts vd $ journalReport opts nullfilterspec $ filterJournalTransactions2 m j
|
|
|
|
|
defaultLayout $ do
|
|
|
|
|
setTitle "hledger-web journal"
|
|
|
|
|
addHamlet $(Settings.hamletFile "journal")
|
|
|
|
@ -69,9 +68,9 @@ postJournalR = handlePost
|
|
|
|
|
-- | The main register view, with accounts sidebar.
|
|
|
|
|
getRegisterR :: Handler RepHtml
|
|
|
|
|
getRegisterR = do
|
|
|
|
|
vd@VD{opts=opts,fspec=fspec,m=m,j=j} <- getViewData
|
|
|
|
|
let sidecontent = balanceReportAsHtml opts vd $ balanceReport opts fspec $ filterJournalPostings2 m j
|
|
|
|
|
maincontent = registerReportAsHtml opts vd $ registerReport opts fspec $ filterJournalPostings2 m j
|
|
|
|
|
vd@VD{opts=opts,m=m,j=j} <- getViewData
|
|
|
|
|
let sidecontent = balanceReportAsHtml opts vd{q=""} $ balanceReport opts nullfilterspec j
|
|
|
|
|
maincontent = registerReportAsHtml opts vd $ registerReport opts nullfilterspec $ filterJournalPostings2 m j
|
|
|
|
|
editform' = editform vd
|
|
|
|
|
defaultLayout $ do
|
|
|
|
|
setTitle "hledger-web register"
|
|
|
|
@ -83,10 +82,10 @@ postRegisterR = handlePost
|
|
|
|
|
-- | A simple journal view, like hledger print (with editing.)
|
|
|
|
|
getJournalOnlyR :: Handler RepHtml
|
|
|
|
|
getJournalOnlyR = do
|
|
|
|
|
vd@VD{opts=opts,fspec=fspec,j=j} <- getViewData
|
|
|
|
|
vd@VD{opts=opts,m=m,j=j} <- getViewData
|
|
|
|
|
defaultLayout $ do
|
|
|
|
|
setTitle "hledger-web journal only"
|
|
|
|
|
addHamlet $ journalReportAsHtml opts vd $ journalReport opts fspec j
|
|
|
|
|
addHamlet $ journalReportAsHtml opts vd $ journalReport opts nullfilterspec $ filterJournalTransactions2 m j
|
|
|
|
|
|
|
|
|
|
postJournalOnlyR :: Handler RepPlain
|
|
|
|
|
postJournalOnlyR = handlePost
|
|
|
|
@ -94,63 +93,60 @@ postJournalOnlyR = handlePost
|
|
|
|
|
-- | A simple postings view, like hledger register (with editing.)
|
|
|
|
|
getRegisterOnlyR :: Handler RepHtml
|
|
|
|
|
getRegisterOnlyR = do
|
|
|
|
|
vd@VD{opts=opts,fspec=fspec,j=j} <- getViewData
|
|
|
|
|
vd@VD{opts=opts,m=m,j=j} <- getViewData
|
|
|
|
|
defaultLayout $ do
|
|
|
|
|
setTitle "hledger-web register only"
|
|
|
|
|
addHamlet $ registerReportAsHtml opts vd $ registerReport opts fspec j
|
|
|
|
|
addHamlet $ registerReportAsHtml opts vd $ registerReport opts nullfilterspec $ filterJournalPostings2 m j
|
|
|
|
|
|
|
|
|
|
postRegisterOnlyR :: Handler RepPlain
|
|
|
|
|
postRegisterOnlyR = handlePost
|
|
|
|
|
|
|
|
|
|
-- | A simple accounts view, like hledger balance. If the Accept header
|
|
|
|
|
-- specifies json, returns the chart of accounts as json.
|
|
|
|
|
getAccountsOnlyR :: Handler RepHtmlJson
|
|
|
|
|
getAccountsOnlyR = do
|
|
|
|
|
vd@VD{opts=opts,fspec=fspec,j=j,a=a} <- getViewData
|
|
|
|
|
let accountNames = journalAccountNames j :: [AccountName]
|
|
|
|
|
accountNames' = filter (matchpats [a]) $ accountNames
|
|
|
|
|
json = jsonMap [("accounts", toJSON $ accountNames')]
|
|
|
|
|
getAccountsR :: Handler RepHtmlJson
|
|
|
|
|
getAccountsR = do
|
|
|
|
|
vd@VD{opts=opts,m=m,j=j} <- getViewData
|
|
|
|
|
let j' = filterJournalPostings2 m j
|
|
|
|
|
html = do
|
|
|
|
|
setTitle "hledger-web accounts"
|
|
|
|
|
addHamlet $ balanceReportAsHtml opts vd $ balanceReport opts fspec j
|
|
|
|
|
addHamlet $ balanceReportAsHtml opts vd $ balanceReport opts nullfilterspec j'
|
|
|
|
|
json = jsonMap [("accounts", toJSON $ journalAccountNames j')]
|
|
|
|
|
defaultLayoutJson html json
|
|
|
|
|
|
|
|
|
|
-- | Return the chart of accounts as json, without needing a special Accept header.
|
|
|
|
|
getAccountsJsonR :: Handler RepJson
|
|
|
|
|
getAccountsJsonR = do
|
|
|
|
|
VD{a=a,j=j} <- getViewData
|
|
|
|
|
let accountNames = journalAccountNames j :: [AccountName]
|
|
|
|
|
accountNames' = filter (matchpats [a]) $ accountNames
|
|
|
|
|
jsonToRepJson $ jsonMap [("accounts", toJSON $ accountNames')]
|
|
|
|
|
VD{m=m,j=j} <- getViewData
|
|
|
|
|
let j' = filterJournalPostings2 m j
|
|
|
|
|
jsonToRepJson $ jsonMap [("accounts", toJSON $ journalAccountNames j')]
|
|
|
|
|
|
|
|
|
|
-- helpers
|
|
|
|
|
|
|
|
|
|
-- | Render a balance report as HTML.
|
|
|
|
|
balanceReportAsHtml :: [Opt] -> ViewData -> BalanceReport -> Hamlet AppRoute
|
|
|
|
|
balanceReportAsHtml _ vd@VD{here=here,a=a,p=p} (items,total) = $(Settings.hamletFile "balancereport")
|
|
|
|
|
balanceReportAsHtml _ vd@VD{here=here,q=q} (items,total) = $(Settings.hamletFile "balancereport")
|
|
|
|
|
where
|
|
|
|
|
itemAsHtml :: ViewData -> BalanceReportItem -> Hamlet AppRoute
|
|
|
|
|
itemAsHtml VD{p=p} (acct, adisplay, adepth, abal) = $(Settings.hamletFile "balancereportitem")
|
|
|
|
|
itemAsHtml VD{here=here,q=q} (acct, adisplay, adepth, abal) = $(Settings.hamletFile "balancereportitem")
|
|
|
|
|
where
|
|
|
|
|
indent = preEscapedString $ concat $ replicate (2 * adepth) " "
|
|
|
|
|
acctpat = accountNameToAccountRegex acct
|
|
|
|
|
pparam = if null p then "" else "&p="++p
|
|
|
|
|
accturl = (here, [("q", pack $ "otheracct:" ++ quoteIfSpaced (accountNameToAccountRegex acct))])
|
|
|
|
|
accountsheading = $(Settings.hamletFile "accountsheading")
|
|
|
|
|
where
|
|
|
|
|
filteringaccts = not $ null a
|
|
|
|
|
showlinks = $(Settings.hamletFile "accountsheadinglinks")
|
|
|
|
|
showmore = case (filteringaccts, items) of
|
|
|
|
|
-- cunning parent account logic
|
|
|
|
|
(True, ((acct, _, _, _):_)) ->
|
|
|
|
|
let a' = if isAccountRegex a then a else acct
|
|
|
|
|
a'' = accountNameToAccountRegex $ parentAccountName $ accountRegexToAccountName a'
|
|
|
|
|
parenturl = (here, [("a",pack a''), ("p",pack p)])
|
|
|
|
|
in $(Settings.hamletFile "accountsheadinglinksmore")
|
|
|
|
|
_ -> nulltemplate
|
|
|
|
|
showall = if filteringaccts
|
|
|
|
|
then $(Settings.hamletFile "accountsheadinglinksall")
|
|
|
|
|
else nulltemplate
|
|
|
|
|
where allurl = (here, [("p",pack p)])
|
|
|
|
|
filtering = not $ null q
|
|
|
|
|
-- showlinks = $(Settings.hamletFile "accountsheadinglinks")
|
|
|
|
|
-- showmore = case (filteringaccts, items) of
|
|
|
|
|
-- -- cunning parent account logic
|
|
|
|
|
-- (True, ((acct, _, _, _):_)) ->
|
|
|
|
|
-- let a' = if isAccountRegex a then a else acct
|
|
|
|
|
-- a'' = accountNameToAccountRegex $ parentAccountName $ accountRegexToAccountName a'
|
|
|
|
|
-- parenturl = (here, [("a",pack a''), ("p",pack p)])
|
|
|
|
|
-- in $(Settings.hamletFile "accountsheadinglinksmore")
|
|
|
|
|
-- _ -> nulltemplate
|
|
|
|
|
-- showall = if filteringaccts
|
|
|
|
|
-- then $(Settings.hamletFile "accountsheadinglinksall")
|
|
|
|
|
-- else nulltemplate
|
|
|
|
|
-- where allurl = (here, [])
|
|
|
|
|
|
|
|
|
|
-- | Render a journal report as HTML.
|
|
|
|
|
journalReportAsHtml :: [Opt] -> ViewData -> JournalReport -> Hamlet AppRoute
|
|
|
|
@ -167,14 +163,13 @@ registerReportAsHtml :: [Opt] -> ViewData -> RegisterReport -> Hamlet AppRoute
|
|
|
|
|
registerReportAsHtml _ vd items = $(Settings.hamletFile "registerreport")
|
|
|
|
|
where
|
|
|
|
|
itemAsHtml :: ViewData -> (Int, RegisterReportItem) -> Hamlet AppRoute
|
|
|
|
|
itemAsHtml VD{here=here,p=p} (n, (ds, posting, b)) = $(Settings.hamletFile "registerreportitem")
|
|
|
|
|
itemAsHtml VD{here=here} (n, (ds, posting, b)) = $(Settings.hamletFile "registerreportitem")
|
|
|
|
|
where
|
|
|
|
|
evenodd = if even n then "even" else "odd" :: String
|
|
|
|
|
(firstposting, date, desc) = case ds of Just (da, de) -> ("firstposting", show da, de)
|
|
|
|
|
Nothing -> ("", "", "") :: (String,String,String)
|
|
|
|
|
acct = paccount posting
|
|
|
|
|
acctpat = accountNameToAccountRegex acct
|
|
|
|
|
pparam = if null p then "" else "&p="++p
|
|
|
|
|
accturl = (here, [("q", pack $ "otheracct:" ++ quoteIfSpaced (accountNameToAccountRegex acct))])
|
|
|
|
|
|
|
|
|
|
mixedAmountAsHtml b = preEscapedString $ addclass $ intercalate "<br>" $ lines $ show b
|
|
|
|
|
where addclass = printf "<span class=\"%s\">%s</span>" (c :: String)
|
|
|
|
@ -318,20 +313,9 @@ handleImport = do
|
|
|
|
|
|
|
|
|
|
-- | Global toolbar/heading area.
|
|
|
|
|
topbar :: ViewData -> Hamlet AppRoute
|
|
|
|
|
topbar VD{p=p,j=j,msg=msg,today=today} = $(Settings.hamletFile "topbar")
|
|
|
|
|
topbar VD{j=j,msg=msg,today=today} = $(Settings.hamletFile "topbar")
|
|
|
|
|
where
|
|
|
|
|
(title, desc) = journalTitleDesc j p today
|
|
|
|
|
|
|
|
|
|
-- | Generate a title and description for the given journal, period
|
|
|
|
|
-- expression, and date.
|
|
|
|
|
journalTitleDesc :: Journal -> String -> Day -> (String, String)
|
|
|
|
|
journalTitleDesc j p today = (title, desc)
|
|
|
|
|
where
|
|
|
|
|
title = printf "%s" (takeFileName $ journalFilePath j) :: String
|
|
|
|
|
desc = printf "%s" (showspan span) :: String
|
|
|
|
|
span = either (const $ DateSpan Nothing Nothing) snd (parsePeriodExpr today p)
|
|
|
|
|
showspan (DateSpan Nothing Nothing) = ""
|
|
|
|
|
showspan s = " (" ++ dateSpanAsText s ++ ")"
|
|
|
|
|
title = takeFileName $ journalFilePath j
|
|
|
|
|
|
|
|
|
|
-- | Links to navigate between the main views.
|
|
|
|
|
navlinks :: ViewData -> Hamlet AppRoute
|
|
|
|
@ -340,9 +324,8 @@ navlinks vd = $(Settings.hamletFile "navlinks")
|
|
|
|
|
accountsjournallink = navlink vd "transactions" JournalR
|
|
|
|
|
accountsregisterlink = navlink vd "postings" RegisterR
|
|
|
|
|
navlink :: ViewData -> String -> AppRoute -> Hamlet AppRoute
|
|
|
|
|
navlink VD{here=here,a=a,p=p} s dest = $(Settings.hamletFile "navlink")
|
|
|
|
|
where u = (dest, concat [(if null a then [] else [("a", pack a)])
|
|
|
|
|
,(if null p then [] else [("p", pack p)])])
|
|
|
|
|
navlink VD{here=here,q=q} s dest = $(Settings.hamletFile "navlink")
|
|
|
|
|
where u = (dest, if null q then [] else [("q", pack q)])
|
|
|
|
|
style | dest == here = "navlinkcurrent"
|
|
|
|
|
| otherwise = "navlink" :: Text
|
|
|
|
|
|
|
|
|
@ -357,10 +340,10 @@ helplink topic label = $(Settings.hamletFile "helplink")
|
|
|
|
|
|
|
|
|
|
-- | Form controlling journal filtering parameters.
|
|
|
|
|
filterform :: ViewData -> Hamlet AppRoute
|
|
|
|
|
filterform VD{here=here,a=a,p=p,q=q} = $(Settings.hamletFile "filterform")
|
|
|
|
|
filterform VD{here=here,q=q} = $(Settings.hamletFile "filterform")
|
|
|
|
|
where
|
|
|
|
|
ahelp = helplink "filter-patterns" "?"
|
|
|
|
|
phelp = helplink "period-expressions" "?"
|
|
|
|
|
-- ahelp = helplink "filter-patterns" "?"
|
|
|
|
|
-- phelp = helplink "period-expressions" "?"
|
|
|
|
|
filtering = not $ null q
|
|
|
|
|
visible = "block" :: String
|
|
|
|
|
filteringclass = if filtering then "filtering" else "" :: String
|
|
|
|
@ -413,17 +396,14 @@ journalselect journalfiles = $(Settings.hamletFile "journalselect")
|
|
|
|
|
-- utilities
|
|
|
|
|
|
|
|
|
|
nulltemplate :: Hamlet AppRoute
|
|
|
|
|
nulltemplate = [$hamlet||]
|
|
|
|
|
nulltemplate = [hamlet||]
|
|
|
|
|
|
|
|
|
|
-- | A bundle of data useful for handlers and their templates.
|
|
|
|
|
data ViewData = VD {
|
|
|
|
|
opts :: [Opt] -- ^ command-line options at startup
|
|
|
|
|
,a :: String -- ^ current a (query) parameter
|
|
|
|
|
,p :: String -- ^ current p (query) parameter
|
|
|
|
|
,q :: String -- ^ current q (query) parameter
|
|
|
|
|
,fspec :: FilterSpec -- ^ a journal filter specification based on the above
|
|
|
|
|
,m :: Matcher -- ^ a search/filter expression based on the above
|
|
|
|
|
,j :: Journal -- ^ an up-to-date parsed journal
|
|
|
|
|
,j :: Journal -- ^ the up-to-date parsed unfiltered journal
|
|
|
|
|
,today :: Day -- ^ the current day
|
|
|
|
|
,here :: AppRoute -- ^ the current route
|
|
|
|
|
,msg :: Maybe Html -- ^ the current UI message if any, possibly from the current request
|
|
|
|
@ -432,10 +412,7 @@ data ViewData = VD {
|
|
|
|
|
mkvd :: ViewData
|
|
|
|
|
mkvd = VD {
|
|
|
|
|
opts = []
|
|
|
|
|
,a = ""
|
|
|
|
|
,p = ""
|
|
|
|
|
,q = ""
|
|
|
|
|
,fspec = nullfilterspec
|
|
|
|
|
,m = MatchOr []
|
|
|
|
|
,j = nulljournal
|
|
|
|
|
,today = ModifiedJulianDay 0
|
|
|
|
@ -446,25 +423,16 @@ mkvd = VD {
|
|
|
|
|
-- | Gather data useful for a hledger-web request handler and its templates.
|
|
|
|
|
getViewData :: Handler ViewData
|
|
|
|
|
getViewData = do
|
|
|
|
|
Just here' <- getCurrentRoute
|
|
|
|
|
(q, opts, fspec, m) <- getCurrentParameters
|
|
|
|
|
(j, err) <- getCurrentJournal opts
|
|
|
|
|
msg <- getMessageOr err
|
|
|
|
|
today <- liftIO getCurrentDay
|
|
|
|
|
return mkvd{opts=opts, q=q, fspec=fspec, m=m, j=j, today=today, here=here', msg=msg}
|
|
|
|
|
app <- getYesod
|
|
|
|
|
let opts = appOpts app
|
|
|
|
|
(j, err) <- getCurrentJournal opts
|
|
|
|
|
msg <- getMessageOr err
|
|
|
|
|
Just here' <- getCurrentRoute
|
|
|
|
|
today <- liftIO getCurrentDay
|
|
|
|
|
q <- getParameter "q"
|
|
|
|
|
let m = parseMatcher today q
|
|
|
|
|
return mkvd{opts=opts, q=q, m=m, j=j, today=today, here=here', msg=msg}
|
|
|
|
|
where
|
|
|
|
|
-- | Get current report parameters for this request.
|
|
|
|
|
getCurrentParameters :: Handler (String, [Opt], FilterSpec, Matcher)
|
|
|
|
|
getCurrentParameters = do
|
|
|
|
|
app <- getYesod
|
|
|
|
|
t <- liftIO $ getCurrentLocalTime
|
|
|
|
|
q <- unpack `fmap` fromMaybe "" <$> lookupGetParam "q"
|
|
|
|
|
let opts = appOpts app -- ++ [Period p']
|
|
|
|
|
args = appArgs app -- ++ words' a'
|
|
|
|
|
fspec = optsToFilterSpec opts args t
|
|
|
|
|
m = parseMatcher q
|
|
|
|
|
return (q, opts, fspec, m)
|
|
|
|
|
|
|
|
|
|
-- | Update our copy of the journal if the file changed. If there is an
|
|
|
|
|
-- error while reloading, keep the old one and return the error, and set a
|
|
|
|
|
-- ui message.
|
|
|
|
@ -480,18 +448,9 @@ getViewData = do
|
|
|
|
|
Left e -> do setMessage $ "error while reading" {- ++ ": " ++ e-}
|
|
|
|
|
return (j, Just e)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
parseMatcher :: String -> Matcher
|
|
|
|
|
parseMatcher s = MatchOr $ map (MatchAcct True) $ words' s
|
|
|
|
|
|
|
|
|
|
parseMatcher2 :: String -> Matcher
|
|
|
|
|
parseMatcher2 s = either (const (MatchOr [])) id $ runParser matcher () "" $ lexmatcher s
|
|
|
|
|
|
|
|
|
|
lexmatcher :: String -> [String]
|
|
|
|
|
lexmatcher s = words' s
|
|
|
|
|
|
|
|
|
|
matcher :: GenParser String () Matcher
|
|
|
|
|
matcher = undefined
|
|
|
|
|
-- | Get the named request parameter.
|
|
|
|
|
getParameter :: String -> Handler String
|
|
|
|
|
getParameter p = unpack `fmap` fromMaybe "" <$> lookupGetParam (pack p)
|
|
|
|
|
|
|
|
|
|
-- | Get the message set by the last request, or the newer message provided, if any.
|
|
|
|
|
getMessageOr :: Maybe String -> Handler (Maybe Html)
|
|
|
|
|