Implement add transaction

This commit is contained in:
wjsjwr 2024-12-29 23:34:31 +08:00
parent ed52f9a34e
commit 84bd7f66d9
4 changed files with 189 additions and 88 deletions

View File

@ -10,19 +10,11 @@
<cargoProject FILE="$PROJECT_DIR$/bookkeeper/Cargo.toml" /> <cargoProject FILE="$PROJECT_DIR$/bookkeeper/Cargo.toml" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="Add index page"> <list default="true" id="76b3b902-7a5c-4bcd-9c1b-8241d748fb44" name="更改" comment="Refine data structure and index page">
<change afterPath="$PROJECT_DIR$/.idea/jsLibraryMappings.xml" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bookkeeper/templates/nav.html.j2" afterDir="false" />
<change afterPath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/finance-consumer.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/finance-consumer.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/Cargo.lock" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/Cargo.lock" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/Cargo.toml" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/Cargo.toml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/entity/src/transaction.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/entity/src/transaction.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/migration/src/m20220101_000001_create_table.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/migration/src/m20220101_000001_create_table.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/src/main.rs" afterDir="false" /> <change beforePath="$PROJECT_DIR$/bookkeeper/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/base.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/base.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" afterDir="false" /> <change beforePath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/index.html.j2" afterDir="false" />
<change beforePath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" beforeDir="false" afterPath="$PROJECT_DIR$/bookkeeper/templates/tx.html.j2" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@ -44,7 +36,7 @@
<option name="directoryName" value="JuSRXtPJ" /> <option name="directoryName" value="JuSRXtPJ" />
</component> </component>
<component name="ProblemsViewState"> <component name="ProblemsViewState">
<option name="selectedTabId" value="ProjectErrors" /> <option name="selectedTabId" value="CurrentFile" />
</component> </component>
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;, &quot;customColor&quot;: &quot;&quot;,
@ -157,7 +149,7 @@
<workItem from="1735391132866" duration="1139000" /> <workItem from="1735391132866" duration="1139000" />
<workItem from="1735392290940" duration="620000" /> <workItem from="1735392290940" duration="620000" />
<workItem from="1735392928494" duration="12910000" /> <workItem from="1735392928494" duration="12910000" />
<workItem from="1735470376833" duration="8472000" /> <workItem from="1735470376833" duration="15697000" />
</task> </task>
<task id="LOCAL-00001" summary="Add bookkeeper"> <task id="LOCAL-00001" summary="Add bookkeeper">
<option name="closed" value="true" /> <option name="closed" value="true" />
@ -191,7 +183,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1735406260589</updated> <updated>1735406260589</updated>
</task> </task>
<option name="localTasksCounter" value="5" /> <task id="LOCAL-00005" summary="Refine data structure and index page">
<option name="closed" value="true" />
<created>1735479514256</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1735479514256</updated>
</task>
<option name="localTasksCounter" value="6" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@ -213,7 +213,8 @@
<MESSAGE value="Insert transaction" /> <MESSAGE value="Insert transaction" />
<MESSAGE value="Format main.rs" /> <MESSAGE value="Format main.rs" />
<MESSAGE value="Add index page" /> <MESSAGE value="Add index page" />
<option name="LAST_COMMIT_MESSAGE" value="Add index page" /> <MESSAGE value="Refine data structure and index page" />
<option name="LAST_COMMIT_MESSAGE" value="Refine data structure and index page" />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

View File

@ -69,7 +69,7 @@ async fn index(conn: Connection<'_, Db>) -> Template {
}) })
} }
#[derive(FromForm)] #[derive(FromForm, Debug)]
struct TransModel<'r> { struct TransModel<'r> {
code: usize, code: usize,
name: &'r str, name: &'r str,
@ -78,10 +78,10 @@ struct TransModel<'r> {
volume: i32, volume: i32,
net_buy: &'r str, net_buy: &'r str,
date: &'r str, date: &'r str,
split_pieces: Vec<SplitPieceForm<'r>>, split_pieces: Option<Vec<SplitPieceForm<'r>>>,
} }
#[derive(FromForm, Serialize)] #[derive(FromForm, Serialize, Debug)]
struct SplitPieceForm<'r> { struct SplitPieceForm<'r> {
date: &'r str, date: &'r str,
sell: &'r str, sell: &'r str,
@ -98,8 +98,17 @@ struct SplitPiece{
volume: i32, volume: i32,
} }
#[get("/add")]
async fn get_add() -> Template {
Template::render("tx", context! {
r: (),
target: "/add",
})
}
#[post("/add", data = "<trans>")] #[post("/add", data = "<trans>")]
async fn add(trans: Form<TransModel<'_>>, conn: Connection<'_, Db>) -> Flash<Redirect> { async fn add(trans: Form<TransModel<'_>>, conn: Connection<'_, Db>) -> Flash<Redirect> {
dbg!(&trans);
let record = translate(trans); let record = translate(trans);
record.insert(conn.into_inner()).await.unwrap(); record.insert(conn.into_inner()).await.unwrap();
@ -120,9 +129,9 @@ fn translate(trans: Form<TransModel>) -> transaction::ActiveModel {
..Default::default() ..Default::default()
}; };
if trans.split_pieces.len() > 0 { if let Some(split_pieces) = &trans.split_pieces {
let mut sp: Vec<SplitPiece> = Vec::new(); let mut sp: Vec<SplitPiece> = Vec::new();
for split_piece in trans.split_pieces.iter() { for split_piece in split_pieces.iter() {
sp.push(SplitPiece{ sp.push(SplitPiece{
date: NaiveDate::parse_from_str(split_piece.date, "%Y-%m-%d").unwrap(), date: NaiveDate::parse_from_str(split_piece.date, "%Y-%m-%d").unwrap(),
sell: Decimal::from_str(split_piece.sell).unwrap(), sell: Decimal::from_str(split_piece.sell).unwrap(),
@ -165,5 +174,5 @@ async fn rocket() -> _ {
.attach(Db::init()) .attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations)) .attach(AdHoc::try_on_ignite("Migrations", run_migrations))
.attach(Template::fairing()) .attach(Template::fairing())
.mount("/", routes![index, add]) .mount("/", routes![index, add, get_add])
} }

View File

@ -2,18 +2,18 @@
{% block title %}交易{% endblock title %} {% block title %}交易{% endblock title %}
{% block container %} {% block container %}
<div class=""> <div class="">
<table class="table is-fullwidth is-striped is-bordered"> <table class="table is-fullwidth is-bordered is-hoverable">
<thead> <thead>
<tr> <tr>
<th>代码</th> <th style="width: fit-content">代码</th>
<th>名称</th> <th style="width: fit-content">名称</th>
<th>买入日期</th> <th>买入日期</th>
<th>买入价</th> <th>买入价</th>
<th>成交量</th> <th>量</th>
<th><abbr title="买入金额+佣金+过户费等各种手续费">实际买入</abbr></th> <th><abbr title="买入金额+佣金+过户费等各种手续费">实际买入</abbr></th>
<th>卖出日期</th> <th>卖出日期</th>
<th>卖出价</th> <th>卖出价</th>
<th>成交量</th> <th>量</th>
<th><abbr title="卖出金额-佣金-各种手续费">实际卖出</abbr></th> <th><abbr title="卖出金额-佣金-各种手续费">实际卖出</abbr></th>
<th>收益</th> <th>收益</th>
<th>实际收益</th> <th>实际收益</th>
@ -24,11 +24,11 @@
{% for r in rows %} {% for r in rows %}
{% if r.split_pieces %} {% if r.split_pieces %}
{% for p in r.split_pieces %} {% for p in r.split_pieces %}
<tr class="{% if r.is_done %}{% if r.net_gain|float > 0 %}is-success{% else %}is-danger{% endif %}{% else %}is-warning{% endif %}"> <tr>
{% if loop.index == 1 %} {% if loop.index == 1 %}
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.code }}</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.code }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.name }}</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.name }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.date }}</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if not r.is_done %}is-warning{% endif %}">{{ r.date }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.buy }}</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.buy }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.volume }}</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.volume }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.net_buy }}</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ r.net_buy }}</td>
@ -36,8 +36,8 @@
<td>{{ p.sell or "" }}</td> <td>{{ p.sell or "" }}</td>
<td>{{ p.volume or "" }}</td> <td>{{ p.volume or "" }}</td>
<td>{{ p.net_sell or "" }}</td> <td>{{ p.net_sell or "" }}</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ (r.gain or 0)|float|round(4) }}%</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if r.gain|float > 0 %}is-success{% else %}is-danger{% endif %}">{{ (r.gain or 0)|float|round(4) }}%</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;">{{ (r.net_gain or 0)|float|round(4) }}%</td> <td rowspan="{{ loop.length }}" style="vertical-align: middle;" class="{% if r.net_gain|float > 0 %}is-success{% else %}is-danger{% endif %}">{{ (r.net_gain or 0)|float|round(4) }}%</td>
<td rowspan="{{ loop.length }}" style="vertical-align: middle;"> <td rowspan="{{ loop.length }}" style="vertical-align: middle;">
<a href="#">修改</a> <a href="#">修改</a>
</td> </td>
@ -50,10 +50,10 @@
</tr> </tr>
{% endfor %} {% endfor %}
{% else %} {% else %}
<tr class="is-warning"> <tr>
<td>{{ r.code }}</td> <td>{{ r.code }}</td>
<td>{{ r.name }}</td> <td>{{ r.name }}</td>
<td>{{ r.date }}</td> <td class="is-warning">{{ r.date }}</td>
<td>{{ r.buy }}</td> <td>{{ r.buy }}</td>
<td>{{ r.volume }}</td> <td>{{ r.volume }}</td>
<td>{{ r.net_buy }}</td> <td>{{ r.net_buy }}</td>
@ -74,4 +74,80 @@
</table> </table>
</div> </div>
<div class="modal" id="modal">
<div class="modal-background"></div>
<div class="modal-content" id="modal-content" style="width: 50rem;">
<!-- Any other Bulma elements you want -->
</div>
<button class="modal-close is-large" aria-label="close"></button>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Functions to open and close a modal
function openModal($el) {
$el.classList.add('is-active');
}
function closeModal($el) {
$el.classList.remove('is-active');
}
function closeAllModals() {
(document.querySelectorAll('.modal') || []).forEach(($modal) => {
closeModal($modal);
});
}
document.getElementById('addEntry').addEventListener('click', async () => {
const $target = document.getElementById("modal");
const response = await fetch("/add");
if (!response.ok) {
return;
}
document.getElementById('modal-content').innerHTML = await response.text();
openModal($target);
});
// Add a click event on various child elements to close the parent modal
(document.querySelectorAll('.modal-background, .modal-close, .modal-card-head .delete, .modal-card-foot .button') || []).forEach(($close) => {
const $target = $close.closest('.modal');
$close.addEventListener('click', () => {
closeModal($target);
});
});
// Add a keyboard event to close all modals
document.addEventListener('keydown', (event) => {
if (event.key === "Escape") {
closeAllModals();
}
});
});
function addRow() {
const this_row = document.getElementById('add-row-line');
this_row.parentElement.insertBefore(this_row.previousElementSibling.cloneNode(true), this_row);
for (let i = 0; i < this_row.parentElement.childElementCount-1; i++) {
let row = this_row.parentElement.children[i];
row.children[0].children[0].name = `split_pieces[${i}].date`;
row.children[1].children[0].name = `split_pieces[${i}].sell`;
row.children[2].children[0].name = `split_pieces[${i}].volume`;
row.children[3].children[0].name = `split_pieces[${i}].net_sell`;
}
}
function deleteRow(el) {
const this_row = el.parentElement.parentElement; // tr
const first_data_row = this_row.parentElement.firstElementChild;
const last_data_row = this_row.parentElement.lastElementChild.previousElementSibling; // last is add button
if (first_data_row === last_data_row) {
console.log("The only row");
} else {
this_row.parentElement.removeChild(this_row);
}
}
</script>
{% endblock container %} {% endblock container %}

View File

@ -1,61 +1,76 @@
<div class=""> <div class="box">
<table class="table is-fullwidth is-striped"> <form action="{{ target }}" method="post">
<thead> <div class="field">
<tr> <label class="label" for="code">代码</label>
<th>代码</th> <div class="control">
<th>名称</th> <input id="code" name="code" class="input" type="number" placeholder="Code" value="{{ r.code or 0 }}">
<th>买入日期</th> </div>
<th>买入价</th> </div>
<th>成交量</th> <div class="field">
<th><abbr title="买入价+佣金+过户费等各种手续费">实际买入价</abbr></th> <label class="label" for="name">名称</label>
<th>卖出日期</th> <div class="control">
<th>卖出价</th> <input id="name" name="name" class="input" type="text" placeholder="Name" value="{{ r.name or '' }}">
<th>成交量</th> </div>
<th><abbr title="卖出价-佣金-各种手续费">实际卖出价</abbr></th> </div>
<th>收益</th> <table class="table is-fullwidth is-bordered">
<th>实际收益</th> <thead>
<th><abbr title="已完成">O</abbr></th> <tr>
<th>操作</th> <th><label class="label" for="date">买入日期</label></th>
</tr> <th><label class="label" for="buy">买入价</label></th>
</thead> <th><label class="label" for="volume">成交量(买)</label></th>
<tbody> <th><label class="label" for="net_buy">实际买入</label></th>
{% for r in rows %} </tr>
<tr> </thead>
<td>{{ r.code }}</td> <tbody>
<td>{{ r.name }}</td> <tr>
<td>{{ r.date }}</td> <td><input id="date" name="date" class="input" type="date" placeholder="YYYY-MM-DD" value="{{ r.date or '' }}"></td>
<td>{{ r.buy }}</td> <td><input id="buy" name="buy" class="input" type="number" placeholder="Buy" value="{{ r.buy or 0 }}"></td>
<td>{{ r.volume }}</td> <td><input id="volume" name="volume" class="input" type="number" placeholder="Volume" value="{{ r.volume or 0 }}"></td>
<td>{{ r.net_buy }}</td> <td><input id="net_buy" name="net_buy" class="input" type="number" placeholder="Net Buy" value="{{ r.net_buy or 0 }}"></td>
{% if r.split_pieces %} </tr>
<td></td> </tbody>
<td></td> </table>
<td>}</td> <table class="table is-fullwidth is-bordered">
<td></td> <thead>
{% else %} <tr>
<td>{{ r.sell_date or "" }}</td> <th><label class="label">卖出日期</label></th>
<td>{{ r.sell or "" }}</td> <th><label class="label">买入价</label></th>
<td>{{ r.sold_volume or "" }}</td> <th><label class="label">成交量</label></th>
<td>{{ r.net_sell or "" }}</td> <th><label class="label">实际买入</label></th>
{% endif %} <th style="min-width: 4rem; vertical-align: middle;">操作</th>
<td>{{ r.gain or "" }}</td> </tr>
<td>{{ r.net_gain or "" }}</td> </thead>
<td> <tbody>
{% if r.sold_volume == r.volume %} <tr>
{% if r.split_pieces %}
{% for split in r.split_pieces %}
<td><input name="split_pieces[].date" class="input" type="date" placeholder="YYYY-MM-DD" value="{{ split.date or '' }}"></td>
<td><input name="split_pieces[].sell" class="input" type="number" placeholder="Sell" value="{{ split.sell or '' }}"></td>
<td><input name="split_pieces[].volume" class="input" type="number" placeholder="Volume" value="{{ split.volume or '' }}"></td>
<td><input name="split_pieces[].net_sell" class="input" type="number" placeholder="Net Sell" value="{{ split.net_sell or '' }}"></td>
<td><a onclick="deleteRow(this)">删除</a></td>
{% endfor %}
{% else %} {% else %}
<td><input name="split_pieces[0].date" class="input" type="date" placeholder="YYYY-MM-DD"></td>
<td><input name="split_pieces[0].sell" class="input" type="number" placeholder="Sell"></td>
<td><input name="split_pieces[0].volume" class="input" type="number" placeholder="Volume"></td>
<td><input name="split_pieces[0].net_sell" class="input" type="number" placeholder="Net Sell"></td>
<td style="vertical-align: middle;"><a onclick="deleteRow(this)">删除</a></td>
{% endif %} {% endif %}
</td> </tr>
<td> <tr class="is-light" id="add-row-line">
<a href="#">修改</a> <td colspan="5" style="text-align: center"><a onclick="addRow()">添加</a></td>
</td> </tr>
</tr> </tbody>
{% endfor %} </table>
<tr> <div class="field is-grouped">
<td colspan="14" style="text-align: center"><a id="addEntry">添加</a></td> <div class="control">
</tr> <button class="button is-link">Submit</button>
</tbody> </div>
</table> <div class="control">
<button class="button is-link is-light">Cancel</button>
</div>
</div>
</form>
</div> </div>