hax-0.0.2: src/HAX/Report.hs
{-# LANGUAGE FlexibleInstances
, OverloadedStrings
#-}
-- | This module allows to convert the final ledgers generated by
-- "Accounting" into strings for display.
--
-- This module is ditry and work in progress. __Look at the source to use it__
module HAX.Report where
import HAX.Accounting
import HAX.Bookkeeping
import HAX.Common hiding ((!))
import Data.Aeson hiding (Array)
import qualified Data.ByteString.Lazy as BL
import Data.List
import Data.List.Split
import Data.Text (Text)
import qualified Data.Map as M
import Data.Maybe
import Data.Ord
import System.Directory
import System.FilePath
import Text.Blaze hiding (text)
import Text.Blaze.Html.Renderer.Utf8
import qualified Text.Blaze.Html5 as H
import Text.Blaze.Html5 hiding (head,map,object,text)
import qualified Text.Blaze.Html5.Attributes as A
import Text.Blaze.Html5.Attributes hiding (id,title)
import qualified Text.PrettyPrint.Boxes as P
import Text.PrettyPrint.Boxes hiding (left,(<>))
-- import Network.Wai
-- import Network.HTTP.Types.Status
-- import Network.Wai.Handler.Warp
-- * HTML
-- | Write the ledger to html files
writeHtml :: FilePath -- ^ output dir
-> World -> IO ()
writeHtml dir world = do
-- copyFile "code.js" $ dir </> "code.js"
createDirectoryIfMissing True dir
res <- generate world
let resHtml = toHtmlTable res
BL.writeFile (dir </> "header.html") $ indexPage $ resHtml
BL.writeFile (dir </> "index.html") $ indexPage $ do
iframe ! src "header.html" ! width "100%" !
customAttribute "scrolling" "no" ! A.style "position:fixed; border: 0px" ! height "37px" $ ""
resHtml
BL.writeFile (dir </> "data.js") . toVar "data" . encode $ res
-- | convert ledger to Html Markup
toHtmlTable (FullLedger (FixedLedger balances logEntries) accounts) = do
table ! A.id "data" $ do
-- header
tr $ mapM_ headerColumn headerFANs
sequence_ $ zipWith f (elems logEntries) $ to2DTable balances
where headerColumn (FAN entity acc) = th ! class_ (fromString entity)
$ toMarkup entity >> br >> toMarkup acc
f logs (d,row) = do
mapM_ (logHtml headerFANs d $ length row) $ reverse logs
tr ! (monthClass "regular" d) $ rowHtml headerFANs show "" (d,row)
headerFANs = FAN "" "Month" : sortedAccountNames accounts ++ [FAN "" "Comment"]
monthClass cl d = class_ $ fromString $ cl ++" "++ if getMonth d == 12 then "endOfYear" else "duringYear"
-- | create one html row with comment and date
rowHtml headerFANs f comment drow = zipWithM_ g headerFANs $ toRow show (f.scale) drow ++ [comment]
where g (FAN entity _) x = td ! class_ (fromString entity) $ toMarkup x
-- | convert log entries to markup
logHtml :: [FullAccountName] -> ADate -> Int -- ^ number of columns present
-> EntityLogEntry -> Html
logHtml _ d len (ent,LComment s) = tr ! (monthClass ("comment " ++ ent) d) $ do
td $ toMarkup $ show d
td ! colspan (toValue $ len + 1) $ fromString s
logHtml headerFANs d len (ent,LTx tx) = tr ! (monthClass ("transaction " ++ ent) d) $
rowHtml headerFANs
(\x -> if x == 0 then "" else show x) comment (d,row)
where (row,comment) = transactionRow (1,len) tx
css x = link ! rel "stylesheet" ! type_ "text/css" ! href x
js x = script ! src x $ ""
-- | the html skeletton
indexPage htmlBody = renderHtml $ docTypeHtml $ do
H.head $ do
meta ! charset "UTF-8"
js "http://code.jquery.com/jquery-1.11.1.min.js"
-- js "htpp://ajax.googleapis.com/ajax/libs/webfont/1.4.7/webfont.js"
-- js "http://d3js.org/d3.v3.min.js" ! charset "utf-8"
css "http://cdn.datatables.net/1.10.2/css/jquery.dataTables.min.css"
js "http://cdn.datatables.net/1.10.2/js/jquery.dataTables.min.js"
js "data.js"
js "../static/code.js"
title "hax results"
body $ do H.table ! A.id "example" $ ""
htmlBody
-- * JSON instances
toVar :: (Monoid a,IsString a) => a -> a -> a
toVar name value = "var " <> name <> " = " <> value
instance ToJSON FullLedger where
toJSON (FullLedger (FixedLedger balances logEntries) accounts) =
object [ "accounts" .= toJSON (FAN "" "Month" : accs)
, "balances" .= (toRow toJSON (toJSON.scale) <$> to2DTable balances)
, "dates" .= range (bounds logEntries)
, "log" .= elems logEntries
, "entities" .= ((\x -> fromString (fEntity $ head x) .= (fAccount <$> x))
<$> groupBy ((==) `on` fEntity) accs :: [(Text, Value)])
]
where accs = sortedAccountNames accounts
instance ToJSON FullAccountName
instance ToJSON LogEntry
instance ToJSON Tx
instance ToJSON Decimal where
toJSON d = toJSON ( conv d :: Double)
instance ToJSON ADate where
toJSON = toJSON . show
-- * String
scale = (roundTo 2.(/1000))
-- zeigeTransaktionen = True
-- zwischenlinien = zeigeTransaktionen
-- -- | Pretty-print the ledger of the world
-- showWorld :: World -> IO String
-- showWorld world = showLedger zeigeTransaktionen
-- zwischenlinien scale <$> generate world
-- -- | Pretty-print the results of 'Accounting.generate'.
-- showLedger :: (Show b) => Bool -> Bool ->
-- Scale b -> (AccountsMap,FixedLedger) -> String
-- showLedger showTxns showSeps scale (accounts,ledger) =
-- ("\n"++) $ renderTable $ (header:) $ seps $
-- toGroups showTxns scale ledger
-- where header :: [Row String]
-- header = [h False $ fEntity,h True $ fAccount]
-- where h m a = (if m then "mm/yy" else ""):
-- (a <$> sortedAccountNames accounts)
-- ++ [if m then "Comment" else ""]
-- seps = if not showSeps then return.concat else id
-- toGroups:: (Show b) => Bool -> Scale b ->
-- FixedLedger -> [Group String]
-- toGroups showTxns scale (FixedLedger bals txns) =
-- zipWith groups txnGroups $ toRow show (show.scale) <$> to2DTable bals
-- where groups :: [LogEntry] -> Row String -> Group String
-- groups txns row = (show <$> (txnRow =<< reverse txns)) ++ [row ++ ["Endsaldo"]]
-- (dateRange,accBounds) = (range1 $ bounds bals, bounds2 $ bounds bals)
-- txnGroups :: [[LogEntry]]
-- txnGroups = if showTxns then elems txns else repeat []
-- txnRow :: LogEntry -> [Row Amount]
-- txnRow (LTx txn) = [transactionRow accBounds txn]
-- txnRow (LComment txn) = [] -- impossible???
-- | generates a row containing the posting's amount at the column
-- corresponding to the posting's account
transactionRow :: (AccountNumber,AccountNumber) -- ^ account number bounds
-> Tx -> (Row Amount,Comment)
transactionRow accBounds txn = (postings,tComment txn)
where postings = elems $ accumArray (+) 0 accBounds
$ tPostings txn
col x = if x == 0 then ""
else (" "++) $ show $ scale x
-- * Helpers
type Row e = [e]
type Group e = [Row e]
type Scale b = (Amount -> b)
-- | Renders a list of grouped rows
renderTable :: [Group String] -> String
renderTable groups = render $ punctuateH top seps
$ fmap renderCol (transpose $ intercalate [vertSep] $ groups)
where seps = vcat P.left $ map text $ replicate
(length groups - 1 + length (head combined)) " | "
renderCol xs = vcat P.left $ map text xs
where rH h = [h,replicate (maximum $ length <$> (h:xs)) '-']
combined = transpose $ concat groups
vertSep = map (\c -> replicate (maximum $ length <$> c) '-') combined
toRow :: (r -> e) -> (a -> e) -> (r,Row a) -> Row e
toRow conv1 conv2 (d,row) = conv1 d : fmap conv2 row
-- | Convert a 2D array into a list of pairs, where the first
-- component contains the first index and the second the coresponding
-- row o the array
to2DTable :: (Ix r,Ix c) => Array (r,c) a -> [(r,Row a)]
to2DTable array = zip (range (r1,r2))
$ chunksOf (rangeSize (c1,c2)) $ elems array
where ((r1,c1),(r2,c2)) = bounds array