Compare commits
12 Commits
develop
...
graphql-ap
Author | SHA1 | Date |
---|---|---|
Paul van Tilburg | fe13723323 | |
Paul van Tilburg | 63fc7a0721 | |
Paul van Tilburg | c3a6b5f572 | |
Paul van Tilburg | 26c86fe03e | |
Paul van Tilburg | 5e24738bba | |
Paul van Tilburg | f8bff689e6 | |
Paul van Tilburg | 368a3ff10e | |
Paul van Tilburg | 68ca95f166 | |
Paul van Tilburg | 0b7471467e | |
Paul van Tilburg | 33c9520d5d | |
Paul van Tilburg | 5d6a26a3ed | |
Paul van Tilburg | ef4507e25e |
|
@ -1,2 +1,9 @@
|
||||||
|
# Ignore Rust stuff
|
||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# Ignore database
|
||||||
|
/db/*.db
|
||||||
|
|
||||||
|
# Ignore dotenv environment file
|
||||||
|
/.env
|
||||||
|
|
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"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
diesel_migrations = "1.4.0"
|
||||||
rocket = "0.4.2"
|
rocket = "0.4.2"
|
||||||
serde = "1.0.92"
|
serde = "1.0.92"
|
||||||
serde_json = "1.0.39"
|
serde_json = "1.0.39"
|
||||||
serde_derive = "1.0.92"
|
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]
|
[dependencies.chrono]
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
|
@ -25,4 +28,7 @@ features = ["chrono", "sqlite"]
|
||||||
[dependencies.rocket_contrib]
|
[dependencies.rocket_contrib]
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["json", "serve"]
|
features = ["diesel_sqlite_pool", "json", "serve"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
dotenv = "0.14.1"
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
[development.databases]
|
||||||
|
stoptime_db = { url = "db/stoptime-dev.db" }
|
||||||
|
|
||||||
|
[production.databases]
|
||||||
|
stoptime_db = { url = "db/stoptime.db" }
|
|
@ -5,7 +5,7 @@ CREATE TABLE customers (
|
||||||
"address_street" VARCHAR(255) NOT NULL,
|
"address_street" VARCHAR(255) NOT NULL,
|
||||||
"email" VARCHAR(255) NOT NULL,
|
"email" VARCHAR(255) NOT NULL,
|
||||||
"financial_contact" VARCHAR(255) NOT NULL,
|
"financial_contact" VARCHAR(255) NOT NULL,
|
||||||
"hourly_rate" FLOAT,
|
"hourly_rate" DOUBLE,
|
||||||
"name" VARCHAR(255) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"phone" VARCHAR(255) NOT NULL,
|
"phone" VARCHAR(255) NOT NULL,
|
||||||
"short_name" VARCHAR(255) NOT NULL,
|
"short_name" VARCHAR(255) NOT NULL,
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
CREATE TABLE tasks (
|
CREATE TABLE tasks (
|
||||||
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
"customer_id" INTEGER NOT NULL,
|
"customer_id" INTEGER NOT NULL,
|
||||||
"fixed_cost" FLOAT,
|
"fixed_cost" DOUBLE,
|
||||||
"hourly_rate" FLOAT,
|
"hourly_rate" DOUBLE,
|
||||||
"invoice_comment" VARCHAR(255) NOT NULL,
|
"invoice_comment" VARCHAR(255) NOT NULL,
|
||||||
"invoice_id" INTEGER,
|
"invoice_id" INTEGER,
|
||||||
"name" VARCHAR(255) NOT NULL,
|
"name" VARCHAR(255) NOT NULL,
|
||||||
"vat_rate" FLOAT NOT NULL,
|
"vat_rate" DOUBLE NOT NULL,
|
||||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
"updated_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::catch;
|
||||||
use rocket_contrib::json;
|
use rocket_contrib::json;
|
||||||
|
@ -12,12 +12,3 @@ pub fn not_found() -> JsonValue {
|
||||||
"reason": "Resource was not found",
|
"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",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
use juniper_rocket::{playground_source, GraphQLRequest, GraphQLResponse};
|
||||||
pub mod customers;
|
use rocket::response::content::Html;
|
||||||
pub mod invoices;
|
use rocket::{get, post, State};
|
||||||
pub mod timeline;
|
|
||||||
|
|
||||||
/// 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("/")]
|
#[get("/")]
|
||||||
pub fn index() {
|
pub fn index() {
|
||||||
unimplemented!()
|
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!()
|
|
||||||
}
|
|
|
@ -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]
|
#[macro_use]
|
||||||
extern crate diesel;
|
extern crate diesel;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate diesel_migrations;
|
||||||
|
|
||||||
|
use rocket::fairing::AdHoc;
|
||||||
use rocket::{catchers, routes, Rocket};
|
use rocket::{catchers, routes, Rocket};
|
||||||
|
use rocket_contrib::database;
|
||||||
use rocket_contrib::serve::StaticFiles;
|
use rocket_contrib::serve::StaticFiles;
|
||||||
|
|
||||||
pub mod catchers;
|
#[macro_use]
|
||||||
pub mod handlers;
|
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 {
|
fn rocket() -> Rocket {
|
||||||
let static_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/static");
|
let static_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/static");
|
||||||
let static_files = StaticFiles::from(static_dir);
|
let static_files = StaticFiles::from(static_dir);
|
||||||
|
|
||||||
rocket::ignite()
|
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(
|
.mount(
|
||||||
"/company",
|
"/",
|
||||||
routes![handlers::company::index, handlers::company::create],
|
|
||||||
)
|
|
||||||
.mount(
|
|
||||||
"/customers",
|
|
||||||
routes![
|
routes![
|
||||||
handlers::customers::index,
|
handlers::index,
|
||||||
handlers::customers::create,
|
handlers::graphql_playground,
|
||||||
handlers::customers::new,
|
handlers::graphql_get,
|
||||||
handlers::customers::show,
|
handlers::graphql_post,
|
||||||
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,
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
.mount("/invoices", routes![handlers::invoices::index])
|
|
||||||
.mount("/static", static_files)
|
.mount("/static", static_files)
|
||||||
.mount(
|
.register(catchers![catchers::not_found])
|
||||||
"/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
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs the Rocket application.
|
||||||
fn main() {
|
fn main() {
|
||||||
rocket().launch();
|
rocket().launch();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
//! The application models
|
//! The application models.
|
||||||
|
|
||||||
mod company_info;
|
mod company_info;
|
||||||
mod customer;
|
mod customer;
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use diesel::prelude::*;
|
||||||
|
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
|
||||||
|
|
||||||
use crate::schema::company_infos;
|
use crate::schema::company_infos;
|
||||||
|
use crate::DbConn;
|
||||||
|
|
||||||
/// The company (information) model.
|
/// The company (information) model.
|
||||||
///
|
///
|
||||||
/// This model represents information about the company or sole proprietorship of the user of
|
/// This model represents information about the company or sole proprietorship of the user of
|
||||||
/// StopTime.
|
/// StopTime.
|
||||||
#[derive(Associations, Debug, Identifiable, Deserialize, Queryable, Serialize)]
|
#[derive(Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||||
#[belongs_to(CompanyInfo, foreign_key = "original_id")]
|
#[belongs_to(CompanyInfo, foreign_key = "original_id")]
|
||||||
#[table_name = "company_infos"]
|
#[table_name = "company_infos"]
|
||||||
pub struct CompanyInfo {
|
pub struct CompanyInfo {
|
||||||
|
@ -57,10 +59,25 @@ pub struct CompanyInfo {
|
||||||
pub updated_at: NaiveDateTime,
|
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.
|
/// The new company (information) model.
|
||||||
///
|
///
|
||||||
/// This model represents new company information that can be inserted into the database.
|
/// 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"]
|
#[table_name = "company_infos"]
|
||||||
pub struct NewCompanyInfo {
|
pub struct NewCompanyInfo {
|
||||||
/// The international bank account number
|
/// The international bank account number
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
use chrono::NaiveDateTime;
|
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::schema::customers;
|
||||||
|
use crate::DbConn;
|
||||||
|
|
||||||
/// The customer model.
|
/// The customer model.
|
||||||
///
|
///
|
||||||
/// This model represents a customer that has projects/tasks for which invoices need to be
|
/// This model represents a customer that has projects/tasks for which invoices need to be
|
||||||
/// generated.
|
/// generated.
|
||||||
#[derive(AsChangeset, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
#[derive(AsChangeset, Associations, Debug, Identifiable, Queryable)]
|
||||||
#[table_name = "customers"]
|
#[table_name = "customers"]
|
||||||
pub struct Customer {
|
pub struct Customer {
|
||||||
/// The unique identification number
|
/// The unique identification number
|
||||||
|
@ -23,7 +26,7 @@ pub struct Customer {
|
||||||
/// The name of the financial contact person/department
|
/// The name of the financial contact person/department
|
||||||
pub financial_contact: String,
|
pub financial_contact: String,
|
||||||
/// The default hourly rate (if applicable)
|
/// The default hourly rate (if applicable)
|
||||||
pub hourly_rate: Option<f32>,
|
pub hourly_rate: Option<f64>,
|
||||||
/// The official (long) name
|
/// The official (long) name
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// The phone number
|
/// The phone number
|
||||||
|
@ -38,10 +41,22 @@ pub struct Customer {
|
||||||
pub updated_at: NaiveDateTime,
|
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
|
/// The new customer model
|
||||||
///
|
///
|
||||||
/// This model represents a new customer that can be inserted into the database.
|
/// 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"]
|
#[table_name = "customers"]
|
||||||
pub struct NewCustomer {
|
pub struct NewCustomer {
|
||||||
/// The city part of the address
|
/// The city part of the address
|
||||||
|
@ -55,7 +70,7 @@ pub struct NewCustomer {
|
||||||
/// The name of the financial contact person/department
|
/// The name of the financial contact person/department
|
||||||
pub financial_contact: String,
|
pub financial_contact: String,
|
||||||
/// The default hourly rate (if applicable)
|
/// The default hourly rate (if applicable)
|
||||||
pub hourly_rate: Option<f32>,
|
pub hourly_rate: Option<f64>,
|
||||||
/// The official (long) name
|
/// The official (long) name
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// The phone number
|
/// The phone number
|
||||||
|
@ -65,3 +80,22 @@ pub struct NewCustomer {
|
||||||
/// Flag whether the customer requires time specificaions
|
/// Flag whether the customer requires time specificaions
|
||||||
pub time_specification: bool,
|
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 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::schema::invoices;
|
||||||
|
use crate::DbConn;
|
||||||
|
|
||||||
/// The invoice model.
|
/// The invoice model.
|
||||||
///
|
///
|
||||||
/// This model represents an invoice for a customer that contains billed tasks and through the
|
/// This model represents an invoice for a customer that contains billed tasks and through the
|
||||||
/// tasks the registered time.
|
/// tasks the registered time.
|
||||||
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||||
#[belongs_to(CompanyInfo)]
|
#[belongs_to(CompanyInfo)]
|
||||||
#[belongs_to(Customer)]
|
#[belongs_to(Customer)]
|
||||||
#[table_name = "invoices"]
|
#[table_name = "invoices"]
|
||||||
|
@ -31,10 +33,33 @@ pub struct Invoice {
|
||||||
pub updated_at: NaiveDateTime,
|
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.
|
/// The new invoice model.
|
||||||
///
|
///
|
||||||
/// This model represents an new invoice for a customer that can be inserted into the database.
|
/// 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"]
|
#[table_name = "invoices"]
|
||||||
pub struct NewInvoice {
|
pub struct NewInvoice {
|
||||||
/// The ID of the company info at the time of billing
|
/// The ID of the company info at the time of billing
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
use chrono::NaiveDateTime;
|
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::schema::tasks;
|
||||||
|
use crate::DbConn;
|
||||||
|
|
||||||
/// The task (or project) model.
|
/// The task (or project) model.
|
||||||
///
|
///
|
||||||
/// This model represents a task (or project) of a customer on which time can be registered.
|
/// This model represents a task (or project) of a customer on which time can be registered.
|
||||||
/// generated.
|
/// generated.
|
||||||
#[derive(AsChangeset, Associations, Debug, Deserialize, Identifiable, Queryable, Serialize)]
|
#[derive(AsChangeset, Associations, Debug, GraphQLObject, Identifiable, Queryable)]
|
||||||
#[belongs_to(Customer)]
|
#[belongs_to(Customer)]
|
||||||
#[belongs_to(Invoice)]
|
#[belongs_to(Invoice)]
|
||||||
#[table_name = "tasks"]
|
#[table_name = "tasks"]
|
||||||
|
@ -18,9 +20,9 @@ pub struct Task {
|
||||||
/// The ID of the associated customer
|
/// The ID of the associated customer
|
||||||
pub customer_id: i32,
|
pub customer_id: i32,
|
||||||
/// The fixed cost of the task (if applicable)
|
/// 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)
|
/// 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
|
/// An extra comment for on the invoice
|
||||||
pub invoice_comment: String,
|
pub invoice_comment: String,
|
||||||
/// The associated invoice (if billed)
|
/// The associated invoice (if billed)
|
||||||
|
@ -28,26 +30,50 @@ pub struct Task {
|
||||||
/// The name/description
|
/// The name/description
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// The VAT rate (at time of billing)
|
/// The VAT rate (at time of billing)
|
||||||
pub vat_rate: f32,
|
pub vat_rate: f64,
|
||||||
/// The time of creation
|
/// The time of creation
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
/// The time of last update
|
/// The time of last update
|
||||||
pub updated_at: NaiveDateTime,
|
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.
|
/// The new task model.
|
||||||
///
|
///
|
||||||
/// This model represents a new task (or project) of a customer that can be inserted into the
|
/// This model represents a new task (or project) of a customer that can be inserted into the
|
||||||
/// database.
|
/// database.
|
||||||
#[derive(Debug, Deserialize, Insertable, Serialize)]
|
#[derive(Debug, GraphQLInputObject, Insertable)]
|
||||||
#[table_name = "tasks"]
|
#[table_name = "tasks"]
|
||||||
pub struct NewTask {
|
pub struct NewTask {
|
||||||
/// The ID of the associated customer
|
/// The ID of the associated customer
|
||||||
pub customer_id: i32,
|
pub customer_id: i32,
|
||||||
/// The fixed cost of the task (if applicable)
|
/// 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)
|
/// 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
|
/// An extra comment for on the invoice
|
||||||
pub invoice_comment: String,
|
pub invoice_comment: String,
|
||||||
/// The associated invoice (if billed)
|
/// The associated invoice (if billed)
|
||||||
|
@ -55,5 +81,5 @@ pub struct NewTask {
|
||||||
/// The name/description
|
/// The name/description
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// The VAT rate (at time of billing)
|
/// The VAT rate (at time of billing)
|
||||||
pub vat_rate: f32,
|
pub vat_rate: f64,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use diesel::prelude::*;
|
||||||
|
use juniper_codegen::{GraphQLInputObject, GraphQLObject};
|
||||||
|
|
||||||
use crate::models::Task;
|
use crate::models::Task;
|
||||||
use crate::schema::time_entries;
|
use crate::schema::time_entries;
|
||||||
|
use crate::DbConn;
|
||||||
|
|
||||||
/// The time entry model.
|
/// The time entry model.
|
||||||
///
|
///
|
||||||
/// This model represents an amount of time that is registered for a certain task.
|
/// 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)]
|
#[belongs_to(Task)]
|
||||||
#[table_name = "time_entries"]
|
#[table_name = "time_entries"]
|
||||||
pub struct TimeEntry {
|
pub struct TimeEntry {
|
||||||
|
@ -29,10 +31,19 @@ pub struct TimeEntry {
|
||||||
pub updated_at: NaiveDateTime,
|
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.
|
/// The new time entry model.
|
||||||
///
|
///
|
||||||
/// This model represents a new registered amount of time that can be inserted into the database.
|
/// 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"]
|
#[table_name = "time_entries"]
|
||||||
pub struct NewTimeEntry {
|
pub struct NewTimeEntry {
|
||||||
/// Flag whether to bill or not
|
/// Flag whether to bill or not
|
||||||
|
|
|
@ -181,10 +181,10 @@ table! {
|
||||||
financial_contact -> Text,
|
financial_contact -> Text,
|
||||||
/// The `hourly_rate` column of the `customers` table.
|
/// The `hourly_rate` column of the `customers` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Nullable<Float>`.
|
/// Its SQL type is `Nullable<Double>`.
|
||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
hourly_rate -> Nullable<Float>,
|
hourly_rate -> Nullable<Double>,
|
||||||
/// The `name` column of the `customers` table.
|
/// The `name` column of the `customers` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Text`.
|
/// Its SQL type is `Text`.
|
||||||
|
@ -299,16 +299,16 @@ table! {
|
||||||
customer_id -> Integer,
|
customer_id -> Integer,
|
||||||
/// The `fixed_cost` column of the `tasks` table.
|
/// The `fixed_cost` column of the `tasks` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Nullable<Float>`.
|
/// Its SQL type is `Nullable<Double>`.
|
||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
fixed_cost -> Nullable<Float>,
|
fixed_cost -> Nullable<Double>,
|
||||||
/// The `hourly_rate` column of the `tasks` table.
|
/// The `hourly_rate` column of the `tasks` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Nullable<Float>`.
|
/// Its SQL type is `Nullable<Double>`.
|
||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
hourly_rate -> Nullable<Float>,
|
hourly_rate -> Nullable<Double>,
|
||||||
/// The `invoice_comment` column of the `tasks` table.
|
/// The `invoice_comment` column of the `tasks` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Text`.
|
/// Its SQL type is `Text`.
|
||||||
|
@ -329,10 +329,10 @@ table! {
|
||||||
name -> Text,
|
name -> Text,
|
||||||
/// The `vat_rate` column of the `tasks` table.
|
/// The `vat_rate` column of the `tasks` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Float`.
|
/// Its SQL type is `Double`.
|
||||||
///
|
///
|
||||||
/// (Automatically generated by Diesel.)
|
/// (Automatically generated by Diesel.)
|
||||||
vat_rate -> Float,
|
vat_rate -> Double,
|
||||||
/// The `created_at` column of the `tasks` table.
|
/// The `created_at` column of the `tasks` table.
|
||||||
///
|
///
|
||||||
/// Its SQL type is `Timestamp`.
|
/// Its SQL type is `Timestamp`.
|
||||||
|
|
Loading…
Reference in New Issue