summaryrefslogtreecommitdiff
path: root/src/FundLab.Web/App.fs
diff options
context:
space:
mode:
Diffstat (limited to 'src/FundLab.Web/App.fs')
-rw-r--r--src/FundLab.Web/App.fs200
1 files changed, 200 insertions, 0 deletions
diff --git a/src/FundLab.Web/App.fs b/src/FundLab.Web/App.fs
index d6e44d9..6ce48f0 100644
--- a/src/FundLab.Web/App.fs
+++ b/src/FundLab.Web/App.fs
@@ -213,6 +213,25 @@ type RawRedemption =
confirmedCostReleased: obj
}
+type RawDividend =
+ {
+ id: string
+ fundId: string
+ instrumentCode: string
+ navDate: string
+ dps: string
+ mode: string
+ sourceKey: string
+ status: string
+ grossCash: obj
+ creditedUnits: obj
+ creditedInvested: obj
+ orderId: obj
+ pendingReason: obj
+ isSynthetic: bool
+ createdAt: string
+ }
+
type RawPositions =
{
availableCash: string
@@ -391,6 +410,24 @@ type RedemptionDetail =
confirmedCostReleased: string option
}
+type DividendRecord =
+ {
+ id: string
+ instrumentCode: string
+ navDate: string
+ dps: string
+ mode: string
+ sourceKey: string
+ status: string
+ grossCash: string option
+ creditedUnits: string option
+ creditedInvested: string option
+ orderId: string option
+ pendingReason: string option
+ isSynthetic: bool
+ createdAt: string
+ }
+
type Positions =
{
availableCash: string
@@ -435,6 +472,9 @@ module Api =
[<Import("confirmRedemption", "./src/api.js")>]
let confirmRedemption (token: string) (fundId: string) (orderId: string) (idempotencyKey: string) : JS.Promise<RawRedemption> = jsNative
+ [<Import("getDividends", "./src/api.js")>]
+ let getDividends (token: string) (fundId: string) : JS.Promise<RawDividend array> = jsNative
+
[<Import("createCapitalDeposit", "./src/api.js")>]
let createCapitalDeposit (token: string) (fundId: string) (payload: CreateCapitalPayload) : JS.Promise<obj> = jsNative
@@ -579,6 +619,24 @@ module Api =
confirmedCostReleased = decodeOptionalText raw.confirmedCostReleased
}
+ let decodeDividend (raw: RawDividend) : DividendRecord =
+ {
+ id = raw.id
+ instrumentCode = raw.instrumentCode
+ navDate = raw.navDate
+ dps = raw.dps
+ mode = raw.mode
+ sourceKey = raw.sourceKey
+ status = raw.status
+ grossCash = decodeOptionalText raw.grossCash
+ creditedUnits = decodeOptionalText raw.creditedUnits
+ creditedInvested = decodeOptionalText raw.creditedInvested
+ orderId = decodeOptionalText raw.orderId
+ pendingReason = decodeOptionalText raw.pendingReason
+ isSynthetic = raw.isSynthetic
+ createdAt = raw.createdAt
+ }
+
type Model =
{
token: string
@@ -645,6 +703,9 @@ type Model =
rebalExecuteInFlight: bool
rebalPlans: RebalancePlan list
rebalResult: RebalanceOutcome list option
+ dividendReadSeq: int
+ dividendInFlight: bool
+ dividends: DividendRecord list
error: string option
}
@@ -720,6 +781,9 @@ type Msg =
| RebalanceExecuteRequested of planId: string
| RebalanceExecuted of requestId: int * result: RawRebalanceExecution
| RebalanceExecuteFailed of requestId: int * message: string
+ | DividendsReadRequested
+ | DividendsReadCompleted of requestId: int * records: RawDividend array
+ | DividendsReadFailed of requestId: int * message: string
let defaultInitialUnitNav = "1.00000000"
@@ -815,6 +879,9 @@ let init () =
rebalExecuteInFlight = false
rebalPlans = []
rebalResult = None
+ dividendReadSeq = 0
+ dividendInFlight = false
+ dividends = []
error = None
}
@@ -950,6 +1017,13 @@ let private executeRebalanceCommand token fundId planId requestId =
(fun result -> RebalanceExecuted(requestId, result))
(fun error -> RebalanceExecuteFailed(requestId, errorText error))
+let private readDividendsCommand token fundId requestId =
+ Cmd.OfPromise.either
+ (fun () -> Api.getDividends token fundId)
+ ()
+ (fun records -> DividendsReadCompleted(requestId, records))
+ (fun error -> DividendsReadFailed(requestId, errorText error))
+
let update message model =
match message with
| TokenChanged token ->
@@ -1018,6 +1092,9 @@ let update message model =
rebalExecuteInFlight = false
rebalPlans = []
rebalResult = None
+ dividendReadSeq = model.dividendReadSeq + 1
+ dividendInFlight = false
+ dividends = []
error = None
},
Cmd.none
@@ -1212,6 +1289,9 @@ let update message model =
rebalExecuteInFlight = false
rebalPlans = []
rebalResult = None
+ dividendReadSeq = model.dividendReadSeq + 1
+ dividendInFlight = false
+ dividends = []
error = None
},
Cmd.ofMsg OrdersReadRequested
@@ -1855,6 +1935,36 @@ let update message model =
{ model with rebalExecuteInFlight = false; error = Some message }, Cmd.none
else
model, Cmd.none
+ | DividendsReadRequested ->
+ match model.createdFund with
+ | Some fund when not (String.IsNullOrWhiteSpace model.token) ->
+ let requestId = model.dividendReadSeq + 1
+
+ {
+ model with
+ dividendReadSeq = requestId
+ dividendInFlight = true
+ error = None
+ },
+ readDividendsCommand model.token fund.id requestId
+ | Some _ -> { model with error = Some "请输入 API token" }, Cmd.none
+ | None -> model, Cmd.none
+ | DividendsReadCompleted (requestId, records) ->
+ if requestId = model.dividendReadSeq then
+ {
+ model with
+ dividends = records |> Array.map Api.decodeDividend |> Array.toList
+ dividendInFlight = false
+ error = None
+ },
+ Cmd.none
+ else
+ model, Cmd.none
+ | DividendsReadFailed (requestId, message) ->
+ if requestId = model.dividendReadSeq then
+ { model with dividendInFlight = false; error = Some message }, Cmd.none
+ else
+ model, Cmd.none
let private navText (text: string) =
@@ -2851,6 +2961,95 @@ let private rebalancePanel model dispatch =
]
+let private dividendModeText (mode: string) =
+ match mode with
+ | "reinvest" -> "红利再投资"
+ | _ -> "现金"
+
+let private dividendStatusText (status: string) =
+ match status with
+ | "cash_credited"
+ | "succeeded" -> "已入账"
+ | "pending_nav" -> "待净值"
+ | other -> other
+
+let private dividendPanel model dispatch =
+ Html.section [
+ prop.className "panel dividend-panel"
+ prop.children [
+ Html.div [
+ prop.className "section-heading"
+ prop.children [
+ Html.div [
+ Html.p [ prop.className "eyebrow"; prop.text "09 / DIVIDEND" ]
+ Html.h2 "分红记录"
+ ]
+ Html.span [ prop.className "section-note"; prop.text "Dividends - booked history" ]
+ ]
+ ]
+ Html.div [
+ prop.className "dividend-history"
+ prop.children [
+ if List.isEmpty model.dividends then
+ Html.p [ prop.className "hint"; prop.text "暂无分红记录" ]
+ else
+ yield!
+ (model.dividends
+ |> List.map (fun record ->
+ Html.div [
+ prop.className "order-row dividend-row"
+ prop.children [
+ Html.span [ prop.className "order-code"; prop.text record.instrumentCode ]
+ Html.span [
+ prop.className "order-cell"
+ prop.text (sprintf "持仓日期 %s" record.navDate)
+ ]
+ Html.span [
+ prop.className "order-cell"
+ prop.text (sprintf "类型 %s" (dividendModeText record.mode))
+ ]
+ Html.span [
+ prop.className "order-cell"
+ prop.text (sprintf "每份分红 %s" record.dps)
+ ]
+ match record.grossCash with
+ | Some cash ->
+ Html.span [
+ prop.className "order-cell"
+ prop.text (sprintf "入账现金 %s" cash)
+ ]
+ | None -> Html.none
+ match record.creditedUnits with
+ | Some units ->
+ Html.span [
+ prop.className "order-cell"
+ prop.text (sprintf "入账份额 %s" units)
+ ]
+ | None -> Html.none
+ Html.span [ prop.className "order-status"; prop.text (dividendStatusText record.status) ]
+ Html.span [
+ prop.className "order-cell dividend-key"
+ prop.title record.sourceKey
+ prop.text record.sourceKey
+ ]
+ ]
+ ]))
+ ]
+ ]
+ Html.div [
+ prop.className "panel-actions"
+ prop.children [
+ Html.button [
+ prop.className "secondary-action dividend-refresh-action"
+ prop.disabled model.dividendInFlight
+ prop.onClick (fun _ -> dispatch DividendsReadRequested)
+ prop.text ((if model.dividendInFlight then "读取中..." else "刷新分红记录"): string)
+ ]
+ ]
+ ]
+ ]
+ ]
+
let view model dispatch =
Html.main [
prop.className "app-shell"
@@ -2911,6 +3110,7 @@ let view model dispatch =
redeemPanel model dispatch
sipPanel model dispatch
rebalancePanel model dispatch
+ dividendPanel model dispatch
Html.footer [ prop.className "footer-note"; prop.text "SOURCE · AKShare / STORAGE · PostgreSQL / LEDGER · CREATE & READ & SUBSCRIBE" ]
]
]