Implement more tables, add more page templates, initial working db example

This commit is contained in:
2026-01-14 00:00:12 +02:00
parent bcbbef1a82
commit 1ecbdde2c0
41 changed files with 583 additions and 147 deletions

View File

@@ -1,19 +1,31 @@
use diesel::{Connection, PgConnection};
use diesel::{Connection, PgConnection, r2d2::ConnectionManager};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use r2d2::Pool;
pub mod schema;
pub mod models;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations");
fn run_migrations(conn: &mut impl MigrationHarness<diesel::pg::Pg>) {
pub type DbPool = Pool<ConnectionManager<PgConnection>>;
fn run_migrations(pool: &mut DbPool) {
let mut conn = pool
.get()
.expect("failed to get db connection :3");
conn.run_pending_migrations(MIGRATIONS).unwrap();
log::info!("Running migrations");
}
pub fn start(cfg: &crate::config::Config) -> anyhow::Result<PgConnection> {
let mut connection = PgConnection::establish(&cfg.database_url()?.to_string())?;
run_migrations(&mut connection);
Ok(connection)
pub fn start(cfg: &crate::config::Config) -> anyhow::Result<DbPool> {
let manager = ConnectionManager::<PgConnection>::new(&cfg.database_url()?.to_string());
let mut pool = Pool::builder()
.build(manager)
.expect("failed to create pool :3");
run_migrations(&mut pool);
Ok(pool)
}

171
src/db/models.rs Normal file
View File

@@ -0,0 +1,171 @@
use diesel::prelude::*;
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(User, foreign_key=worker_user_id))]
#[diesel(table_name = super::schema::clients)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Client {
pub id: i64,
pub email: String,
pub first_name: String,
pub last_name: String,
pub date_of_birth: time::Date,
pub phone_number: String,
pub gov_id_number: String,
pub house_number: String,
pub address_line: String,
pub city: String,
pub state: String,
pub postal_code: String,
pub country: String,
pub worker_user_id: Option<i64>,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(User))]
#[diesel(belongs_to(Warehouse))]
#[diesel(table_name = super::schema::assigned_warehouse_managers)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct AssignedWarehouseManager {
pub id: i64,
pub user_id: i64,
pub warehouse_id: i64,
pub assigned_at: time::OffsetDateTime,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(User))]
#[diesel(table_name = super::schema::attachments)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Attachment {
pub id: i64,
pub user_id: i64,
pub comment_id: i64,
pub created_at: time::OffsetDateTime,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(Warehouse))]
#[diesel(belongs_to(InventoryCatalogEntry, foreign_key=catalog_id))]
#[diesel(table_name = super::schema::inventory)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Inventory {
pub id: i64,
pub warehouse_id: i64,
pub catalog_id: i64,
pub count: i64
}
#[derive(Queryable, Selectable)]
#[diesel(table_name = super::schema::inventory_catalog)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct InventoryCatalogEntry {
pub id: i64,
pub name: String,
pub code: String,
pub description: Option<String>,
pub created_at: time::OffsetDateTime,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(Client))]
#[diesel(table_name = super::schema::invoices)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Invoice {
pub id: i64,
pub client_id: i64,
pub amount: f32
}
#[derive(Queryable, Selectable)]
#[diesel(table_name = super::schema::service_catalog)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct ServiceCatalogEntry {
pub id: i64,
pub name: String,
pub description: Option<String>,
pub value_string: Option<String>,
pub created_at: time::OffsetDateTime,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(Client))]
#[diesel(belongs_to(ServiceCatalogEntry, foreign_key=catalog_id))]
#[diesel(table_name = super::schema::assigned_services)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct AssignedService {
pub id: i64,
pub client_id: i64,
pub catalog_id: i64,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(User))]
#[diesel(belongs_to(Ticket))]
#[diesel(table_name = super::schema::ticket_comments)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct TicketComment {
pub id: i64,
pub user_id: i64,
pub ticket_id: i64,
pub created_at: time::OffsetDateTime,
pub modified_at: Option<time::OffsetDateTime>,
pub content: Option<String>
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(AssignedService, foreign_key=service_id))]
#[diesel(table_name = super::schema::tickets)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Ticket {
pub id: i64,
pub title: String,
pub description: Option<String>,
pub created_at: time::OffsetDateTime,
pub created_by_user_id: i64,
pub service_id: i64
}
#[derive(Queryable, Selectable)]
#[diesel(table_name = super::schema::users)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct User {
pub id: i64,
pub username: String,
pub email: String,
pub password_hash: String,
pub password_salt: String,
pub first_name: String,
pub last_name: String,
pub display_name: Option<String>,
pub date_of_birth: Option<time::Date>,
pub phone_number: Option<String>,
pub created_at: time::OffsetDateTime,
pub last_login_at: Option<time::OffsetDateTime>,
pub permissions: i64,
}
#[derive(Queryable, Selectable, Associations)]
#[diesel(belongs_to(User))]
#[diesel(belongs_to(Warehouse))]
#[diesel(table_name = super::schema::warehouse_actions)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct WarehouseAction {
pub id: i64,
pub user_id: i64,
pub warehouse_id: i64,
pub count: i64,
pub reason: String,
pub timestamp: time::OffsetDateTime,
}
#[derive(Queryable, Selectable)]
#[diesel(table_name = super::schema::warehouses)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Warehouse {
pub id: i64,
pub name: String,
pub created_at: time::OffsetDateTime,
}

View File

@@ -1,5 +1,14 @@
// @generated automatically by Diesel CLI.
diesel::table! {
assigned_services (id) {
id -> Int8,
name -> Text,
client_id -> Int8,
catalog_id -> Int8,
}
}
diesel::table! {
assigned_warehouse_managers (id) {
id -> Int8,
@@ -9,21 +18,30 @@ diesel::table! {
}
}
diesel::table! {
attachments (id) {
id -> Int8,
user_id -> Int8,
comment_id -> Int8,
created_at -> Timestamptz,
}
}
diesel::table! {
clients (id) {
id -> Int8,
email -> Text,
first_name -> Text,
last_name -> Text,
date_of_birth -> Nullable<Date>,
phone_number -> Nullable<Text>,
gov_id_number -> Nullable<Text>,
house_number -> Nullable<Text>,
address_line -> Nullable<Text>,
city -> Nullable<Text>,
state -> Nullable<Text>,
postal_code -> Nullable<Text>,
country -> Nullable<Text>,
date_of_birth -> Date,
phone_number -> Text,
gov_id_number -> Text,
house_number -> Text,
address_line -> Text,
city -> Text,
state -> Text,
postal_code -> Text,
country -> Text,
worker_user_id -> Nullable<Int8>,
}
}
@@ -42,6 +60,7 @@ diesel::table! {
id -> Int8,
name -> Text,
description -> Nullable<Text>,
code -> Text,
created_at -> Timestamptz,
}
}
@@ -55,16 +74,34 @@ diesel::table! {
}
diesel::table! {
services (id) {
service_catalog (id) {
id -> Int8,
name -> Text,
client_id -> Int8,
description -> Nullable<Text>,
value_string -> Nullable<Text>,
created_at -> Timestamptz,
}
}
diesel::table! {
ticket_comments (id) {
id -> Int8,
user_id -> Int8,
ticket_id -> Int8,
created_at -> Timestamptz,
modified_at -> Nullable<Timestamptz>,
content -> Nullable<Text>,
}
}
diesel::table! {
tickets (id) {
id -> Int8,
title -> Text,
description -> Nullable<Text>,
created_at -> Timestamptz,
service_id -> Int8,
created_by_user_id -> Int8,
}
}
@@ -82,7 +119,7 @@ diesel::table! {
phone_number -> Nullable<Text>,
created_at -> Timestamptz,
last_login_at -> Nullable<Timestamptz>,
permissions -> Numeric,
permissions -> Int8,
}
}
@@ -105,23 +142,33 @@ diesel::table! {
}
}
diesel::joinable!(assigned_services -> clients (client_id));
diesel::joinable!(assigned_services -> service_catalog (catalog_id));
diesel::joinable!(assigned_warehouse_managers -> users (user_id));
diesel::joinable!(assigned_warehouse_managers -> warehouses (warehouse_id));
diesel::joinable!(attachments -> ticket_comments (comment_id));
diesel::joinable!(attachments -> users (user_id));
diesel::joinable!(clients -> users (worker_user_id));
diesel::joinable!(inventory -> inventory_catalog (catalog_id));
diesel::joinable!(inventory -> warehouses (warehouse_id));
diesel::joinable!(invoices -> clients (client_id));
diesel::joinable!(services -> clients (client_id));
diesel::joinable!(ticket_comments -> tickets (ticket_id));
diesel::joinable!(ticket_comments -> users (user_id));
diesel::joinable!(tickets -> assigned_services (service_id));
diesel::joinable!(tickets -> users (created_by_user_id));
diesel::joinable!(warehouse_actions -> users (user_id));
diesel::joinable!(warehouse_actions -> warehouses (warehouse_id));
diesel::allow_tables_to_appear_in_same_query!(
assigned_services,
assigned_warehouse_managers,
attachments,
clients,
inventory,
inventory_catalog,
invoices,
services,
service_catalog,
ticket_comments,
tickets,
users,
warehouse_actions,

View File

@@ -19,6 +19,6 @@ async fn main() -> anyhow::Result<()> {
}
let db = db::start(&cfg)?;
web::start(&cfg).await?;
web::start(&cfg, db).await?;
Ok(())
}

View File

@@ -1,22 +1,27 @@
use axum::{Router, routing::get};
use diesel::PgConnection;
use tower::ServiceBuilder;
use tower_http::services::ServeDir;
use crate::db::DbPool;
pub mod pages;
pub async fn start(cfg: &crate::config::Config) -> anyhow::Result<()> {
pub async fn start(cfg: &crate::config::Config, db: DbPool) -> anyhow::Result<()> {
let addr = format!("{}:{}", cfg.web.host, cfg.web.port);
let app = Router::new()
.route("/", get(pages::home::get_page))
.route("/login", get(pages::login::get_page))
.route("/clients", get(pages::clients::get_page))
.nest_service(
"/static",
ServiceBuilder::new()
.service(ServeDir::new("static"))
);
)
.with_state(db);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
log::info!("Listening on http://{addr}");

View File

@@ -0,0 +1,55 @@
use askama::Template;
use axum::extract::State;
use axum::response::{Html, IntoResponse, Response};
use axum::http::StatusCode;
use crate::db::DbPool;
use crate::db::models::Client;
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "clients/index.html")]
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
pub clients: Vec<Client>
}
impl BaseTemplate for PageTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
&mut self.ctx
}
}
#[axum::debug_handler]
pub async fn get_page(State(pool): State<DbPool>) -> Response {
async fn inner(pool: &DbPool) -> anyhow::Result<(StatusCode, String)> {
use diesel::prelude::*;
use crate::db::schema::clients::dsl::*;
let results = clients
.order(id.asc())
.limit(50)
.load::<Client>(&mut pool.get()?)?;
let mut template = PageTemplate {
ctx: Default::default(),
clients: results
};
template.set_title("Clients");
Ok((StatusCode::OK, template.render()?))
}
match inner(&pool).await {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;
(StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
}
}
}

View File

@@ -1,51 +0,0 @@
use askama::Template;
use axum::response::{Html, IntoResponse, Response};
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
pub struct HomeTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for HomeTemplate {
fn ctx(&self) -> &BaseTemplateCtx {
&self.ctx
}
fn ctx_mut(&mut self) -> &mut BaseTemplateCtx {
&mut self.ctx
}
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = HomeTemplate {
ctx: Default::default()
};
template.set_title("Home");
Ok((StatusCode::OK, template.render()?))
}
match inner() {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;
(StatusCode::INTERNAL_SERVER_ERROR, Html(s)).into_response()
}
}
}

View File

@@ -1,22 +1,15 @@
use askama::Template;
use axum::extract::State;
use axum::response::{Html, IntoResponse, Response};
use axum::http::StatusCode;
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::db::DbPool;
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
#[template(path = "inventory/index.html")]
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for PageTemplate {
@@ -30,18 +23,18 @@ impl BaseTemplate for PageTemplate {
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
pub async fn get_page(State(pool): State<DbPool>) -> Response {
async fn inner(_pool: &DbPool) -> anyhow::Result<(StatusCode, String)> {
let mut template = PageTemplate {
ctx: Default::default()
ctx: Default::default(),
};
template.set_title("Tickets");
template.set_title("Clients");
Ok((StatusCode::OK, template.render()?))
}
match inner() {
match inner(&pool).await {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;

View File

@@ -1,4 +1,5 @@
use askama::Template;
use axum::extract::State;
use axum::response::{Html, IntoResponse, Response};
use axum::{
@@ -7,6 +8,7 @@ use axum::{
Json, Router,
};
use crate::db::DbPool;
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
@@ -27,7 +29,7 @@ impl BaseTemplate for PageTemplate {
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
pub async fn get_page(State(_db): State<DbPool>) -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
let mut template = PageTemplate {
ctx: Default::default()

View File

@@ -1,22 +1,15 @@
use askama::Template;
use axum::extract::State;
use axum::response::{Html, IntoResponse, Response};
use axum::http::StatusCode;
use axum::{
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::db::DbPool;
use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)]
#[template(path = "home.html")]
#[template(path = "tickets/index.html")]
pub struct PageTemplate {
pub ctx: BaseTemplateCtx,
}
impl BaseTemplate for PageTemplate {
@@ -30,18 +23,18 @@ impl BaseTemplate for PageTemplate {
}
#[axum::debug_handler]
pub async fn get_page() -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> {
pub async fn get_page(State(pool): State<DbPool>) -> Response {
async fn inner(_pool: &DbPool) -> anyhow::Result<(StatusCode, String)> {
let mut template = PageTemplate {
ctx: Default::default()
ctx: Default::default(),
};
template.set_title("Home");
template.set_title("Clients");
Ok((StatusCode::OK, template.render()?))
}
match inner() {
match inner(&pool).await {
Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await;