feat: shifts
Oh boy what a mess this was lmao, I definitely should have done this is more atomic commits But it could also be argued that very little of this is standalone
This commit is contained in:
parent
1515294500
commit
96dd76e463
18 changed files with 1860 additions and 13 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1 +1,2 @@
|
||||||
/target
|
/target
|
||||||
|
.env
|
||||||
|
|
1606
Cargo.lock
generated
1606
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -4,6 +4,10 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
[lib]
|
||||||
|
name = "cm_lib"
|
||||||
|
path = "src/lib/mod.rs"
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.6.20"
|
axum = "0.6.20"
|
||||||
|
@ -14,6 +18,12 @@ tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tower-http = { version = "0.4.4", features = ["full"] }
|
tower-http = { version = "0.4.4", features = ["full"] }
|
||||||
ructe.workspace = true
|
ructe.workspace = true
|
||||||
|
tokio-stream = "0.1.14"
|
||||||
|
futures-util = "0.3.28"
|
||||||
|
diesel = { version = "2.1.0", features = ["chrono"] }
|
||||||
|
diesel-async = { version = "0.3.1", features = ["mysql", "deadpool"] }
|
||||||
|
dotenvy = "0.15"
|
||||||
|
chrono = "0.4.31"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
ructe.workspace = true
|
ructe.workspace = true
|
||||||
|
|
9
diesel.toml
Normal file
9
diesel.toml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# For documentation on how to configure this file,
|
||||||
|
# see https://diesel.rs/guides/configuring-diesel-cli
|
||||||
|
|
||||||
|
[print_schema]
|
||||||
|
file = "src/lib/schema.rs"
|
||||||
|
custom_type_derives = ["diesel::query_builder::QueryId"]
|
||||||
|
|
||||||
|
[migrations_directory]
|
||||||
|
dir = "migrations"
|
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# This DC is just to get a dev mysql db, nothing else.
|
||||||
|
# Use root/example as user/password credentials
|
||||||
|
version: '3.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: mysql:8.0.34
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 3306:3306
|
||||||
|
environment:
|
||||||
|
MYSQL_HOST: "%"
|
||||||
|
MYSQL_USER: clubmanager
|
||||||
|
MYSQL_PASSWORD: clubmanager
|
||||||
|
MYSQL_DATABASE: clubmanager
|
||||||
|
MYSQL_ROOT_PASSWORD: admin
|
||||||
|
TZ: America/Los_Angeles
|
||||||
|
|
||||||
|
adminer:
|
||||||
|
image: adminer
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
4
example.env
Normal file
4
example.env
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# This defines the mysql db url that clubmanager will use
|
||||||
|
# The default shown here is what the docker-compose.yml will generate
|
||||||
|
# Please don't use it for prod
|
||||||
|
DATABASE_URL=mysql://clubmanager:clubmanager@127.0.0.1/clubmanager
|
2
migrations/2023-09-24-104018_create_shifts/down.sql
Normal file
2
migrations/2023-09-24-104018_create_shifts/down.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE shifts
|
6
migrations/2023-09-24-104018_create_shifts/up.sql
Normal file
6
migrations/2023-09-24-104018_create_shifts/up.sql
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE shifts
|
||||||
|
(
|
||||||
|
id INT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
start DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
end DATETIME DEFAULT NULL
|
||||||
|
)
|
5
mprocs.yaml
Normal file
5
mprocs.yaml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
procs:
|
||||||
|
server:
|
||||||
|
shell: "cargo watch -x run"
|
||||||
|
docker:
|
||||||
|
shell: "docker compose up"
|
68
src/api.rs
Normal file
68
src/api.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
use axum::{extract::State, response::IntoResponse, routing::post};
|
||||||
|
use cm_lib::models::Shift;
|
||||||
|
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, SelectableHelper};
|
||||||
|
use diesel_async::{scoped_futures::ScopedFutureExt, AsyncConnection, RunQueryDsl};
|
||||||
|
|
||||||
|
use crate::axum_ructe::render;
|
||||||
|
use crate::AppState;
|
||||||
|
|
||||||
|
pub(crate) fn router() -> axum::Router<AppState> {
|
||||||
|
axum::Router::new()
|
||||||
|
.route("/shifts/open", post(open_shift))
|
||||||
|
.route("/shifts/close", post(close_shift))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_shift(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
let shift = {
|
||||||
|
let mut conn = state.connection.get().await.unwrap();
|
||||||
|
conn.transaction(|conn| {
|
||||||
|
use cm_lib::schema::shifts::dsl::*;
|
||||||
|
|
||||||
|
async move {
|
||||||
|
diesel::insert_into(shifts)
|
||||||
|
.default_values()
|
||||||
|
.execute(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
shifts
|
||||||
|
.order(id.desc())
|
||||||
|
.select(Shift::as_select())
|
||||||
|
.first(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.scope_boxed()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.optional()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
render!(crate::templates::home_html, shift)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close_shift(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
|
{
|
||||||
|
let mut conn = state.connection.get().await.unwrap();
|
||||||
|
conn.transaction(|conn| {
|
||||||
|
use cm_lib::schema::shifts::dsl::*;
|
||||||
|
|
||||||
|
async move {
|
||||||
|
let open_shift = shifts
|
||||||
|
.filter(end.is_null())
|
||||||
|
.select(Shift::as_select())
|
||||||
|
.first(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
diesel::update(shifts.filter(id.eq(open_shift.id)))
|
||||||
|
.set(end.eq(Some(chrono::Utc::now().naive_local())))
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.scope_boxed()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
render!(crate::templates::home_html, None)
|
||||||
|
}
|
2
src/lib/mod.rs
Normal file
2
src/lib/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod models;
|
||||||
|
pub mod schema;
|
10
src/lib/models.rs
Normal file
10
src/lib/models.rs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
use diesel::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Queryable, Selectable, Debug)]
|
||||||
|
#[diesel(table_name = crate::schema::shifts)]
|
||||||
|
#[diesel(check_for_backend(diesel::mysql::Mysql))]
|
||||||
|
pub struct Shift {
|
||||||
|
pub id: u32,
|
||||||
|
pub start: chrono::NaiveDateTime,
|
||||||
|
pub end: Option<chrono::NaiveDateTime>,
|
||||||
|
}
|
9
src/lib/schema.rs
Normal file
9
src/lib/schema.rs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
// @generated automatically by Diesel CLI.
|
||||||
|
|
||||||
|
diesel::table! {
|
||||||
|
shifts (id) {
|
||||||
|
id -> Unsigned<Integer>,
|
||||||
|
start -> Datetime,
|
||||||
|
end -> Nullable<Datetime>,
|
||||||
|
}
|
||||||
|
}
|
87
src/main.rs
87
src/main.rs
|
@ -1,19 +1,54 @@
|
||||||
|
mod api;
|
||||||
mod axum_ructe;
|
mod axum_ructe;
|
||||||
|
|
||||||
use axum_ructe::render;
|
use axum_ructe::render;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
|
extract::State,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
|
use cm_lib::models::Shift;
|
||||||
|
use diesel::result::OptionalExtension;
|
||||||
|
use diesel::{ExpressionMethods, QueryDsl, SelectableHelper};
|
||||||
|
use diesel_async::{
|
||||||
|
pooled_connection::{deadpool::Pool, AsyncDieselConnectionManager},
|
||||||
|
AsyncMysqlConnection, RunQueryDsl,
|
||||||
|
};
|
||||||
|
use dotenvy::dotenv;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
include!(concat!(env!("OUT_DIR"), "/templates.rs"));
|
||||||
|
|
||||||
|
async fn establish_connection() -> Pool<AsyncMysqlConnection> {
|
||||||
|
dotenv().ok();
|
||||||
|
|
||||||
|
let database_url = std::env::var("DATABASE_URL").expect("You must set DATABASE_URL");
|
||||||
|
let config = AsyncDieselConnectionManager::<AsyncMysqlConnection>::new(database_url);
|
||||||
|
|
||||||
|
Pool::builder(config)
|
||||||
|
.build()
|
||||||
|
.expect("Error making connection pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct AppState {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
connection: Pool<AsyncMysqlConnection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
async fn init() -> Self {
|
||||||
|
Self {
|
||||||
|
connection: establish_connection().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
// initialize tracing
|
// initialize tracing
|
||||||
|
@ -26,10 +61,38 @@ async fn main() {
|
||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
|
let state = AppState::init().await;
|
||||||
|
|
||||||
|
// let shift = {
|
||||||
|
// let mut conn = state.connection.get().await.unwrap();
|
||||||
|
// conn.transaction(|conn| {
|
||||||
|
// use cm_lib::schema::shifts;
|
||||||
|
//
|
||||||
|
// async move {
|
||||||
|
// diesel::insert_into(shifts::table)
|
||||||
|
// .default_values()
|
||||||
|
// .execute(conn)
|
||||||
|
// .await?;
|
||||||
|
//
|
||||||
|
// shifts::table
|
||||||
|
// .order(shifts::id.desc())
|
||||||
|
// .select(Shift::as_select())
|
||||||
|
// .first(conn)
|
||||||
|
// .await
|
||||||
|
// }
|
||||||
|
// .scope_boxed()
|
||||||
|
// })
|
||||||
|
// .await
|
||||||
|
// };
|
||||||
|
|
||||||
|
// tracing::debug!("{shift:?}");
|
||||||
|
|
||||||
// build our application with a route
|
// build our application with a route
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
|
.nest("/api", api::router())
|
||||||
.route("/", get(root))
|
.route("/", get(root))
|
||||||
.route("/users", post(create_user));
|
.route("/users", post(create_user))
|
||||||
|
.with_state(state);
|
||||||
|
|
||||||
// run our app with hyper
|
// run our app with hyper
|
||||||
// `axum::Server` is a re-export of `hyper::Server`
|
// `axum::Server` is a re-export of `hyper::Server`
|
||||||
|
@ -42,8 +105,26 @@ async fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// basic handler that responds with a static string
|
// basic handler that responds with a static string
|
||||||
async fn root() -> impl IntoResponse {
|
async fn root(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
render!(templates::home_html)
|
let mut conn = state.connection.get().await.unwrap();
|
||||||
|
|
||||||
|
let open_shift: Option<Shift> = {
|
||||||
|
use cm_lib::schema::shifts::dsl::*;
|
||||||
|
|
||||||
|
shifts
|
||||||
|
.filter(end.is_null())
|
||||||
|
.select(Shift::as_select())
|
||||||
|
.first(&mut conn)
|
||||||
|
.await
|
||||||
|
.optional()
|
||||||
|
.expect("Query failed")
|
||||||
|
};
|
||||||
|
|
||||||
|
tracing::debug!("{open_shift:?}");
|
||||||
|
|
||||||
|
// let is_open_shift = open_shift.is_some();
|
||||||
|
|
||||||
|
render!(templates::home_html, open_shift)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_user(
|
async fn create_user(
|
||||||
|
|
|
@ -19,15 +19,14 @@
|
||||||
<!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> -->
|
<!-- <link rel="icon" href="/icon.svg" type="image/svg+xml"> -->
|
||||||
|
|
||||||
<script src="https://unpkg.com/htmx.org@@1.9.5"></script>
|
<script src="https://unpkg.com/htmx.org@@1.9.5"></script>
|
||||||
|
<script src="https://unpkg.com/htmx.org/dist/ext/sse.js"></script>
|
||||||
|
|
||||||
<!-- <link rel="manifest" href="site.webmanifest"> -->
|
<!-- <link rel="manifest" href="site.webmanifest"> -->
|
||||||
<meta name="theme-color" content="#fafafa">
|
<meta name="theme-color" content="#fafafa">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
@:body()
|
||||||
@:body()
|
|
||||||
</main>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
5
templates/components/close_shift_button.rs.html
Normal file
5
templates/components/close_shift_button.rs.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@()
|
||||||
|
|
||||||
|
<button hx-post="/api/shifts/close" hx-swap="outerHTML" type="button">
|
||||||
|
Close Shift
|
||||||
|
</button>
|
5
templates/components/open_shift_button.rs.html
Normal file
5
templates/components/open_shift_button.rs.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@()
|
||||||
|
|
||||||
|
<button hx-post="/api/shifts/open" hx-swap="outerHTML" type="button">
|
||||||
|
Open Shift
|
||||||
|
</button>
|
|
@ -1,9 +1,20 @@
|
||||||
@use super::base_html;
|
@use super::base_html;
|
||||||
|
@use super::components::close_shift_button_html;
|
||||||
|
@use super::components::open_shift_button_html;
|
||||||
|
@use cm_lib::models::Shift;
|
||||||
|
|
||||||
@()
|
@(open_shift: Option<Shift>)
|
||||||
|
|
||||||
@:base_html({
|
@:base_html({
|
||||||
|
|
||||||
<h1>Welcome to clubmanager!</h1>
|
<main>
|
||||||
|
<div>
|
||||||
|
@if open_shift.is_none() {
|
||||||
|
@:open_shift_button_html()
|
||||||
|
} else {
|
||||||
|
@:close_shift_button_html()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue