receipts/list: Reverse sort and add pagination
dustin/receipts/pipeline/head This commit looks good Details

Now that there are quite a few receipts in the database, scrolling to
the end to see the most recent entries is rather cumbersome.  Let's show
the most recent receipts first, and hide older ones by default by
splitting the list into multiple pages.
master
Dustin 2025-05-18 16:00:26 -05:00
parent ad1c857c97
commit b919bd8f0d
7 changed files with 112 additions and 7 deletions

View File

@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n count(id) AS \"count!\"\nFROM\n receipts\n",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "count!",
"type_info": "Int8"
}
],
"parameters": {
"Left": []
},
"nullable": [
null
]
},
"hash": "34f56cde503100c09bbb378ce656af95abd81949be0c369a5d7225272e6c9c58"
}

View File

@ -1,6 +1,6 @@
{ {
"db_name": "PostgreSQL", "db_name": "PostgreSQL",
"query": "SELECT\n id, vendor, date, amount, notes, filename\nFROM\n receipts\nORDER BY date\n", "query": "SELECT\n id, vendor, date, amount, notes, filename\nFROM\n receipts\nORDER BY\n date DESC,\n id DESC\nLIMIT $1\nOFFSET $2\n",
"describe": { "describe": {
"columns": [ "columns": [
{ {
@ -35,7 +35,10 @@
} }
], ],
"parameters": { "parameters": {
"Left": [] "Left": [
"Int8",
"Int8"
]
}, },
"nullable": [ "nullable": [
false, false,
@ -46,5 +49,5 @@
false false
] ]
}, },
"hash": "71dcdc6a24d99eff2dd7af673a0ebb6fda45b0ebd5244309472921a934e1b829" "hash": "ed7bf495d2eefe7b479a79cc2fc77de3b5a3db4415cd55ecbd21c28c108274a6"
} }

View File

@ -0,0 +1,4 @@
SELECT
count(id) AS "count!"
FROM
receipts

View File

@ -2,4 +2,8 @@ SELECT
id, vendor, date, amount, notes, filename id, vendor, date, amount, notes, filename
FROM FROM
receipts receipts
ORDER BY date ORDER BY
date DESC,
id DESC
LIMIT $1
OFFSET $2

View File

@ -138,12 +138,21 @@ impl ReceiptsRepository {
pub async fn list_receipts( pub async fn list_receipts(
&mut self, &mut self,
limit: i64,
offset: i64,
) -> Result<Vec<ReceiptJson>, sqlx::Error> { ) -> Result<Vec<ReceiptJson>, sqlx::Error> {
sqlx::query_file_as!(ReceiptJson, "sql/receipts/list-receipts.sql") sqlx::query_file_as!(ReceiptJson, "sql/receipts/list-receipts.sql", limit, offset)
.fetch_all(&mut **self.conn) .fetch_all(&mut **self.conn)
.await .await
} }
pub async fn count_receipts(&mut self) -> Result<i64, sqlx::Error> {
Ok(sqlx::query_file!("sql/receipts/count-receipts.sql")
.fetch_one(&mut **self.conn)
.await?
.count)
}
pub async fn add_receipt( pub async fn add_receipt(
&mut self, &mut self,
data: &ReceiptPostData, data: &ReceiptPostData,

View File

@ -1,3 +1,5 @@
use std::ops::RangeInclusive;
use rocket::form::Form; use rocket::form::Form;
use rocket::http::{ContentType, Header, MediaType, Status}; use rocket::http::{ContentType, Header, MediaType, Status};
use rocket::serde::json::Json; use rocket::serde::json::Json;
@ -12,18 +14,62 @@ use crate::imaging;
use crate::receipts::*; use crate::receipts::*;
use crate::{Context, Database}; use crate::{Context, Database};
#[rocket::get("/")] fn paginate(total: i64, count: i64, current: i64) -> Vec<String> {
let start = 1;
let end = (total / count).max(1);
let pages = RangeInclusive::new(start, end);
if end < 10 {
pages.map(|p| format!("{}", p)).collect()
} else {
pages
.filter_map(|p| {
if p == start
|| (current - 2 <= p && p <= current + 2)
|| p == end
{
Some(format!("{}", p))
} else if p == current - 3 || p == current + 3 {
Some("...".into())
} else {
None
}
})
.collect()
}
}
#[rocket::get("/?<page>&<count>")]
pub async fn list_receipts( pub async fn list_receipts(
db: DatabaseConnection<Database>, db: DatabaseConnection<Database>,
page: Option<i64>,
count: Option<i64>,
) -> (Status, Template) { ) -> (Status, Template) {
let mut repo = ReceiptsRepository::new(db); let mut repo = ReceiptsRepository::new(db);
match repo.list_receipts().await { let count = count.unwrap_or(25);
let page = page.unwrap_or(1);
let total = match repo.count_receipts().await {
Ok(r) => r,
Err(e) => {
return (
Status::InternalServerError,
Template::render(
"error",
context! {
error: e.to_string(),
},
),
)
},
};
match repo.list_receipts(count, (page - 1) * count).await {
Ok(r) => ( Ok(r) => (
Status::Ok, Status::Ok,
Template::render( Template::render(
"receipt-list", "receipt-list",
context! { context! {
receipts: r, receipts: r,
pages: paginate(total, count, page),
count: count,
}, },
), ),
), ),

View File

@ -80,6 +80,18 @@
#confirm-delete dl dd { #confirm-delete dl dd {
margin-left: 0; margin-left: 0;
} }
ul.pagination {
margin: 0;
padding: 0;
list-style-type: none;
text-align: center;
}
ul.pagination li {
display: inline-block;
margin: 1em;
}
</style> </style>
{% endblock %} {% block main %} {% endblock %} {% block main %}
<h1>Receipts</h1> <h1>Receipts</h1>
@ -114,6 +126,13 @@
</sl-card> </sl-card>
{% endfor %} {% endfor %}
</article> </article>
<ul class="pagination">
{%- for page in pages %}
<li>{% if page == "..." %}...{% else
%}<sl-button href="?page={{ page }}&count={{ count }}">{{ page }}</sl-button>{%
endif %}</li>
{%- endfor %}
</ul>
<sl-dialog id="confirm-delete" label="Delete Receipt"> <sl-dialog id="confirm-delete" label="Delete Receipt">
<p> <p>
Are you sure you want to delete receipt <span class="receipt-id"></span>? Are you sure you want to delete receipt <span class="receipt-id"></span>?