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 %}
|
||||
<article>
|
||||
<article id="item-{{item.id}}-card">
|
||||
<div class="grid">
|
||||
<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>
|
||||
</article>
|
||||
|
||||
Loading…
Reference in new issue