diff --git a/src-tauri/src/apps/contractapp/filters.rs b/src-tauri/src/apps/contractapp/filters.rs index acf636f..5c19e2f 100644 --- a/src-tauri/src/apps/contractapp/filters.rs +++ b/src-tauri/src/apps/contractapp/filters.rs @@ -1,6 +1,6 @@ use che_tauri::{Filter, FilterSet}; -use super::models::{Contract, ContractApplicationFile, Counterparty}; +use super::models::{Contract, ContractApplicationFile, ContractCategory, Counterparty}; static CONTRACTAPP_FILTERS: &[Filter] = &[ Filter::exact("id"), @@ -16,6 +16,7 @@ static CONTRACTAPP_FILTERS: &[Filter] = &[ Filter::contains("contract_type"), Filter::exact("category"), Filter::contains("category"), + Filter::exact("category_id"), Filter::contains("counterparty_name"), Filter::contains("name_of_product"), Filter::exact("counterparty_id"), @@ -24,6 +25,14 @@ static CONTRACTAPP_FILTERS: &[Filter] = &[ Filter::exact("employee_id"), ]; +static CONTRACT_CATEGORY_FILTERS: &[Filter] = &[ + Filter::exact("id"), + Filter::exact("name"), + Filter::contains("name"), + Filter::exact("name_group"), + Filter::contains("name_group"), +]; + static COUNTERPARTY_FILTERS: &[Filter] = &[ Filter::exact("id"), Filter::exact("name"), @@ -43,6 +52,10 @@ pub fn contractapp_filterset() -> FilterSet { FilterSet::new(CONTRACTAPP_FILTERS) } +pub fn contract_category_filterset() -> FilterSet { + FilterSet::new(CONTRACT_CATEGORY_FILTERS) +} + pub fn counterparty_filterset() -> FilterSet { FilterSet::new(COUNTERPARTY_FILTERS) } diff --git a/src-tauri/src/apps/contractapp/migrations/0006_category_model.sql b/src-tauri/src/apps/contractapp/migrations/0006_category_model.sql new file mode 100644 index 0000000..55c9d6b --- /dev/null +++ b/src-tauri/src/apps/contractapp/migrations/0006_category_model.sql @@ -0,0 +1,11 @@ +CREATE TABLE IF NOT EXISTS contract_category ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + name_group TEXT NOT NULL, + template TEXT, + is_questionnair BOOLEAN NOT NULL, + parent INTEGER, + secure_group INTEGER +); + +ALTER TABLE contractapp ADD COLUMN category_id INTEGER REFERENCES contract_category(id); diff --git a/src-tauri/src/apps/contractapp/migrations/schema.json b/src-tauri/src/apps/contractapp/migrations/schema.json index c4455af..47a150c 100644 --- a/src-tauri/src/apps/contractapp/migrations/schema.json +++ b/src-tauri/src/apps/contractapp/migrations/schema.json @@ -173,6 +173,88 @@ } ] }, + { + "table": "contract_category", + "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": "name_group", + "ty": "text", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "template", + "ty": "text", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "is_questionnair", + "ty": "boolean", + "primary_key": false, + "nullable": false, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "parent", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + }, + { + "name": "secure_group", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": null + } + ] + }, { "table": "contractapp", "fields": [ @@ -242,6 +324,20 @@ "default": null, "foreign_key": null }, + { + "name": "category_id", + "ty": "integer", + "primary_key": false, + "nullable": true, + "auto": false, + "unique": false, + "max_length": null, + "default": null, + "foreign_key": { + "table": "contract_category", + "column": "id" + } + }, { "name": "amount_total_display", "ty": "text", diff --git a/src-tauri/src/apps/contractapp/mod.rs b/src-tauri/src/apps/contractapp/mod.rs index a26f4da..580c16a 100644 --- a/src-tauri/src/apps/contractapp/mod.rs +++ b/src-tauri/src/apps/contractapp/mod.rs @@ -21,6 +21,11 @@ impl AppModule for ContractappModule { serializers::contractapp_serializer(), filters::contractapp_filterset(), ); + ctx.resource::( + "contract_category", + serializers::contract_category_serializer(), + filters::contract_category_filterset(), + ); ctx.resource::( "counterparty", serializers::counterparty_serializer(), diff --git a/src-tauri/src/apps/contractapp/models.rs b/src-tauri/src/apps/contractapp/models.rs index d8303ab..0cace4c 100644 --- a/src-tauri/src/apps/contractapp/models.rs +++ b/src-tauri/src/apps/contractapp/models.rs @@ -15,6 +15,10 @@ pub struct Contract { pub absolute_url: String, pub contract_type: String, pub category: String, + + #[field(foreign_key = ContractCategory)] + pub category_id: Option, + pub amount_total_display: String, pub amount_by_ds: String, pub comment: String, @@ -48,6 +52,20 @@ pub struct Contract { pub employee_id: Option, } +#[derive(Debug, Clone, Model)] +#[model(table = "contract_category")] +pub struct ContractCategory { + #[field(primary_key)] + pub id: i64, + + pub name: String, + pub name_group: String, + pub template: Option, + pub is_questionnair: bool, + pub parent: Option, + pub secure_group: Option, +} + #[derive(Debug, Clone, Model)] #[model(table = "counterparty")] pub struct Counterparty { diff --git a/src-tauri/src/apps/contractapp/serializers.rs b/src-tauri/src/apps/contractapp/serializers.rs index a375405..e179024 100644 --- a/src-tauri/src/apps/contractapp/serializers.rs +++ b/src-tauri/src/apps/contractapp/serializers.rs @@ -6,7 +6,7 @@ use crate::apps::{ projectapp::{models::Project, serializers::project_serializer}, }; -use super::models::{Contract, ContractApplicationFile, Counterparty}; +use super::models::{Contract, ContractApplicationFile, ContractCategory, Counterparty}; static CONTRACT_FIELDS: &[Field] = &[ Field::new("id").read_only(), @@ -15,6 +15,7 @@ static CONTRACT_FIELDS: &[Field] = &[ Field::new("absolute_url"), Field::new("contract_type"), Field::new("category"), + Field::new("category_id").required(false).nullable(), Field::new("amount_total_display"), Field::new("amount_by_ds"), Field::new("comment"), @@ -38,12 +39,22 @@ static CONTRACT_FIELDS: &[Field] = &[ Field::new("project_id").required(false).nullable(), Field::new("frc_id").required(false).nullable(), Field::new("employee_id").required(false).nullable(), + Field::related("category_ref", "category_id", &CATEGORY_RELATION), 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 CONTRACT_CATEGORY_FIELDS: &[Field] = &[ + Field::new("id").read_only(), + Field::new("name"), + Field::new("name_group"), + Field::new("template").required(false).nullable(), + Field::new("is_questionnair"), + Field::new("parent").required(false).nullable(), + Field::new("secure_group").required(false).nullable(), +]; static COUNTERPARTY_FIELDS: &[Field] = &[Field::new("id").read_only(), Field::new("name")]; static CONTRACT_APPLICATION_FILE_FIELDS: &[Field] = &[ Field::new("id").read_only(), @@ -65,6 +76,8 @@ static CONTRACT_APPLICATION_FILE_FIELDS: &[Field] = &[ ]; static COUNTERPARTY_RELATION: RelatedModel = RelatedModel::new(counterparty_serializer); +static CATEGORY_RELATION: RelatedModel = + RelatedModel::new(contract_category_serializer); static CONTRACT_RELATION: RelatedModel = RelatedModel::new(contractapp_serializer); static PROJECT_RELATION: RelatedModel = RelatedModel::new(project_serializer); static FRC_RELATION: RelatedModel = RelatedModel::new(frc_serializer); @@ -74,6 +87,10 @@ pub fn contractapp_serializer() -> ModelSerializer { ModelSerializer::new(CONTRACT_FIELDS) } +pub fn contract_category_serializer() -> ModelSerializer { + ModelSerializer::new(CONTRACT_CATEGORY_FIELDS) +} + pub fn counterparty_serializer() -> ModelSerializer { ModelSerializer::new(COUNTERPARTY_FIELDS) } diff --git a/src-tauri/src/sync.rs b/src-tauri/src/sync.rs index 72ff5ee..253d5ad 100644 --- a/src-tauri/src/sync.rs +++ b/src-tauri/src/sync.rs @@ -18,6 +18,12 @@ struct ContractPage { results: Vec, } +#[derive(Debug, Deserialize)] +struct ContractCategoryPage { + links: PageLinks, + results: Vec, +} + #[derive(Debug, Deserialize)] struct PageLinks { next: Option, @@ -34,6 +40,8 @@ struct RemoteContract { contract_type: String, #[serde(rename = "get_category")] category: String, + #[serde(default)] + category_id: Option, #[serde(rename = "get_amount_total")] amount_total_display: String, #[serde(rename = "get_amount_by_ds")] @@ -62,6 +70,17 @@ struct RemoteContract { get_employee: Option, } +#[derive(Debug, Deserialize)] +struct RemoteContractCategory { + id: i64, + name: String, + name_group: String, + template: Option, + is_questionnair: bool, + parent: Option, + secure_group: Option, +} + #[derive(Debug, Deserialize)] struct RemoteBillCost { #[serde(rename = "cost__sum")] @@ -151,6 +170,8 @@ pub async fn sync_contracts( .as_ref() .ok_or_else(|| ApiError::bad_request("sync requires [remote].base_url config"))?; let client = reqwest::Client::new(); + sync_contract_categories(api, &client, &token, &remote.base_url).await?; + let mut next_url = Some(format!( "{}/api/contract/?page_size=100", remote.base_url.trim_end_matches('/') @@ -182,12 +203,14 @@ pub async fn sync_contracts( for contract in &page.results { upsert_contract_dependencies(api, contract).await?; upsert_contract(api, contract).await?; - let detail = fetch_contract_detail(&client, &token, &remote.base_url, contract.id).await?; + let detail = + fetch_contract_detail(&client, &token, &remote.base_url, contract.id).await?; for application in &detail.contractapplicationfile_set { let local_path = download_application_file(&client, &token, &app_data_dir, application).await?; - upsert_contract_application_file(api, contract.id, application, &local_path).await?; + upsert_contract_application_file(api, contract.id, application, &local_path) + .await?; applications += 1; if !local_path.is_empty() { files_downloaded += 1; @@ -208,6 +231,74 @@ pub async fn sync_contracts( }) } +async fn sync_contract_categories( + api: &TauriApi, + client: &reqwest::Client, + token: &str, + base_url: &str, +) -> Result<(), ApiError> { + let mut next_url = Some(format!( + "{}/api/cont/category/?page_size=100", + base_url.trim_end_matches('/') + )); + + 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 category request failed with {status}: {detail}"), + )); + } + + let page = response.json::().await?; + for category in &page.results { + upsert_contract_category(api, category).await?; + } + next_url = page.links.next; + } + + Ok(()) +} + +async fn upsert_contract_category( + api: &TauriApi, + category: &RemoteContractCategory, +) -> Result<(), ApiError> { + sqlx::query( + "INSERT INTO contract_category ( + id, name, name_group, template, is_questionnair, parent, secure_group + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + name_group = excluded.name_group, + template = excluded.template, + is_questionnair = excluded.is_questionnair, + parent = excluded.parent, + secure_group = excluded.secure_group", + ) + .bind(category.id) + .bind(&category.name) + .bind(&category.name_group) + .bind(category.template.as_deref()) + .bind(category.is_questionnair) + .bind(category.parent) + .bind(category.secure_group) + .execute(api.state().db().pool()) + .await + .map_err(database_error)?; + + Ok(()) +} + async fn fetch_contract_detail( client: &reqwest::Client, token: &str, @@ -250,7 +341,11 @@ async fn download_application_file( .await .map_err(file_error)?; - let file_name = format!("{}_{}", application.id, sanitize_file_name(&application.scan_name)); + let file_name = format!( + "{}_{}", + application.id, + sanitize_file_name(&application.scan_name) + ); let path = directory.join(file_name); let response = client @@ -379,7 +474,7 @@ async fn upsert_contract_dependencies( 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, + id, name, number, absolute_url, contract_type, category, category_id, 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, @@ -387,7 +482,7 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<() ) 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 + ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30 ) ON CONFLICT(id) DO UPDATE SET name = excluded.name, @@ -395,6 +490,7 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<() absolute_url = excluded.absolute_url, contract_type = excluded.contract_type, category = excluded.category, + category_id = excluded.category_id, amount_total_display = excluded.amount_total_display, amount_by_ds = excluded.amount_by_ds, comment = excluded.comment, @@ -425,6 +521,7 @@ async fn upsert_contract(api: &TauriApi, contract: &RemoteContract) -> Result<() .bind(&contract.absolute_url) .bind(&contract.contract_type) .bind(&contract.category) + .bind(contract.category_id) .bind(&contract.amount_total_display) .bind(&contract.amount_by_ds) .bind(&contract.comment) diff --git a/src/App.vue b/src/App.vue index 1f0b088..ce7fd4a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -47,118 +47,23 @@ async function logoutAndRedirect() { - - Задачи - Контракты - Пользователи + + Задачи + Контракты + Пользователи - + diff --git a/src/components/ContractCategorySelect.vue b/src/components/ContractCategorySelect.vue new file mode 100644 index 0000000..b464b1d --- /dev/null +++ b/src/components/ContractCategorySelect.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/src/components/FrcSelect.vue b/src/components/FrcSelect.vue new file mode 100644 index 0000000..020e856 --- /dev/null +++ b/src/components/FrcSelect.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/components/ProjectSelect.vue b/src/components/ProjectSelect.vue new file mode 100644 index 0000000..fecd2c1 --- /dev/null +++ b/src/components/ProjectSelect.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/generated/api.ts b/src/generated/api.ts index 5ecc261..6b211bd 100644 --- a/src/generated/api.ts +++ b/src/generated/api.ts @@ -8,6 +8,10 @@ import type { ContractApplicationFileCreate, ContractApplicationFileUpdate, ContractApplicationFileListParams, + ContractCategory, + ContractCategoryCreate, + ContractCategoryUpdate, + ContractCategoryListParams, Counterparty, CounterpartyCreate, CounterpartyUpdate, @@ -40,6 +44,7 @@ import type { export const contractApi = createModelApi("contract"); export const contractApplicationFileApi = createModelApi("contract_application_file"); +export const contractCategoryApi = createModelApi("contract_category"); export const counterpartyApi = createModelApi("counterparty"); export const employeeApi = createModelApi("employee"); export const frcApi = createModelApi("frc"); diff --git a/src/generated/models.ts b/src/generated/models.ts index 73aa1ad..e42678f 100644 --- a/src/generated/models.ts +++ b/src/generated/models.ts @@ -7,6 +7,7 @@ export interface Contract { absolute_url: string; contract_type: string; category: string; + category_id: number | null; amount_total_display: string; amount_by_ds: string; comment: string; @@ -30,6 +31,7 @@ export interface Contract { project_id: number | null; frc_id: number | null; employee_id: number | null; + category_ref: ContractCategory | null; counterparty: Counterparty | null; project: Project | null; frc: Frc | null; @@ -42,6 +44,7 @@ export interface ContractCreate { absolute_url: string; contract_type: string; category: string; + category_id?: number | null; amount_total_display: string; amount_by_ds: string; comment: string; @@ -73,6 +76,7 @@ export interface ContractUpdate { absolute_url?: string; contract_type?: string; category?: string; + category_id?: number | null; amount_total_display?: string; amount_by_ds?: string; comment?: string; @@ -112,6 +116,7 @@ export interface ContractListParams extends ListParams { contract_type__contains?: string; category?: string; category__contains?: string; + category_id?: number | null; counterparty_name__contains?: string; name_of_product__contains?: string; counterparty_id?: number | null; @@ -182,6 +187,42 @@ export interface ContractApplicationFileListParams extends ListParams { status?: string; } +export interface ContractCategory { + id: number; + name: string; + name_group: string; + template: string | null; + is_questionnair: boolean; + parent: number | null; + secure_group: number | null; +} + +export interface ContractCategoryCreate { + name: string; + name_group: string; + template?: string | null; + is_questionnair: boolean; + parent?: number | null; + secure_group?: number | null; +} + +export interface ContractCategoryUpdate { + name?: string; + name_group?: string; + template?: string | null; + is_questionnair?: boolean; + parent?: number | null; + secure_group?: number | null; +} + +export interface ContractCategoryListParams extends ListParams { + id?: number; + name?: string; + name__contains?: string; + name_group?: string; + name_group__contains?: string; +} + export interface Counterparty { id: number; name: string; diff --git a/src/views/ContractDetailView.vue b/src/views/ContractDetailView.vue index 12c0edf..a922fb5 100644 --- a/src/views/ContractDetailView.vue +++ b/src/views/ContractDetailView.vue @@ -1,15 +1,19 @@