parent
949b6b0238
commit
cf65324827
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="inventory-app.db" uuid="a0dc3bc5-119e-4708-b51f-2c3820c30deb">
|
||||||
|
<driver-ref>sqlite.xerial</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:sqlite:inventory-app.db</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/migrations/20241107225934_initial.sql" dialect="SQLite" />
|
||||||
|
<file url="PROJECT" dialect="SQLite" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
Binary file not shown.
@ -0,0 +1,30 @@
|
|||||||
|
use crate::error::{AppError};
|
||||||
|
use crate::ingest::{ingest_catalog_bytes};
|
||||||
|
use crate::session::SessionUser;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use askama_axum::Response;
|
||||||
|
use axum::extract::{Multipart, State};
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use std::format;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub async fn catalog(
|
||||||
|
State(db): State<SqlitePool>,
|
||||||
|
user: SessionUser,
|
||||||
|
mut multipart: Multipart,
|
||||||
|
) -> Result<Response, AppError> {
|
||||||
|
let mut filename = "".to_owned();
|
||||||
|
while let Some(field) = multipart.next_field().await? {
|
||||||
|
filename = field.file_name().ok_or(anyhow!("field missing filename"))?.to_string();
|
||||||
|
|
||||||
|
let name = field.name().ok_or(anyhow!("field missing name"))?.to_string();
|
||||||
|
let content_type = field.content_type().ok_or(anyhow!("field missing content type"))?.to_string();
|
||||||
|
let data = field.bytes().await?;
|
||||||
|
|
||||||
|
info!("Name: {}, file: {}, content: {}, data: {} bytes", name, filename, content_type, data.len());
|
||||||
|
|
||||||
|
ingest_catalog_bytes(data, db.clone(), user.id).await?;
|
||||||
|
}
|
||||||
|
Ok(format!("File {} uploaded successfully", filename).into_response())
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
use crate::db::user::{add_user, get_user_by_name, DbUserRole};
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BootstrapData {
|
||||||
|
users: Vec<BootstrapUser>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct BootstrapUser {
|
||||||
|
name: String,
|
||||||
|
role: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn bootstrap_database(db: &SqlitePool) -> Result<()> {
|
||||||
|
|
||||||
|
let bootstrap_str = match std::env::var("BOOTSTRAP_DATA") {
|
||||||
|
Ok(s) => {
|
||||||
|
info!("bootstrap data found, updating db: {}", s);
|
||||||
|
s
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
info!("no Bootstrap data found");
|
||||||
|
return Ok(());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = ron::from_str::<BootstrapData>(&bootstrap_str)?;
|
||||||
|
|
||||||
|
for user in &data.users {
|
||||||
|
let role = DbUserRole::try_from_str(&user.role).ok_or(anyhow!("invalid role {}", user.role))?;
|
||||||
|
if get_user_by_name(db, &user.name).await?.is_none() {
|
||||||
|
let new_id = add_user(db, &user.name, role).await?;
|
||||||
|
info!("bootstrap new user {}:{} ({})", new_id, user.name, user.role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct DbPositiveAdjustment {
|
||||||
|
pub id: i64,
|
||||||
|
pub item: i64,
|
||||||
|
pub user: i64,
|
||||||
|
pub create_date: String,
|
||||||
|
pub target_date: String,
|
||||||
|
pub amount: f64,
|
||||||
|
pub unit_price: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_positive_adjustment(db: &SqlitePool, item: i64, user: i64,
|
||||||
|
create_date: &str, target_date: &str,
|
||||||
|
amount: f64, unit_price: i64)
|
||||||
|
-> Result<i64> {
|
||||||
|
let res = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO PositiveAdjustment (item, user, create_date, target_date, amount, unit_price)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
|
"#,
|
||||||
|
item, user, create_date, target_date, amount, unit_price
|
||||||
|
).execute(db).await?;
|
||||||
|
|
||||||
|
let new_id = res.last_insert_rowid();
|
||||||
|
|
||||||
|
Ok(new_id)
|
||||||
|
}
|
||||||
@ -0,0 +1,87 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use serde::Serialize;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct DbUser {
|
||||||
|
pub id: i64,
|
||||||
|
pub name: String,
|
||||||
|
pub role: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_user_by_name(db: &SqlitePool, user: &str) -> Result<Option<DbUser>> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
DbUser,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
role
|
||||||
|
FROM
|
||||||
|
User
|
||||||
|
WHERE
|
||||||
|
name = ?
|
||||||
|
"#,
|
||||||
|
user
|
||||||
|
).fetch_optional(db).await
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_user(db: &SqlitePool, name: &str, role: DbUserRole) -> Result<i64> {
|
||||||
|
let role = role.value();
|
||||||
|
let res = sqlx::query!(
|
||||||
|
r#"
|
||||||
|
INSERT INTO User(name, role)
|
||||||
|
VALUES (?, ?)
|
||||||
|
"#,
|
||||||
|
name, role
|
||||||
|
).execute(db).await?;
|
||||||
|
|
||||||
|
let new_id = res.last_insert_rowid();
|
||||||
|
|
||||||
|
Ok(new_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum DbUserRole {
|
||||||
|
Admin,
|
||||||
|
Editor,
|
||||||
|
Viewer,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl DbUserRole {
|
||||||
|
pub fn value(&self) -> i64 {
|
||||||
|
match self {
|
||||||
|
DbUserRole::Admin => {1}
|
||||||
|
DbUserRole::Editor => {10}
|
||||||
|
DbUserRole::Viewer => {99}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_str(s: &str) -> Self {
|
||||||
|
Self::try_from_str(s).unwrap_or_else(|| DbUserRole::Viewer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_from_str(s: &str) -> Option<Self> {
|
||||||
|
match s {
|
||||||
|
"admin" => Some(DbUserRole::Admin),
|
||||||
|
"editor" => Some(DbUserRole::Editor),
|
||||||
|
"viewer" => Some(DbUserRole::Viewer),
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
match self {
|
||||||
|
DbUserRole::Admin => { String::from("admin") }
|
||||||
|
DbUserRole::Editor => { String::from("editor") }
|
||||||
|
DbUserRole::Viewer => { String::from("viewer") }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use axum::body::Bytes;
|
||||||
|
use sqlx::SqlitePool;
|
||||||
|
use time::format_description::well_known::Iso8601;
|
||||||
|
use tracing::info;
|
||||||
|
use crate::db::inventory_item::add_inventory_item;
|
||||||
|
use crate::db::positive_adjustment::add_positive_adjustment;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct CatalogRecord {
|
||||||
|
name: String,
|
||||||
|
#[serde(alias = "qty")]
|
||||||
|
quantity: f64,
|
||||||
|
unit: String,
|
||||||
|
fractional: bool,
|
||||||
|
#[serde(alias = "reorder")]
|
||||||
|
reorder_point: f64,
|
||||||
|
#[serde(alias = "price")]
|
||||||
|
unit_price: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ingest_catalog_bytes(bytes: Bytes, db: SqlitePool, user_id: i64) -> Result<()> {
|
||||||
|
let reader = csv::Reader::from_reader(bytes.as_ref());
|
||||||
|
|
||||||
|
ingest_catalog(reader, db, user_id).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn ingest_catalog<T: std::io::Read>(mut reader: csv::Reader<T>, db: SqlitePool, user_id: i64) -> Result<()>
|
||||||
|
{
|
||||||
|
//TODO Is this how we want to do dates?
|
||||||
|
let timestamp = time::OffsetDateTime::now_utc().format(&Iso8601::DEFAULT)?;
|
||||||
|
|
||||||
|
for result in reader.deserialize() {
|
||||||
|
let record: CatalogRecord = result?;
|
||||||
|
|
||||||
|
let new_entry_id = add_inventory_item(&db,
|
||||||
|
&record.name,
|
||||||
|
record.reorder_point,
|
||||||
|
record.fractional,
|
||||||
|
&record.unit).await?;
|
||||||
|
|
||||||
|
let new_positive_adjustment = add_positive_adjustment(&db, new_entry_id,
|
||||||
|
user_id, ×tamp, ×tamp, record.quantity, record.unit_price).await?;
|
||||||
|
|
||||||
|
info!("Added new item: {}/{} - {}", new_entry_id, new_positive_adjustment, record.name);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
|
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<article>
|
<article id="item-{{item.id}}-card">
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<div><a href="/item/{{item.id}}/">{{ item.name }}</a></div>
|
<div><a href="/item/{{item.id}}/">{{ item.name }}</a></div>
|
||||||
|
<div>Count: <span id="item-{{item.id}}-count" hx-get="/item/{{item.id}}/count" hx-trigger="load">0</span></div>
|
||||||
<div>Reorder Point: {{ item.reorder_point }}</div>
|
<div>Reorder Point: {{ item.reorder_point }}</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Loading…
Reference in new issue