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

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

71
Cargo.lock generated
View File

@ -466,6 +466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde_core",
] ]
[[package]] [[package]]
@ -482,6 +483,7 @@ dependencies = [
"itoa", "itoa",
"libc", "libc",
"pq-sys", "pq-sys",
"r2d2",
"time", "time",
"uuid", "uuid",
] ]
@ -625,8 +627,10 @@ dependencies = [
"env_logger", "env_logger",
"ipnet", "ipnet",
"log", "log",
"r2d2",
"serde", "serde",
"serde_json", "serde_json",
"time",
"tokio", "tokio",
"toml", "toml",
"tower", "tower",
@ -1024,6 +1028,15 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.29"
@ -1127,6 +1140,29 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@ -1225,6 +1261,17 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.9.2" version = "0.9.2"
@ -1254,6 +1301,15 @@ dependencies = [
"getrandom", "getrandom",
] ]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.12.2" version = "1.12.2"
@ -1301,6 +1357,21 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
[[package]]
name = "scheduled-thread-pool"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19"
dependencies = [
"parking_lot",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"

View File

@ -9,13 +9,15 @@ askama = "0.15.1"
axum = { version = "0.8.8", features = ["macros", "ws"] } axum = { version = "0.8.8", features = ["macros", "ws"] }
camino = "1.2.2" camino = "1.2.2"
clap = { version = "4.5.54", features = ["derive"] } clap = { version = "4.5.54", features = ["derive"] }
diesel = { version = "2.3.5", features = ["uuid", "time", "postgres", "ipnet-address"] } diesel = { version = "2.3.5", features = ["uuid", "time", "postgres", "ipnet-address", "r2d2"] }
diesel_migrations = { version = "2.3.1", features = ["postgres"] } diesel_migrations = { version = "2.3.1", features = ["postgres"] }
env_logger = "0.11.8" env_logger = "0.11.8"
ipnet = "2.11.0" ipnet = "2.11.0"
log = "0.4.29" log = "0.4.29"
r2d2 = "0.8.10"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
time = { version = "0.3.45", features = ["macros", "serde"] }
tokio = { version = "1.49.0", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] } tokio = { version = "1.49.0", features = ["io-util", "macros", "net", "rt-multi-thread", "time"] }
toml = "0.9.11" toml = "0.9.11"
tower = { version = "0.5.3", features = ["full"] } tower = { version = "0.5.3", features = ["full"] }

View File

@ -13,6 +13,6 @@ CREATE TABLE IF NOT EXISTS users (
created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_login_at TIMESTAMPTZ, last_login_at TIMESTAMPTZ,
-- u128 bitfield for permissions -- i64 bitfield for permissions
permissions NUMERIC(39,0) NOT NULL DEFAULT 0 permissions BIGINT NOT NULL DEFAULT 0
) )

View File

@ -5,15 +5,15 @@ CREATE TABLE IF NOT EXISTS clients (
first_name TEXT NOT NULL, first_name TEXT NOT NULL,
last_name TEXT NOT NULL, last_name TEXT NOT NULL,
date_of_birth DATE, date_of_birth DATE NOT NULL,
phone_number TEXT, phone_number TEXT NOT NULL,
gov_id_number TEXT, gov_id_number TEXT NOT NULL,
house_number TEXT, house_number TEXT NOT NULL,
address_line TEXT, address_line TEXT NOT NULL,
city TEXT, city TEXT NOT NULL,
state TEXT, state TEXT NOT NULL,
postal_code TEXT, postal_code TEXT NOT NULL,
country TEXT, country TEXT NOT NULL,
worker_user_id BIGINT, worker_user_id BIGINT,

View File

@ -2,5 +2,6 @@ CREATE TABLE IF NOT EXISTS inventory_catalog (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT, description TEXT,
code TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now() created_at TIMESTAMPTZ NOT NULL DEFAULT now()
) )

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS service_catalog;

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS service_catalog (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
description TEXT,
value_string TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
)

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS assigned_services;

View File

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS assigned_services (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
client_id BIGINT NOT NULL,
catalog_id BIGINT NOT NULL,
CONSTRAINT fk_client_id FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE,
CONSTRAINT fk_catalog_id FOREIGN KEY (catalog_id) REFERENCES service_catalog(id) ON DELETE CASCADE
)

View File

@ -1,3 +1,11 @@
CREATE TABLE IF NOT EXISTS tickets ( CREATE TABLE IF NOT EXISTS tickets (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
service_id BIGINT NOT NULL,
created_by_user_id BIGINT NOT NULL,
CONSTRAINT fk_service_id FOREIGN KEY (service_id) REFERENCES assigned_services(id) ON DELETE CASCADE,
CONSTRAINT fk_user_id FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE CASCADE
) )

View File

@ -1,2 +0,0 @@
DROP TABLE IF EXISTS services;

View File

@ -1,7 +0,0 @@
CREATE TABLE IF NOT EXISTS services (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
client_id BIGINT NOT NULL,
CONSTRAINT fk_client_id FOREIGN KEY (client_id) REFERENCES clients(id) ON DELETE CASCADE
)

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS ticket_comments;

View File

@ -0,0 +1,11 @@
CREATE TABLE IF NOT EXISTS ticket_comments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
ticket_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
modified_at TIMESTAMPTZ,
content TEXT,
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_ticket_id FOREIGN KEY (ticket_id) REFERENCES tickets(id) ON DELETE CASCADE
)

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS attachments;

View File

@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS attachments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL,
comment_id BIGINT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
-- stored in file storage by attachment id
CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
CONSTRAINT fk_comment_id FOREIGN KEY (comment_id) REFERENCES ticket_comments(id) ON DELETE CASCADE
)

View File

@ -1,19 +1,31 @@
use diesel::{Connection, PgConnection}; use diesel::{Connection, PgConnection, r2d2::ConnectionManager};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use r2d2::Pool;
pub mod schema; pub mod schema;
pub mod models;
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!("./migrations"); 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(); conn.run_pending_migrations(MIGRATIONS).unwrap();
log::info!("Running migrations"); log::info!("Running migrations");
} }
pub fn start(cfg: &crate::config::Config) -> anyhow::Result<PgConnection> { pub fn start(cfg: &crate::config::Config) -> anyhow::Result<DbPool> {
let mut connection = PgConnection::establish(&cfg.database_url()?.to_string())?; let manager = ConnectionManager::<PgConnection>::new(&cfg.database_url()?.to_string());
run_migrations(&mut connection); let mut pool = Pool::builder()
Ok(connection) .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. // @generated automatically by Diesel CLI.
diesel::table! {
assigned_services (id) {
id -> Int8,
name -> Text,
client_id -> Int8,
catalog_id -> Int8,
}
}
diesel::table! { diesel::table! {
assigned_warehouse_managers (id) { assigned_warehouse_managers (id) {
id -> Int8, 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! { diesel::table! {
clients (id) { clients (id) {
id -> Int8, id -> Int8,
email -> Text, email -> Text,
first_name -> Text, first_name -> Text,
last_name -> Text, last_name -> Text,
date_of_birth -> Nullable<Date>, date_of_birth -> Date,
phone_number -> Nullable<Text>, phone_number -> Text,
gov_id_number -> Nullable<Text>, gov_id_number -> Text,
house_number -> Nullable<Text>, house_number -> Text,
address_line -> Nullable<Text>, address_line -> Text,
city -> Nullable<Text>, city -> Text,
state -> Nullable<Text>, state -> Text,
postal_code -> Nullable<Text>, postal_code -> Text,
country -> Nullable<Text>, country -> Text,
worker_user_id -> Nullable<Int8>, worker_user_id -> Nullable<Int8>,
} }
} }
@ -42,6 +60,7 @@ diesel::table! {
id -> Int8, id -> Int8,
name -> Text, name -> Text,
description -> Nullable<Text>, description -> Nullable<Text>,
code -> Text,
created_at -> Timestamptz, created_at -> Timestamptz,
} }
} }
@ -55,16 +74,34 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
services (id) { service_catalog (id) {
id -> Int8, id -> Int8,
name -> Text, 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! { diesel::table! {
tickets (id) { tickets (id) {
id -> Int8, 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>, phone_number -> Nullable<Text>,
created_at -> Timestamptz, created_at -> Timestamptz,
last_login_at -> Nullable<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 -> users (user_id));
diesel::joinable!(assigned_warehouse_managers -> warehouses (warehouse_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!(clients -> users (worker_user_id));
diesel::joinable!(inventory -> inventory_catalog (catalog_id)); diesel::joinable!(inventory -> inventory_catalog (catalog_id));
diesel::joinable!(inventory -> warehouses (warehouse_id)); diesel::joinable!(inventory -> warehouses (warehouse_id));
diesel::joinable!(invoices -> clients (client_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 -> users (user_id));
diesel::joinable!(warehouse_actions -> warehouses (warehouse_id)); diesel::joinable!(warehouse_actions -> warehouses (warehouse_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
assigned_services,
assigned_warehouse_managers, assigned_warehouse_managers,
attachments,
clients, clients,
inventory, inventory,
inventory_catalog, inventory_catalog,
invoices, invoices,
services, service_catalog,
ticket_comments,
tickets, tickets,
users, users,
warehouse_actions, warehouse_actions,

View File

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

View File

@ -1,22 +1,27 @@
use axum::{Router, routing::get}; use axum::{Router, routing::get};
use diesel::PgConnection;
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use crate::db::DbPool;
pub mod pages; 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 addr = format!("{}:{}", cfg.web.host, cfg.web.port);
let app = Router::new() let app = Router::new()
.route("/", get(pages::home::get_page)) .route("/", get(pages::home::get_page))
.route("/login", get(pages::login::get_page)) .route("/login", get(pages::login::get_page))
.route("/clients", get(pages::clients::get_page))
.nest_service( .nest_service(
"/static", "/static",
ServiceBuilder::new() ServiceBuilder::new()
.service(ServeDir::new("static")) .service(ServeDir::new("static"))
); )
.with_state(db);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap(); let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
log::info!("Listening on http://{addr}"); 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 askama::Template;
use axum::extract::State;
use axum::response::{Html, IntoResponse, Response}; use axum::response::{Html, IntoResponse, Response};
use axum::http::StatusCode;
use axum::{ use crate::db::DbPool;
routing::{get, post},
http::StatusCode,
Json, Router,
};
use crate::web::pages::{BaseTemplate, BaseTemplateCtx}; use crate::web::pages::{BaseTemplate, BaseTemplateCtx};
#[derive(Template)] #[derive(Template)]
#[template(path = "home.html")] #[template(path = "inventory/index.html")]
pub struct PageTemplate { pub struct PageTemplate {
pub ctx: BaseTemplateCtx, pub ctx: BaseTemplateCtx,
} }
impl BaseTemplate for PageTemplate { impl BaseTemplate for PageTemplate {
@ -30,18 +23,18 @@ impl BaseTemplate for PageTemplate {
} }
#[axum::debug_handler] #[axum::debug_handler]
pub async fn get_page() -> Response { pub async fn get_page(State(pool): State<DbPool>) -> Response {
fn inner() -> anyhow::Result<(StatusCode, String)> { async fn inner(_pool: &DbPool) -> anyhow::Result<(StatusCode, String)> {
let mut template = PageTemplate { let mut template = PageTemplate {
ctx: Default::default() ctx: Default::default(),
}; };
template.set_title("Home"); template.set_title("Clients");
Ok((StatusCode::OK, template.render()?)) Ok((StatusCode::OK, template.render()?))
} }
match inner() { match inner(&pool).await {
Ok((status, s)) => (status, Html(s)).into_response(), Ok((status, s)) => (status, Html(s)).into_response(),
Err(e) => { Err(e) => {
let s = crate::web::pages::error::get_error_page(e.to_string()).await; let s = crate::web::pages::error::get_error_page(e.to_string()).await;

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

View File

@ -50,21 +50,4 @@ footer {
} }
.login-form {
background: gray;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px;
}
.login-form input[type="email"],input[type="password"] {
width: 95%;
}
.login-form input[type="submit"] {
width: 98%;
margin-top: 10px;
}

18
static/login.css Normal file
View File

@ -0,0 +1,18 @@
.login-form {
background: gray;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 10px;
}
.login-form input[type="email"],input[type="password"] {
width: 95%;
}
.login-form input[type="submit"] {
width: 98%;
margin-top: 10px;
}

View File

@ -3,7 +3,8 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{{ self.title() }}</title> <title>{{ self.title() }}</title>
<link rel="stylesheet" href="/static/index.css"> <link rel="stylesheet" href="/static/base.css">
{% block headers %}{% endblock %}
</head> </head>
<body> <body>
{% include "header.html" %} {% include "header.html" %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
<table>
<tr>
<td>First Name</td>
<td>Last Name</td>
<td>Address</td>
<td>Number</td>
<td>Email</td>
</tr>
{% for client in clients %}
<tr>
<td>{{ client.first_name }}</td>
<td>{{ client.last_name }}</td>
<td>
{{ client.country}}
{{ client.city}}
{{ client.state}}
{{ client.address_line}}
{{ client.house_number}}
{{ client.postal_code}}
</td>
<td>{{ client.phone_number }}</td>
<td>{{ client.email }}</td>
</tr>
{% endfor %}
</table>
{% endblock %}

View File

@ -1,5 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %} {% block content %}
A server error happened: {{ error }} A server error happened: {{ error }}
{% endblock %} {% endblock %}

View File

@ -1,5 +1,10 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %} {% block content %}
Hewo world Hewo world

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}

View File

@ -1,5 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block headers %}
<link rel="stylesheet" href="/static/login.css">
{% endblock %}
{% block content %} {% block content %}
<form class="login-form" action_url="/login" method="POST"> <form class="login-form" action_url="/login" method="POST">

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block headers %}
{% endblock %}
{% block content %}
Hewwo wowld!!!!
{% endblock %}