diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fccae86..174196f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1175,6 +1175,7 @@ version = "0.1.0" dependencies = [ "che-orm", "che-tauri", + "reqwest 0.12.28", "serde", "serde_json", "tauri", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d0ec1e6..bba9909 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -26,3 +26,4 @@ tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +reqwest = { version = "0.12", features = ["json"] } diff --git a/src-tauri/app.toml b/src-tauri/app.toml index 693cc9a..f5e1f53 100644 --- a/src-tauri/app.toml +++ b/src-tauri/app.toml @@ -2,4 +2,5 @@ url = "sqlite://ewa-mobile.sqlite?mode=rwc" [remote] -base_url = "http://localhost:3000" +base_url = "http://127.0.0.1:8000" +auth_path = "/api-token-auth/" diff --git a/src-tauri/src/apps/contractapp/filters.rs b/src-tauri/src/apps/contractapp/filters.rs new file mode 100644 index 0000000..e7ecd66 --- /dev/null +++ b/src-tauri/src/apps/contractapp/filters.rs @@ -0,0 +1,39 @@ +use che_tauri::{Filter, FilterSet}; + +use super::models::{Contract, Counterparty}; + +static CONTRACTAPP_FILTERS: &[Filter] = &[ + Filter::exact("id"), + Filter::exact("name"), + Filter::contains("name"), + Filter::exact("number"), + Filter::contains("number"), + Filter::exact("date"), + Filter::exact("status"), + Filter::exact("status_name"), + Filter::contains("status_name"), + Filter::exact("contract_type"), + Filter::contains("contract_type"), + Filter::exact("category"), + Filter::contains("category"), + Filter::contains("counterparty_name"), + Filter::contains("name_of_product"), + Filter::exact("counterparty_id"), + Filter::exact("project_id"), + Filter::exact("frc_id"), + Filter::exact("employee_id"), +]; + +static COUNTERPARTY_FILTERS: &[Filter] = &[ + Filter::exact("id"), + Filter::exact("name"), + Filter::contains("name"), +]; + +pub fn contractapp_filterset() -> FilterSet { + FilterSet::new(CONTRACTAPP_FILTERS) +} + +pub fn counterparty_filterset() -> FilterSet { + FilterSet::new(COUNTERPARTY_FILTERS) +} diff --git a/src-tauri/src/apps/contractapp/migrations/0001_auto.sql b/src-tauri/src/apps/contractapp/migrations/0001_auto.sql new file mode 100644 index 0000000..1a888d6 --- /dev/null +++ b/src-tauri/src/apps/contractapp/migrations/0001_auto.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS contractapp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + number TEXT NOT NULL +); diff --git a/src-tauri/src/apps/contractapp/migrations/0002_counterparty.sql b/src-tauri/src/apps/contractapp/migrations/0002_counterparty.sql new file mode 100644 index 0000000..590f1c1 --- /dev/null +++ b/src-tauri/src/apps/contractapp/migrations/0002_counterparty.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS counterparty ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL +); + +ALTER TABLE contractapp ADD COLUMN counterparty_id INTEGER REFERENCES counterparty(id); diff --git a/src-tauri/src/apps/contractapp/migrations/0003_remote_relations.sql b/src-tauri/src/apps/contractapp/migrations/0003_remote_relations.sql new file mode 100644 index 0000000..6435694 --- /dev/null +++ b/src-tauri/src/apps/contractapp/migrations/0003_remote_relations.sql @@ -0,0 +1,5 @@ +ALTER TABLE contractapp ADD COLUMN project_id INTEGER REFERENCES project(id); + +ALTER TABLE contractapp ADD COLUMN frc_id INTEGER REFERENCES frc(id); + +ALTER TABLE contractapp ADD COLUMN employee_id INTEGER REFERENCES employee(id); diff --git a/src-tauri/src/apps/contractapp/migrations/0004_auto.sql b/src-tauri/src/apps/contractapp/migrations/0004_auto.sql new file mode 100644 index 0000000..9862ad8 --- /dev/null +++ b/src-tauri/src/apps/contractapp/migrations/0004_auto.sql @@ -0,0 +1,43 @@ +ALTER TABLE contractapp ADD COLUMN absolute_url TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN contract_type TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN category TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN amount_total_display TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN amount_by_ds TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN comment TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN date TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN status_name TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN status TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN nds TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN name_of_product TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN counterparty_name TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN amount REAL NOT NULL DEFAULT 0; + +ALTER TABLE contractapp ADD COLUMN month_pay REAL; + +ALTER TABLE contractapp ADD COLUMN avans_pay TEXT NOT NULL DEFAULT ''; + +ALTER TABLE contractapp ADD COLUMN amount_total REAL NOT NULL DEFAULT 0; + +ALTER TABLE contractapp ADD COLUMN estimate_nds_cost REAL NOT NULL DEFAULT 0; + +ALTER TABLE contractapp ADD COLUMN estimate_nds_cert REAL NOT NULL DEFAULT 0; + +ALTER TABLE contractapp ADD COLUMN bill_cost_sum REAL; + +ALTER TABLE contractapp ADD COLUMN bill_paid_sum REAL; + +ALTER TABLE contractapp ADD COLUMN income_total REAL NOT NULL DEFAULT 0; + +ALTER TABLE contractapp ADD COLUMN arrears REAL NOT NULL DEFAULT 0; diff --git a/src-tauri/src/apps/contractapp/migrations/schema.json b/src-tauri/src/apps/contractapp/migrations/schema.json new file mode 100644 index 0000000..5af1edb --- /dev/null +++ b/src-tauri/src/apps/contractapp/migrations/schema.json @@ -0,0 +1,367 @@ +{ + "models": [ + { + "table": "contractapp", + "fields": [ + { + "name": "id", + "ty": "integer", + "primary_key": true, + "nullable": false, + "auto": true, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "number", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "absolute_url", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "contract_type", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "category", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "amount_total_display", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "amount_by_ds", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "comment", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "date", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "status_name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "status", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "nds", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "name_of_product", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "counterparty_name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "amount", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "month_pay", + "ty": "real", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "avans_pay", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "amount_total", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "estimate_nds_cost", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "estimate_nds_cert", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "bill_cost_sum", + "ty": "real", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "bill_paid_sum", + "ty": "real", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "income_total", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "arrears", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "counterparty_id", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": { + "table": "counterparty", + "column": "id" + } + }, + { + "name": "project_id", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": { + "table": "project", + "column": "id" + } + }, + { + "name": "frc_id", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": { + "table": "frc", + "column": "id" + } + }, + { + "name": "employee_id", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": { + "table": "employee", + "column": "id" + } + } + ] + }, + { + "table": "counterparty", + "fields": [ + { + "name": "id", + "ty": "integer", + "primary_key": true, + "nullable": false, + "auto": true, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + } + ] + } + ] +} diff --git a/src-tauri/src/apps/contractapp/mod.rs b/src-tauri/src/apps/contractapp/mod.rs new file mode 100644 index 0000000..892a94f --- /dev/null +++ b/src-tauri/src/apps/contractapp/mod.rs @@ -0,0 +1,30 @@ +pub mod filters; +pub mod models; +pub mod serializers; + +use che_tauri::{AppModule, ModuleContext}; + +pub fn module() -> ContractappModule { + ContractappModule +} + +pub struct ContractappModule; + +impl AppModule for ContractappModule { + fn name(&self) -> &'static str { + "contractapp" + } + + fn init(&self, ctx: &mut ModuleContext) { + ctx.resource::( + "contract", + serializers::contractapp_serializer(), + filters::contractapp_filterset(), + ); + ctx.resource::( + "counterparty", + serializers::counterparty_serializer(), + filters::counterparty_filterset(), + ); + } +} diff --git a/src-tauri/src/apps/contractapp/models.rs b/src-tauri/src/apps/contractapp/models.rs new file mode 100644 index 0000000..f042738 --- /dev/null +++ b/src-tauri/src/apps/contractapp/models.rs @@ -0,0 +1,58 @@ +use che_orm::Model; + +use crate::apps::{ + frcapp::models::Frc, personemanagment::models::Employee, projectapp::models::Project, +}; + +#[derive(Debug, Clone, Model)] +#[model(table = "contractapp")] +pub struct Contract { + #[field(primary_key)] + pub id: i64, + + pub name: String, + pub number: String, + pub absolute_url: String, + pub contract_type: String, + pub category: String, + pub amount_total_display: String, + pub amount_by_ds: String, + pub comment: String, + pub date: String, + pub status_name: String, + pub status: String, + pub nds: String, + pub name_of_product: String, + pub counterparty_name: String, + pub amount: f64, + pub month_pay: Option, + pub avans_pay: String, + pub amount_total: f64, + pub estimate_nds_cost: f64, + pub estimate_nds_cert: f64, + pub bill_cost_sum: Option, + pub bill_paid_sum: Option, + pub income_total: f64, + pub arrears: f64, + + #[field(foreign_key = Counterparty)] + pub counterparty_id: Option, + + #[field(foreign_key = Project)] + pub project_id: Option, + + #[field(foreign_key = Frc)] + pub frc_id: Option, + + #[field(foreign_key = Employee)] + pub employee_id: Option, +} + +#[derive(Debug, Clone, Model)] +#[model(table = "counterparty")] +pub struct Counterparty { + #[field(primary_key)] + pub id: i64, + + pub name: String, +} diff --git a/src-tauri/src/apps/contractapp/serializers.rs b/src-tauri/src/apps/contractapp/serializers.rs new file mode 100644 index 0000000..baf94d0 --- /dev/null +++ b/src-tauri/src/apps/contractapp/serializers.rs @@ -0,0 +1,60 @@ +use che_tauri::{Field, ModelSerializer, RelatedModel}; + +use crate::apps::{ + frcapp::{models::Frc, serializers::frc_serializer}, + personemanagment::{models::Employee, serializers::employee_serializer}, + projectapp::{models::Project, serializers::project_serializer}, +}; + +use super::models::{Contract, Counterparty}; + +static CONTRACT_FIELDS: &[Field] = &[ + Field::new("id").read_only(), + Field::new("name"), + Field::new("number"), + Field::new("absolute_url"), + Field::new("contract_type"), + Field::new("category"), + Field::new("amount_total_display"), + Field::new("amount_by_ds"), + Field::new("comment"), + Field::new("date"), + Field::new("status_name"), + Field::new("status"), + Field::new("nds"), + Field::new("name_of_product"), + Field::new("counterparty_name"), + Field::new("amount"), + Field::new("month_pay").required(false).nullable(), + Field::new("avans_pay"), + Field::new("amount_total"), + Field::new("estimate_nds_cost"), + Field::new("estimate_nds_cert"), + Field::new("bill_cost_sum").required(false).nullable(), + Field::new("bill_paid_sum").required(false).nullable(), + Field::new("income_total"), + Field::new("arrears"), + Field::new("counterparty_id").required(false).nullable(), + Field::new("project_id").required(false).nullable(), + Field::new("frc_id").required(false).nullable(), + Field::new("employee_id").required(false).nullable(), + Field::related("counterparty", "counterparty_id", &COUNTERPARTY_RELATION), + Field::related("project", "project_id", &PROJECT_RELATION), + Field::related("frc", "frc_id", &FRC_RELATION), + Field::related("employee", "employee_id", &EMPLOYEE_RELATION), +]; + +static COUNTERPARTY_FIELDS: &[Field] = &[Field::new("id").read_only(), Field::new("name")]; +static COUNTERPARTY_RELATION: RelatedModel = + RelatedModel::new(counterparty_serializer); +static PROJECT_RELATION: RelatedModel = RelatedModel::new(project_serializer); +static FRC_RELATION: RelatedModel = RelatedModel::new(frc_serializer); +static EMPLOYEE_RELATION: RelatedModel = RelatedModel::new(employee_serializer); + +pub fn contractapp_serializer() -> ModelSerializer { + ModelSerializer::new(CONTRACT_FIELDS) +} + +pub fn counterparty_serializer() -> ModelSerializer { + ModelSerializer::new(COUNTERPARTY_FIELDS) +} diff --git a/src-tauri/src/apps/frcapp/filters.rs b/src-tauri/src/apps/frcapp/filters.rs new file mode 100644 index 0000000..5f26a25 --- /dev/null +++ b/src-tauri/src/apps/frcapp/filters.rs @@ -0,0 +1,13 @@ +use che_tauri::{Filter, FilterSet}; + +use super::models::Frc; + +static FRC_FILTERS: &[Filter] = &[ + Filter::exact("id"), + Filter::exact("name"), + Filter::contains("name"), +]; + +pub fn frc_filterset() -> FilterSet { + FilterSet::new(FRC_FILTERS) +} diff --git a/src-tauri/src/apps/frcapp/migrations/0001_initial.sql b/src-tauri/src/apps/frcapp/migrations/0001_initial.sql new file mode 100644 index 0000000..1b50430 --- /dev/null +++ b/src-tauri/src/apps/frcapp/migrations/0001_initial.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS frc ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + icon TEXT NOT NULL, + balance REAL NOT NULL +); diff --git a/src-tauri/src/apps/frcapp/migrations/schema.json b/src-tauri/src/apps/frcapp/migrations/schema.json new file mode 100644 index 0000000..3e9d820 --- /dev/null +++ b/src-tauri/src/apps/frcapp/migrations/schema.json @@ -0,0 +1,53 @@ +{ + "models": [ + { + "table": "frc", + "fields": [ + { + "name": "id", + "ty": "integer", + "primary_key": true, + "nullable": false, + "auto": true, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "icon", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "balance", + "ty": "real", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + } + ] + } + ] +} diff --git a/src-tauri/src/apps/frcapp/mod.rs b/src-tauri/src/apps/frcapp/mod.rs new file mode 100644 index 0000000..81ccdae --- /dev/null +++ b/src-tauri/src/apps/frcapp/mod.rs @@ -0,0 +1,25 @@ +pub mod filters; +pub mod models; +pub mod serializers; + +use che_tauri::{AppModule, ModuleContext}; + +pub fn module() -> FrcModule { + FrcModule +} + +pub struct FrcModule; + +impl AppModule for FrcModule { + fn name(&self) -> &'static str { + "frcapp" + } + + fn init(&self, ctx: &mut ModuleContext) { + ctx.resource::( + "frc", + serializers::frc_serializer(), + filters::frc_filterset(), + ); + } +} diff --git a/src-tauri/src/apps/frcapp/models.rs b/src-tauri/src/apps/frcapp/models.rs new file mode 100644 index 0000000..e19759c --- /dev/null +++ b/src-tauri/src/apps/frcapp/models.rs @@ -0,0 +1,12 @@ +use che_orm::Model; + +#[derive(Debug, Clone, Model)] +#[model(table = "frc")] +pub struct Frc { + #[field(primary_key)] + pub id: i64, + + pub name: String, + pub icon: String, + pub balance: f64, +} diff --git a/src-tauri/src/apps/frcapp/serializers.rs b/src-tauri/src/apps/frcapp/serializers.rs new file mode 100644 index 0000000..28fb796 --- /dev/null +++ b/src-tauri/src/apps/frcapp/serializers.rs @@ -0,0 +1,14 @@ +use che_tauri::{Field, ModelSerializer}; + +use super::models::Frc; + +static FRC_FIELDS: &[Field] = &[ + Field::new("id").read_only(), + Field::new("name"), + Field::new("icon"), + Field::new("balance"), +]; + +pub fn frc_serializer() -> ModelSerializer { + ModelSerializer::new(FRC_FIELDS) +} diff --git a/src-tauri/src/apps/mod.rs b/src-tauri/src/apps/mod.rs index 081d489..0bfc9e4 100644 --- a/src-tauri/src/apps/mod.rs +++ b/src-tauri/src/apps/mod.rs @@ -1,4 +1,6 @@ +pub mod frcapp; pub mod personemanagment; +pub mod projectapp; pub mod users; use che_tauri::InstalledApps; @@ -6,5 +8,9 @@ use che_tauri::InstalledApps; pub fn installed_apps() -> InstalledApps { InstalledApps::new() .add(users::module()) + .add(frcapp::module()) + .add(projectapp::module()) .add(personemanagment::module()) + .add(contractapp::module()) } +pub mod contractapp; diff --git a/src-tauri/src/apps/personemanagment/filters.rs b/src-tauri/src/apps/personemanagment/filters.rs index 27cb566..864d665 100644 --- a/src-tauri/src/apps/personemanagment/filters.rs +++ b/src-tauri/src/apps/personemanagment/filters.rs @@ -6,6 +6,8 @@ static EMPLOYEE_FILTERS: &[Filter] = &[ Filter::exact("id"), Filter::exact("name"), Filter::contains("name"), + Filter::exact("short_name"), + Filter::contains("short_name"), ]; static TASK_FILTERS: &[Filter] = &[ diff --git a/src-tauri/src/apps/personemanagment/migrations/0004_remote_employee.sql b/src-tauri/src/apps/personemanagment/migrations/0004_remote_employee.sql new file mode 100644 index 0000000..d591e6b --- /dev/null +++ b/src-tauri/src/apps/personemanagment/migrations/0004_remote_employee.sql @@ -0,0 +1,5 @@ +ALTER TABLE employee ADD COLUMN short_name TEXT NOT NULL DEFAULT ''; + +ALTER TABLE employee ADD COLUMN avatar_small TEXT; + +ALTER TABLE employee DROP COLUMN avatar; diff --git a/src-tauri/src/apps/personemanagment/migrations/schema.json b/src-tauri/src/apps/personemanagment/migrations/schema.json index 1f04b18..992bee1 100644 --- a/src-tauri/src/apps/personemanagment/migrations/schema.json +++ b/src-tauri/src/apps/personemanagment/migrations/schema.json @@ -26,7 +26,7 @@ "foreign_key": null }, { - "name": "avatar", + "name": "short_name", "ty": "text", "primary_key": false, "nullable": false, @@ -35,6 +35,17 @@ "max_length": null, "default": null, "foreign_key": null + }, + { + "name": "avatar_small", + "ty": "text", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null } ] }, diff --git a/src-tauri/src/apps/personemanagment/models.rs b/src-tauri/src/apps/personemanagment/models.rs index 2798294..69c0296 100644 --- a/src-tauri/src/apps/personemanagment/models.rs +++ b/src-tauri/src/apps/personemanagment/models.rs @@ -8,7 +8,9 @@ pub struct Employee { pub name: String, - pub avatar: String, + pub short_name: String, + + pub avatar_small: Option, } #[derive(Debug, Clone, Model)] diff --git a/src-tauri/src/apps/personemanagment/serializers.rs b/src-tauri/src/apps/personemanagment/serializers.rs index 6ce1516..4691f64 100644 --- a/src-tauri/src/apps/personemanagment/serializers.rs +++ b/src-tauri/src/apps/personemanagment/serializers.rs @@ -5,7 +5,8 @@ use super::models::{Employee, Message, Task}; static EMPLOYEE_FIELDS: &[Field] = &[ Field::new("id").read_only(), Field::new("name"), - Field::new("avatar"), + Field::new("short_name"), + Field::new("avatar_small").required(false).nullable(), ]; static TASK_FIELDS: &[Field] = &[ diff --git a/src-tauri/src/apps/projectapp/filters.rs b/src-tauri/src/apps/projectapp/filters.rs new file mode 100644 index 0000000..403193b --- /dev/null +++ b/src-tauri/src/apps/projectapp/filters.rs @@ -0,0 +1,15 @@ +use che_tauri::{Filter, FilterSet}; + +use super::models::Project; + +static PROJECT_FILTERS: &[Filter] = &[ + Filter::exact("id"), + Filter::exact("name"), + Filter::contains("name"), + Filter::exact("short_name"), + Filter::contains("short_name"), +]; + +pub fn project_filterset() -> FilterSet { + FilterSet::new(PROJECT_FILTERS) +} diff --git a/src-tauri/src/apps/projectapp/migrations/0001_initial.sql b/src-tauri/src/apps/projectapp/migrations/0001_initial.sql new file mode 100644 index 0000000..838ac46 --- /dev/null +++ b/src-tauri/src/apps/projectapp/migrations/0001_initial.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS project ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + full_name TEXT NOT NULL, + short_name TEXT NOT NULL, + locality_id INTEGER, + locality_name TEXT +); diff --git a/src-tauri/src/apps/projectapp/migrations/schema.json b/src-tauri/src/apps/projectapp/migrations/schema.json new file mode 100644 index 0000000..a2e43ff --- /dev/null +++ b/src-tauri/src/apps/projectapp/migrations/schema.json @@ -0,0 +1,75 @@ +{ + "models": [ + { + "table": "project", + "fields": [ + { + "name": "id", + "ty": "integer", + "primary_key": true, + "nullable": false, + "auto": true, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "full_name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "short_name", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "locality_id", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "locality_name", + "ty": "text", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + } + ] + } + ] +} diff --git a/src-tauri/src/apps/projectapp/mod.rs b/src-tauri/src/apps/projectapp/mod.rs new file mode 100644 index 0000000..e351576 --- /dev/null +++ b/src-tauri/src/apps/projectapp/mod.rs @@ -0,0 +1,25 @@ +pub mod filters; +pub mod models; +pub mod serializers; + +use che_tauri::{AppModule, ModuleContext}; + +pub fn module() -> ProjectModule { + ProjectModule +} + +pub struct ProjectModule; + +impl AppModule for ProjectModule { + fn name(&self) -> &'static str { + "projectapp" + } + + fn init(&self, ctx: &mut ModuleContext) { + ctx.resource::( + "project", + serializers::project_serializer(), + filters::project_filterset(), + ); + } +} diff --git a/src-tauri/src/apps/projectapp/models.rs b/src-tauri/src/apps/projectapp/models.rs new file mode 100644 index 0000000..f6d2570 --- /dev/null +++ b/src-tauri/src/apps/projectapp/models.rs @@ -0,0 +1,14 @@ +use che_orm::Model; + +#[derive(Debug, Clone, Model)] +#[model(table = "project")] +pub struct Project { + #[field(primary_key)] + pub id: i64, + + pub name: String, + pub full_name: String, + pub short_name: String, + pub locality_id: Option, + pub locality_name: Option, +} diff --git a/src-tauri/src/apps/projectapp/serializers.rs b/src-tauri/src/apps/projectapp/serializers.rs new file mode 100644 index 0000000..ba1fb5a --- /dev/null +++ b/src-tauri/src/apps/projectapp/serializers.rs @@ -0,0 +1,16 @@ +use che_tauri::{Field, ModelSerializer}; + +use super::models::Project; + +static PROJECT_FIELDS: &[Field] = &[ + Field::new("id").read_only(), + Field::new("name"), + Field::new("full_name"), + Field::new("short_name"), + Field::new("locality_id").required(false).nullable(), + Field::new("locality_name").required(false).nullable(), +]; + +pub fn project_serializer() -> ModelSerializer { + ModelSerializer::new(PROJECT_FIELDS) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 463a023..7859e1e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,9 @@ pub mod apps; +pub mod sync; -use che_tauri::{ApiError, ApiRequest, AppState, TauriApi}; +use che_tauri::{ApiError, ApiRequest, AppState, AuthTokenResponse, TauriApi}; + +use crate::sync::SyncContractsResult; #[tauri::command] fn greet(name: &str) -> String { @@ -15,8 +18,44 @@ async fn che_api( api.dispatch(request).await } +#[tauri::command] +async fn auth_login( + api: tauri::State<'_, TauriApi>, + username: String, + password: String, +) -> Result { + api.login(username, password).await +} + +#[tauri::command] +fn auth_set_token(api: tauri::State<'_, TauriApi>, token: String) { + api.set_auth_token(token); +} + +#[tauri::command] +fn auth_logout(api: tauri::State<'_, TauriApi>) { + api.logout(); +} + +#[tauri::command] +fn auth_status(api: tauri::State<'_, TauriApi>) -> bool { + api.is_authenticated() +} + +#[tauri::command] +async fn sync_contracts(api: tauri::State<'_, TauriApi>) -> Result { + sync::sync_contracts(&api).await +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + #[cfg(target_os = "linux")] + if std::env::var_os("WAYLAND_DISPLAY").is_some() + && std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() + { + std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + } + let api = tauri::async_runtime::block_on(async { let state = AppState::from_config_file("app.toml").await?; TauriApi::new(state) @@ -29,7 +68,15 @@ pub fn run() { tauri::Builder::default() .manage(api) .plugin(tauri_plugin_opener::init()) - .invoke_handler(tauri::generate_handler![greet, che_api]) + .invoke_handler(tauri::generate_handler![ + greet, + che_api, + auth_login, + auth_set_token, + auth_logout, + auth_status, + sync_contracts + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/src-tauri/src/sync.rs b/src-tauri/src/sync.rs new file mode 100644 index 0000000..d65194f --- /dev/null +++ b/src-tauri/src/sync.rs @@ -0,0 +1,321 @@ +use che_orm::__private::sqlx; +use che_tauri::{ApiError, TauriApi}; +use serde::Deserialize; + +#[derive(Debug, serde::Serialize)] +pub struct SyncContractsResult { + pub synced: usize, + pub pages: usize, +} + +#[derive(Debug, Deserialize)] +struct ContractPage { + links: PageLinks, + results: Vec, +} + +#[derive(Debug, Deserialize)] +struct PageLinks { + next: Option, +} + +#[derive(Debug, Deserialize)] +struct RemoteContract { + id: i64, + name: String, + number: String, + #[serde(rename = "get_absolute_url")] + absolute_url: String, + #[serde(rename = "get_type")] + contract_type: String, + #[serde(rename = "get_category")] + category: String, + #[serde(rename = "get_amount_total")] + amount_total_display: String, + #[serde(rename = "get_amount_by_ds")] + amount_by_ds: String, + comment: String, + date: String, + #[serde(rename = "get_status")] + status_name: String, + status: String, + nds: String, + name_of_product: String, + counterparty: String, + amount: f64, + month_pay: Option, + avans_pay: String, + amount_total: f64, + estimate_nds_cost: f64, + estimate_nds_cert: f64, + #[serde(rename = "get_bill_cost")] + bill_cost: Option, + income_total: f64, + arrears: f64, + company: Option, + project: Option, + frc: Option, + get_employee: Option, +} + +#[derive(Debug, Deserialize)] +struct RemoteBillCost { + #[serde(rename = "cost__sum")] + cost_sum: Option, + #[serde(rename = "paid__sum")] + paid_sum: Option, +} + +#[derive(Debug, Deserialize)] +struct RemoteCompany { + id: i64, + name: String, +} + +#[derive(Debug, Deserialize)] +struct RemoteProject { + id: i64, + name: String, + full_name: String, + #[serde(rename = "get_short_name")] + short_name: String, + locality: Option, +} + +#[derive(Debug, Deserialize)] +#[serde(untagged)] +enum RemoteLocality { + Object { id: i64, name: String }, + Id(i64), +} + +#[derive(Debug, Deserialize)] +struct RemoteFrc { + id: i64, + name: String, + icon: Option, + #[serde(rename = "get_balance")] + balance: Option, +} + +#[derive(Debug, Deserialize)] +struct RemoteEmployee { + id: i64, + name: String, + short_name: String, + avatar_small: Option, +} + +pub async fn sync_contracts(api: &TauriApi) -> Result { + let state = api.state(); + let token = state + .auth_token() + .ok_or_else(|| ApiError::new("not_authenticated", "authentication token is missing"))?; + let remote = state + .config + .remote + .as_ref() + .ok_or_else(|| ApiError::bad_request("sync requires [remote].base_url config"))?; + let client = reqwest::Client::new(); + let mut next_url = Some(format!( + "{}/api/contract/?page_size=100", + remote.base_url.trim_end_matches('/') + )); + let mut synced = 0; + let mut pages = 0; + + while let Some(url) = next_url { + let response = client + .get(url) + .header(reqwest::header::AUTHORIZATION, format!("Token {token}")) + .send() + .await?; + + if !response.status().is_success() { + let status = response.status(); + let detail = response.text().await.unwrap_or_default(); + return Err(ApiError::new( + "remote_error", + format!("remote request failed with {status}: {detail}"), + )); + } + + let page = response.json::().await?; + pages += 1; + + for contract in &page.results { + upsert_contract_dependencies(api, contract).await?; + upsert_contract(api, contract).await?; + synced += 1; + } + + next_url = page.links.next; + } + + Ok(SyncContractsResult { synced, pages }) +} + +async fn upsert_contract_dependencies( + api: &TauriApi, + contract: &RemoteContract, +) -> Result<(), ApiError> { + let pool = api.state().db().pool(); + + if let Some(company) = &contract.company { + sqlx::query( + "INSERT INTO counterparty (id, name) VALUES (?1, ?2) + ON CONFLICT(id) DO UPDATE SET name = excluded.name", + ) + .bind(company.id) + .bind(&company.name) + .execute(pool) + .await + .map_err(database_error)?; + } + + if let Some(project) = &contract.project { + let (locality_id, locality_name) = match &project.locality { + Some(RemoteLocality::Object { id, name }) => (Some(*id), Some(name.as_str())), + Some(RemoteLocality::Id(id)) => (Some(*id), None), + None => (None, None), + }; + sqlx::query( + "INSERT INTO project (id, name, full_name, short_name, locality_id, locality_name) + VALUES (?1, ?2, ?3, ?4, ?5, ?6) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + full_name = excluded.full_name, + short_name = excluded.short_name, + locality_id = excluded.locality_id, + locality_name = excluded.locality_name", + ) + .bind(project.id) + .bind(&project.name) + .bind(&project.full_name) + .bind(&project.short_name) + .bind(locality_id) + .bind(locality_name) + .execute(pool) + .await + .map_err(database_error)?; + } + + if let Some(frc) = &contract.frc { + sqlx::query( + "INSERT INTO frc (id, name, icon, balance) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + icon = excluded.icon, + balance = excluded.balance", + ) + .bind(frc.id) + .bind(&frc.name) + .bind(frc.icon.as_deref().unwrap_or_default()) + .bind(frc.balance.unwrap_or_default()) + .execute(pool) + .await + .map_err(database_error)?; + } + + if let Some(employee) = &contract.get_employee { + sqlx::query( + "INSERT INTO employee (id, name, short_name, avatar_small) VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + short_name = excluded.short_name, + avatar_small = excluded.avatar_small", + ) + .bind(employee.id) + .bind(&employee.name) + .bind(&employee.short_name) + .bind(employee.avatar_small.as_deref()) + .execute(pool) + .await + .map_err(database_error)?; + } + + Ok(()) +} + +async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<(), ApiError> { + sqlx::query( + "INSERT INTO contractapp ( + id, name, number, absolute_url, contract_type, category, amount_total_display, + amount_by_ds, comment, date, status_name, status, nds, name_of_product, + counterparty_name, amount, month_pay, avans_pay, amount_total, estimate_nds_cost, + estimate_nds_cert, bill_cost_sum, bill_paid_sum, income_total, arrears, + counterparty_id, project_id, frc_id, employee_id + ) + VALUES ( + ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, + ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29 + ) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + number = excluded.number, + absolute_url = excluded.absolute_url, + contract_type = excluded.contract_type, + category = excluded.category, + amount_total_display = excluded.amount_total_display, + amount_by_ds = excluded.amount_by_ds, + comment = excluded.comment, + date = excluded.date, + status_name = excluded.status_name, + status = excluded.status, + nds = excluded.nds, + name_of_product = excluded.name_of_product, + counterparty_name = excluded.counterparty_name, + amount = excluded.amount, + month_pay = excluded.month_pay, + avans_pay = excluded.avans_pay, + amount_total = excluded.amount_total, + estimate_nds_cost = excluded.estimate_nds_cost, + estimate_nds_cert = excluded.estimate_nds_cert, + bill_cost_sum = excluded.bill_cost_sum, + bill_paid_sum = excluded.bill_paid_sum, + income_total = excluded.income_total, + arrears = excluded.arrears, + counterparty_id = excluded.counterparty_id, + project_id = excluded.project_id, + frc_id = excluded.frc_id, + employee_id = excluded.employee_id", + ) + .bind(contract.id) + .bind(&contract.name) + .bind(&contract.number) + .bind(&contract.absolute_url) + .bind(&contract.contract_type) + .bind(&contract.category) + .bind(&contract.amount_total_display) + .bind(&contract.amount_by_ds) + .bind(&contract.comment) + .bind(&contract.date) + .bind(&contract.status_name) + .bind(&contract.status) + .bind(&contract.nds) + .bind(&contract.name_of_product) + .bind(&contract.counterparty) + .bind(contract.amount) + .bind(contract.month_pay) + .bind(&contract.avans_pay) + .bind(contract.amount_total) + .bind(contract.estimate_nds_cost) + .bind(contract.estimate_nds_cert) + .bind(contract.bill_cost.as_ref().and_then(|bill_cost| bill_cost.cost_sum)) + .bind(contract.bill_cost.as_ref().and_then(|bill_cost| bill_cost.paid_sum)) + .bind(contract.income_total) + .bind(contract.arrears) + .bind(contract.company.as_ref().map(|company| company.id)) + .bind(contract.project.as_ref().map(|project| project.id)) + .bind(contract.frc.as_ref().map(|frc| frc.id)) + .bind(contract.get_employee.as_ref().map(|employee| employee.id)) + .execute(api.state().db().pool()) + .await + .map_err(database_error)?; + + Ok(()) +} + +fn database_error(error: sqlx::Error) -> ApiError { + ApiError::new("database_error", error.to_string()) +} diff --git a/src/App.vue b/src/App.vue index 86aa63d..1f0b088 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,12 +1,18 @@ @@ -65,30 +86,6 @@ body { padding: 24px 14px 40px; } -.hero { - margin: 12px 2px 18px; -} - -.eyebrow { - margin: 0 0 6px; - color: #1989fa; - font-size: 0.75rem; - font-weight: 800; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -h1 { - margin: 0; - font-size: clamp(2rem, 9vw, 4rem); - line-height: 1; -} - -.subtitle { - margin: 12px 0 0; - color: #646566; -} - .notice { margin-bottom: 12px; border-radius: 14px; diff --git a/src/components/CounterpartySelect.vue b/src/components/CounterpartySelect.vue new file mode 100644 index 0000000..c94ad25 --- /dev/null +++ b/src/components/CounterpartySelect.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/src/components/EmployeeSelect.vue b/src/components/EmployeeSelect.vue index e01b1f9..9f8377d 100644 --- a/src/components/EmployeeSelect.vue +++ b/src/components/EmployeeSelect.vue @@ -1,28 +1,27 @@