Compare commits
12 commits
develop
...
graphql-ap
Author | SHA1 | Date | |
---|---|---|---|
fe13723323 | |||
63fc7a0721 | |||
c3a6b5f572 | |||
26c86fe03e | |||
5e24738bba | |||
f8bff689e6 | |||
368a3ff10e | |||
68ca95f166 | |||
0b7471467e | |||
33c9520d5d | |||
5d6a26a3ed | |||
ef4507e25e |
25 changed files with 874 additions and 393 deletions
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -1,2 +1,9 @@
|
|||
# Ignore Rust stuff
|
||||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
# Ignore database
|
||||
/db/*.db
|
||||
|
||||
# Ignore dotenv environment file
|
||||
/.env
|
||||
|
|
557
Cargo.lock
generated
557
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
10
Cargo.toml
10
Cargo.toml
|
@ -8,11 +8,14 @@ A web application for task/project time registration and invoicing.
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
diesel_migrations = "1.4.0"
|
||||
rocket = "0.4.2"
|
||||
serde = "1.0.92"
|
||||
serde_json = "1.0.39"
|
||||
serde_derive = "1.0.92"
|
||||
dotenv = "0.14.1"
|
||||
juniper = "0.12.0"
|
||||
juniper_rocket = "0.3.0"
|
||||
juniper_codegen = "0.12.0"
|
||||
|
||||
[dependencies.chrono]
|
||||
version = "0.4.6"
|
||||
|
@ -25,4 +28,7 @@ features = ["chrono", "sqlite"]
|
|||
[dependencies.rocket_contrib]
|
||||
version = "0.4.2"
|
||||
default-features = false
|
||||
features = ["json", "serve"]
|
||||
features = ["diesel_sqlite_pool", "json", "serve"]
|
||||
|
||||
[dev-dependencies]
|
||||
dotenv = "0.14.1"
|
||||
|
|
5
Rocket.toml
Normal file
5
Rocket.toml
Normal file
|
@ -0,0 +1,5 @@
|
|||
[development.databases]
|
||||
stoptime_db = { url = "db/stoptime-dev.db" }
|
||||
|
||||
[production.databases]
|
||||
stoptime_db = { url = "db/stoptime.db" }
|
0
db/.gitkeep
Normal file
0
db/.gitkeep
Normal file
|
@ -5,7 +5,7 @@ CREATE TABLE customers (
|
|||
"address_street" VARCHAR(255) NOT NULL,
|
||||
"email" VARCHAR(255) NOT NULL,
|
||||
"financial_contact" VARCHAR(255) NOT NULL,
|
||||
"hourly_rate" FLOAT,
|
||||
"hourly_rate" DOUBLE,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"phone" VARCHAR(255) NOT NULL,
|
||||
"short_name" VARCHAR(255) NOT NULL,
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
CREATE TABLE tasks (
|
||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
"customer_id" INTEGER NOT NULL,
|
||||
"fixed_cost" FLOAT,
|
||||
"hourly_rate" FLOAT,
|
||||
"fixed_cost" DOUBLE,
|
||||
"hourly_rate" DOUBLE,
|
||||
"invoice_comment" VARCHAR(255) NOT NULL,
|
||||
"invoice_id" INTEGER,
|
||||
"name" VARCHAR(255) NOT NULL,
|
||||
"vat_rate" FLOAT NOT NULL,
|
||||
"vat_rate" DOUBLE NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! The application error catchers
|
||||
//! The application error catchers.
|
||||
|
||||
use rocket::catch;
|
||||
use rocket_contrib::json;
|
||||
|
@ -12,12 +12,3 @@ pub fn not_found() -> JsonValue {
|
|||
"reason": "Resource was not found",
|
||||
})
|
||||
}
|
||||
|
||||
/// Catches an HTTP 422 (Unprocessable Entity) error.
|
||||
#[catch(422)]
|
||||
pub fn unprocessable_entity() -> JsonValue {
|
||||
json!({
|
||||
"status": "error",
|
||||
"reason": "Could not parse JSON body or fields were missing",
|
||||
})
|
||||
}
|
||||
|
|
92
src/graphql.rs
Normal file
92
src/graphql.rs
Normal file
|
@ -0,0 +1,92 @@
|
|||
//! The GraphQL schema implementation.
|
||||
//!
|
||||
//! Provides the schema with the root query and mutation.
|
||||
use crate::models::{
|
||||
CompanyInfo, Customer, Invoice, NewCustomer, NewInvoice, NewTimeEntry, TimeEntry,
|
||||
};
|
||||
use crate::DbConn;
|
||||
|
||||
use diesel::prelude::*;
|
||||
use juniper::{object, Context, FieldResult, RootNode};
|
||||
|
||||
impl Context for DbConn {}
|
||||
|
||||
/// The GraphQL schema.
|
||||
pub type Schema = RootNode<'static, Query, Mutation>;
|
||||
|
||||
/// The GraphQL query root.
|
||||
pub struct Query;
|
||||
|
||||
#[object(Context = DbConn)]
|
||||
impl Query {
|
||||
/// Returns the current API version.
|
||||
fn api_version() -> &'static str {
|
||||
"1.0"
|
||||
}
|
||||
|
||||
/// Returns the company info with the given ID.
|
||||
fn company_info(context: &DbConn, id: i32) -> FieldResult<CompanyInfo> {
|
||||
retrieve!(CompanyInfo, id, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns all known customers.
|
||||
fn customers(context: &DbConn) -> FieldResult<Vec<Customer>> {
|
||||
all!(Customer, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the customer with the given ID.
|
||||
fn customer(context: &DbConn, id: i32) -> FieldResult<Customer> {
|
||||
retrieve!(Customer, id, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns all known customers.
|
||||
fn customers(context: &DbConn) -> FieldResult<Vec<Customer>> {
|
||||
all!(Customer, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the customer with the given ID.
|
||||
fn invoice(context: &DbConn, id: i32) -> FieldResult<Invoice> {
|
||||
retrieve!(Invoice, id, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns all known invoices.
|
||||
fn invoices(context: &DbConn) -> FieldResult<Vec<Invoice>> {
|
||||
all!(Invoice, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the time entry with the given ID.
|
||||
fn time_entry(context: &DbConn, id: i32) -> FieldResult<TimeEntry> {
|
||||
retrieve!(TimeEntry, id, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns all known invoices.
|
||||
fn time_entries(context: &DbConn) -> FieldResult<Vec<TimeEntry>> {
|
||||
all!(TimeEntry, **context).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
/// The GraphQL mutation root.
|
||||
pub struct Mutation;
|
||||
|
||||
#[object(Context = DbConn)]
|
||||
impl Mutation {
|
||||
/// Returns the current API version.
|
||||
fn api_version() -> &'static str {
|
||||
"1.0"
|
||||
}
|
||||
|
||||
/// Creates a new customer.
|
||||
fn create_customer(context: &DbConn, new_customer: NewCustomer) -> FieldResult<Customer> {
|
||||
create!(Customer, new_customer, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Creates a new invoice.
|
||||
fn create_invoice(context: &DbConn, new_invoice: NewInvoice) -> FieldResult<Invoice> {
|
||||
create!(Invoice, new_invoice, **context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Creates a new time entry.
|
||||
fn create_time_entry(context: &DbConn, new_time_entry: NewTimeEntry) -> FieldResult<TimeEntry> {
|
||||
create!(TimeEntry, new_time_entry, **context).map_err(Into::into)
|
||||
}
|
||||
}
|
|
@ -1,16 +1,44 @@
|
|||
//! The root handlers
|
||||
//! The request handlers.
|
||||
|
||||
use rocket::get;
|
||||
use crate::graphql::Schema;
|
||||
use crate::DbConn;
|
||||
|
||||
pub mod company;
|
||||
pub mod customers;
|
||||
pub mod invoices;
|
||||
pub mod timeline;
|
||||
use juniper_rocket::{playground_source, GraphQLRequest, GraphQLResponse};
|
||||
use rocket::response::content::Html;
|
||||
use rocket::{get, post, State};
|
||||
|
||||
/// Presents the dashboard/overview as start/home view
|
||||
/// Presents the main web application.
|
||||
///
|
||||
/// It lists the running tasks and projects per customer and shows a global summary.
|
||||
/// FIXME: Not implemented yet.
|
||||
#[get("/")]
|
||||
pub fn index() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Presents a playground GraphQL web application.
|
||||
///
|
||||
/// This can be used to test the GraphQL backend.
|
||||
#[get("/graphql/playground")]
|
||||
pub fn graphql_playground() -> Html<String> {
|
||||
playground_source("/graphql")
|
||||
}
|
||||
|
||||
/// Handles a GraphQL GET request.
|
||||
#[get("/graphql?<request>")]
|
||||
pub fn graphql_get(
|
||||
request: GraphQLRequest,
|
||||
conn: DbConn,
|
||||
schema: State<Schema>,
|
||||
) -> GraphQLResponse {
|
||||
request.execute(&schema, &conn)
|
||||
}
|
||||
|
||||
/// Handles a GraphQL POST request.
|
||||
#[post("/graphql", data = "<request>")]
|
||||
pub fn graphql_post(
|
||||
request: GraphQLRequest,
|
||||
conn: DbConn,
|
||||
schema: State<Schema>,
|
||||
) -> GraphQLResponse {
|
||||
request.execute(&schema, &conn)
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
//! The company (information) handlers
|
||||
//!
|
||||
//! These handlers are for showing and updating information of the company of the user.
|
||||
|
||||
use rocket::{get, post};
|
||||
|
||||
/// Shows a form with the company information that allows for updating it.
|
||||
///
|
||||
/// When updating, it will create a new revision. The handler allows showing other revisions.
|
||||
// FIXME: Implement revisions!
|
||||
#[get("/")]
|
||||
pub fn index() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Updates the company information by creating a new revision.
|
||||
#[post("/")]
|
||||
pub fn create() {
|
||||
unimplemented!()
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
//! The customer handlers
|
||||
//!
|
||||
//! Handlers for viewing a list of existing customers or creating a new one.
|
||||
|
||||
use rocket::{delete, get, post, put};
|
||||
|
||||
pub mod invoices;
|
||||
pub mod tasks;
|
||||
|
||||
/// Shows the list of customers.
|
||||
#[get("/")]
|
||||
pub fn index() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Creates a new customer and redirects to the show handler.
|
||||
#[post("/")]
|
||||
pub fn create() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Provides the form for the data required to create a new customer.
|
||||
#[get("/new")]
|
||||
pub fn new() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Shows a form for viewing and updating information of the customer with the given ID.
|
||||
#[get("/<_id>")]
|
||||
pub fn show(_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Updates the customer with the given ID.
|
||||
#[put("/<_id>")]
|
||||
pub fn update(_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Destroys the customer with the given ID and redirects to the index handler.
|
||||
#[delete("/<_id>")]
|
||||
pub fn destroy(_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
//! The invoice handlers
|
||||
//!
|
||||
//! These handlers are for creating and viewing invoices for a specific customer.
|
||||
|
||||
use rocket::{get, post, put};
|
||||
|
||||
/// Creates a new invoice object for the customer the given ID.
|
||||
///
|
||||
/// A unique number is generated for the invoice by taking the year and a sequence number.
|
||||
///
|
||||
/// Fixed cost tasks are directly tied to the invoice.
|
||||
///
|
||||
/// For a task with an hourly rate, a task copy is created with the select time entries that need
|
||||
/// to be billed and put in the invoice; the remaining unbilled time entries are left in the
|
||||
/// original task.
|
||||
#[post("/<_customer_id>/invoices")]
|
||||
pub fn create(_customer_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Generates the form to create a new invoice object by listing unbulled fixed cost tasks and
|
||||
/// unbilled registered time (for tasks with an hourly rate) of the customer with the given ID so
|
||||
/// that a selection can be made.
|
||||
#[get("/<_customer_id>/invoices/new")]
|
||||
pub fn new(_customer_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Shows a form for the invoice with the given number for the customer with the given ID
|
||||
/// and shows a firm for updating it.
|
||||
// FIXME: Handle PDF and LaTex generation here too!
|
||||
#[get("/<_customer_id>/invoices/<_number>", rank = 2)] // Number could be "new"
|
||||
pub fn show(_customer_id: u32, _number: String) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Updates the invoices with the given number for the customer with the given ID.
|
||||
#[put("/<_customer_id>/invoices/<_number>")]
|
||||
pub fn update(_customer_id: u32, _number: String) {
|
||||
unimplemented!()
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
//! The tasks handlers
|
||||
//!
|
||||
//! These handlers are for creating, editing and deleting a task for a specific customer.
|
||||
|
||||
use rocket::{delete, get, post, put};
|
||||
|
||||
/// Creates a new task oject for a customer with the given ID.
|
||||
#[post("/<_customer_id>/tasks")]
|
||||
pub fn create(_customer_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Provides the form for the data required to create a new task for a customer with the given ID.
|
||||
#[get("/<_customer_id>/tasks/new")]
|
||||
pub fn new(_customer_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Shows a form for viewing and updating information of the task with the given ID for
|
||||
/// a customer with the given ID.
|
||||
#[get("/<_customer_id>/tasks/<_id>", rank = 2)] // FIXME: Why is rank 2 necessary?
|
||||
pub fn show(_customer_id: u32, _id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Updates the task with the given ID for a customer with the given ID.
|
||||
#[put("/<_customer_id>/tasks/<_id>")]
|
||||
pub fn update(_customer_id: u32, _id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Destroys the task with the given ID of a customer with the given ID.
|
||||
#[delete("/<_customer_id>/tasks/<_id>")]
|
||||
pub fn destroy(_customer_id: u32, _id: u32) {
|
||||
unimplemented!()
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
//! The invoices handlers
|
||||
//!
|
||||
//! The handler is used for showing a list of all invoices.
|
||||
|
||||
use rocket::get;
|
||||
|
||||
/// Shows a list of invoices, sorted per customer.
|
||||
#[get("/")]
|
||||
pub fn index() {
|
||||
unimplemented!()
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
//! The timeline handlers
|
||||
//!
|
||||
//! These handlers are used for presenting a timeline of registered time and also for quickly
|
||||
//! registering new time entries.
|
||||
|
||||
use rocket::{delete, get, post, put};
|
||||
|
||||
/// Retrieves all registered time entries in descending order to present the timeline.
|
||||
#[get("/")]
|
||||
pub fn index() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Registers a time entry and redirects back.
|
||||
#[post("/")]
|
||||
pub fn create() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Shows a form for quickly registering time using a list of customers and tasks and the current
|
||||
/// date and time.
|
||||
#[get("/new")]
|
||||
pub fn new() {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Show a form of the time entry with the given ID for updating it.
|
||||
#[get("/<_id>")]
|
||||
pub fn show(_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Update the time entry with the given ID.
|
||||
#[put("/<_id>")]
|
||||
pub fn update(_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
/// Destroys the time entry with the given ID.
|
||||
#[delete("/<_id>")]
|
||||
pub fn destroy(_id: u32) {
|
||||
unimplemented!()
|
||||
}
|
74
src/helpers.rs
Normal file
74
src/helpers.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
//! Some ORM helpers.
|
||||
//!
|
||||
//! These macros can be used to shorten repetative ORM queries.
|
||||
|
||||
#![allow(unused_macros)]
|
||||
|
||||
macro_rules! all {
|
||||
($model_name:ident, $conn:expr) => {{
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
let table = $crate::models::$model_name::table();
|
||||
table.load(&$conn)
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! retrieve {
|
||||
($model_name:ident, $primary_key:expr, $conn:expr) => {{
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
let table = $crate::models::$model_name::table();
|
||||
table.find($primary_key).first(&$conn)
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! create {
|
||||
($model_name:ident, $object:expr, $conn:expr) => {{
|
||||
use diesel::associations::HasTable;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, Table};
|
||||
|
||||
let table = $crate::models::$model_name::table();
|
||||
let primary_key = table.primary_key();
|
||||
$conn.transaction::<$model_name, diesel::result::Error, _>(|| {
|
||||
diesel::insert_into(table).values($object).execute(&$conn)?;
|
||||
table.order(primary_key.desc()).first(&$conn)
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! update {
|
||||
($model_name:ident, $primary_key:expr, $object:expr, $conn:expr) => {{
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
let table = $crate::models::$model_name::table();
|
||||
$conn.transaction::<_, diesel::result::Error, _>(|| {
|
||||
diesel::update(table.find($primary_key))
|
||||
.set($object)
|
||||
.execute(&$conn)?;
|
||||
table.find($primary_key).first(&$conn)
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! destroy {
|
||||
($model_name:ident, $primary_key:expr, $conn:expr) => {{
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
let table = $crate::models::$model_name::table();
|
||||
let result: Result<$model_name, _> = get!($model_name, $primary_key, $conn);
|
||||
match result {
|
||||
Ok(_) => diesel::delete(table.find($primary_key)).execute(&$conn),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! destroy_all {
|
||||
($model_name:ident, $conn:expr) => {{
|
||||
use diesel::associations::HasTable;
|
||||
use diesel::RunQueryDsl;
|
||||
|
||||
let table = $crate::models::$model_name::table();
|
||||
diesel::delete(table).execute(&$conn)
|
||||
}};
|
||||
}
|
83
src/main.rs
83
src/main.rs
|
@ -3,62 +3,67 @@
|
|||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
#[macro_use]
|
||||
extern crate diesel_migrations;
|
||||
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket::{catchers, routes, Rocket};
|
||||
use rocket_contrib::database;
|
||||
use rocket_contrib::serve::StaticFiles;
|
||||
|
||||
pub mod catchers;
|
||||
pub mod handlers;
|
||||
#[macro_use]
|
||||
pub mod helpers;
|
||||
|
||||
pub mod catchers;
|
||||
pub mod graphql;
|
||||
pub mod handlers;
|
||||
pub mod models;
|
||||
/// The application database schema.
|
||||
pub mod schema;
|
||||
|
||||
// This macro from `diesel_migrations` defines an `embedded_migrations` module containing a
|
||||
// function named `run`. This allows the example to be run and tested without any outside setup
|
||||
// of the database.
|
||||
embed_migrations!();
|
||||
|
||||
#[database("stoptime_db")]
|
||||
pub struct DbConn(diesel::SqliteConnection);
|
||||
|
||||
/// Runs the database migrations.
|
||||
fn run_db_migrations(rocket: Rocket) -> Result<Rocket, Rocket> {
|
||||
let conn = DbConn::get_one(&rocket).expect("Failed to get a database connection");
|
||||
match embedded_migrations::run(&*conn) {
|
||||
Ok(()) => Ok(rocket),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to run database migrations: {:?}", e);
|
||||
Err(rocket)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets up the Rocket application.
|
||||
fn rocket() -> Rocket {
|
||||
let static_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/static");
|
||||
let static_files = StaticFiles::from(static_dir);
|
||||
|
||||
rocket::ignite()
|
||||
.mount("/", routes![handlers::index])
|
||||
.attach(DbConn::fairing())
|
||||
.attach(AdHoc::on_attach("Database Migrations", run_db_migrations))
|
||||
.manage(graphql::Schema::new(graphql::Query, graphql::Mutation))
|
||||
.mount(
|
||||
"/company",
|
||||
routes![handlers::company::index, handlers::company::create],
|
||||
)
|
||||
.mount(
|
||||
"/customers",
|
||||
"/",
|
||||
routes![
|
||||
handlers::customers::index,
|
||||
handlers::customers::create,
|
||||
handlers::customers::new,
|
||||
handlers::customers::show,
|
||||
handlers::customers::update,
|
||||
handlers::customers::destroy,
|
||||
handlers::customers::invoices::create,
|
||||
handlers::customers::invoices::new,
|
||||
handlers::customers::invoices::show,
|
||||
handlers::customers::invoices::update,
|
||||
handlers::customers::tasks::create,
|
||||
handlers::customers::tasks::new,
|
||||
handlers::customers::tasks::show,
|
||||
handlers::customers::tasks::update,
|
||||
handlers::customers::tasks::destroy,
|
||||
handlers::index,
|
||||
handlers::graphql_playground,
|
||||
handlers::graphql_get,
|
||||
handlers::graphql_post,
|
||||
],
|
||||
)
|
||||
.mount("/invoices", routes![handlers::invoices::index])
|
||||
.mount("/static", static_files)
|
||||
.mount(
|
||||
"/timeline",
|
||||
routes![
|
||||
handlers::timeline::index,
|
||||
handlers::timeline::create,
|
||||
handlers::timeline::new,
|
||||
handlers::timeline::show,
|
||||
handlers::timeline::update,
|
||||
handlers::timeline::destroy,
|
||||
],
|
||||
)
|
||||
.register(catchers![
|
||||
catchers::not_found,
|
||||
catchers::unprocessable_entity
|
||||
])
|
||||
.register(catchers![catchers::not_found])
|
||||
}
|
||||
|
||||
/// Runs the Rocket application.
|
||||
fn main() {
|
||||
rocket().launch();
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//! The application models
|
||||
//! The application models.
|
||||
|
||||
mod company_info;
|
||||
mod customer;
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use diesel::prelude::*;
|
||||
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
|
||||
|
||||
use crate::schema::company_infos;
|
||||
use crate::DbConn;
|
||||
|
||||
/// The company (information) model.
|
||||
///
|
||||
/// This model represents information about the company or sole proprietorship of the user of
|
||||
/// StopTime.
|
||||
#[derive(Associations, Debug, Identifiable, Deserialize, Queryable, Serialize)]
|
||||
#[derive(Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||
#[belongs_to(CompanyInfo, foreign_key = "original_id")]
|
||||
#[table_name = "company_infos"]
|
||||
pub struct CompanyInfo {
|
||||
|
@ -57,10 +59,25 @@ pub struct CompanyInfo {
|
|||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl CompanyInfo {
|
||||
/// Returns the original/previous company information model (if any).
|
||||
pub fn original(&self, conn: &DbConn) -> QueryResult<Option<CompanyInfo>> {
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
match self.original_id {
|
||||
Some(original_id) => CompanyInfo::table()
|
||||
.find(original_id)
|
||||
.first(&**conn)
|
||||
.optional(),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The new company (information) model.
|
||||
///
|
||||
/// This model represents new company information that can be inserted into the database.
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[derive(Debug, GraphQLInputObject, Insertable)]
|
||||
#[table_name = "company_infos"]
|
||||
pub struct NewCompanyInfo {
|
||||
/// The international bank account number
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use diesel::prelude::*;
|
||||
use juniper_codegen::GraphQLInputObject;
|
||||
|
||||
use crate::models::{Invoice, Task};
|
||||
use crate::schema::customers;
|
||||
use crate::DbConn;
|
||||
|
||||
/// The customer model.
|
||||
///
|
||||
/// This model represents a customer that has projects/tasks for which invoices need to be
|
||||
/// generated.
|
||||
#[derive(AsChangeset, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
||||
#[derive(AsChangeset, Associations, Debug, Identifiable, Queryable)]
|
||||
#[table_name = "customers"]
|
||||
pub struct Customer {
|
||||
/// The unique identification number
|
||||
|
@ -23,7 +26,7 @@ pub struct Customer {
|
|||
/// The name of the financial contact person/department
|
||||
pub financial_contact: String,
|
||||
/// The default hourly rate (if applicable)
|
||||
pub hourly_rate: Option<f32>,
|
||||
pub hourly_rate: Option<f64>,
|
||||
/// The official (long) name
|
||||
pub name: String,
|
||||
/// The phone number
|
||||
|
@ -38,10 +41,22 @@ pub struct Customer {
|
|||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Customer {
|
||||
/// Returns the invoices billed to the customer.
|
||||
pub fn invoices(&self, conn: &DbConn) -> QueryResult<Vec<Invoice>> {
|
||||
Invoice::belonging_to(self).load(&**conn)
|
||||
}
|
||||
|
||||
/// Returns the project/tasks associated with the customer.
|
||||
pub fn tasks(&self, conn: &DbConn) -> QueryResult<Vec<Task>> {
|
||||
Task::belonging_to(self).load(&**conn)
|
||||
}
|
||||
}
|
||||
|
||||
/// The new customer model
|
||||
///
|
||||
/// This model represents a new customer that can be inserted into the database.
|
||||
#[derive(Default, Deserialize, Insertable, Serialize)]
|
||||
#[derive(Default, GraphQLInputObject, Insertable)]
|
||||
#[table_name = "customers"]
|
||||
pub struct NewCustomer {
|
||||
/// The city part of the address
|
||||
|
@ -55,7 +70,7 @@ pub struct NewCustomer {
|
|||
/// The name of the financial contact person/department
|
||||
pub financial_contact: String,
|
||||
/// The default hourly rate (if applicable)
|
||||
pub hourly_rate: Option<f32>,
|
||||
pub hourly_rate: Option<f64>,
|
||||
/// The official (long) name
|
||||
pub name: String,
|
||||
/// The phone number
|
||||
|
@ -65,3 +80,22 @@ pub struct NewCustomer {
|
|||
/// Flag whether the customer requires time specificaions
|
||||
pub time_specification: bool,
|
||||
}
|
||||
|
||||
mod graphql {
|
||||
use super::{Customer, DbConn, Invoice, Task};
|
||||
|
||||
use juniper::{object, FieldResult};
|
||||
|
||||
#[object(Context = DbConn)]
|
||||
impl Customer {
|
||||
/// Returns the invoices billed to the customer.
|
||||
fn invoices(&self, context: &DbConn) -> FieldResult<Vec<Invoice>> {
|
||||
self.invoices(context).map_err(Into::into)
|
||||
}
|
||||
|
||||
/// Returns the project/tasks associated with the customer.
|
||||
fn tasks(&self, context: &DbConn) -> FieldResult<Vec<Task>> {
|
||||
self.tasks(context).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use diesel::prelude::*;
|
||||
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
|
||||
|
||||
use crate::models::{CompanyInfo, Customer};
|
||||
use crate::models::{CompanyInfo, Customer, Task};
|
||||
use crate::schema::invoices;
|
||||
use crate::DbConn;
|
||||
|
||||
/// The invoice model.
|
||||
///
|
||||
/// This model represents an invoice for a customer that contains billed tasks and through the
|
||||
/// tasks the registered time.
|
||||
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
||||
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||
#[belongs_to(CompanyInfo)]
|
||||
#[belongs_to(Customer)]
|
||||
#[table_name = "invoices"]
|
||||
|
@ -31,10 +33,33 @@ pub struct Invoice {
|
|||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Invoice {
|
||||
/// Returns the associated company info at the time of billing.
|
||||
pub fn company_info(&self, conn: &DbConn) -> QueryResult<CompanyInfo> {
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
CompanyInfo::table()
|
||||
.find(self.company_info_id)
|
||||
.first(&**conn)
|
||||
}
|
||||
|
||||
/// Returns the associated customer.
|
||||
pub fn customer(&self, conn: &DbConn) -> QueryResult<Customer> {
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
Customer::table().find(self.customer_id).first(&**conn)
|
||||
}
|
||||
|
||||
/// Returns the billed tasks included in the invoice.
|
||||
pub fn tasks(&self, conn: &DbConn) -> QueryResult<Vec<Task>> {
|
||||
Task::belonging_to(self).load(&**conn)
|
||||
}
|
||||
}
|
||||
|
||||
/// The new invoice model.
|
||||
///
|
||||
/// This model represents an new invoice for a customer that can be inserted into the database.
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[derive(Debug, GraphQLInputObject, Insertable)]
|
||||
#[table_name = "invoices"]
|
||||
pub struct NewInvoice {
|
||||
/// The ID of the company info at the time of billing
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use diesel::prelude::*;
|
||||
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
|
||||
|
||||
use crate::models::{Customer, Invoice};
|
||||
use crate::models::{Customer, Invoice, TimeEntry};
|
||||
use crate::schema::tasks;
|
||||
use crate::DbConn;
|
||||
|
||||
/// The task (or project) model.
|
||||
///
|
||||
/// This model represents a task (or project) of a customer on which time can be registered.
|
||||
/// generated.
|
||||
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
||||
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||
#[belongs_to(Customer)]
|
||||
#[belongs_to(Invoice)]
|
||||
#[table_name = "tasks"]
|
||||
|
@ -18,9 +20,9 @@ pub struct Task {
|
|||
/// The ID of the associated customer
|
||||
pub customer_id: i32,
|
||||
/// The fixed cost of the task (if applicable)
|
||||
pub fixed_cost: Option<f32>,
|
||||
pub fixed_cost: Option<f64>,
|
||||
/// The hourly rate of the task (if applicable)
|
||||
pub hourly_rate: Option<f32>,
|
||||
pub hourly_rate: Option<f64>,
|
||||
/// An extra comment for on the invoice
|
||||
pub invoice_comment: String,
|
||||
/// The associated invoice (if billed)
|
||||
|
@ -28,26 +30,50 @@ pub struct Task {
|
|||
/// The name/description
|
||||
pub name: String,
|
||||
/// The VAT rate (at time of billing)
|
||||
pub vat_rate: f32,
|
||||
pub vat_rate: f64,
|
||||
/// The time of creation
|
||||
pub created_at: NaiveDateTime,
|
||||
/// The time of last update
|
||||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Task {
|
||||
/// Returns the associated customer.
|
||||
pub fn customer(&self, conn: &DbConn) -> QueryResult<Customer> {
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
Customer::table().find(self.customer_id).first(&**conn)
|
||||
}
|
||||
|
||||
/// Returns the associated invoice (if billed)
|
||||
pub fn invoice(&self, conn: &DbConn) -> QueryResult<Option<Invoice>> {
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
match self.invoice_id {
|
||||
Some(invoice_id) => Invoice::table().find(invoice_id).first(&**conn).optional(),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the registered time entries.
|
||||
pub fn time_entries(&self, conn: &DbConn) -> QueryResult<Vec<TimeEntry>> {
|
||||
TimeEntry::belonging_to(self).load(&**conn)
|
||||
}
|
||||
}
|
||||
|
||||
/// The new task model.
|
||||
///
|
||||
/// This model represents a new task (or project) of a customer that can be inserted into the
|
||||
/// database.
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[derive(Debug, GraphQLInputObject, Insertable)]
|
||||
#[table_name = "tasks"]
|
||||
pub struct NewTask {
|
||||
/// The ID of the associated customer
|
||||
pub customer_id: i32,
|
||||
/// The fixed cost of the task (if applicable)
|
||||
pub fixed_cost: Option<f32>,
|
||||
pub fixed_cost: Option<f64>,
|
||||
/// The hourly rate of the task (if applicable)
|
||||
pub hourly_rate: Option<f32>,
|
||||
pub hourly_rate: Option<f64>,
|
||||
/// An extra comment for on the invoice
|
||||
pub invoice_comment: String,
|
||||
/// The associated invoice (if billed)
|
||||
|
@ -55,5 +81,5 @@ pub struct NewTask {
|
|||
/// The name/description
|
||||
pub name: String,
|
||||
/// The VAT rate (at time of billing)
|
||||
pub vat_rate: f32,
|
||||
pub vat_rate: f64,
|
||||
}
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
use chrono::NaiveDateTime;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use diesel::prelude::*;
|
||||
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
|
||||
|
||||
use crate::models::Task;
|
||||
use crate::schema::time_entries;
|
||||
use crate::DbConn;
|
||||
|
||||
/// The time entry model.
|
||||
///
|
||||
/// This model represents an amount of time that is registered for a certain task.
|
||||
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
||||
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||
#[belongs_to(Task)]
|
||||
#[table_name = "time_entries"]
|
||||
pub struct TimeEntry {
|
||||
|
@ -29,10 +31,19 @@ pub struct TimeEntry {
|
|||
pub updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl TimeEntry {
|
||||
/// Returns the task the entry is registered for.
|
||||
pub fn customer(&self, conn: &DbConn) -> QueryResult<Task> {
|
||||
use diesel::associations::HasTable;
|
||||
|
||||
Task::table().find(self.task_id).first(&**conn)
|
||||
}
|
||||
}
|
||||
|
||||
/// The new time entry model.
|
||||
///
|
||||
/// This model represents a new registered amount of time that can be inserted into the database.
|
||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
||||
#[derive(Debug, GraphQLInputObject, Insertable)]
|
||||
#[table_name = "time_entries"]
|
||||
pub struct NewTimeEntry {
|
||||
/// Flag whether to bill or not
|
||||
|
|
|
@ -181,10 +181,10 @@ table! {
|
|||
financial_contact -> Text,
|
||||
/// The `hourly_rate` column of the `customers` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Float>`.
|
||||
/// Its SQL type is `Nullable<Double>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
hourly_rate -> Nullable<Float>,
|
||||
hourly_rate -> Nullable<Double>,
|
||||
/// The `name` column of the `customers` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
|
@ -299,16 +299,16 @@ table! {
|
|||
customer_id -> Integer,
|
||||
/// The `fixed_cost` column of the `tasks` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Float>`.
|
||||
/// Its SQL type is `Nullable<Double>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
fixed_cost -> Nullable<Float>,
|
||||
fixed_cost -> Nullable<Double>,
|
||||
/// The `hourly_rate` column of the `tasks` table.
|
||||
///
|
||||
/// Its SQL type is `Nullable<Float>`.
|
||||
/// Its SQL type is `Nullable<Double>`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
hourly_rate -> Nullable<Float>,
|
||||
hourly_rate -> Nullable<Double>,
|
||||
/// The `invoice_comment` column of the `tasks` table.
|
||||
///
|
||||
/// Its SQL type is `Text`.
|
||||
|
@ -329,10 +329,10 @@ table! {
|
|||
name -> Text,
|
||||
/// The `vat_rate` column of the `tasks` table.
|
||||
///
|
||||
/// Its SQL type is `Float`.
|
||||
/// Its SQL type is `Double`.
|
||||
///
|
||||
/// (Automatically generated by Diesel.)
|
||||
vat_rate -> Float,
|
||||
vat_rate -> Double,
|
||||
/// The `created_at` column of the `tasks` table.
|
||||
///
|
||||
/// Its SQL type is `Timestamp`.
|
||||
|
|
Loading…
Reference in a new issue