Move database operations to separate module
This can be seen as partially reverting
e83bc8316e
, which moved everything to the
place where it was used.
This commit is contained in:
parent
a320f3834a
commit
f33f3c8b9c
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT\n items.name,\n item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "05d5b6e3618b5bb676d0a262a531efedec34bf08f99dda0397df4ca873859be6"
|
||||||
|
}
|
|
@ -22,5 +22,5 @@
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "857bbaea0fa73c37679d927aa627ee841c5b89c86af12f32493ba05c0f0ead91"
|
"hash": "087164492bb5f51006e5091f9382b5e27a5a90908d65b7a6f6d6587f8a3e2a10"
|
||||||
}
|
}
|
|
@ -24,5 +24,5 @@
|
||||||
null
|
null
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "460f15b3c5ec2774c237fc581166bf44ecdb0b6145f8abba155478643a474125"
|
"hash": "265104a03d3ce2a41b58b1fa5cac5a55622b6d4f035d0276d1644f6500491989"
|
||||||
}
|
}
|
|
@ -1,74 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || parent.name,\n cte.parent_class_names || parent_class.name\n FROM cte\n JOIN items\n ON items.parent = cte.id\n JOIN items AS \"parent\"\n ON parent.id = cte.id\n JOIN item_classes AS \"parent_class\"\n ON parent.class = parent_class.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\",\n item_states.state AS \"state!: ItemState\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n ORDER BY items.created_at\n ",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "id!",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 1,
|
|
||||||
"name": "name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 2,
|
|
||||||
"name": "class",
|
|
||||||
"type_info": "Uuid"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 3,
|
|
||||||
"name": "class_name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 4,
|
|
||||||
"name": "parents!",
|
|
||||||
"type_info": "UuidArray"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 5,
|
|
||||||
"name": "parent_names!: Vec<Option<String>>",
|
|
||||||
"type_info": "VarcharArray"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 6,
|
|
||||||
"name": "parent_class_names!",
|
|
||||||
"type_info": "VarcharArray"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 7,
|
|
||||||
"name": "state!: ItemState",
|
|
||||||
"type_info": {
|
|
||||||
"Custom": {
|
|
||||||
"name": "item_state",
|
|
||||||
"kind": {
|
|
||||||
"Enum": [
|
|
||||||
"borrowed",
|
|
||||||
"inactive",
|
|
||||||
"loaned",
|
|
||||||
"owned"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": []
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
true
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "3fe94e76159c7911db688854710271e351ca34273dfdb21a7499f588715a91ee"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
{
|
|
||||||
"db_name": "PostgreSQL",
|
|
||||||
"query": "SELECT type as \"type!\"\n FROM (SELECT id, 'item' AS \"type\" FROM items\n UNION ALL\n SELECT id, 'item_class' AS \"type\" FROM item_classes) id_mapping\n WHERE id = $1",
|
|
||||||
"describe": {
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"ordinal": 0,
|
|
||||||
"name": "type!",
|
|
||||||
"type_info": "Text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": {
|
|
||||||
"Left": [
|
|
||||||
"Uuid"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"nullable": [
|
|
||||||
null
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"hash": "719aff68ead7ada499158fec5e9f8f3c3841a4424da04aee0136c7e4f8df79e7"
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"db_name": "PostgreSQL",
|
"db_name": "PostgreSQL",
|
||||||
"query": "SELECT\n items.name,\n items.parent,\n items.class,\n item_classes.name AS \"class_name\",\n items.original_packaging,\n items.description\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id\n WHERE items.id = $1",
|
"query": "SELECT\n items.name,\n items.parent,\n items.class,\n items.original_packaging,\n items.description\n FROM items\n WHERE items.id = $1",
|
||||||
"describe": {
|
"describe": {
|
||||||
"columns": [
|
"columns": [
|
||||||
{
|
{
|
||||||
|
@ -20,16 +20,11 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 3,
|
"ordinal": 3,
|
||||||
"name": "class_name",
|
|
||||||
"type_info": "Varchar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"ordinal": 4,
|
|
||||||
"name": "original_packaging",
|
"name": "original_packaging",
|
||||||
"type_info": "Uuid"
|
"type_info": "Uuid"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"ordinal": 5,
|
"ordinal": 4,
|
||||||
"name": "description",
|
"name": "description",
|
||||||
"type_info": "Varchar"
|
"type_info": "Varchar"
|
||||||
}
|
}
|
||||||
|
@ -43,10 +38,9 @@
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
false,
|
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "c49b88eda9a62743783bc894f01bb6198594f94a3e0856abde0efdb4e49dbab8"
|
"hash": "7b1265f893e68df4ebde549fed8dc1872c723d1294521a72d9d2251f4ddd8da6"
|
||||||
}
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT items.id, items.name, item_classes.name AS \"class_name\"\n FROM items\n JOIN item_classes\n ON items.class = item_classes.id",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "84db69654e377998324906a80bb939d83454bb504082cb2efa723b80fde7b159"
|
||||||
|
}
|
|
@ -34,5 +34,5 @@
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "c7e35dee5f56da8da3c083e191f396b95917b15768ebe0250ce7840391036616"
|
"hash": "8d9766469e77b291eceffc2ba17ff78c11283a29411233597bd27a52a934a595"
|
||||||
}
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "\n WITH RECURSIVE cte AS (\n SELECT\n id,\n ARRAY[]::UUID[] AS parents,\n ARRAY[]::VARCHAR[] AS parent_names,\n ARRAY[]::VARCHAR[] AS parent_class_names\n FROM items\n WHERE parent IS NULL\n\n UNION ALL\n\n SELECT\n items.id,\n cte.parents || items.parent,\n cte.parent_names || parent.name,\n cte.parent_class_names || parent_class.name\n FROM cte\n JOIN items\n ON items.parent = cte.id\n JOIN items AS \"parent\"\n ON parent.id = cte.id\n JOIN item_classes AS \"parent_class\"\n ON parent.class = parent_class.id\n )\n SELECT\n cte.id AS \"id!\",\n items.name,\n items.class,\n item_classes.name AS \"class_name\",\n cte.parents AS \"parents!\",\n cte.parent_names AS \"parent_names!: Vec<Option<String>>\",\n cte.parent_class_names AS \"parent_class_names!\",\n item_states.state AS \"state!: ItemState\"\n FROM cte\n JOIN items\n ON cte.id = items.id\n JOIN item_classes\n ON items.class = item_classes.id\n JOIN item_states\n ON items.id = item_states.item\n ORDER BY items.created_at\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "id!",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 1,
|
||||||
|
"name": "name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 2,
|
||||||
|
"name": "class",
|
||||||
|
"type_info": "Uuid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 3,
|
||||||
|
"name": "class_name",
|
||||||
|
"type_info": "Varchar"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 4,
|
||||||
|
"name": "parents!",
|
||||||
|
"type_info": "UuidArray"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 5,
|
||||||
|
"name": "parent_names!: Vec<Option<String>>",
|
||||||
|
"type_info": "VarcharArray"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 6,
|
||||||
|
"name": "parent_class_names!",
|
||||||
|
"type_info": "VarcharArray"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ordinal": 7,
|
||||||
|
"name": "state!: ItemState",
|
||||||
|
"type_info": {
|
||||||
|
"Custom": {
|
||||||
|
"name": "item_state",
|
||||||
|
"kind": {
|
||||||
|
"Enum": [
|
||||||
|
"borrowed",
|
||||||
|
"inactive",
|
||||||
|
"loaned",
|
||||||
|
"owned"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": []
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "cdb1777fdc044c9b34e82dde93e429177ea5af957398553046590aedd0f4040b"
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"db_name": "PostgreSQL",
|
||||||
|
"query": "SELECT type as \"type!\"\n FROM (SELECT id, 'item' AS \"type\" FROM items\n UNION ALL\n SELECT id, 'item_class' AS \"type\" FROM item_classes) id_mapping\n WHERE id = $1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"ordinal": 0,
|
||||||
|
"name": "type!",
|
||||||
|
"type_info": "Text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Left": [
|
||||||
|
"Uuid"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
null
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "f16457df9365535d153723fc1ffd8e79849d10798c8d55fa01f99d5245398f6a"
|
||||||
|
}
|
|
@ -68,5 +68,5 @@
|
||||||
false
|
false
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"hash": "1b860018f6cd1b18d28a87ba9da1f96fd7c8021c5e2ea652e6f6fe11a823c32c"
|
"hash": "fecd395ba131e0b0767b8b4799eded503e6bf446c2562cbddae918c971403240"
|
||||||
}
|
}
|
38
src/database/item_classes/add.rs
Normal file
38
src/database/item_classes/add.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::query_scalar;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemClassRepository;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ItemClassAddForm {
|
||||||
|
pub name: String,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ItemClassAddFormPrefilled {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub async fn add(&self, data: ItemClassAddForm) -> sqlx::Result<Uuid> {
|
||||||
|
query_scalar!(
|
||||||
|
"INSERT INTO item_classes (name, parent, description)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
RETURNING id",
|
||||||
|
data.name,
|
||||||
|
data.parent,
|
||||||
|
data.description
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
26
src/database/item_classes/datalist.rs
Normal file
26
src/database/item_classes/datalist.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
|
||||||
|
use super::ItemClassRepository;
|
||||||
|
use crate::frontend::templates::datalist::{Datalist, DatalistOption};
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub async fn datalist(&self) -> sqlx::Result<Datalist> {
|
||||||
|
Ok(Datalist {
|
||||||
|
name: "item-classes".to_string(),
|
||||||
|
link_prefix: Some("/item-class/".to_string()),
|
||||||
|
options: query!("SELECT id, name FROM item_classes")
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| DatalistOption {
|
||||||
|
value: row.id.to_string(),
|
||||||
|
text: Box::new(row.name),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
18
src/database/item_classes/delete.rs
Normal file
18
src/database/item_classes/delete.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemClassRepository;
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub async fn delete(&self, id: Uuid) -> sqlx::Result<()> {
|
||||||
|
query!("DELETE FROM item_classes WHERE id = $1", id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
44
src/database/item_classes/edit.rs
Normal file
44
src/database/item_classes/edit.rs
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::{query, query_as};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemClassRepository;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ItemClassEditForm {
|
||||||
|
pub name: String,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub async fn edit_form(&self, id: Uuid) -> sqlx::Result<ItemClassEditForm> {
|
||||||
|
query_as!(
|
||||||
|
ItemClassEditForm,
|
||||||
|
"SELECT name, parent, description FROM item_classes WHERE id = $1",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn edit(&self, id: Uuid, data: &ItemClassEditForm) -> sqlx::Result<()> {
|
||||||
|
query!(
|
||||||
|
"UPDATE item_classes
|
||||||
|
SET name = $2, parent = $3, description = $4
|
||||||
|
WHERE id = $1",
|
||||||
|
id,
|
||||||
|
data.name,
|
||||||
|
data.parent,
|
||||||
|
data.description
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
36
src/database/item_classes/list.rs
Normal file
36
src/database/item_classes/list.rs
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::{ItemClassPreview, ItemClassRepository};
|
||||||
|
|
||||||
|
pub struct ItemClassListEntry {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub parent: Option<ItemClassPreview>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub async fn list(&self) -> sqlx::Result<Vec<ItemClassListEntry>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT class.id, class.name, class.parent, parent.name AS "parent_name?"
|
||||||
|
FROM item_classes AS "class"
|
||||||
|
LEFT JOIN item_classes AS "parent"
|
||||||
|
ON class.parent = parent.id
|
||||||
|
ORDER BY class.created_at
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.map(|row| ItemClassListEntry {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
parent: row
|
||||||
|
.parent
|
||||||
|
.map(|id| ItemClassPreview::new(id, row.parent_name.unwrap())),
|
||||||
|
})
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
38
src/database/item_classes/mod.rs
Normal file
38
src/database/item_classes/mod.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
mod add;
|
||||||
|
mod datalist;
|
||||||
|
mod delete;
|
||||||
|
mod edit;
|
||||||
|
mod list;
|
||||||
|
mod show;
|
||||||
|
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub use add::{ItemClassAddForm, ItemClassAddFormPrefilled};
|
||||||
|
pub use edit::ItemClassEditForm;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ItemClassRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ItemClassPreview {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassPreview {
|
||||||
|
pub fn new(id: Uuid, name: String) -> Self {
|
||||||
|
Self { id, name }
|
||||||
|
}
|
||||||
|
}
|
78
src/database/item_classes/show.rs
Normal file
78
src/database/item_classes/show.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::{query, query_as};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::{ItemClassPreview, ItemClassRepository};
|
||||||
|
use crate::database::item_states::ItemState;
|
||||||
|
use crate::database::items::ItemPreview;
|
||||||
|
|
||||||
|
pub struct ItemClassDetails {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub description: String,
|
||||||
|
pub parent: Option<ItemClassPreview>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemClassRepository {
|
||||||
|
pub async fn details(&self, id: Uuid) -> sqlx::Result<ItemClassDetails> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
class.id,
|
||||||
|
class.name,
|
||||||
|
class.description,
|
||||||
|
class.parent,
|
||||||
|
parent.name AS "parent_name?"
|
||||||
|
FROM item_classes AS "class"
|
||||||
|
LEFT JOIN item_classes AS "parent"
|
||||||
|
ON class.parent = parent.id
|
||||||
|
WHERE class.id = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| ItemClassDetails {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
parent: row
|
||||||
|
.parent
|
||||||
|
.map(|id| ItemClassPreview::new(id, row.parent_name.unwrap())),
|
||||||
|
})
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn children(&self, id: Uuid) -> sqlx::Result<Vec<ItemClassPreview>> {
|
||||||
|
query_as!(
|
||||||
|
ItemClassPreview,
|
||||||
|
"SELECT id, name FROM item_classes WHERE parent = $1",
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn items(&self, id: Uuid) -> sqlx::Result<Vec<ItemPreview>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
items.id,
|
||||||
|
items.name,
|
||||||
|
item_classes.name AS "class_name",
|
||||||
|
item_states.state AS "state!: ItemState"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
JOIN item_states
|
||||||
|
ON items.id = item_states.item
|
||||||
|
WHERE items.class = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| {
|
||||||
|
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name)
|
||||||
|
.with_state(row.state)
|
||||||
|
})
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
96
src/database/item_events.rs
Normal file
96
src/database/item_events.rs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{query, query_as, query_scalar, PgPool};
|
||||||
|
use time::Date;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ItemEventRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemEventRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize, Serialize, sqlx::Type)]
|
||||||
|
#[sqlx(rename_all = "snake_case", type_name = "item_event")]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum ItemEvent {
|
||||||
|
Acquire,
|
||||||
|
Borrow,
|
||||||
|
Buy,
|
||||||
|
Dispose,
|
||||||
|
Gift,
|
||||||
|
Loan,
|
||||||
|
Lose,
|
||||||
|
RecieveGift,
|
||||||
|
ReturnBorrowed,
|
||||||
|
ReturnLoaned,
|
||||||
|
Sell,
|
||||||
|
Use,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct EventAddForm {
|
||||||
|
pub date: Date,
|
||||||
|
pub event: ItemEvent,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemEventRepository {
|
||||||
|
pub async fn add(&self, item: Uuid, data: EventAddForm) -> sqlx::Result<()> {
|
||||||
|
query!(
|
||||||
|
"INSERT INTO item_events (item, date, event, description)
|
||||||
|
VALUES ($1, $2, $3, $4)",
|
||||||
|
item,
|
||||||
|
data.date,
|
||||||
|
data.event as ItemEvent,
|
||||||
|
data.description
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
|
||||||
|
impl ItemEventRepository {
|
||||||
|
pub async fn delete(&self, id: i32) -> sqlx::Result<Uuid> {
|
||||||
|
query_scalar!("DELETE FROM item_events WHERE id = $1 RETURNING item", id)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get
|
||||||
|
|
||||||
|
pub struct ItemEventDetails {
|
||||||
|
pub id: i32,
|
||||||
|
pub date: Date,
|
||||||
|
pub event: ItemEvent,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemEventRepository {
|
||||||
|
pub async fn for_item(&self, item: Uuid) -> sqlx::Result<Vec<ItemEventDetails>> {
|
||||||
|
query_as!(
|
||||||
|
ItemEventDetails,
|
||||||
|
r#"SELECT id, date, event AS "event: ItemEvent", description FROM item_events WHERE item = $1"#,
|
||||||
|
item
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
73
src/database/item_states.rs
Normal file
73
src/database/item_states.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use maud::{html, Markup, Render};
|
||||||
|
use sqlx::{query, PgPool};
|
||||||
|
|
||||||
|
use crate::frontend::templates::helpers::Colour;
|
||||||
|
|
||||||
|
use super::item_events::ItemEvent;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, sqlx::Type)]
|
||||||
|
#[sqlx(rename_all = "snake_case", type_name = "item_state")]
|
||||||
|
pub enum ItemState {
|
||||||
|
Borrowed,
|
||||||
|
Inactive,
|
||||||
|
Loaned,
|
||||||
|
Owned,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemState {
|
||||||
|
pub fn colour(&self) -> Colour {
|
||||||
|
match self {
|
||||||
|
ItemState::Borrowed => Colour::Warning,
|
||||||
|
ItemState::Inactive => Colour::Secondary,
|
||||||
|
ItemState::Loaned => Colour::Danger,
|
||||||
|
ItemState::Owned => Colour::Primary,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ItemState {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
span .badge.(self.colour().text_bg()) {
|
||||||
|
@match self {
|
||||||
|
ItemState::Borrowed => "borrowed",
|
||||||
|
ItemState::Inactive => "inactive",
|
||||||
|
ItemState::Loaned => "loaned",
|
||||||
|
ItemState::Owned => "owned",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ItemStateRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemStateRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn possible_events(
|
||||||
|
&self,
|
||||||
|
state: ItemState,
|
||||||
|
) -> sqlx::Result<Vec<(ItemEvent, ItemState)>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
event AS "event: ItemEvent",
|
||||||
|
next AS "next: ItemState"
|
||||||
|
FROM item_events_transitions
|
||||||
|
WHERE state = $1"#,
|
||||||
|
state as ItemState
|
||||||
|
)
|
||||||
|
.map(|row| (row.event, row.next))
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
67
src/database/items/add.rs
Normal file
67
src/database/items/add.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::query_scalar;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemRepository;
|
||||||
|
|
||||||
|
pub fn default_quantity() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ItemAddForm {
|
||||||
|
#[serde(default = "default_quantity")]
|
||||||
|
pub quantity: usize,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Uuid,
|
||||||
|
pub original_packaging: Option<Uuid>,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ItemAddFormPrefilled {
|
||||||
|
pub quantity: Option<usize>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Option<Uuid>,
|
||||||
|
pub original_packaging: Option<Uuid>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn add(&self, data: ItemAddForm) -> sqlx::Result<Vec<Uuid>> {
|
||||||
|
if data.quantity == 1 {
|
||||||
|
query_scalar!(
|
||||||
|
"INSERT INTO items (name, parent, class, original_packaging, description)
|
||||||
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
|
RETURNING id",
|
||||||
|
data.name,
|
||||||
|
data.parent,
|
||||||
|
data.class,
|
||||||
|
data.original_packaging,
|
||||||
|
data.description
|
||||||
|
)
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map(|id| vec![id])
|
||||||
|
} else {
|
||||||
|
query_scalar!(
|
||||||
|
"INSERT INTO items (name, parent, class, original_packaging, description)
|
||||||
|
SELECT * FROM UNNEST($1::VARCHAR[], $2::UUID[], $3::UUID[], $4::UUID[], $5::VARCHAR[])
|
||||||
|
RETURNING id",
|
||||||
|
&vec![data.name; data.quantity] as &[Option<String>],
|
||||||
|
&vec![data.parent; data.quantity] as &[Option<Uuid>],
|
||||||
|
&vec![data.class; data.quantity],
|
||||||
|
&vec![data.original_packaging; data.quantity] as &[Option<Uuid>],
|
||||||
|
&vec![data.description; data.quantity]
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
31
src/database/items/datalist.rs
Normal file
31
src/database/items/datalist.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
|
||||||
|
use super::{ItemName, ItemRepository};
|
||||||
|
use crate::frontend::templates::datalist::{Datalist, DatalistOption};
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn datalist(&self) -> sqlx::Result<Datalist> {
|
||||||
|
Ok(Datalist {
|
||||||
|
name: "items".to_string(),
|
||||||
|
link_prefix: Some("/item/".to_string()),
|
||||||
|
options: query!(
|
||||||
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id"#
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| DatalistOption {
|
||||||
|
value: row.id.to_string(),
|
||||||
|
text: Box::new(ItemName::new(row.name.as_ref(), &row.class_name)),
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
18
src/database/items/delete.rs
Normal file
18
src/database/items/delete.rs
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemRepository;
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn delete(&self, id: Uuid) -> sqlx::Result<()> {
|
||||||
|
query!("DELETE FROM items WHERE id = $1", id)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
61
src/database/items/edit.rs
Normal file
61
src/database/items/edit.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemRepository;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct ItemEditForm {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Uuid,
|
||||||
|
pub original_packaging: Option<Uuid>,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn edit_form(&self, id: Uuid) -> sqlx::Result<ItemEditForm> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
items.name,
|
||||||
|
items.parent,
|
||||||
|
items.class,
|
||||||
|
items.original_packaging,
|
||||||
|
items.description
|
||||||
|
FROM items
|
||||||
|
WHERE items.id = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| ItemEditForm {
|
||||||
|
name: row.name,
|
||||||
|
parent: row.parent,
|
||||||
|
class: row.class,
|
||||||
|
original_packaging: row.original_packaging,
|
||||||
|
description: row.description,
|
||||||
|
})
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn edit(&self, id: Uuid, data: &ItemEditForm) -> sqlx::Result<()> {
|
||||||
|
query!(
|
||||||
|
"UPDATE items
|
||||||
|
SET name = $2, parent = $3, class = $4, original_packaging = $5, description = $6
|
||||||
|
WHERE id = $1",
|
||||||
|
id,
|
||||||
|
data.name,
|
||||||
|
data.parent,
|
||||||
|
data.class,
|
||||||
|
data.original_packaging,
|
||||||
|
data.description
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
23
src/database/items/label.rs
Normal file
23
src/database/items/label.rs
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query_as;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemRepository;
|
||||||
|
use crate::label::LabelPage;
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn label_pages(&self, ids: &[Uuid]) -> sqlx::Result<Vec<LabelPage>> {
|
||||||
|
query_as!(
|
||||||
|
LabelPage,
|
||||||
|
r#"SELECT id AS "id?", to_char(short_id, '000000') AS "short_id?"
|
||||||
|
FROM items
|
||||||
|
WHERE id = ANY ($1)"#,
|
||||||
|
ids
|
||||||
|
)
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
95
src/database/items/list.rs
Normal file
95
src/database/items/list.rs
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemRepository;
|
||||||
|
use super::{ItemName, ItemPreview};
|
||||||
|
use crate::database::item_states::ItemState;
|
||||||
|
|
||||||
|
pub struct ItemListEntry {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: ItemName,
|
||||||
|
pub class: Uuid,
|
||||||
|
pub class_name: String,
|
||||||
|
pub parents: Vec<ItemPreview>,
|
||||||
|
pub state: ItemState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn list(&self) -> sqlx::Result<Vec<ItemListEntry>> {
|
||||||
|
query!(
|
||||||
|
r#"
|
||||||
|
WITH RECURSIVE cte AS (
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
ARRAY[]::UUID[] AS parents,
|
||||||
|
ARRAY[]::VARCHAR[] AS parent_names,
|
||||||
|
ARRAY[]::VARCHAR[] AS parent_class_names
|
||||||
|
FROM items
|
||||||
|
WHERE parent IS NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
items.id,
|
||||||
|
cte.parents || items.parent,
|
||||||
|
cte.parent_names || parent.name,
|
||||||
|
cte.parent_class_names || parent_class.name
|
||||||
|
FROM cte
|
||||||
|
JOIN items
|
||||||
|
ON items.parent = cte.id
|
||||||
|
JOIN items AS "parent"
|
||||||
|
ON parent.id = cte.id
|
||||||
|
JOIN item_classes AS "parent_class"
|
||||||
|
ON parent.class = parent_class.id
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
cte.id AS "id!",
|
||||||
|
items.name,
|
||||||
|
items.class,
|
||||||
|
item_classes.name AS "class_name",
|
||||||
|
cte.parents AS "parents!",
|
||||||
|
cte.parent_names AS "parent_names!: Vec<Option<String>>",
|
||||||
|
cte.parent_class_names AS "parent_class_names!",
|
||||||
|
item_states.state AS "state!: ItemState"
|
||||||
|
FROM cte
|
||||||
|
JOIN items
|
||||||
|
ON cte.id = items.id
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
JOIN item_states
|
||||||
|
ON items.id = item_states.item
|
||||||
|
ORDER BY items.created_at
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.map(|row| ItemListEntry {
|
||||||
|
id: row.id,
|
||||||
|
name: ItemName::new(row.name.as_ref(), &row.class_name),
|
||||||
|
class: row.class,
|
||||||
|
class_name: row.class_name,
|
||||||
|
parents: itertools::izip!(row.parents, row.parent_names, row.parent_class_names)
|
||||||
|
.map(|(id, name, class_name)| {
|
||||||
|
ItemPreview::from_parts(id, name.as_ref(), &class_name)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
state: row.state,
|
||||||
|
})
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn previews(&self) -> sqlx::Result<Vec<ItemPreview>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id"#
|
||||||
|
)
|
||||||
|
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
126
src/database/items/mod.rs
Normal file
126
src/database/items/mod.rs
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
mod add;
|
||||||
|
mod datalist;
|
||||||
|
mod delete;
|
||||||
|
mod edit;
|
||||||
|
mod label;
|
||||||
|
mod list;
|
||||||
|
mod name;
|
||||||
|
mod show;
|
||||||
|
|
||||||
|
use sqlx::{query, PgPool};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::item_states::ItemState;
|
||||||
|
|
||||||
|
pub use add::{default_quantity, ItemAddForm, ItemAddFormPrefilled};
|
||||||
|
pub use edit::ItemEditForm;
|
||||||
|
pub use name::ItemName;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ItemRepository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub fn new(pool: PgPool) -> Self {
|
||||||
|
Self { pool }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ItemPreview {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: ItemName,
|
||||||
|
pub state: Option<ItemState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemPreview {
|
||||||
|
pub fn new(id: Uuid, name: ItemName) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_parts(id: Uuid, item_name: Option<&String>, class_name: &String) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
name: ItemName::new(item_name, class_name),
|
||||||
|
state: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_state(mut self, state: ItemState) -> Self {
|
||||||
|
self.state = Some(state);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn parents(&self, id: Uuid) -> sqlx::Result<Vec<ItemPreview>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
||||||
|
FROM items
|
||||||
|
JOIN unnest((SELECT parents FROM item_tree WHERE id = $1))
|
||||||
|
WITH ORDINALITY AS parents(id, n)
|
||||||
|
ON items.id = parents.id
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
ORDER BY parents.n"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn children(&self, id: Uuid) -> sqlx::Result<Vec<ItemPreview>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
items.id,
|
||||||
|
items.name,
|
||||||
|
item_classes.name AS "class_name",
|
||||||
|
item_states.state AS "state!: ItemState"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
JOIN item_states
|
||||||
|
ON items.id = item_states.item
|
||||||
|
WHERE items.parent = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| {
|
||||||
|
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name)
|
||||||
|
.with_state(row.state)
|
||||||
|
})
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn original_packaging_of(&self, id: Uuid) -> sqlx::Result<Vec<ItemPreview>> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
items.id,
|
||||||
|
items.name,
|
||||||
|
item_classes.name AS "class_name",
|
||||||
|
item_states.state AS "state!: ItemState"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
JOIN item_states
|
||||||
|
ON items.id = item_states.item
|
||||||
|
WHERE items.original_packaging = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| {
|
||||||
|
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name)
|
||||||
|
.with_state(row.state)
|
||||||
|
})
|
||||||
|
.fetch_all(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
50
src/database/items/name.rs
Normal file
50
src/database/items/name.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::ItemRepository;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum ItemName {
|
||||||
|
Item(String),
|
||||||
|
Class(String),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemName {
|
||||||
|
pub fn new(item_name: Option<&String>, class_name: &String) -> Self {
|
||||||
|
if let Some(ref name) = item_name {
|
||||||
|
Self::Item(name.to_string())
|
||||||
|
} else {
|
||||||
|
Self::Class(class_name.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn terse(self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Item(_) => self,
|
||||||
|
Self::Class(_) | Self::None => Self::None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn name(&self, id: Uuid) -> sqlx::Result<ItemName> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
items.name,
|
||||||
|
item_classes.name AS "class_name"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
WHERE items.id = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| ItemName::new(row.name.as_ref(), &row.class_name))
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
71
src/database/items/show.rs
Normal file
71
src/database/items/show.rs
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::query;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::{ItemRepository, ItemName, ItemPreview};
|
||||||
|
use crate::database::item_states::ItemState;
|
||||||
|
|
||||||
|
pub struct ItemDetails {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub short_id: i32,
|
||||||
|
pub name: ItemName,
|
||||||
|
pub class: Uuid,
|
||||||
|
pub class_name: String,
|
||||||
|
pub original_packaging: Option<ItemPreview>,
|
||||||
|
pub description: String,
|
||||||
|
pub state: ItemState,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ItemRepository {
|
||||||
|
pub async fn details(&self, id: Uuid) -> sqlx::Result<ItemDetails> {
|
||||||
|
query!(
|
||||||
|
r#"SELECT
|
||||||
|
items.id,
|
||||||
|
items.short_id,
|
||||||
|
items.name,
|
||||||
|
items.class,
|
||||||
|
item_classes.name AS "class_name",
|
||||||
|
items.original_packaging,
|
||||||
|
op.name AS "original_packaging_name?",
|
||||||
|
op_class.name AS "original_packaging_class_name?",
|
||||||
|
op_state.state AS "original_packaging_state: ItemState",
|
||||||
|
items.description,
|
||||||
|
item_states.state AS "state!: ItemState"
|
||||||
|
FROM items
|
||||||
|
JOIN item_classes
|
||||||
|
ON items.class = item_classes.id
|
||||||
|
JOIN item_states
|
||||||
|
ON items.id = item_states.item
|
||||||
|
LEFT JOIN items AS "op"
|
||||||
|
ON items.original_packaging = op.id
|
||||||
|
LEFT JOIN item_classes AS "op_class"
|
||||||
|
ON op.class = op_class.id
|
||||||
|
LEFT JOIN item_states AS "op_state"
|
||||||
|
ON op.id = op_state.item
|
||||||
|
WHERE items.id = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| ItemDetails {
|
||||||
|
id: row.id,
|
||||||
|
short_id: row.short_id,
|
||||||
|
name: ItemName::new(row.name.as_ref(), &row.class_name),
|
||||||
|
class: row.class,
|
||||||
|
class_name: row.class_name,
|
||||||
|
original_packaging: row.original_packaging.map(|id| {
|
||||||
|
ItemPreview::from_parts(
|
||||||
|
id,
|
||||||
|
row.original_packaging_name.as_ref(),
|
||||||
|
&row.original_packaging_class_name.unwrap(),
|
||||||
|
)
|
||||||
|
.with_state(row.original_packaging_state.unwrap())
|
||||||
|
}),
|
||||||
|
description: row.description,
|
||||||
|
state: row.state,
|
||||||
|
})
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
14
src/database/mod.rs
Normal file
14
src/database/mod.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
pub mod item_classes;
|
||||||
|
pub mod item_events;
|
||||||
|
pub mod item_states;
|
||||||
|
pub mod items;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
|
pub use item_classes::ItemClassRepository;
|
||||||
|
pub use item_events::ItemEventRepository;
|
||||||
|
pub use item_states::ItemStateRepository;
|
||||||
|
pub use items::ItemRepository;
|
41
src/database/search.rs
Normal file
41
src/database/search.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use sqlx::{query, query_scalar, PgPool};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub enum Entity {
|
||||||
|
Item(Uuid),
|
||||||
|
ItemClass(Uuid),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn search_id(pool: &PgPool, id: &str) -> sqlx::Result<Option<Entity>> {
|
||||||
|
if let Ok(id) = Uuid::parse_str(id) {
|
||||||
|
query!(
|
||||||
|
r#"SELECT type as "type!"
|
||||||
|
FROM (SELECT id, 'item' AS "type" FROM items
|
||||||
|
UNION ALL
|
||||||
|
SELECT id, 'item_class' AS "type" FROM item_classes) id_mapping
|
||||||
|
WHERE id = $1"#,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.map(|row| match row.r#type.as_str() {
|
||||||
|
"item" => Entity::Item(id),
|
||||||
|
"item_class" => Entity::ItemClass(id),
|
||||||
|
_ => unreachable!("database returned impossible type"),
|
||||||
|
})
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await
|
||||||
|
} else if let Ok(short_id) = id.parse::<i32>() {
|
||||||
|
Ok(
|
||||||
|
query_scalar!("SELECT id FROM items WHERE short_id = $1", short_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map(|id| Some(Entity::Item(id)))
|
||||||
|
.unwrap_or(None),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,52 +7,31 @@ use std::fmt::Display;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{query_scalar, PgPool};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::frontend::templates::{self, datalist, forms, helpers::PageActionGroup, TemplateConfig};
|
use crate::database::{
|
||||||
|
items::{default_quantity, ItemAddForm, ItemAddFormPrefilled},
|
||||||
|
ItemClassRepository, ItemRepository,
|
||||||
|
};
|
||||||
|
use crate::frontend::templates::{self, forms, helpers::PageActionGroup, TemplateConfig};
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get).service(post);
|
cfg.service(get).service(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_quantity() -> usize {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct NewItemForm {
|
|
||||||
#[serde(default = "default_quantity")]
|
|
||||||
quantity: usize,
|
|
||||||
name: Option<String>,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
class: Uuid,
|
|
||||||
original_packaging: Option<Uuid>,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct NewItemFormPrefilled {
|
|
||||||
quantity: Option<usize>,
|
|
||||||
name: Option<String>,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
class: Option<Uuid>,
|
|
||||||
original_packaging: Option<Uuid>,
|
|
||||||
description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/items/add")]
|
#[get("/items/add")]
|
||||||
async fn get(
|
async fn get(
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
form: web::Query<NewItemFormPrefilled>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
|
form: web::Query<ItemAddFormPrefilled>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let datalist_items = datalist::items(&pool)
|
let datalist_items = item_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
let datalist_item_classes = item_class_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
@ -123,47 +102,26 @@ async fn get(
|
||||||
#[post("/items/add")]
|
#[post("/items/add")]
|
||||||
async fn post(
|
async fn post(
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
data: web::Form<NewItemForm>,
|
data: web::Form<ItemAddForm>,
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
if data.quantity == 1 {
|
|
||||||
query_scalar!(
|
let ids = item_repo
|
||||||
"INSERT INTO items (name, parent, class, original_packaging, description)
|
.add(data)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
|
||||||
RETURNING id",
|
|
||||||
data.name,
|
|
||||||
data.parent,
|
|
||||||
data.class,
|
|
||||||
data.original_packaging,
|
|
||||||
data.description
|
|
||||||
)
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
.map(|id| {
|
|
||||||
web::Redirect::to("/item/".to_owned() + &id.to_string())
|
if ids.len() == 1 {
|
||||||
|
Ok(
|
||||||
|
web::Redirect::to("/item/".to_owned() + &ids.first().unwrap().to_string())
|
||||||
.see_other()
|
.see_other()
|
||||||
.respond_to(&req)
|
.respond_to(&req)
|
||||||
.map_into_boxed_body()
|
.map_into_boxed_body(),
|
||||||
})
|
|
||||||
} else {
|
|
||||||
query_scalar!(
|
|
||||||
"INSERT INTO items (name, parent, class, original_packaging, description)
|
|
||||||
SELECT * FROM UNNEST($1::VARCHAR[], $2::UUID[], $3::UUID[], $4::UUID[], $5::VARCHAR[])
|
|
||||||
RETURNING id",
|
|
||||||
&vec![data.name; data.quantity] as &[Option<String>],
|
|
||||||
&vec![data.parent; data.quantity] as &[Option<Uuid>],
|
|
||||||
&vec![data.class; data.quantity],
|
|
||||||
&vec![data.original_packaging; data.quantity] as &[Option<Uuid>],
|
|
||||||
&vec![data.description; data.quantity]
|
|
||||||
)
|
)
|
||||||
.fetch_all(pool.as_ref())
|
} else {
|
||||||
.await
|
Ok(templates::base(
|
||||||
.map_err(error::ErrorInternalServerError)
|
|
||||||
.map(|ids| {
|
|
||||||
templates::base(
|
|
||||||
TemplateConfig {
|
TemplateConfig {
|
||||||
path: "/items/add",
|
path: "/items/add",
|
||||||
title: Some("Added Items"),
|
title: Some("Added Items"),
|
||||||
|
@ -185,7 +143,6 @@ async fn post(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.respond_to(&req)
|
.respond_to(&req)
|
||||||
.map_into_boxed_body()
|
.map_into_boxed_body())
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,23 +4,24 @@
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, post, web, Responder};
|
use actix_web::{error, post, web, Responder};
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::ItemRepository;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(post);
|
cfg.service(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/item/{id}/delete")]
|
#[post("/item/{id}/delete")]
|
||||||
async fn post(
|
async fn post(
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
query!("DELETE FROM items WHERE id = $1", id)
|
item_repo
|
||||||
.execute(pool.as_ref())
|
.delete(id)
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -7,68 +7,40 @@ use std::fmt::Display;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, post, web, Responder};
|
use actix_web::{error, get, post, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::frontend::templates::{self, datalist, forms, helpers::ItemName, TemplateConfig};
|
use crate::database::{items::ItemEditForm, ItemClassRepository, ItemRepository};
|
||||||
|
use crate::frontend::templates::{self, forms, TemplateConfig};
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get).service(post);
|
cfg.service(get).service(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ItemEditForm {
|
|
||||||
name: Option<String>,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
class: Uuid,
|
|
||||||
original_packaging: Option<Uuid>,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item/{id}/edit")]
|
#[get("/item/{id}/edit")]
|
||||||
async fn get(
|
async fn get(
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let (item_name, form) = query!(
|
let item_name = item_repo
|
||||||
r#"SELECT
|
.name(id)
|
||||||
items.name,
|
.await
|
||||||
items.parent,
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
items.class,
|
let form = item_repo
|
||||||
item_classes.name AS "class_name",
|
.edit_form(id)
|
||||||
items.original_packaging,
|
|
||||||
items.description
|
|
||||||
FROM items
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
WHERE items.id = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| {
|
|
||||||
(
|
|
||||||
ItemName::new(row.name.as_ref(), &row.class_name),
|
|
||||||
ItemEditForm {
|
|
||||||
name: row.name,
|
|
||||||
parent: row.parent,
|
|
||||||
class: row.class,
|
|
||||||
original_packaging: row.original_packaging,
|
|
||||||
description: row.description,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let datalist_items = datalist::items(&pool)
|
let datalist_items = item_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
let datalist_item_classes = item_class_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
@ -149,25 +121,15 @@ async fn get(
|
||||||
|
|
||||||
#[post("/item/{id}/edit")]
|
#[post("/item/{id}/edit")]
|
||||||
async fn post(
|
async fn post(
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
data: web::Form<ItemEditForm>,
|
data: web::Form<ItemEditForm>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
query!(
|
item_repo
|
||||||
"UPDATE items
|
.edit(id, &data)
|
||||||
SET name = $2, parent = $3, class = $4, original_packaging = $5, description = $6
|
|
||||||
WHERE id = $1",
|
|
||||||
id,
|
|
||||||
data.name,
|
|
||||||
data.parent,
|
|
||||||
data.class,
|
|
||||||
data.original_packaging,
|
|
||||||
data.description
|
|
||||||
)
|
|
||||||
.execute(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,10 @@
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, post, web, Responder};
|
use actix_web::{error, post, web, Responder};
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{query, query_scalar, PgPool};
|
|
||||||
use time::Date;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::frontend::templates::helpers::ItemEvent;
|
use crate::database::item_events::EventAddForm;
|
||||||
|
use crate::database::ItemEventRepository;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(delete).service(add);
|
cfg.service(delete).service(add);
|
||||||
|
@ -18,47 +16,31 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
// not the best HTTP method, but there is no (non-JS) way of sending a DELETE request
|
// not the best HTTP method, but there is no (non-JS) way of sending a DELETE request
|
||||||
#[post("/items/event/{id}/delete")]
|
#[post("/items/event/{id}/delete")]
|
||||||
async fn delete(
|
async fn delete(
|
||||||
pool: web::Data<PgPool>,
|
item_event_repo: web::Data<ItemEventRepository>,
|
||||||
path: web::Path<i32>,
|
path: web::Path<i32>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let item_id = query_scalar!("DELETE FROM item_events WHERE id = $1 RETURNING item", id)
|
let item_id = item_event_repo
|
||||||
.fetch_one(pool.as_ref())
|
.delete(id)
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Ok(web::Redirect::to(format!("/item/{}", item_id)).see_other())
|
Ok(web::Redirect::to(format!("/item/{}", item_id)).see_other())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct NewEvent {
|
|
||||||
date: Date,
|
|
||||||
event: ItemEvent,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item/{id}/events/add")]
|
#[post("/item/{id}/events/add")]
|
||||||
async fn add(
|
async fn add(
|
||||||
pool: web::Data<PgPool>,
|
item_event_repo: web::Data<ItemEventRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
data: web::Form<NewEvent>,
|
data: web::Form<EventAddForm>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let data = data.into_inner();
|
item_event_repo
|
||||||
|
.add(id, data.into_inner())
|
||||||
query!(
|
|
||||||
"INSERT INTO item_events (item, date, event, description)
|
|
||||||
VALUES ($1, $2, $3, $4)",
|
|
||||||
id,
|
|
||||||
data.date,
|
|
||||||
data.event as ItemEvent,
|
|
||||||
data.description
|
|
||||||
)
|
|
||||||
.execute(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -5,14 +5,11 @@
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{error, get, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
|
use crate::database::{items::ItemPreview, ItemRepository};
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self,
|
self,
|
||||||
helpers::{
|
helpers::{Colour, PageAction, PageActionGroup, PageActionMethod},
|
||||||
Colour, ItemName, ItemPreview, ItemState, PageAction, PageActionGroup, PageActionMethod,
|
|
||||||
},
|
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -20,73 +17,13 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get);
|
cfg.service(get);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemListEntry {
|
|
||||||
id: Uuid,
|
|
||||||
name: ItemName,
|
|
||||||
class: Uuid,
|
|
||||||
class_name: String,
|
|
||||||
parents: Vec<ItemPreview>,
|
|
||||||
state: ItemState,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/items")]
|
#[get("/items")]
|
||||||
async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
async fn get(
|
||||||
let items = query!(
|
item_repo: web::Data<ItemRepository>,
|
||||||
r#"
|
user: Identity,
|
||||||
WITH RECURSIVE cte AS (
|
) -> actix_web::Result<impl Responder> {
|
||||||
SELECT
|
let items = item_repo
|
||||||
id,
|
.list()
|
||||||
ARRAY[]::UUID[] AS parents,
|
|
||||||
ARRAY[]::VARCHAR[] AS parent_names,
|
|
||||||
ARRAY[]::VARCHAR[] AS parent_class_names
|
|
||||||
FROM items
|
|
||||||
WHERE parent IS NULL
|
|
||||||
|
|
||||||
UNION
|
|
||||||
|
|
||||||
SELECT
|
|
||||||
items.id,
|
|
||||||
cte.parents || items.parent,
|
|
||||||
cte.parent_names || parent.name,
|
|
||||||
cte.parent_class_names || parent_class.name
|
|
||||||
FROM cte
|
|
||||||
JOIN items
|
|
||||||
ON items.parent = cte.id
|
|
||||||
JOIN items AS "parent"
|
|
||||||
ON parent.id = cte.id
|
|
||||||
JOIN item_classes AS "parent_class"
|
|
||||||
ON parent.class = parent_class.id
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
cte.id AS "id!",
|
|
||||||
items.name,
|
|
||||||
items.class,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
cte.parents AS "parents!",
|
|
||||||
cte.parent_names AS "parent_names!: Vec<Option<String>>",
|
|
||||||
cte.parent_class_names AS "parent_class_names!",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM cte
|
|
||||||
JOIN items
|
|
||||||
ON cte.id = items.id
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
ORDER BY items.created_at
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.map(|row| ItemListEntry {
|
|
||||||
id: row.id,
|
|
||||||
name: ItemName::new(row.name.as_ref(), &row.class_name),
|
|
||||||
class: row.class,
|
|
||||||
class_name: row.class_name,
|
|
||||||
parents: itertools::izip!(row.parents, row.parent_names, row.parent_class_names)
|
|
||||||
.map(|(id, name, class_name)| ItemPreview::from_parts(id, name.as_ref(), &class_name))
|
|
||||||
.collect(),
|
|
||||||
state: row.state,
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -6,16 +6,13 @@ use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{error, get, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde_variant::to_variant_name;
|
use serde_variant::to_variant_name;
|
||||||
use sqlx::{query, query_as, PgPool};
|
use time::OffsetDateTime;
|
||||||
use time::{Date, OffsetDateTime};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::{ItemEventRepository, ItemRepository, ItemStateRepository};
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self, forms,
|
self, forms,
|
||||||
helpers::{
|
helpers::{Colour, PageAction, PageActionGroup, PageActionMethod},
|
||||||
Colour, ItemEvent, ItemName, ItemPreview, ItemState, PageAction, PageActionGroup,
|
|
||||||
PageActionMethod,
|
|
||||||
},
|
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -23,157 +20,43 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get);
|
cfg.service(get);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemDetails {
|
|
||||||
id: Uuid,
|
|
||||||
short_id: i32,
|
|
||||||
name: ItemName,
|
|
||||||
class: Uuid,
|
|
||||||
class_name: String,
|
|
||||||
original_packaging: Option<ItemPreview>,
|
|
||||||
description: String,
|
|
||||||
state: ItemState,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ItemEventDetails {
|
|
||||||
id: i32,
|
|
||||||
date: Date,
|
|
||||||
event: ItemEvent,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item/{id}")]
|
#[get("/item/{id}")]
|
||||||
async fn get(
|
async fn get(
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
|
item_event_repo: web::Data<ItemEventRepository>,
|
||||||
|
item_state_repo: web::Data<ItemStateRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let item = query!(
|
let item = item_repo
|
||||||
r#"SELECT
|
.details(id)
|
||||||
items.id,
|
|
||||||
items.short_id,
|
|
||||||
items.name,
|
|
||||||
items.class,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
items.original_packaging,
|
|
||||||
op.name AS "original_packaging_name?",
|
|
||||||
op_class.name AS "original_packaging_class_name?",
|
|
||||||
op_state.state AS "original_packaging_state: ItemState",
|
|
||||||
items.description,
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
LEFT JOIN items AS "op"
|
|
||||||
ON items.original_packaging = op.id
|
|
||||||
LEFT JOIN item_classes AS "op_class"
|
|
||||||
ON op.class = op_class.id
|
|
||||||
LEFT JOIN item_states AS "op_state"
|
|
||||||
ON op.id = op_state.item
|
|
||||||
WHERE items.id = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| ItemDetails {
|
|
||||||
id: row.id,
|
|
||||||
short_id: row.short_id,
|
|
||||||
name: ItemName::new(row.name.as_ref(), &row.class_name),
|
|
||||||
class: row.class,
|
|
||||||
class_name: row.class_name,
|
|
||||||
original_packaging: row.original_packaging.map(|id| {
|
|
||||||
ItemPreview::from_parts(
|
|
||||||
id,
|
|
||||||
row.original_packaging_name.as_ref(),
|
|
||||||
&row.original_packaging_class_name.unwrap(),
|
|
||||||
)
|
|
||||||
.with_state(row.original_packaging_state.unwrap())
|
|
||||||
}),
|
|
||||||
description: row.description,
|
|
||||||
state: row.state,
|
|
||||||
})
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let events = query_as!(
|
let events = item_event_repo
|
||||||
ItemEventDetails,
|
.for_item(id)
|
||||||
r#"SELECT id, date, event AS "event: ItemEvent", description FROM item_events WHERE item = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let possible_events = query!(
|
let possible_events = item_state_repo
|
||||||
r#"SELECT
|
.possible_events(item.state)
|
||||||
event AS "event: ItemEvent",
|
|
||||||
next AS "next: ItemState"
|
|
||||||
FROM item_events_transitions
|
|
||||||
WHERE state = $1"#,
|
|
||||||
item.state as ItemState
|
|
||||||
)
|
|
||||||
.map(|row| (row.event, row.next))
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let parents = query!(
|
let parents = item_repo
|
||||||
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
.parents(id)
|
||||||
FROM items
|
|
||||||
JOIN unnest((SELECT parents FROM item_tree WHERE id = $1))
|
|
||||||
WITH ORDINALITY AS parents(id, n)
|
|
||||||
ON items.id = parents.id
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
ORDER BY parents.n"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let children = query!(
|
let children = item_repo
|
||||||
r#"SELECT
|
.children(id)
|
||||||
items.id,
|
|
||||||
items.name,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
WHERE items.parent = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| {
|
|
||||||
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name).with_state(row.state)
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let original_packaging_of = query!(
|
let original_packaging_of = item_repo
|
||||||
r#"SELECT
|
.original_packaging_of(id)
|
||||||
items.id,
|
|
||||||
items.name,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
WHERE items.original_packaging = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| {
|
|
||||||
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name).with_state(row.state)
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -7,37 +7,25 @@ use std::fmt::Display;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, post, web, Responder};
|
use actix_web::{error, get, post, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{query_scalar, PgPool};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::frontend::templates::{self, datalist, forms, TemplateConfig};
|
use crate::database::{
|
||||||
|
item_classes::{ItemClassAddForm, ItemClassAddFormPrefilled},
|
||||||
|
ItemClassRepository,
|
||||||
|
};
|
||||||
|
use crate::frontend::templates::{self, forms, TemplateConfig};
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get).service(post);
|
cfg.service(get).service(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct NewItemClassForm {
|
|
||||||
name: String,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct NewItemClassFormPrefilled {
|
|
||||||
name: Option<String>,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-classes/add")]
|
#[get("/item-classes/add")]
|
||||||
async fn get(
|
async fn get(
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
form: web::Query<NewItemClassFormPrefilled>,
|
form: web::Query<ItemClassAddFormPrefilled>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
let datalist_item_classes = item_class_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
@ -86,21 +74,14 @@ async fn get(
|
||||||
|
|
||||||
#[post("/item-classes/add")]
|
#[post("/item-classes/add")]
|
||||||
async fn post(
|
async fn post(
|
||||||
data: web::Form<NewItemClassForm>,
|
data: web::Form<ItemClassAddForm>,
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let data = data.into_inner();
|
let data = data.into_inner();
|
||||||
|
|
||||||
let id = query_scalar!(
|
let id = item_class_repo
|
||||||
"INSERT INTO item_classes (name, parent, description)
|
.add(data)
|
||||||
VALUES ($1, $2, $3)
|
|
||||||
RETURNING id",
|
|
||||||
data.name,
|
|
||||||
data.parent,
|
|
||||||
data.description
|
|
||||||
)
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -4,23 +4,24 @@
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, post, web, Responder};
|
use actix_web::{error, post, web, Responder};
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::ItemClassRepository;
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(post);
|
cfg.service(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/item-class/{id}/delete")]
|
#[post("/item-class/{id}/delete")]
|
||||||
async fn post(
|
async fn post(
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
query!("DELETE FROM item_classes WHERE id = $1", id)
|
item_class_repo
|
||||||
.execute(pool.as_ref())
|
.delete(id)
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -7,41 +7,31 @@ use std::fmt::Display;
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, post, web, Responder};
|
use actix_web::{error, get, post, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::{query, query_as, PgPool};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::frontend::templates::{self, datalist, forms, TemplateConfig};
|
use crate::database::item_classes::ItemClassEditForm;
|
||||||
|
use crate::database::ItemClassRepository;
|
||||||
|
use crate::frontend::templates::{self, forms, TemplateConfig};
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get).service(post);
|
cfg.service(get).service(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ItemClassEditForm {
|
|
||||||
name: String,
|
|
||||||
parent: Option<Uuid>,
|
|
||||||
description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-class/{id}/edit")]
|
#[get("/item-class/{id}/edit")]
|
||||||
async fn get(
|
async fn get(
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let form = query_as!(
|
let form = item_class_repo
|
||||||
ItemClassEditForm,
|
.edit_form(id)
|
||||||
"SELECT name, parent, description FROM item_classes WHERE id = $1",
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
let datalist_item_classes = item_class_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
@ -102,23 +92,15 @@ async fn get(
|
||||||
|
|
||||||
#[post("/item-class/{id}/edit")]
|
#[post("/item-class/{id}/edit")]
|
||||||
async fn post(
|
async fn post(
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
data: web::Form<ItemClassEditForm>,
|
data: web::Form<ItemClassEditForm>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
query!(
|
item_class_repo
|
||||||
"UPDATE item_classes
|
.edit(id, &data)
|
||||||
SET name = $2, parent = $3, description = $4
|
|
||||||
WHERE id = $1",
|
|
||||||
id,
|
|
||||||
data.name,
|
|
||||||
data.parent,
|
|
||||||
data.description
|
|
||||||
)
|
|
||||||
.execute(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,12 @@
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{error, get, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
|
use crate::database::item_classes::ItemClassPreview;
|
||||||
|
use crate::database::ItemClassRepository;
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self,
|
self,
|
||||||
helpers::{Colour, ItemClassPreview, PageAction, PageActionGroup, PageActionMethod},
|
helpers::{Colour, PageAction, PageActionGroup, PageActionMethod},
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,30 +18,13 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get);
|
cfg.service(get);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemClassListEntry {
|
|
||||||
id: Uuid,
|
|
||||||
name: String,
|
|
||||||
parent: Option<ItemClassPreview>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-classes")]
|
#[get("/item-classes")]
|
||||||
async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
async fn get(
|
||||||
let item_classes = query!(
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
r#"SELECT class.id, class.name, class.parent, parent.name AS "parent_name?"
|
user: Identity,
|
||||||
FROM item_classes AS "class"
|
) -> actix_web::Result<impl Responder> {
|
||||||
LEFT JOIN item_classes AS "parent"
|
let item_classes = item_class_repo
|
||||||
ON class.parent = parent.id
|
.list()
|
||||||
ORDER BY class.created_at
|
|
||||||
"#
|
|
||||||
)
|
|
||||||
.map(|row| ItemClassListEntry {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
parent: row
|
|
||||||
.parent
|
|
||||||
.map(|id| ItemClassPreview::new(id, row.parent_name.unwrap())),
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,12 @@
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{error, get, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use sqlx::{query, query_as, PgPool};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::ItemClassRepository;
|
||||||
use crate::frontend::templates::{
|
use crate::frontend::templates::{
|
||||||
self,
|
self,
|
||||||
helpers::{
|
helpers::{Colour, PageAction, PageActionGroup, PageActionMethod},
|
||||||
Colour, ItemClassPreview, ItemPreview, ItemState, PageAction, PageActionGroup,
|
|
||||||
PageActionMethod,
|
|
||||||
},
|
|
||||||
TemplateConfig,
|
TemplateConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -21,73 +18,26 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get);
|
cfg.service(get);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ItemClassDetails {
|
|
||||||
id: Uuid,
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
parent: Option<ItemClassPreview>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-class/{id}")]
|
#[get("/item-class/{id}")]
|
||||||
async fn get(
|
async fn get(
|
||||||
pool: web::Data<PgPool>,
|
item_class_repo: web::Data<ItemClassRepository>,
|
||||||
path: web::Path<Uuid>,
|
path: web::Path<Uuid>,
|
||||||
user: Identity,
|
user: Identity,
|
||||||
) -> actix_web::Result<impl Responder> {
|
) -> actix_web::Result<impl Responder> {
|
||||||
let id = path.into_inner();
|
let id = path.into_inner();
|
||||||
|
|
||||||
let item_class = query!(
|
let item_class = item_class_repo
|
||||||
r#"SELECT
|
.details(id)
|
||||||
class.id,
|
|
||||||
class.name,
|
|
||||||
class.description,
|
|
||||||
class.parent,
|
|
||||||
parent.name AS "parent_name?"
|
|
||||||
FROM item_classes AS "class"
|
|
||||||
LEFT JOIN item_classes AS "parent"
|
|
||||||
ON class.parent = parent.id
|
|
||||||
WHERE class.id = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| ItemClassDetails {
|
|
||||||
id: row.id,
|
|
||||||
name: row.name,
|
|
||||||
description: row.description,
|
|
||||||
parent: row
|
|
||||||
.parent
|
|
||||||
.map(|id| ItemClassPreview::new(id, row.parent_name.unwrap())),
|
|
||||||
})
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let children = query_as!(
|
let children = item_class_repo
|
||||||
ItemClassPreview,
|
.children(id)
|
||||||
"SELECT id, name FROM item_classes WHERE parent = $1",
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let items = query!(
|
let items = item_class_repo
|
||||||
r#"SELECT
|
.items(id)
|
||||||
items.id,
|
|
||||||
items.name,
|
|
||||||
item_classes.name AS "class_name",
|
|
||||||
item_states.state AS "state!: ItemState"
|
|
||||||
FROM items
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id
|
|
||||||
JOIN item_states
|
|
||||||
ON items.id = item_states.item
|
|
||||||
WHERE items.class = $1"#,
|
|
||||||
id
|
|
||||||
)
|
|
||||||
.map(|row| {
|
|
||||||
ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name).with_state(row.state)
|
|
||||||
})
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -5,19 +5,14 @@
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{error, get, web, Responder};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{query, query_scalar, PgPool};
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
|
||||||
|
use crate::database::search::{search_id, Entity};
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(get);
|
cfg.service(get);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
pub enum EntityType {
|
|
||||||
Item,
|
|
||||||
ItemClass,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct JumpData {
|
struct JumpData {
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -29,45 +24,18 @@ async fn get(
|
||||||
data: web::Query<JumpData>,
|
data: web::Query<JumpData>,
|
||||||
_user: Identity, // this endpoint leaks information about the existence of items
|
_user: Identity, // this endpoint leaks information about the existence of items
|
||||||
) -> Result<impl Responder, error::Error> {
|
) -> Result<impl Responder, error::Error> {
|
||||||
let mut id = data.id.clone();
|
let id = &data.id;
|
||||||
|
|
||||||
let entity_type = if let Ok(id) = Uuid::parse_str(&id) {
|
let entity = search_id(&pool, &data.id)
|
||||||
query!(
|
.await
|
||||||
r#"SELECT type as "type!"
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
FROM (SELECT id, 'item' AS "type" FROM items
|
|
||||||
UNION ALL
|
if let Some(entity) = entity {
|
||||||
SELECT id, 'item_class' AS "type" FROM item_classes) id_mapping
|
Ok(web::Redirect::to(match entity {
|
||||||
WHERE id = $1"#,
|
Entity::Item(id) => format!("/item/{id}"),
|
||||||
id
|
Entity::ItemClass(id) => format!("/item-class/{id}"),
|
||||||
)
|
|
||||||
.map(|row| match row.r#type.as_str() {
|
|
||||||
"item" => EntityType::Item,
|
|
||||||
"item_class" => EntityType::ItemClass,
|
|
||||||
_ => unreachable!("database returned impossible type"),
|
|
||||||
})
|
})
|
||||||
.fetch_optional(pool.as_ref())
|
.see_other())
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?
|
|
||||||
} else if let Ok(short_id) = id.parse::<i32>() {
|
|
||||||
if let Ok(id_) = query_scalar!("SELECT id FROM items WHERE short_id = $1", short_id)
|
|
||||||
.fetch_one(pool.as_ref())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)
|
|
||||||
{
|
|
||||||
id = id_.to_string();
|
|
||||||
Some(EntityType::Item)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(prefix) = entity_type.map(|entity_type| match entity_type {
|
|
||||||
EntityType::Item => "item",
|
|
||||||
EntityType::ItemClass => "item-class",
|
|
||||||
}) {
|
|
||||||
Ok(web::Redirect::to(format!("/{prefix}/{id}")).see_other())
|
|
||||||
} else {
|
} else {
|
||||||
Ok(web::Redirect::to(format!(
|
Ok(web::Redirect::to(format!(
|
||||||
"/items/add?{}",
|
"/items/add?{}",
|
||||||
|
|
|
@ -7,11 +7,10 @@ use actix_web::{error, get, post, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_variant::to_variant_name;
|
use serde_variant::to_variant_name;
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::templates::{self, datalist, TemplateConfig};
|
use super::templates::{self, TemplateConfig};
|
||||||
use crate::frontend::templates::helpers::ItemPreview;
|
use crate::database::ItemRepository;
|
||||||
use crate::label::{Label, LabelPreset};
|
use crate::label::{Label, LabelPreset};
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
@ -26,7 +25,10 @@ struct GenerateParams {
|
||||||
preset: LabelPreset,
|
preset: LabelPreset,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn generate(pool: &PgPool, params: GenerateParams) -> actix_web::Result<impl Responder> {
|
async fn generate(
|
||||||
|
item_repo: &ItemRepository,
|
||||||
|
params: GenerateParams,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
let ids = params
|
let ids = params
|
||||||
.ids
|
.ids
|
||||||
.split(',')
|
.split(',')
|
||||||
|
@ -35,43 +37,41 @@ async fn generate(pool: &PgPool, params: GenerateParams) -> actix_web::Result<im
|
||||||
.collect::<Result<Vec<Uuid>, uuid::Error>>()
|
.collect::<Result<Vec<Uuid>, uuid::Error>>()
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
Label::for_items(pool, &ids, params.preset.clone().into())
|
Label::for_items(item_repo, &ids, params.preset.clone().into())
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)
|
.map_err(error::ErrorInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/labels/generate")]
|
#[post("/labels/generate")]
|
||||||
async fn generate_post(
|
async fn generate_post(
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
params: web::Form<GenerateParams>,
|
params: web::Form<GenerateParams>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
generate(&pool, params.into_inner()).await
|
generate(&item_repo, params.into_inner()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/labels/generate")]
|
#[get("/labels/generate")]
|
||||||
async fn generate_get(
|
async fn generate_get(
|
||||||
pool: web::Data<PgPool>,
|
item_repo: web::Data<ItemRepository>,
|
||||||
_user: Identity,
|
_user: Identity,
|
||||||
params: web::Query<GenerateParams>,
|
params: web::Query<GenerateParams>,
|
||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
generate(&pool, params.into_inner()).await
|
generate(&item_repo, params.into_inner()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/labels")]
|
#[get("/labels")]
|
||||||
async fn form(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
async fn form(
|
||||||
let items = query!(
|
item_repo: web::Data<ItemRepository>,
|
||||||
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
user: Identity,
|
||||||
FROM items
|
) -> actix_web::Result<impl Responder> {
|
||||||
JOIN item_classes
|
let items = item_repo
|
||||||
ON items.class = item_classes.id"#
|
.previews()
|
||||||
)
|
|
||||||
.map(|row| ItemPreview::from_parts(row.id, row.name.as_ref(), &row.class_name))
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
let datalist_items = datalist::items(&pool)
|
let datalist_items = item_repo
|
||||||
|
.datalist()
|
||||||
.await
|
.await
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ mod item;
|
||||||
mod item_class;
|
mod item_class;
|
||||||
mod jump;
|
mod jump;
|
||||||
mod labels;
|
mod labels;
|
||||||
mod templates;
|
pub mod templates;
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{get, web, Responder};
|
use actix_web::{get, web, Responder};
|
||||||
|
|
|
@ -3,14 +3,11 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use maud::{html, Markup, Render};
|
use maud::{html, Markup, Render};
|
||||||
use sqlx::{query, PgPool};
|
|
||||||
|
|
||||||
use super::helpers::ItemName;
|
|
||||||
|
|
||||||
pub struct Datalist {
|
pub struct Datalist {
|
||||||
name: String,
|
pub name: String,
|
||||||
options: Vec<DatalistOption>,
|
pub options: Vec<DatalistOption>,
|
||||||
link_prefix: Option<String>,
|
pub link_prefix: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Datalist {
|
impl Datalist {
|
||||||
|
@ -32,8 +29,8 @@ impl Render for Datalist {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DatalistOption {
|
pub struct DatalistOption {
|
||||||
value: String,
|
pub value: String,
|
||||||
text: Box<dyn Render>,
|
pub text: Box<dyn Render>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for DatalistOption {
|
impl Render for DatalistOption {
|
||||||
|
@ -41,40 +38,3 @@ impl Render for DatalistOption {
|
||||||
html! { option value=(self.value) { (self.text) } }
|
html! { option value=(self.value) { (self.text) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn items(pool: &PgPool) -> Result<Datalist, sqlx::Error> {
|
|
||||||
Ok(Datalist {
|
|
||||||
name: "items".to_string(),
|
|
||||||
link_prefix: Some("/item/".to_string()),
|
|
||||||
options: query!(
|
|
||||||
r#"SELECT items.id, items.name, item_classes.name AS "class_name"
|
|
||||||
FROM items
|
|
||||||
JOIN item_classes
|
|
||||||
ON items.class = item_classes.id"#
|
|
||||||
)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| DatalistOption {
|
|
||||||
value: row.id.to_string(),
|
|
||||||
text: Box::new(ItemName::new(row.name.as_ref(), &row.class_name)),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn item_classes(pool: &PgPool) -> Result<Datalist, sqlx::Error> {
|
|
||||||
Ok(Datalist {
|
|
||||||
name: "item-classes".to_string(),
|
|
||||||
link_prefix: Some("/item-class/".to_string()),
|
|
||||||
options: query!("SELECT id, name FROM item_classes")
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|row| DatalistOption {
|
|
||||||
value: row.id.to_string(),
|
|
||||||
text: Box::new(row.name),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -2,13 +2,14 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use std::fmt::{self, Display};
|
use std::fmt;
|
||||||
|
|
||||||
use crate::label::LabelPreset;
|
|
||||||
use maud::{html, Markup, PreEscaped, Render};
|
use maud::{html, Markup, PreEscaped, Render};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::database::items::{ItemName, ItemPreview};
|
||||||
|
use crate::label::LabelPreset;
|
||||||
|
|
||||||
pub enum Css<'a> {
|
pub enum Css<'a> {
|
||||||
File(&'a str),
|
File(&'a str),
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -88,113 +89,6 @@ impl Colour {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ItemName {
|
|
||||||
Item(String),
|
|
||||||
Class(String),
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemName {
|
|
||||||
pub fn new(item_name: Option<&String>, class_name: &String) -> Self {
|
|
||||||
if let Some(ref name) = item_name {
|
|
||||||
Self::Item(name.to_string())
|
|
||||||
} else {
|
|
||||||
Self::Class(class_name.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemName {
|
|
||||||
pub fn terse(self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::Item(_) => self,
|
|
||||||
Self::Class(_) | Self::None => Self::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for ItemName {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::Item(name) => write!(f, "{name}"),
|
|
||||||
Self::Class(name) => write!(f, "*{name}*"),
|
|
||||||
Self::None => write!(f, "[no name]"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ItemName {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
html! {
|
|
||||||
@match self {
|
|
||||||
Self::Item(name) => { (name) },
|
|
||||||
Self::Class(name) => { em { (name) } },
|
|
||||||
Self::None => { em { "[no name]" } },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ItemPreview {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: ItemName,
|
|
||||||
pub state: Option<ItemState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemPreview {
|
|
||||||
pub fn new(id: Uuid, name: ItemName) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
state: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_parts(id: Uuid, item_name: Option<&String>, class_name: &String) -> Self {
|
|
||||||
Self {
|
|
||||||
id,
|
|
||||||
name: ItemName::new(item_name, class_name),
|
|
||||||
state: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_state(mut self, state: ItemState) -> Self {
|
|
||||||
self.state = Some(state);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ItemPreview {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
html! {
|
|
||||||
a .me-1[self.state.is_some()] href={ "/item/" (self.id) } { (self.name) }
|
|
||||||
@if let Some(ref state) = self.state {
|
|
||||||
(state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct ItemClassPreview {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemClassPreview {
|
|
||||||
pub fn new(id: Uuid, name: String) -> Self {
|
|
||||||
Self { id, name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ItemClassPreview {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
html! {
|
|
||||||
a href={ "/item-class/" (self.id) } { (self.name) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum PageActionMethod {
|
pub enum PageActionMethod {
|
||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
|
@ -298,77 +192,3 @@ pub fn parents_breadcrumb(name: ItemName, parents: &[ItemPreview], full: bool) -
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Is this module the right place for ItemState and ItemEvent?
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, sqlx::Type)]
|
|
||||||
#[sqlx(rename_all = "snake_case", type_name = "item_state")]
|
|
||||||
pub enum ItemState {
|
|
||||||
Borrowed,
|
|
||||||
Inactive,
|
|
||||||
Loaned,
|
|
||||||
Owned,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ItemState {
|
|
||||||
pub fn colour(&self) -> Colour {
|
|
||||||
match self {
|
|
||||||
ItemState::Borrowed => Colour::Warning,
|
|
||||||
ItemState::Inactive => Colour::Secondary,
|
|
||||||
ItemState::Loaned => Colour::Danger,
|
|
||||||
ItemState::Owned => Colour::Primary,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for ItemState {
|
|
||||||
fn render(&self) -> Markup {
|
|
||||||
html! {
|
|
||||||
span .badge.(self.colour().text_bg()) {
|
|
||||||
@match self {
|
|
||||||
ItemState::Borrowed => "borrowed",
|
|
||||||
ItemState::Inactive => "inactive",
|
|
||||||
ItemState::Loaned => "loaned",
|
|
||||||
ItemState::Owned => "owned",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, sqlx::Type)]
|
|
||||||
#[sqlx(rename_all = "snake_case", type_name = "item_event")]
|
|
||||||
#[serde(rename_all = "snake_case")]
|
|
||||||
pub enum ItemEvent {
|
|
||||||
Acquire,
|
|
||||||
Borrow,
|
|
||||||
Buy,
|
|
||||||
Dispose,
|
|
||||||
Gift,
|
|
||||||
Loan,
|
|
||||||
Lose,
|
|
||||||
RecieveGift,
|
|
||||||
ReturnBorrowed,
|
|
||||||
ReturnLoaned,
|
|
||||||
Sell,
|
|
||||||
Use,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ItemEvent {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
ItemEvent::Acquire => write!(f, "acquire"),
|
|
||||||
ItemEvent::Borrow => write!(f, "borrow"),
|
|
||||||
ItemEvent::Buy => write!(f, "buy"),
|
|
||||||
ItemEvent::Dispose => write!(f, "dispose"),
|
|
||||||
ItemEvent::Gift => write!(f, "gift"),
|
|
||||||
ItemEvent::Loan => write!(f, "loan"),
|
|
||||||
ItemEvent::Lose => write!(f, "lose"),
|
|
||||||
ItemEvent::RecieveGift => write!(f, "recieve gift"),
|
|
||||||
ItemEvent::ReturnBorrowed => write!(f, "return borrowed"),
|
|
||||||
ItemEvent::ReturnLoaned => write!(f, "return loaned"),
|
|
||||||
ItemEvent::Sell => write!(f, "sell"),
|
|
||||||
ItemEvent::Use => write!(f, "use"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
pub mod datalist;
|
pub mod datalist;
|
||||||
pub mod forms;
|
pub mod forms;
|
||||||
pub mod helpers;
|
pub mod helpers;
|
||||||
|
mod render;
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use maud::{html, Markup, Render, DOCTYPE};
|
use maud::{html, Markup, Render, DOCTYPE};
|
||||||
|
|
70
src/frontend/templates/render.rs
Normal file
70
src/frontend/templates/render.rs
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
|
||||||
|
use maud::{html, Markup, Render};
|
||||||
|
|
||||||
|
use crate::database::items::ItemPreview;
|
||||||
|
use crate::database::{item_classes::ItemClassPreview, item_events::ItemEvent, items::ItemName};
|
||||||
|
|
||||||
|
impl Render for ItemClassPreview {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
a href={ "/item-class/" (self.id) } { (self.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ItemEvent {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
ItemEvent::Acquire => write!(f, "acquire"),
|
||||||
|
ItemEvent::Borrow => write!(f, "borrow"),
|
||||||
|
ItemEvent::Buy => write!(f, "buy"),
|
||||||
|
ItemEvent::Dispose => write!(f, "dispose"),
|
||||||
|
ItemEvent::Gift => write!(f, "gift"),
|
||||||
|
ItemEvent::Loan => write!(f, "loan"),
|
||||||
|
ItemEvent::Lose => write!(f, "lose"),
|
||||||
|
ItemEvent::RecieveGift => write!(f, "recieve gift"),
|
||||||
|
ItemEvent::ReturnBorrowed => write!(f, "return borrowed"),
|
||||||
|
ItemEvent::ReturnLoaned => write!(f, "return loaned"),
|
||||||
|
ItemEvent::Sell => write!(f, "sell"),
|
||||||
|
ItemEvent::Use => write!(f, "use"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for ItemName {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Item(name) => write!(f, "{name}"),
|
||||||
|
Self::Class(name) => write!(f, "*{name}*"),
|
||||||
|
Self::None => write!(f, "[no name]"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ItemName {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
@match self {
|
||||||
|
Self::Item(name) => { (name) },
|
||||||
|
Self::Class(name) => { em { (name) } },
|
||||||
|
Self::None => { em { "[no name]" } },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ItemPreview {
|
||||||
|
fn render(&self) -> Markup {
|
||||||
|
html! {
|
||||||
|
a .me-1[self.state.is_some()] href={ "/item/" (self.id) } { (self.name) }
|
||||||
|
@if let Some(ref state) = self.state {
|
||||||
|
(state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,12 +12,13 @@ use barcode::{encode_code128, encode_data_matrix};
|
||||||
use pdf::{IndirectFontRef, PdfLayerReference};
|
use pdf::{IndirectFontRef, PdfLayerReference};
|
||||||
use printpdf as pdf;
|
use printpdf as pdf;
|
||||||
use printpdf::{ImageTransform, Mm, PdfDocument, PdfDocumentReference, Pt, Px};
|
use printpdf::{ImageTransform, Mm, PdfDocument, PdfDocumentReference, Pt, Px};
|
||||||
use sqlx::{query_as, PgPool};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
pub use preset::LabelPreset;
|
pub use preset::LabelPreset;
|
||||||
|
|
||||||
|
use crate::database::ItemRepository;
|
||||||
|
|
||||||
const FONT: Cursor<&[u8]> = Cursor::new(include_bytes!(
|
const FONT: Cursor<&[u8]> = Cursor::new(include_bytes!(
|
||||||
"../../assets/fonts/IosevkaLi7y-Regular.subset.ttf"
|
"../../assets/fonts/IosevkaLi7y-Regular.subset.ttf"
|
||||||
));
|
));
|
||||||
|
@ -32,6 +33,8 @@ pub enum Error {
|
||||||
Io(#[from] std::io::Error),
|
Io(#[from] std::io::Error),
|
||||||
#[error("PDF error: {0}")]
|
#[error("PDF error: {0}")]
|
||||||
PrintPdf(#[from] printpdf::Error),
|
PrintPdf(#[from] printpdf::Error),
|
||||||
|
#[error("SQLx error: {0}")]
|
||||||
|
Sqlx(#[from] sqlx::Error),
|
||||||
|
|
||||||
#[error("data is incomplete ({0} not given in data, but required in config)")]
|
#[error("data is incomplete ({0} not given in data, but required in config)")]
|
||||||
DataIncomplete(String),
|
DataIncomplete(String),
|
||||||
|
@ -247,17 +250,13 @@ impl Label {
|
||||||
Ok(doc.ok_or(Error::NoPages)?.save_to_bytes()?)
|
Ok(doc.ok_or(Error::NoPages)?.save_to_bytes()?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn for_items(pool: &PgPool, ids: &[Uuid], config: LabelConfig) -> sqlx::Result<Self> {
|
pub async fn for_items(
|
||||||
|
item_repo: &ItemRepository,
|
||||||
|
ids: &[Uuid],
|
||||||
|
config: LabelConfig,
|
||||||
|
) -> Result<Self> {
|
||||||
Ok(Label {
|
Ok(Label {
|
||||||
pages: query_as!(
|
pages: item_repo.label_pages(ids).await?,
|
||||||
LabelPage,
|
|
||||||
r#"SELECT id AS "id?", to_char(short_id, '000000') AS "short_id?"
|
|
||||||
FROM items
|
|
||||||
WHERE id = ANY ($1)"#,
|
|
||||||
ids
|
|
||||||
)
|
|
||||||
.fetch_all(pool)
|
|
||||||
.await?,
|
|
||||||
config,
|
config,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
pub mod database;
|
||||||
pub mod frontend;
|
pub mod frontend;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
|
|
@ -8,6 +8,10 @@ use actix_web::middleware::ErrorHandlers;
|
||||||
use actix_web::{cookie::Key, http::StatusCode, web, App, HttpResponse, HttpServer};
|
use actix_web::{cookie::Key, http::StatusCode, web, App, HttpResponse, HttpServer};
|
||||||
use base64::prelude::{Engine as _, BASE64_STANDARD};
|
use base64::prelude::{Engine as _, BASE64_STANDARD};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use li7y::database::item_events::ItemEventRepository;
|
||||||
|
use li7y::database::item_states::ItemStateRepository;
|
||||||
|
use li7y::database::items::ItemRepository;
|
||||||
|
use li7y::database::ItemClassRepository;
|
||||||
use log::{info, warn};
|
use log::{info, warn};
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
|
@ -54,6 +58,10 @@ async fn main() -> std::io::Result<()> {
|
||||||
App::new()
|
App::new()
|
||||||
.app_data(web::Data::new(config.clone()))
|
.app_data(web::Data::new(config.clone()))
|
||||||
.app_data(web::Data::new(pool.clone()))
|
.app_data(web::Data::new(pool.clone()))
|
||||||
|
.app_data(web::Data::new(ItemClassRepository::new(pool.clone())))
|
||||||
|
.app_data(web::Data::new(ItemEventRepository::new(pool.clone())))
|
||||||
|
.app_data(web::Data::new(ItemStateRepository::new(pool.clone())))
|
||||||
|
.app_data(web::Data::new(ItemRepository::new(pool.clone())))
|
||||||
.service(web::scope("/static").route(
|
.service(web::scope("/static").route(
|
||||||
"/{_:.*}",
|
"/{_:.*}",
|
||||||
web::get().to(|path: web::Path<String>| async {
|
web::get().to(|path: web::Path<String>| async {
|
||||||
|
|
Loading…
Reference in a new issue