implement contracts

This commit is contained in:
Timur Gordon
2025-04-22 03:06:58 +02:00
parent 36d605829f
commit 951af7dec7
11 changed files with 1625 additions and 2 deletions

View File

@@ -0,0 +1,344 @@
use actix_web::{web, HttpResponse, Responder, Result};
use tera::{Context, Tera};
use chrono::{Utc, Duration};
use uuid::Uuid;
use serde::{Deserialize, Serialize};
use crate::models::contract::{Contract, ContractStatus, ContractType, ContractStatistics, ContractSigner, ContractRevision, SignerStatus, ContractFilter};
use crate::controllers::auth::Claims;
#[derive(Debug, Deserialize)]
pub struct ContractForm {
pub title: String,
pub description: String,
pub contract_type: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct SignerForm {
pub name: String,
pub email: String,
}
pub struct ContractController;
impl ContractController {
// Display the contracts dashboard
pub async fn index(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
let contracts = Self::get_mock_contracts();
let stats = ContractStatistics::new(&contracts);
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
// Add stats
context.insert("stats", &serde_json::to_value(stats).unwrap());
// Add recent contracts
let recent_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.take(5)
.map(|c| Self::contract_to_json(c))
.collect();
context.insert("recent_contracts", &recent_contracts);
// Add pending signature contracts
let pending_signature_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.filter(|c| c.status == ContractStatus::PendingSignatures)
.map(|c| Self::contract_to_json(c))
.collect();
context.insert("pending_signature_contracts", &pending_signature_contracts);
// Add draft contracts
let draft_contracts: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.filter(|c| c.status == ContractStatus::Draft)
.map(|c| Self::contract_to_json(c))
.collect();
context.insert("draft_contracts", &draft_contracts);
Ok(HttpResponse::Ok().content_type("text/html").body(
tmpl.render("contracts/index.html", &context).unwrap()
))
}
// Display the list of all contracts
pub async fn list(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
let contracts = Self::get_mock_contracts();
let contracts_data: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.map(|c| Self::contract_to_json(c))
.collect();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
context.insert("contracts", &contracts_data);
context.insert("filter", &"all");
Ok(HttpResponse::Ok().content_type("text/html").body(
tmpl.render("contracts/contracts.html", &context).unwrap()
))
}
// Display the list of user's contracts
pub async fn my_contracts(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
let contracts = Self::get_mock_contracts();
let contracts_data: Vec<serde_json::Map<String, serde_json::Value>> = contracts
.iter()
.map(|c| Self::contract_to_json(c))
.collect();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
context.insert("contracts", &contracts_data);
Ok(HttpResponse::Ok().content_type("text/html").body(
tmpl.render("contracts/my_contracts.html", &context).unwrap()
))
}
// Display a specific contract
pub async fn detail(tmpl: web::Data<Tera>, path: web::Path<String>) -> Result<HttpResponse> {
let contract_id = path.into_inner();
let mut context = Context::new();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
// Find the contract by ID
let contracts = Self::get_mock_contracts();
let contract = contracts.iter().find(|c| c.id == contract_id);
match contract {
Some(contract) => {
// Convert contract to JSON
let contract_json = Self::contract_to_json(contract);
context.insert("contract", &contract_json);
context.insert("user_has_signed", &false); // Mock data
Ok(HttpResponse::Ok().content_type("text/html").body(
tmpl.render("contracts/contract_detail.html", &context).unwrap()
))
},
None => {
Ok(HttpResponse::NotFound().finish())
}
}
}
// Display the create contract form
pub async fn create_form(tmpl: web::Data<Tera>) -> Result<HttpResponse> {
let mut context = Context::new();
// Add active_page for navigation highlighting
context.insert("active_page", &"contracts");
// Add contract types for dropdown
let contract_types = vec![
("Service", "Service Agreement"),
("Employment", "Employment Contract"),
("NDA", "Non-Disclosure Agreement"),
("SLA", "Service Level Agreement"),
("Other", "Other")
];
context.insert("contract_types", &contract_types);
Ok(HttpResponse::Ok().content_type("text/html").body(
tmpl.render("contracts/create_contract.html", &context).unwrap()
))
}
// Process the create contract form
pub async fn create(
tmpl: web::Data<Tera>,
form: web::Form<ContractForm>,
) -> Result<HttpResponse> {
// In a real application, we would save the contract to the database
// For now, we'll just redirect to the contracts list
Ok(HttpResponse::Found().header("Location", "/contracts").finish())
}
// Helper method to convert Contract to a JSON object for templates
fn contract_to_json(contract: &Contract) -> serde_json::Map<String, serde_json::Value> {
let mut map = serde_json::Map::new();
map.insert("id".to_string(), serde_json::Value::String(contract.id.clone()));
map.insert("title".to_string(), serde_json::Value::String(contract.title.clone()));
map.insert("description".to_string(), serde_json::Value::String(contract.description.clone()));
map.insert("status".to_string(), serde_json::Value::String(format!("{:?}", contract.status)));
map.insert("contract_type".to_string(), serde_json::Value::String(format!("{:?}", contract.contract_type)));
map.insert("created_at".to_string(), serde_json::Value::String(contract.created_at.format("%Y-%m-%d").to_string()));
map.insert("updated_at".to_string(), serde_json::Value::String(contract.updated_at.format("%Y-%m-%d").to_string()));
map.insert("created_by".to_string(), serde_json::Value::String("John Doe".to_string())); // Mock data
// Add signers
let signers: Vec<serde_json::Value> = contract.signers.iter()
.map(|s| {
let mut signer_map = serde_json::Map::new();
signer_map.insert("id".to_string(), serde_json::Value::String(s.id.clone()));
signer_map.insert("name".to_string(), serde_json::Value::String(s.name.clone()));
signer_map.insert("email".to_string(), serde_json::Value::String(s.email.clone()));
signer_map.insert("status".to_string(), serde_json::Value::String(format!("{:?}", s.status)));
if let Some(signed_at) = s.signed_at {
signer_map.insert("signed_at".to_string(), serde_json::Value::String(signed_at.format("%Y-%m-%d").to_string()));
}
serde_json::Value::Object(signer_map)
})
.collect();
map.insert("signers".to_string(), serde_json::Value::Array(signers));
// Add signed_signers count for templates
let signed_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Signed).count();
map.insert("signed_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(signed_signers)));
// Add pending_signers count for templates
let pending_signers = contract.signers.iter().filter(|s| s.status == SignerStatus::Pending).count();
map.insert("pending_signers".to_string(), serde_json::Value::Number(serde_json::Number::from(pending_signers)));
// Add revisions
let revisions: Vec<serde_json::Value> = contract.revisions.iter()
.map(|r| {
let mut revision_map = serde_json::Map::new();
revision_map.insert("version".to_string(), serde_json::Value::Number(serde_json::Number::from(r.version)));
revision_map.insert("content".to_string(), serde_json::Value::String(r.content.clone()));
revision_map.insert("created_at".to_string(), serde_json::Value::String(r.created_at.format("%Y-%m-%d").to_string()));
revision_map.insert("created_by".to_string(), serde_json::Value::String(r.created_by.clone()));
if let Some(comments) = &r.comments {
revision_map.insert("comments".to_string(), serde_json::Value::String(comments.clone()));
// Add notes field using comments since ContractRevision doesn't have a notes field
revision_map.insert("notes".to_string(), serde_json::Value::String(comments.clone()));
}
serde_json::Value::Object(revision_map)
})
.collect();
map.insert("revisions".to_string(), serde_json::Value::Array(revisions));
// Add effective and expiration dates if present
if let Some(effective_date) = &contract.effective_date {
map.insert("effective_date".to_string(), serde_json::Value::String(effective_date.format("%Y-%m-%d").to_string()));
}
if let Some(expiration_date) = &contract.expiration_date {
map.insert("expiration_date".to_string(), serde_json::Value::String(expiration_date.format("%Y-%m-%d").to_string()));
}
map
}
// Generate mock contracts for testing
fn get_mock_contracts() -> Vec<Contract> {
let now = Utc::now();
let mut contracts = Vec::new();
// Contract 1 - Draft
let mut contract1 = Contract::new(
"Employment Agreement - Marketing Manager".to_string(),
"Standard employment contract for the Marketing Manager position".to_string(),
ContractType::Employment,
"John Doe".to_string(),
Some("Acme Corp".to_string())
);
contract1.effective_date = Some(now + Duration::days(30));
contract1.expiration_date = Some(now + Duration::days(395)); // ~1 year
contract1.add_revision(
"<p>This is a draft employment contract for the Marketing Manager position.</p>".to_string(),
"John Doe".to_string(),
Some("Initial draft".to_string())
);
// Contract 2 - Pending Signatures
let mut contract2 = Contract::new(
"Non-Disclosure Agreement - Vendor XYZ".to_string(),
"NDA for our partnership with Vendor XYZ".to_string(),
ContractType::NDA,
"John Doe".to_string(),
Some("Acme Corp".to_string())
);
contract2.effective_date = Some(now);
contract2.expiration_date = Some(now + Duration::days(730)); // 2 years
contract2.add_revision(
"<p>This is the first version of the NDA with Vendor XYZ.</p>".to_string(),
"John Doe".to_string(),
Some("Initial draft".to_string())
);
contract2.add_revision(
"<p>This is the revised version of the NDA with Vendor XYZ.</p>".to_string(),
"John Doe".to_string(),
Some("Added confidentiality period".to_string())
);
contract2.add_signer("Jane Smith".to_string(), "jane@example.com".to_string());
contract2.add_signer("Bob Johnson".to_string(), "bob@vendorxyz.com".to_string());
// Mark Jane as signed
if let Some(signer) = contract2.signers.iter_mut().next() {
signer.sign(None);
}
// Send for signatures
let _ = contract2.send_for_signatures();
// Contract 3 - Signed
let mut contract3 = Contract::new(
"Service Agreement - Website Maintenance".to_string(),
"Agreement for ongoing website maintenance services".to_string(),
ContractType::Service,
"John Doe".to_string(),
Some("Acme Corp".to_string())
);
contract3.effective_date = Some(now - Duration::days(7));
contract3.expiration_date = Some(now + Duration::days(358)); // ~1 year from effective date
contract3.add_revision(
"<p>This is a service agreement for website maintenance.</p>".to_string(),
"John Doe".to_string(),
Some("Initial version".to_string())
);
contract3.add_signer("Jane Smith".to_string(), "jane@example.com".to_string());
contract3.add_signer("Alice Brown".to_string(), "alice@webmaintenance.com".to_string());
// Mark both signers as signed
for signer in contract3.signers.iter_mut() {
signer.sign(None);
}
// Mark as signed
contract3.status = ContractStatus::Signed;
contracts.push(contract1);
contracts.push(contract2);
contracts.push(contract3);
contracts
}
}

View File

@@ -5,6 +5,7 @@ pub mod ticket;
pub mod calendar;
pub mod governance;
pub mod flow;
pub mod contract;
// Re-export controllers for easier imports
pub use home::HomeController;
@@ -12,4 +13,5 @@ pub use auth::AuthController;
pub use ticket::TicketController;
pub use calendar::CalendarController;
pub use governance::GovernanceController;
pub use flow::FlowController;
pub use flow::FlowController;
pub use contract::ContractController;