feat: Enhance contract management with new features
- Implement comprehensive contract listing with filtering by status and type, and search functionality. - Add contract cloning, sharing, and cancellation features. - Improve contract details view with enhanced UI and activity timeline. - Implement signer management with add/update/delete and status updates, including signature data handling and rejection. - Introduce contract creation and editing functionalities with markdown support. - Add error handling for contract not found scenarios. - Implement reminder system for pending signatures with rate limiting and status tracking. - Add API endpoint for retrieving contract statistics. - Improve logging with more descriptive messages. - Refactor code for better structure and maintainability.
This commit is contained in:
parent
7e95391a9c
commit
464e253739
File diff suppressed because it is too large
Load Diff
121
actix_mvc_app/src/controllers/error.rs
Normal file
121
actix_mvc_app/src/controllers/error.rs
Normal file
@ -0,0 +1,121 @@
|
||||
use actix_web::{Error, HttpResponse, web};
|
||||
use tera::{Context, Tera};
|
||||
|
||||
pub struct ErrorController;
|
||||
|
||||
impl ErrorController {
|
||||
/// Renders a 404 Not Found page with customizable content
|
||||
pub async fn not_found(
|
||||
tmpl: web::Data<Tera>,
|
||||
error_title: Option<&str>,
|
||||
error_message: Option<&str>,
|
||||
return_url: Option<&str>,
|
||||
return_text: Option<&str>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let mut context = Context::new();
|
||||
|
||||
// Set default or custom error content
|
||||
context.insert("error_title", &error_title.unwrap_or("Page Not Found"));
|
||||
context.insert(
|
||||
"error_message",
|
||||
&error_message
|
||||
.unwrap_or("The page you're looking for doesn't exist or has been moved."),
|
||||
);
|
||||
|
||||
// Optional return URL and text
|
||||
if let Some(url) = return_url {
|
||||
context.insert("return_url", &url);
|
||||
context.insert("return_text", &return_text.unwrap_or("Return"));
|
||||
}
|
||||
|
||||
// Render the 404 template with 404 status
|
||||
match tmpl.render("errors/404.html", &context) {
|
||||
Ok(rendered) => Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(rendered)),
|
||||
Err(e) => {
|
||||
log::error!("Failed to render 404 template: {}", e);
|
||||
// Fallback to simple text response
|
||||
Ok(HttpResponse::NotFound()
|
||||
.content_type("text/plain")
|
||||
.body("404 - Page Not Found"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a 404 page for contract not found
|
||||
pub async fn contract_not_found(
|
||||
tmpl: web::Data<Tera>,
|
||||
contract_id: Option<&str>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let error_title = "Contract Not Found";
|
||||
let error_message = if let Some(id) = contract_id {
|
||||
format!(
|
||||
"The contract with ID '{}' doesn't exist or has been removed.",
|
||||
id
|
||||
)
|
||||
} else {
|
||||
"The contract you're looking for doesn't exist or has been removed.".to_string()
|
||||
};
|
||||
|
||||
Self::not_found(
|
||||
tmpl,
|
||||
Some(error_title),
|
||||
Some(&error_message),
|
||||
Some("/contracts"),
|
||||
Some("Back to Contracts"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Renders a 404 page for calendar event not found
|
||||
pub async fn calendar_event_not_found(
|
||||
tmpl: web::Data<Tera>,
|
||||
event_id: Option<&str>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let error_title = "Calendar Event Not Found";
|
||||
let error_message = if let Some(id) = event_id {
|
||||
format!(
|
||||
"The calendar event with ID '{}' doesn't exist or has been removed.",
|
||||
id
|
||||
)
|
||||
} else {
|
||||
"The calendar event you're looking for doesn't exist or has been removed.".to_string()
|
||||
};
|
||||
|
||||
Self::not_found(
|
||||
tmpl,
|
||||
Some(error_title),
|
||||
Some(&error_message),
|
||||
Some("/calendar"),
|
||||
Some("Back to Calendar"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Renders a generic 404 page
|
||||
pub async fn generic_not_found(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
Self::not_found(tmpl, None, None, None, None).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper function to quickly render a contract not found response
|
||||
pub async fn render_contract_not_found(
|
||||
tmpl: &web::Data<Tera>,
|
||||
contract_id: Option<&str>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ErrorController::contract_not_found(tmpl.clone(), contract_id).await
|
||||
}
|
||||
|
||||
/// Helper function to quickly render a calendar event not found response
|
||||
pub async fn render_calendar_event_not_found(
|
||||
tmpl: &web::Data<Tera>,
|
||||
event_id: Option<&str>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
ErrorController::calendar_event_not_found(tmpl.clone(), event_id).await
|
||||
}
|
||||
|
||||
/// Helper function to quickly render a generic not found response
|
||||
pub async fn render_generic_not_found(tmpl: web::Data<Tera>) -> Result<HttpResponse, Error> {
|
||||
ErrorController::generic_not_found(tmpl).await
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
// Export controllers
|
||||
pub mod home;
|
||||
pub mod auth;
|
||||
pub mod ticket;
|
||||
pub mod calendar;
|
||||
pub mod governance;
|
||||
pub mod flow;
|
||||
pub mod contract;
|
||||
pub mod asset;
|
||||
pub mod defi;
|
||||
pub mod marketplace;
|
||||
pub mod auth;
|
||||
pub mod calendar;
|
||||
pub mod company;
|
||||
pub mod contract;
|
||||
pub mod defi;
|
||||
pub mod error;
|
||||
pub mod flow;
|
||||
pub mod governance;
|
||||
pub mod home;
|
||||
pub mod marketplace;
|
||||
pub mod ticket;
|
||||
|
||||
// Re-export controllers for easier imports
|
||||
|
@ -97,7 +97,7 @@ pub fn get_calendars() -> Result<Vec<Calendar>, String> {
|
||||
let calendars = match collection.get_all() {
|
||||
Ok(calendars) => calendars,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading calendars: {:?}", e);
|
||||
log::error!("Error loading calendars: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
@ -113,7 +113,7 @@ pub fn get_events() -> Result<Vec<Event>, String> {
|
||||
let events = match collection.get_all() {
|
||||
Ok(events) => events,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading events: {:?}", e);
|
||||
log::error!("Error loading events: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
@ -129,7 +129,7 @@ pub fn get_calendar_by_id(calendar_id: u32) -> Result<Option<Calendar>, String>
|
||||
match collection.get_by_id(calendar_id) {
|
||||
Ok(calendar) => Ok(calendar),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching calendar by id {}: {:?}", calendar_id, e);
|
||||
log::error!("Error fetching calendar by id {}: {:?}", calendar_id, e);
|
||||
Err(format!("Failed to fetch calendar: {:?}", e))
|
||||
}
|
||||
}
|
||||
@ -144,7 +144,7 @@ pub fn get_event_by_id(event_id: u32) -> Result<Option<Event>, String> {
|
||||
match collection.get_by_id(event_id) {
|
||||
Ok(event) => Ok(event),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching event by id {}: {:?}", event_id, e);
|
||||
log::error!("Error fetching event by id {}: {:?}", event_id, e);
|
||||
Err(format!("Failed to fetch event: {:?}", e))
|
||||
}
|
||||
}
|
||||
@ -178,7 +178,7 @@ pub fn get_attendee_by_id(attendee_id: u32) -> Result<Option<Attendee>, String>
|
||||
match collection.get_by_id(attendee_id) {
|
||||
Ok(attendee) => Ok(attendee),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching attendee by id {}: {:?}", attendee_id, e);
|
||||
log::error!("Error fetching attendee by id {}: {:?}", attendee_id, e);
|
||||
Err(format!("Failed to fetch attendee: {:?}", e))
|
||||
}
|
||||
}
|
||||
@ -332,7 +332,7 @@ pub fn get_or_create_user_calendar(user_id: u32, user_name: &str) -> Result<Cale
|
||||
let calendars = match collection.get_all() {
|
||||
Ok(calendars) => calendars,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading calendars: {:?}", e);
|
||||
log::error!("Error loading calendars: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
|
458
actix_mvc_app/src/db/contracts.rs
Normal file
458
actix_mvc_app/src/db/contracts.rs
Normal file
@ -0,0 +1,458 @@
|
||||
use heromodels::{
|
||||
db::{Collection, Db},
|
||||
models::legal::{Contract, ContractRevision, ContractSigner, ContractStatus, SignerStatus},
|
||||
};
|
||||
|
||||
use super::db::get_db;
|
||||
|
||||
/// Creates a new contract and saves it to the database. Returns the saved contract and its ID.
|
||||
pub fn create_new_contract(
|
||||
base_id: u32,
|
||||
contract_id: &str,
|
||||
title: &str,
|
||||
description: &str,
|
||||
contract_type: &str,
|
||||
status: ContractStatus,
|
||||
created_by: &str,
|
||||
terms_and_conditions: Option<&str>,
|
||||
start_date: Option<u64>,
|
||||
end_date: Option<u64>,
|
||||
renewal_period_days: Option<u32>,
|
||||
) -> Result<(u32, Contract), String> {
|
||||
let db = get_db().expect("Can get DB");
|
||||
|
||||
// Create a new contract using the heromodels Contract::new constructor
|
||||
let mut contract = Contract::new(base_id, contract_id.to_string())
|
||||
.title(title)
|
||||
.description(description)
|
||||
.contract_type(contract_type.to_string())
|
||||
.status(status)
|
||||
.created_by(created_by.to_string());
|
||||
|
||||
if let Some(terms) = terms_and_conditions {
|
||||
contract = contract.terms_and_conditions(terms);
|
||||
}
|
||||
if let Some(start) = start_date {
|
||||
contract = contract.start_date(start);
|
||||
}
|
||||
if let Some(end) = end_date {
|
||||
contract = contract.end_date(end);
|
||||
}
|
||||
if let Some(renewal) = renewal_period_days {
|
||||
contract = contract.renewal_period_days(renewal as i32);
|
||||
}
|
||||
|
||||
// Save the contract to the database
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.expect("can open contract collection");
|
||||
let (contract_id, saved_contract) = collection.set(&contract).expect("can save contract");
|
||||
|
||||
Ok((contract_id, saved_contract))
|
||||
}
|
||||
|
||||
/// Loads all contracts from the database and returns them as a Vec<Contract>.
|
||||
pub fn get_contracts() -> Result<Vec<Contract>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.expect("can open contract collection");
|
||||
|
||||
// Try to load all contracts, but handle deserialization errors gracefully
|
||||
let contracts = match collection.get_all() {
|
||||
Ok(contracts) => contracts,
|
||||
Err(e) => {
|
||||
log::error!("Failed to load contracts from database: {:?}", e);
|
||||
vec![] // Return empty vector if there's an error
|
||||
}
|
||||
};
|
||||
Ok(contracts)
|
||||
}
|
||||
|
||||
/// Fetches a single contract by its ID from the database.
|
||||
pub fn get_contract_by_id(contract_id: u32) -> Result<Option<Contract>, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
match collection.get_by_id(contract_id) {
|
||||
Ok(contract) => Ok(contract),
|
||||
Err(e) => {
|
||||
log::error!("Error fetching contract by id {}: {:?}", contract_id, e);
|
||||
Err(format!("Failed to fetch contract: {:?}", e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a contract's basic information in the database and returns the updated contract.
|
||||
pub fn update_contract(
|
||||
contract_id: u32,
|
||||
title: &str,
|
||||
description: &str,
|
||||
contract_type: &str,
|
||||
terms_and_conditions: Option<&str>,
|
||||
start_date: Option<u64>,
|
||||
end_date: Option<u64>,
|
||||
) -> Result<Contract, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut contract) = collection
|
||||
.get_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
||||
{
|
||||
// Update the contract fields
|
||||
contract = contract
|
||||
.title(title)
|
||||
.description(description)
|
||||
.contract_type(contract_type.to_string());
|
||||
|
||||
if let Some(terms) = terms_and_conditions {
|
||||
contract = contract.terms_and_conditions(terms);
|
||||
}
|
||||
if let Some(start) = start_date {
|
||||
contract = contract.start_date(start);
|
||||
}
|
||||
if let Some(end) = end_date {
|
||||
contract = contract.end_date(end);
|
||||
}
|
||||
|
||||
let (_, updated_contract) = collection
|
||||
.set(&contract)
|
||||
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
||||
Ok(updated_contract)
|
||||
} else {
|
||||
Err("Contract not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a contract's status in the database and returns the updated contract.
|
||||
pub fn update_contract_status(
|
||||
contract_id: u32,
|
||||
status: ContractStatus,
|
||||
) -> Result<Contract, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut contract) = collection
|
||||
.get_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
||||
{
|
||||
contract = contract.status(status);
|
||||
let (_, updated_contract) = collection
|
||||
.set(&contract)
|
||||
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
||||
Ok(updated_contract)
|
||||
} else {
|
||||
Err("Contract not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a signer to a contract and returns the updated contract.
|
||||
pub fn add_signer_to_contract(
|
||||
contract_id: u32,
|
||||
signer_id: &str,
|
||||
name: &str,
|
||||
email: &str,
|
||||
) -> Result<Contract, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut contract) = collection
|
||||
.get_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
||||
{
|
||||
let signer =
|
||||
ContractSigner::new(signer_id.to_string(), name.to_string(), email.to_string());
|
||||
contract = contract.add_signer(signer);
|
||||
let (_, updated_contract) = collection
|
||||
.set(&contract)
|
||||
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
||||
Ok(updated_contract)
|
||||
} else {
|
||||
Err("Contract not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a revision to a contract and returns the updated contract.
|
||||
pub fn add_revision_to_contract(
|
||||
contract_id: u32,
|
||||
version: u32,
|
||||
content: &str,
|
||||
created_by: &str,
|
||||
comments: Option<&str>,
|
||||
) -> Result<Contract, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut contract) = collection
|
||||
.get_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
||||
{
|
||||
let revision = ContractRevision::new(
|
||||
version,
|
||||
content.to_string(),
|
||||
current_timestamp_secs(),
|
||||
created_by.to_string(),
|
||||
)
|
||||
.comments(comments.unwrap_or("").to_string());
|
||||
|
||||
contract = contract.add_revision(revision);
|
||||
let (_, updated_contract) = collection
|
||||
.set(&contract)
|
||||
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
||||
Ok(updated_contract)
|
||||
} else {
|
||||
Err("Contract not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates a signer's status for a contract and returns the updated contract.
|
||||
pub fn update_signer_status(
|
||||
contract_id: u32,
|
||||
signer_id: &str,
|
||||
status: SignerStatus,
|
||||
comments: Option<&str>,
|
||||
) -> Result<Contract, String> {
|
||||
update_signer_status_with_signature(contract_id, signer_id, status, comments, None)
|
||||
}
|
||||
|
||||
/// Updates a signer's status with signature data for a contract and returns the updated contract.
|
||||
pub fn update_signer_status_with_signature(
|
||||
contract_id: u32,
|
||||
signer_id: &str,
|
||||
status: SignerStatus,
|
||||
comments: Option<&str>,
|
||||
signature_data: Option<&str>,
|
||||
) -> Result<Contract, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
if let Some(mut contract) = collection
|
||||
.get_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
||||
{
|
||||
// Find and update the signer
|
||||
let mut signer_found = false;
|
||||
for signer in &mut contract.signers {
|
||||
if signer.id == signer_id {
|
||||
signer.status = status.clone();
|
||||
if status == SignerStatus::Signed {
|
||||
signer.signed_at = Some(current_timestamp_secs());
|
||||
}
|
||||
if let Some(comment) = comments {
|
||||
signer.comments = Some(comment.to_string());
|
||||
}
|
||||
if let Some(sig_data) = signature_data {
|
||||
signer.signature_data = Some(sig_data.to_string());
|
||||
}
|
||||
signer_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !signer_found {
|
||||
return Err(format!("Signer with ID {} not found", signer_id));
|
||||
}
|
||||
|
||||
let (_, updated_contract) = collection
|
||||
.set(&contract)
|
||||
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
||||
Ok(updated_contract)
|
||||
} else {
|
||||
Err("Contract not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a contract from the database.
|
||||
pub fn delete_contract(contract_id: u32) -> Result<(), String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.map_err(|e| format!("Collection error: {:?}", e))?;
|
||||
|
||||
collection
|
||||
.delete_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to delete contract: {:?}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Gets contracts by status
|
||||
pub fn get_contracts_by_status(status: ContractStatus) -> Result<Vec<Contract>, String> {
|
||||
let contracts = get_contracts()?;
|
||||
let filtered_contracts = contracts
|
||||
.into_iter()
|
||||
.filter(|contract| contract.status == status)
|
||||
.collect();
|
||||
Ok(filtered_contracts)
|
||||
}
|
||||
|
||||
/// Gets contracts by creator
|
||||
pub fn get_contracts_by_creator(created_by: &str) -> Result<Vec<Contract>, String> {
|
||||
let contracts = get_contracts()?;
|
||||
let filtered_contracts = contracts
|
||||
.into_iter()
|
||||
.filter(|contract| contract.created_by == created_by)
|
||||
.collect();
|
||||
Ok(filtered_contracts)
|
||||
}
|
||||
|
||||
/// Gets contracts that need renewal (approaching end date)
|
||||
pub fn get_contracts_needing_renewal(days_ahead: u64) -> Result<Vec<Contract>, String> {
|
||||
let contracts = get_contracts()?;
|
||||
let threshold_timestamp = current_timestamp_secs() + (days_ahead * 24 * 60 * 60);
|
||||
|
||||
let filtered_contracts = contracts
|
||||
.into_iter()
|
||||
.filter(|contract| {
|
||||
if let Some(end_date) = contract.end_date {
|
||||
end_date <= threshold_timestamp && contract.status == ContractStatus::Active
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(filtered_contracts)
|
||||
}
|
||||
|
||||
/// Gets expired contracts
|
||||
pub fn get_expired_contracts() -> Result<Vec<Contract>, String> {
|
||||
let contracts = get_contracts()?;
|
||||
let current_time = current_timestamp_secs();
|
||||
|
||||
let filtered_contracts = contracts
|
||||
.into_iter()
|
||||
.filter(|contract| {
|
||||
if let Some(end_date) = contract.end_date {
|
||||
end_date < current_time && contract.status != ContractStatus::Expired
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Ok(filtered_contracts)
|
||||
}
|
||||
|
||||
/// Updates multiple contracts to expired status
|
||||
pub fn mark_contracts_as_expired(contract_ids: Vec<u32>) -> Result<Vec<Contract>, String> {
|
||||
let mut updated_contracts = Vec::new();
|
||||
|
||||
for contract_id in contract_ids {
|
||||
match update_contract_status(contract_id, ContractStatus::Expired) {
|
||||
Ok(contract) => updated_contracts.push(contract),
|
||||
Err(e) => log::error!("Failed to update contract {}: {}", contract_id, e),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updated_contracts)
|
||||
}
|
||||
|
||||
/// Updates a signer's reminder timestamp for a contract and returns the updated contract.
|
||||
pub fn update_signer_reminder_timestamp(
|
||||
contract_id: u32,
|
||||
signer_id: &str,
|
||||
_timestamp: u64,
|
||||
) -> Result<Contract, String> {
|
||||
let db = get_db().map_err(|e| format!("DB error: {}", e))?;
|
||||
let collection = db
|
||||
.collection::<Contract>()
|
||||
.expect("can open contract collection");
|
||||
|
||||
if let Some(mut contract) = collection
|
||||
.get_by_id(contract_id)
|
||||
.map_err(|e| format!("Failed to fetch contract: {:?}", e))?
|
||||
{
|
||||
let mut signer_found = false;
|
||||
for signer in &mut contract.signers {
|
||||
if signer.id == signer_id {
|
||||
// TODO: Update reminder timestamp when field is available in heromodels
|
||||
// signer.last_reminder_mail_sent_at = Some(timestamp);
|
||||
signer_found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !signer_found {
|
||||
return Err(format!("Signer with ID {} not found", signer_id));
|
||||
}
|
||||
|
||||
let (_, updated_contract) = collection
|
||||
.set(&contract)
|
||||
.map_err(|e| format!("Failed to update contract: {:?}", e))?;
|
||||
Ok(updated_contract)
|
||||
} else {
|
||||
Err("Contract not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets contract statistics
|
||||
pub fn get_contract_statistics() -> Result<ContractStatistics, String> {
|
||||
let contracts = get_contracts()?;
|
||||
|
||||
let total = contracts.len();
|
||||
let draft = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Draft)
|
||||
.count();
|
||||
let pending = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::PendingSignatures)
|
||||
.count();
|
||||
let signed = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Signed)
|
||||
.count();
|
||||
let active = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Active)
|
||||
.count();
|
||||
let expired = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Expired)
|
||||
.count();
|
||||
let cancelled = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Cancelled)
|
||||
.count();
|
||||
|
||||
Ok(ContractStatistics {
|
||||
total_contracts: total,
|
||||
draft_contracts: draft,
|
||||
pending_signature_contracts: pending,
|
||||
signed_contracts: signed,
|
||||
active_contracts: active,
|
||||
expired_contracts: expired,
|
||||
cancelled_contracts: cancelled,
|
||||
})
|
||||
}
|
||||
|
||||
/// A helper for current timestamp (seconds since epoch)
|
||||
fn current_timestamp_secs() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
/// Contract statistics structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ContractStatistics {
|
||||
pub total_contracts: usize,
|
||||
pub draft_contracts: usize,
|
||||
pub pending_signature_contracts: usize,
|
||||
pub signed_contracts: usize,
|
||||
pub active_contracts: usize,
|
||||
pub expired_contracts: usize,
|
||||
pub cancelled_contracts: usize,
|
||||
}
|
@ -54,7 +54,7 @@ pub fn get_proposals() -> Result<Vec<Proposal>, String> {
|
||||
let proposals = match collection.get_all() {
|
||||
Ok(props) => props,
|
||||
Err(e) => {
|
||||
eprintln!("Error loading proposals: {:?}", e);
|
||||
log::error!("Error loading proposals: {:?}", e);
|
||||
vec![] // Return an empty vector if there's an error
|
||||
}
|
||||
};
|
||||
@ -70,7 +70,7 @@ pub fn get_proposal_by_id(proposal_id: u32) -> Result<Option<Proposal>, String>
|
||||
match collection.get_by_id(proposal_id) {
|
||||
Ok(proposal) => Ok(Some(proposal.expect("proposal not found"))),
|
||||
Err(e) => {
|
||||
eprintln!("Error fetching proposal by id {}: {:?}", proposal_id, e);
|
||||
log::error!("Error fetching proposal by id {}: {:?}", proposal_id, e);
|
||||
Err(format!("Failed to fetch proposal: {:?}", e))
|
||||
}
|
||||
}
|
||||
|
@ -50,10 +50,18 @@ async fn main() -> io::Result<()> {
|
||||
// Load configuration
|
||||
let config = config::get_config();
|
||||
|
||||
// Check for port override from command line arguments
|
||||
// Check for port override from environment variable or command line arguments
|
||||
let args: Vec<String> = env::args().collect();
|
||||
let mut port = config.server.port;
|
||||
|
||||
// First check environment variable
|
||||
if let Ok(env_port) = env::var("PORT") {
|
||||
if let Ok(p) = env_port.parse::<u16>() {
|
||||
port = p;
|
||||
}
|
||||
}
|
||||
|
||||
// Then check command line arguments (takes precedence over env var)
|
||||
for i in 1..args.len() {
|
||||
if args[i] == "--port" && i + 1 < args.len() {
|
||||
if let Ok(p) = args[i + 1].parse::<u16>() {
|
||||
@ -111,6 +119,8 @@ async fn main() -> io::Result<()> {
|
||||
.app_data(web::Data::new(tera))
|
||||
// Configure routes
|
||||
.configure(routes::configure_routes)
|
||||
// Add default handler for 404 errors
|
||||
.default_service(web::route().to(controllers::error::render_generic_not_found))
|
||||
})
|
||||
.bind(bind_address)?
|
||||
.workers(num_cpus::get())
|
||||
|
@ -2,6 +2,96 @@ use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Contract activity types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ContractActivityType {
|
||||
Created,
|
||||
SignerAdded,
|
||||
SignerRemoved,
|
||||
SentForSignatures,
|
||||
Signed,
|
||||
Rejected,
|
||||
StatusChanged,
|
||||
Revised,
|
||||
}
|
||||
|
||||
impl ContractActivityType {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
ContractActivityType::Created => "Contract Created",
|
||||
ContractActivityType::SignerAdded => "Signer Added",
|
||||
ContractActivityType::SignerRemoved => "Signer Removed",
|
||||
ContractActivityType::SentForSignatures => "Sent for Signatures",
|
||||
ContractActivityType::Signed => "Contract Signed",
|
||||
ContractActivityType::Rejected => "Contract Rejected",
|
||||
ContractActivityType::StatusChanged => "Status Changed",
|
||||
ContractActivityType::Revised => "Contract Revised",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contract activity model
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContractActivity {
|
||||
pub id: String,
|
||||
pub contract_id: u32,
|
||||
pub activity_type: ContractActivityType,
|
||||
pub description: String,
|
||||
pub user_name: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ContractActivity {
|
||||
/// Creates a new contract activity
|
||||
pub fn new(
|
||||
contract_id: u32,
|
||||
activity_type: ContractActivityType,
|
||||
description: String,
|
||||
user_name: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
contract_id,
|
||||
activity_type,
|
||||
description,
|
||||
user_name,
|
||||
created_at: Utc::now(),
|
||||
metadata: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a contract creation activity
|
||||
pub fn contract_created(contract_id: u32, contract_title: &str, user_name: &str) -> Self {
|
||||
Self::new(
|
||||
contract_id,
|
||||
ContractActivityType::Created,
|
||||
format!("Created contract '{}'", contract_title),
|
||||
user_name.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a signer added activity
|
||||
pub fn signer_added(contract_id: u32, signer_name: &str, user_name: &str) -> Self {
|
||||
Self::new(
|
||||
contract_id,
|
||||
ContractActivityType::SignerAdded,
|
||||
format!("Added signer: {}", signer_name),
|
||||
user_name.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a sent for signatures activity
|
||||
pub fn sent_for_signatures(contract_id: u32, signer_count: usize, user_name: &str) -> Self {
|
||||
Self::new(
|
||||
contract_id,
|
||||
ContractActivityType::SentForSignatures,
|
||||
format!("Sent contract for signatures to {} signer(s)", signer_count),
|
||||
user_name.to_string(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Contract status enum
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ContractStatus {
|
||||
@ -10,7 +100,7 @@ pub enum ContractStatus {
|
||||
Signed,
|
||||
Active,
|
||||
Expired,
|
||||
Cancelled
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
impl ContractStatus {
|
||||
@ -37,7 +127,7 @@ pub enum ContractType {
|
||||
Distribution,
|
||||
License,
|
||||
Membership,
|
||||
Other
|
||||
Other,
|
||||
}
|
||||
|
||||
impl ContractType {
|
||||
@ -61,7 +151,7 @@ impl ContractType {
|
||||
pub enum SignerStatus {
|
||||
Pending,
|
||||
Signed,
|
||||
Rejected
|
||||
Rejected,
|
||||
}
|
||||
|
||||
impl SignerStatus {
|
||||
@ -127,7 +217,12 @@ pub struct ContractRevision {
|
||||
#[allow(dead_code)]
|
||||
impl ContractRevision {
|
||||
/// Creates a new contract revision
|
||||
pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
version: u32,
|
||||
content: String,
|
||||
created_by: String,
|
||||
comments: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
version,
|
||||
content,
|
||||
@ -171,7 +266,13 @@ pub struct Contract {
|
||||
#[allow(dead_code)]
|
||||
impl Contract {
|
||||
/// Creates a new contract
|
||||
pub fn new(title: String, description: String, contract_type: ContractType, created_by: String, organization_id: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
title: String,
|
||||
description: String,
|
||||
contract_type: ContractType,
|
||||
created_by: String,
|
||||
organization_id: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
title,
|
||||
@ -229,7 +330,9 @@ impl Contract {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.signers.iter().all(|signer| signer.status == SignerStatus::Signed)
|
||||
self.signers
|
||||
.iter()
|
||||
.all(|signer| signer.status == SignerStatus::Signed)
|
||||
}
|
||||
|
||||
/// Marks the contract as signed if all signers have signed
|
||||
@ -261,17 +364,26 @@ impl Contract {
|
||||
|
||||
/// Gets the number of pending signers
|
||||
pub fn pending_signers_count(&self) -> usize {
|
||||
self.signers.iter().filter(|s| s.status == SignerStatus::Pending).count()
|
||||
self.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Pending)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Gets the number of signed signers
|
||||
pub fn signed_signers_count(&self) -> usize {
|
||||
self.signers.iter().filter(|s| s.status == SignerStatus::Signed).count()
|
||||
self.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Signed)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Gets the number of rejected signers
|
||||
pub fn rejected_signers_count(&self) -> usize {
|
||||
self.signers.iter().filter(|s| s.status == SignerStatus::Rejected).count()
|
||||
self.signers
|
||||
.iter()
|
||||
.filter(|s| s.status == SignerStatus::Rejected)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,11 +411,26 @@ impl ContractStatistics {
|
||||
/// Creates new contract statistics from a list of contracts
|
||||
pub fn new(contracts: &[Contract]) -> Self {
|
||||
let total_contracts = contracts.len();
|
||||
let draft_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Draft).count();
|
||||
let pending_signature_contracts = contracts.iter().filter(|c| c.status == ContractStatus::PendingSignatures).count();
|
||||
let signed_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Signed).count();
|
||||
let expired_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Expired).count();
|
||||
let cancelled_contracts = contracts.iter().filter(|c| c.status == ContractStatus::Cancelled).count();
|
||||
let draft_contracts = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Draft)
|
||||
.count();
|
||||
let pending_signature_contracts = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::PendingSignatures)
|
||||
.count();
|
||||
let signed_contracts = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Signed)
|
||||
.count();
|
||||
let expired_contracts = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Expired)
|
||||
.count();
|
||||
let cancelled_contracts = contracts
|
||||
.iter()
|
||||
.filter(|c| c.status == ContractStatus::Cancelled)
|
||||
.count();
|
||||
|
||||
Self {
|
||||
total_contracts,
|
||||
|
@ -127,11 +127,90 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
|
||||
.service(
|
||||
web::scope("/contracts")
|
||||
.route("", web::get().to(ContractController::index))
|
||||
.route("/", web::get().to(ContractController::index)) // Handle trailing slash
|
||||
.route("/list", web::get().to(ContractController::list))
|
||||
.route("/my", web::get().to(ContractController::my_contracts))
|
||||
.route("/{id}", web::get().to(ContractController::detail))
|
||||
.route("/list/", web::get().to(ContractController::list)) // Handle trailing slash
|
||||
.route(
|
||||
"/my-contracts",
|
||||
web::get().to(ContractController::my_contracts),
|
||||
)
|
||||
.route(
|
||||
"/my-contracts/",
|
||||
web::get().to(ContractController::my_contracts),
|
||||
) // Handle trailing slash
|
||||
.route("/create", web::get().to(ContractController::create_form))
|
||||
.route("/create", web::post().to(ContractController::create)),
|
||||
.route("/create/", web::get().to(ContractController::create_form)) // Handle trailing slash
|
||||
.route("/create", web::post().to(ContractController::create))
|
||||
.route("/create/", web::post().to(ContractController::create)) // Handle trailing slash
|
||||
.route("/statistics", web::get().to(ContractController::statistics))
|
||||
.route(
|
||||
"/activities",
|
||||
web::get().to(ContractController::all_activities),
|
||||
)
|
||||
.route("/{id}/edit", web::get().to(ContractController::edit_form))
|
||||
.route("/{id}/edit", web::post().to(ContractController::update))
|
||||
.route(
|
||||
"/filter/{status}",
|
||||
web::get().to(ContractController::filter_by_status),
|
||||
)
|
||||
.route("/{id}", web::get().to(ContractController::detail))
|
||||
.route(
|
||||
"/{id}/status/{status}",
|
||||
web::post().to(ContractController::update_status),
|
||||
)
|
||||
.route("/{id}/delete", web::post().to(ContractController::delete))
|
||||
.route(
|
||||
"/{id}/add-signer",
|
||||
web::get().to(ContractController::add_signer_form),
|
||||
)
|
||||
.route(
|
||||
"/{id}/add-signer",
|
||||
web::post().to(ContractController::add_signer),
|
||||
)
|
||||
.route(
|
||||
"/{id}/remind",
|
||||
web::post().to(ContractController::remind_to_sign),
|
||||
)
|
||||
.route(
|
||||
"/{id}/send",
|
||||
web::post().to(ContractController::send_for_signatures),
|
||||
)
|
||||
.route(
|
||||
"/{id}/reminder-status",
|
||||
web::get().to(ContractController::get_reminder_status),
|
||||
)
|
||||
.route(
|
||||
"/{id}/add-revision",
|
||||
web::post().to(ContractController::add_revision),
|
||||
)
|
||||
.route(
|
||||
"/{id}/signer/{signer_id}/status/{status}",
|
||||
web::post().to(ContractController::update_signer_status),
|
||||
)
|
||||
.route(
|
||||
"/{id}/sign/{signer_id}",
|
||||
web::post().to(ContractController::sign_contract),
|
||||
)
|
||||
.route(
|
||||
"/{id}/reject/{signer_id}",
|
||||
web::post().to(ContractController::reject_contract),
|
||||
)
|
||||
.route(
|
||||
"/{id}/cancel",
|
||||
web::post().to(ContractController::cancel_contract),
|
||||
)
|
||||
.route(
|
||||
"/{id}/clone",
|
||||
web::post().to(ContractController::clone_contract),
|
||||
)
|
||||
.route(
|
||||
"/{id}/signed/{signer_id}",
|
||||
web::get().to(ContractController::view_signed_document),
|
||||
)
|
||||
.route(
|
||||
"/{id}/share",
|
||||
web::post().to(ContractController::share_contract),
|
||||
),
|
||||
)
|
||||
// Asset routes
|
||||
.service(
|
||||
|
@ -1,5 +1,6 @@
|
||||
use actix_web::{Error, HttpResponse};
|
||||
use chrono::{DateTime, Utc};
|
||||
use pulldown_cmark::{Options, Parser, html};
|
||||
use std::error::Error as StdError;
|
||||
use tera::{self, Context, Function, Tera, Value};
|
||||
|
||||
@ -224,6 +225,26 @@ pub fn truncate_string(s: &str, max_length: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses markdown content and returns HTML
|
||||
pub fn parse_markdown(markdown_content: &str) -> String {
|
||||
// Set up markdown parser options
|
||||
let mut options = Options::empty();
|
||||
options.insert(Options::ENABLE_TABLES);
|
||||
options.insert(Options::ENABLE_FOOTNOTES);
|
||||
options.insert(Options::ENABLE_STRIKETHROUGH);
|
||||
options.insert(Options::ENABLE_TASKLISTS);
|
||||
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||
|
||||
// Create parser
|
||||
let parser = Parser::new_ext(markdown_content, options);
|
||||
|
||||
// Render to HTML
|
||||
let mut html_output = String::new();
|
||||
html::push_html(&mut html_output, parser);
|
||||
|
||||
html_output
|
||||
}
|
||||
|
||||
/// Renders a template with error handling
|
||||
///
|
||||
/// This function attempts to render a template and handles any errors by rendering
|
||||
|
200
actix_mvc_app/src/views/contracts/add_signer.html
Normal file
200
actix_mvc_app/src/views/contracts/add_signer.html
Normal file
@ -0,0 +1,200 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Add Signer - {{ contract.title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/contracts">Contracts</a></li>
|
||||
<li class="breadcrumb-item"><a href="/contracts/{{ contract.id }}">{{ contract.title }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Add Signer</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h3 mb-0">Add Signer</h1>
|
||||
<p class="text-muted mb-0">Add a new signer to "{{ contract.title }}"</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to Contract
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Add Signer Form -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-person-plus me-2"></i>Signer Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if error %}
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="/contracts/{{ contract.id }}/add-signer">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">
|
||||
Full Name <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
placeholder="Enter signer's full name" required>
|
||||
<div class="form-text">The full legal name of the person who will sign</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">
|
||||
Email Address <span class="text-danger">*</span>
|
||||
</label>
|
||||
<input type="email" class="form-control" id="email" name="email"
|
||||
placeholder="Enter signer's email address" required>
|
||||
<div class="form-text">Email where signing instructions will be sent</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Note:</strong> The signer will receive an email with a secure link to sign the contract once you send it for signatures.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-person-plus me-1"></i> Add Signer
|
||||
</button>
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-secondary">
|
||||
Cancel
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Summary -->
|
||||
<div class="col-lg-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Summary</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{{ contract.title }}</h6>
|
||||
<p class="card-text text-muted">{{ contract.description }}</p>
|
||||
|
||||
<hr>
|
||||
|
||||
<div class="row text-center">
|
||||
<div class="col-6">
|
||||
<div class="border-end">
|
||||
<div class="h4 mb-0 text-primary">{{ contract.signers|length }}</div>
|
||||
<small class="text-muted">Current Signers</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="h4 mb-0 text-success">{{ contract.signed_signers }}</div>
|
||||
<small class="text-muted">Signed</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Signers List -->
|
||||
{% if contract.signers|length > 0 %}
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Current Signers</h6>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for signer in contract.signers %}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-medium">{{ signer.name }}</div>
|
||||
<small class="text-muted">{{ signer.email }}</small>
|
||||
</div>
|
||||
<span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
|
||||
{{ signer.status }}
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Form validation
|
||||
const form = document.querySelector('form');
|
||||
const nameInput = document.getElementById('name');
|
||||
const emailInput = document.getElementById('email');
|
||||
|
||||
form.addEventListener('submit', function(e) {
|
||||
let isValid = true;
|
||||
|
||||
// Clear previous validation states
|
||||
nameInput.classList.remove('is-invalid');
|
||||
emailInput.classList.remove('is-invalid');
|
||||
|
||||
// Validate name
|
||||
if (nameInput.value.trim().length < 2) {
|
||||
nameInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate email format
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(emailInput.value)) {
|
||||
emailInput.classList.add('is-invalid');
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
// Real-time validation feedback
|
||||
nameInput.addEventListener('input', function() {
|
||||
if (this.value.trim().length >= 2) {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
} else {
|
||||
this.classList.remove('is-valid');
|
||||
}
|
||||
});
|
||||
|
||||
emailInput.addEventListener('input', function() {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (emailRegex.test(this.value)) {
|
||||
this.classList.remove('is-invalid');
|
||||
this.classList.add('is-valid');
|
||||
} else {
|
||||
this.classList.remove('is-valid');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
128
actix_mvc_app/src/views/contracts/all_activities.html
Normal file
128
actix_mvc_app/src/views/contracts/all_activities.html
Normal file
@ -0,0 +1,128 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}All Contract Activities{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<!-- Header -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h1 class="display-5 mb-3">Contract Activities</h1>
|
||||
<p class="lead">Complete history of contract actions and events across your organization.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activities List -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title mb-0">
|
||||
<i class="bi bi-activity"></i> Contract Activity History
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if activities %}
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="50">Type</th>
|
||||
<th>User</th>
|
||||
<th>Action</th>
|
||||
<th>Contract</th>
|
||||
<th width="150">Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for activity in activities %}
|
||||
<tr>
|
||||
<td>
|
||||
<i class="{{ activity.icon }}"></i>
|
||||
</td>
|
||||
<td>
|
||||
<strong>{{ activity.user }}</strong>
|
||||
</td>
|
||||
<td>
|
||||
{{ activity.action }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="text-decoration-none">
|
||||
{{ activity.contract_title }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{{ activity.created_at | date(format="%Y-%m-%d %H:%M") }}
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-activity display-1 text-muted"></i>
|
||||
<h4 class="mt-3">No Activities Yet</h4>
|
||||
<p class="text-muted">
|
||||
Contract activities will appear here as users create contracts and add signers.
|
||||
</p>
|
||||
<a href="/contracts/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Create First Contract
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Activity Statistics -->
|
||||
{% if activities %}
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ activities | length }}</h5>
|
||||
<p class="card-text text-muted">Total Activities</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-file-earmark-text text-primary"></i>
|
||||
</h5>
|
||||
<p class="card-text text-muted">Contract Timeline</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card text-center">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<i class="bi bi-people text-success"></i>
|
||||
</h5>
|
||||
<p class="card-text text-muted">Team Collaboration</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Back to Dashboard -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 text-center">
|
||||
<a href="/contracts" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Back to Contracts Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
File diff suppressed because it is too large
Load Diff
@ -36,27 +36,41 @@
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="PendingSignatures">Pending Signatures</option>
|
||||
<option value="Signed">Signed</option>
|
||||
<option value="Expired">Expired</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
<option value="Draft" {% if current_status_filter=="Draft" %}selected{% endif %}>Draft
|
||||
</option>
|
||||
<option value="PendingSignatures" {% if current_status_filter=="PendingSignatures"
|
||||
%}selected{% endif %}>Pending Signatures</option>
|
||||
<option value="Signed" {% if current_status_filter=="Signed" %}selected{% endif %}>
|
||||
Signed</option>
|
||||
<option value="Expired" {% if current_status_filter=="Expired" %}selected{% endif %}>
|
||||
Expired</option>
|
||||
<option value="Cancelled" {% if current_status_filter=="Cancelled" %}selected{% endif
|
||||
%}>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="type" class="form-label">Contract Type</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<option value="">All Types</option>
|
||||
<option value="Service">Service Agreement</option>
|
||||
<option value="Employment">Employment Contract</option>
|
||||
<option value="NDA">Non-Disclosure Agreement</option>
|
||||
<option value="SLA">Service Level Agreement</option>
|
||||
<option value="Other">Other</option>
|
||||
<option value="Service Agreement" {% if current_type_filter=="Service Agreement"
|
||||
%}selected{% endif %}>Service Agreement</option>
|
||||
<option value="Employment Contract" {% if current_type_filter=="Employment Contract"
|
||||
%}selected{% endif %}>Employment Contract</option>
|
||||
<option value="Non-Disclosure Agreement" {% if
|
||||
current_type_filter=="Non-Disclosure Agreement" %}selected{% endif %}>Non-Disclosure
|
||||
Agreement</option>
|
||||
<option value="Service Level Agreement" {% if
|
||||
current_type_filter=="Service Level Agreement" %}selected{% endif %}>Service Level
|
||||
Agreement</option>
|
||||
<option value="Other" {% if current_type_filter=="Other" %}selected{% endif %}>Other
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label for="search" class="form-label">Search</label>
|
||||
<input type="text" class="form-control" id="search" name="search" placeholder="Search by title or description">
|
||||
<input type="text" class="form-control" id="search" name="search"
|
||||
placeholder="Search by title or description"
|
||||
value="{{ current_search_filter | default(value='') }}">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Apply Filters</button>
|
||||
@ -98,7 +112,8 @@
|
||||
</td>
|
||||
<td>{{ contract.contract_type }}</td>
|
||||
<td>
|
||||
<span class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
|
||||
<span
|
||||
class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
|
||||
{{ contract.status }}
|
||||
</span>
|
||||
</td>
|
||||
@ -112,9 +127,14 @@
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
|
||||
<a href="/contracts/{{ contract.id }}/edit"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
onclick="deleteContract({{ contract.id }}, '{{ contract.title | replace(from="'", to="\\'") }}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@ -137,4 +157,70 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Delete Contract</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone!
|
||||
</div>
|
||||
<p>Are you sure you want to delete the contract "<strong id="contractTitle"></strong>"?</p>
|
||||
<p>This will permanently remove the contract and all its associated data.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash me-1"></i> Delete Contract
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
console.log('Contracts list scripts loading...');
|
||||
|
||||
// Delete function using Bootstrap modal
|
||||
window.deleteContract = function (contractId, contractTitle) {
|
||||
console.log('Delete function called:', contractId, contractTitle);
|
||||
|
||||
// Set the contract title in the modal
|
||||
document.getElementById('contractTitle').textContent = contractTitle;
|
||||
|
||||
// Store the contract ID for later use
|
||||
window.currentDeleteContractId = contractId;
|
||||
|
||||
// Show the modal
|
||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
deleteModal.show();
|
||||
};
|
||||
|
||||
console.log('deleteContract function defined:', typeof window.deleteContract);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Handle confirm delete button click
|
||||
document.getElementById('confirmDeleteBtn').addEventListener('click', function () {
|
||||
console.log('User confirmed deletion, submitting form...');
|
||||
|
||||
// Create and submit form
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/contracts/' + window.currentDeleteContractId + '/delete';
|
||||
form.style.display = 'none';
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -25,12 +25,14 @@
|
||||
<div class="card-body">
|
||||
<form action="/contracts/create" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Contract Title <span class="text-danger">*</span></label>
|
||||
<label for="title" class="form-label">Contract Title <span
|
||||
class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contract_type" class="form-label">Contract Type <span class="text-danger">*</span></label>
|
||||
<label for="contract_type" class="form-label">Contract Type <span
|
||||
class="text-danger">*</span></label>
|
||||
<select class="form-select" id="contract_type" name="contract_type" required>
|
||||
<option value="" selected disabled>Select a contract type</option>
|
||||
{% for type in contract_types %}
|
||||
@ -40,14 +42,45 @@
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3" required></textarea>
|
||||
<label for="description" class="form-label">Description <span
|
||||
class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"
|
||||
required></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="content" class="form-label">Contract Content</label>
|
||||
<textarea class="form-control" id="content" name="content" rows="10"></textarea>
|
||||
<div class="form-text">You can leave this blank and add content later.</div>
|
||||
<label for="content" class="form-label">Contract Content (Markdown)</label>
|
||||
<textarea class="form-control" id="content" name="content" rows="10" placeholder="# Contract Title
|
||||
|
||||
## 1. Introduction
|
||||
This contract outlines the terms and conditions...
|
||||
|
||||
## 2. Scope of Work
|
||||
- Task 1
|
||||
- Task 2
|
||||
- Task 3
|
||||
|
||||
## 3. Payment Terms
|
||||
Payment will be made according to the following schedule:
|
||||
|
||||
| Milestone | Amount | Due Date |
|
||||
|-----------|--------|----------|
|
||||
| Start | $1,000 | Upon signing |
|
||||
| Completion | $2,000 | Upon delivery |
|
||||
|
||||
## 4. Terms and Conditions
|
||||
**Important:** All parties must agree to these terms.
|
||||
|
||||
> This is a blockquote for important notices.
|
||||
|
||||
---
|
||||
|
||||
*For questions, contact [support@example.com](mailto:support@example.com)*"></textarea>
|
||||
<div class="form-text">
|
||||
<strong>Markdown Support:</strong> You can use markdown formatting including headers
|
||||
(#), lists (-), tables (|), bold (**text**), italic (*text*), links, and more.
|
||||
<a href="/editor" target="_blank">Open Markdown Editor</a> for a live preview.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@ -75,7 +108,8 @@
|
||||
<h5 class="mb-0">Tips</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Creating a new contract is just the first step. After creating the contract, you'll be able to:</p>
|
||||
<p>Creating a new contract is just the first step. After creating the contract, you'll be able to:
|
||||
</p>
|
||||
<ul>
|
||||
<li>Add signers who need to approve the contract</li>
|
||||
<li>Edit the contract content</li>
|
||||
@ -93,16 +127,20 @@
|
||||
<div class="card-body">
|
||||
<p>You can use one of our pre-defined templates to get started quickly:</p>
|
||||
<div class="list-group">
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('nda')">
|
||||
<button type="button" class="list-group-item list-group-item-action"
|
||||
onclick="loadTemplate('nda')">
|
||||
Non-Disclosure Agreement
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('service')">
|
||||
<button type="button" class="list-group-item list-group-item-action"
|
||||
onclick="loadTemplate('service')">
|
||||
Service Agreement
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('employment')">
|
||||
<button type="button" class="list-group-item list-group-item-action"
|
||||
onclick="loadTemplate('employment')">
|
||||
Employment Contract
|
||||
</button>
|
||||
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('sla')">
|
||||
<button type="button" class="list-group-item list-group-item-action"
|
||||
onclick="loadTemplate('sla')">
|
||||
Service Level Agreement
|
||||
</button>
|
||||
</div>
|
||||
@ -122,18 +160,100 @@
|
||||
let content = '';
|
||||
let contractType = '';
|
||||
|
||||
switch(type) {
|
||||
switch (type) {
|
||||
case 'nda':
|
||||
title = 'Non-Disclosure Agreement';
|
||||
description = 'Standard NDA for protecting confidential information';
|
||||
contractType = 'Non-Disclosure Agreement';
|
||||
content = 'This Non-Disclosure Agreement (the "Agreement") is entered into as of [DATE] by and between [PARTY A] and [PARTY B].\n\n1. Definition of Confidential Information\n2. Obligations of Receiving Party\n3. Term\n...';
|
||||
content = `# Non-Disclosure Agreement
|
||||
|
||||
This Non-Disclosure Agreement (the "**Agreement**") is entered into as of **[DATE]** by and between **[PARTY A]** and **[PARTY B]**.
|
||||
|
||||
## 1. Definition of Confidential Information
|
||||
|
||||
"Confidential Information" means any and all information disclosed by either party to the other party, whether orally or in writing, whether or not marked, designated or otherwise identified as "confidential."
|
||||
|
||||
## 2. Obligations of Receiving Party
|
||||
|
||||
The receiving party agrees to:
|
||||
- Hold all Confidential Information in strict confidence
|
||||
- Not disclose any Confidential Information to third parties
|
||||
- Use Confidential Information solely for the purpose of evaluating potential business relationships
|
||||
|
||||
## 3. Term
|
||||
|
||||
This Agreement shall remain in effect for a period of **[DURATION]** years from the date first written above.
|
||||
|
||||
## 4. Return of Materials
|
||||
|
||||
Upon termination of this Agreement, each party shall promptly return all documents and materials containing Confidential Information.
|
||||
|
||||
---
|
||||
|
||||
**IN WITNESS WHEREOF**, the parties have executed this Agreement as of the date first written above.
|
||||
|
||||
**[PARTY A]** **[PARTY B]**
|
||||
|
||||
_____________________ _____________________
|
||||
Signature Signature
|
||||
|
||||
_____________________ _____________________
|
||||
Print Name Print Name
|
||||
|
||||
_____________________ _____________________
|
||||
Date Date`;
|
||||
break;
|
||||
case 'service':
|
||||
title = 'Service Agreement';
|
||||
description = 'Agreement for providing professional services';
|
||||
contractType = 'Service Agreement';
|
||||
content = 'This Service Agreement (the "Agreement") is made and entered into as of [DATE] by and between [SERVICE PROVIDER] and [CLIENT].\n\n1. Services to be Provided\n2. Compensation\n3. Term and Termination\n...';
|
||||
content = `# Service Agreement
|
||||
|
||||
This Service Agreement (the "**Agreement**") is made and entered into as of **[DATE]** by and between **[SERVICE PROVIDER]** and **[CLIENT]**.
|
||||
|
||||
## 1. Services to be Provided
|
||||
|
||||
The Service Provider agrees to provide the following services:
|
||||
|
||||
- **[SERVICE 1]**: Description of service
|
||||
- **[SERVICE 2]**: Description of service
|
||||
- **[SERVICE 3]**: Description of service
|
||||
|
||||
## 2. Compensation
|
||||
|
||||
| Service | Rate | Payment Terms |
|
||||
|---------|------|---------------|
|
||||
| [SERVICE 1] | $[AMOUNT] | [TERMS] |
|
||||
| [SERVICE 2] | $[AMOUNT] | [TERMS] |
|
||||
|
||||
**Total Contract Value**: $[TOTAL_AMOUNT]
|
||||
|
||||
## 3. Payment Schedule
|
||||
|
||||
- **Deposit**: [PERCENTAGE]% upon signing
|
||||
- **Milestone 1**: [PERCENTAGE]% upon [MILESTONE]
|
||||
- **Final Payment**: [PERCENTAGE]% upon completion
|
||||
|
||||
## 4. Term and Termination
|
||||
|
||||
This Agreement shall commence on **[START_DATE]** and shall continue until **[END_DATE]** unless terminated earlier.
|
||||
|
||||
> **Important**: Either party may terminate this agreement with [NUMBER] days written notice.
|
||||
|
||||
## 5. Deliverables
|
||||
|
||||
The Service Provider shall deliver:
|
||||
|
||||
1. [DELIVERABLE 1]
|
||||
2. [DELIVERABLE 2]
|
||||
3. [DELIVERABLE 3]
|
||||
|
||||
---
|
||||
|
||||
**Service Provider** **Client**
|
||||
|
||||
_____________________ _____________________
|
||||
Signature Signature`;
|
||||
break;
|
||||
case 'employment':
|
||||
title = 'Employment Contract';
|
||||
@ -155,8 +275,8 @@
|
||||
|
||||
// Set the select option
|
||||
const selectElement = document.getElementById('contract_type');
|
||||
for(let i = 0; i < selectElement.options.length; i++) {
|
||||
if(selectElement.options[i].text === contractType) {
|
||||
for (let i = 0; i < selectElement.options.length; i++) {
|
||||
if (selectElement.options[i].text === contractType) {
|
||||
selectElement.selectedIndex = i;
|
||||
break;
|
||||
}
|
||||
|
215
actix_mvc_app/src/views/contracts/edit_contract.html
Normal file
215
actix_mvc_app/src/views/contracts/edit_contract.html
Normal file
@ -0,0 +1,215 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Contract{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/contracts">Contracts Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="/contracts/{{ contract.id }}">{{ contract.title }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Edit Contract</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<h1 class="display-5 mb-3">Edit Contract</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Details</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/contracts/{{ contract.id }}/edit" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="title" class="form-label">Contract Title <span
|
||||
class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="title" name="title" value="{{ contract.title }}"
|
||||
required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="contract_type" class="form-label">Contract Type <span
|
||||
class="text-danger">*</span></label>
|
||||
<select class="form-select" id="contract_type" name="contract_type" required>
|
||||
{% for type in contract_types %}
|
||||
<option value="{{ type }}" {% if contract.contract_type==type %}selected{% endif %}>{{
|
||||
type }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">Description <span
|
||||
class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="description" name="description" rows="3"
|
||||
required>{{ contract.description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="content" class="form-label">Contract Content</label>
|
||||
<textarea class="form-control" id="content" name="content"
|
||||
rows="10">{{ contract.terms_and_conditions | default(value='') }}</textarea>
|
||||
<div class="form-text">Edit the contract content as needed.</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="effective_date" class="form-label">Effective Date</label>
|
||||
<input type="date" class="form-control" id="effective_date" name="effective_date"
|
||||
value="{% if contract.start_date %}{{ contract.start_date | date(format='%Y-%m-%d') }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="expiration_date" class="form-label">Expiration Date</label>
|
||||
<input type="date" class="form-control" id="expiration_date" name="expiration_date"
|
||||
value="{% if contract.end_date %}{{ contract.end_date | date(format='%Y-%m-%d') }}{% endif %}">
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-outline-secondary me-md-2">Cancel</a>
|
||||
<button type="submit" class="btn btn-primary">Update Contract</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Contract Info</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Status:</strong>
|
||||
<span class="badge bg-secondary">{{ contract.status }}</span>
|
||||
</p>
|
||||
<p><strong>Created:</strong> {{ contract.created_at | date(format="%Y-%m-%d %H:%M") }}</p>
|
||||
<p><strong>Last Updated:</strong> {{ contract.updated_at | date(format="%Y-%m-%d %H:%M") }}</p>
|
||||
<p><strong>Version:</strong> {{ contract.current_version }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Edit Notes</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Note:</strong> Only contracts in <strong>Draft</strong> status can be edited.
|
||||
Once a contract is sent for signatures, you'll need to create a new revision instead.
|
||||
</div>
|
||||
<p>After updating the contract:</p>
|
||||
<ul>
|
||||
<li>The contract will remain in Draft status</li>
|
||||
<li>You can continue to make changes</li>
|
||||
<li>Add signers when ready</li>
|
||||
<li>Send for signatures when complete</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Quick Actions</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-outline-primary">
|
||||
<i class="bi bi-eye me-1"></i> View Contract
|
||||
</a>
|
||||
<a href="/contracts/{{ contract.id }}/add-signer" class="btn btn-outline-success">
|
||||
<i class="bi bi-person-plus me-1"></i> Add Signer
|
||||
</a>
|
||||
<button class="btn btn-outline-danger"
|
||||
onclick="deleteContract({{ contract.id }}, '{{ contract.title | replace(from="'", to="\\'") }}')">
|
||||
<i class="bi bi-trash me-1"></i> Delete Contract
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Delete Contract</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone!
|
||||
</div>
|
||||
<p>Are you sure you want to delete the contract "<strong id="contractTitle"></strong>"?</p>
|
||||
<p>This will permanently remove the contract and all its associated data.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash me-1"></i> Delete Contract
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
console.log('Edit contract scripts loading...');
|
||||
|
||||
// Delete function using Bootstrap modal
|
||||
window.deleteContract = function (contractId, contractTitle) {
|
||||
console.log('Delete function called:', contractId, contractTitle);
|
||||
|
||||
// Set the contract title in the modal
|
||||
document.getElementById('contractTitle').textContent = contractTitle;
|
||||
|
||||
// Store the contract ID for later use
|
||||
window.currentDeleteContractId = contractId;
|
||||
|
||||
// Show the modal
|
||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
deleteModal.show();
|
||||
};
|
||||
|
||||
console.log('deleteContract function defined:', typeof window.deleteContract);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Handle confirm delete button click
|
||||
document.getElementById('confirmDeleteBtn').addEventListener('click', function () {
|
||||
console.log('User confirmed deletion, submitting form...');
|
||||
|
||||
// Create and submit form
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/contracts/' + window.currentDeleteContractId + '/delete';
|
||||
form.style.display = 'none';
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
|
||||
// Auto-resize textarea
|
||||
const textarea = document.getElementById('content');
|
||||
if (textarea) {
|
||||
textarea.addEventListener('input', function () {
|
||||
this.style.height = 'auto';
|
||||
this.style.height = this.scrollHeight + 'px';
|
||||
});
|
||||
// Initial resize
|
||||
textarea.style.height = textarea.scrollHeight + 'px';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -11,58 +11,108 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if stats.total_contracts > 0 %}
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card text-white bg-primary h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Total</h5>
|
||||
<p class="display-4">{{ stats.total_contracts }}</p>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-1">Total</h5>
|
||||
<h3 class="mb-0">{{ stats.total_contracts }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card text-white bg-secondary h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Draft</h5>
|
||||
<p class="display-4">{{ stats.draft_contracts }}</p>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-1">Draft</h5>
|
||||
<h3 class="mb-0">{{ stats.draft_contracts }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card text-white bg-warning h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Pending</h5>
|
||||
<p class="display-4">{{ stats.pending_signature_contracts }}</p>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-1">Pending</h5>
|
||||
<h3 class="mb-0">{{ stats.pending_signature_contracts }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card text-white bg-success h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Signed</h5>
|
||||
<p class="display-4">{{ stats.signed_contracts }}</p>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-1">Signed</h5>
|
||||
<h3 class="mb-0">{{ stats.signed_contracts }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card text-white bg-danger h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Expired</h5>
|
||||
<p class="display-4">{{ stats.expired_contracts }}</p>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-1">Expired</h5>
|
||||
<h3 class="mb-0">{{ stats.expired_contracts }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card text-white bg-dark h-100">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">Cancelled</h5>
|
||||
<p class="display-4">{{ stats.cancelled_contracts }}</p>
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title mb-1">Cancelled</h5>
|
||||
<h3 class="mb-0">{{ stats.cancelled_contracts }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<!-- Empty State Welcome Message -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 bg-light">
|
||||
<div class="card-body text-center py-5">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-file-earmark-text display-1 text-muted"></i>
|
||||
</div>
|
||||
<h3 class="text-muted mb-3">Welcome to Contract Management</h3>
|
||||
<p class="lead text-muted mb-4">
|
||||
You haven't created any contracts yet. Get started by creating your first contract to manage
|
||||
legal agreements and track signatures.
|
||||
</p>
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-6">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 border-primary">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-plus-circle text-primary fs-2 mb-2"></i>
|
||||
<h6 class="card-title">Create Contract</h6>
|
||||
<p class="card-text small text-muted">Start with a new legal agreement</p>
|
||||
<a href="/contracts/create" class="btn btn-primary btn-sm">Get Started</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-body text-center">
|
||||
<i class="bi bi-question-circle text-success fs-2 mb-2"></i>
|
||||
<h6 class="card-title">Need Help?</h6>
|
||||
<p class="card-text small text-muted">Learn how to use the system</p>
|
||||
<button class="btn btn-outline-success btn-sm"
|
||||
onclick="showHelpModal()">Learn More</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
{% if stats.total_contracts > 0 %}
|
||||
<!-- Quick Actions -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
@ -86,6 +136,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Pending Signature Contracts -->
|
||||
{% if pending_signature_contracts and pending_signature_contracts | length > 0 %}
|
||||
@ -168,7 +219,8 @@
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
|
||||
<a href="/contracts/{{ contract.id }}/edit"
|
||||
class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
</div>
|
||||
@ -183,5 +235,115 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Recent Activity Section -->
|
||||
{% if recent_activities and recent_activities | length > 0 %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Recent Activity</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for activity in recent_activities %}
|
||||
<div class="list-group-item border-start-0 border-end-0 py-3">
|
||||
<div class="d-flex">
|
||||
<div class="me-3">
|
||||
<i class="{{ activity.icon }} fs-5"></i>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ activity.user }}</strong>
|
||||
<small class="text-muted">{{ activity.timestamp | date(format="%H:%M")
|
||||
}}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ activity.description }}</p>
|
||||
<small class="text-muted">{{ activity.title }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<a href="/contracts/activities" class="btn btn-sm btn-outline-info">See More Activities</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Help Modal -->
|
||||
<div class="modal fade" id="helpModal" tabindex="-1" aria-labelledby="helpModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="helpModalLabel">
|
||||
<i class="bi bi-question-circle me-2"></i>Getting Started with Contract Management
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-1-circle text-primary me-2"></i>Create Your First Contract</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
Start by creating a new contract. Choose from various contract types like Service
|
||||
Agreements, NDAs, or Employment Contracts.
|
||||
</p>
|
||||
|
||||
<h6><i class="bi bi-2-circle text-primary me-2"></i>Add Contract Details</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
Fill in the contract title, description, and terms. You can use Markdown formatting for rich
|
||||
text content.
|
||||
</p>
|
||||
|
||||
<h6><i class="bi bi-3-circle text-primary me-2"></i>Add Signers</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
Add people who need to sign the contract. Each signer will receive a unique signing link.
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h6><i class="bi bi-4-circle text-success me-2"></i>Send for Signatures</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
Once your contract is ready, send it for signatures. Signers can review and sign digitally.
|
||||
</p>
|
||||
|
||||
<h6><i class="bi bi-5-circle text-success me-2"></i>Track Progress</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
Monitor signature progress, send reminders, and view signed documents from the dashboard.
|
||||
</p>
|
||||
|
||||
<h6><i class="bi bi-6-circle text-success me-2"></i>Manage Contracts</h6>
|
||||
<p class="small text-muted mb-3">
|
||||
View all contracts, filter by status, and manage the complete contract lifecycle.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info mt-3">
|
||||
<i class="bi bi-lightbulb me-2"></i>
|
||||
<strong>Tip:</strong> You can save contracts as drafts and come back to edit them later before
|
||||
sending for signatures.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<a href="/contracts/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> Create My First Contract
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function showHelpModal() {
|
||||
const helpModal = new bootstrap.Modal(document.getElementById('helpModal'));
|
||||
helpModal.show();
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
@ -13,7 +13,10 @@
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="display-5 mb-0">My Contracts</h1>
|
||||
<p class="text-muted mb-0">Manage and track your personal contracts</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a href="/contracts/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> Create New Contract
|
||||
@ -23,38 +26,132 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-title">Total Contracts</h6>
|
||||
<h3 class="mb-0">{{ contracts|length }}</h3>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="bi bi-file-earmark-text fs-2"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-title">Pending Signatures</h6>
|
||||
<h3 class="mb-0" id="pending-count">0</h3>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="bi bi-clock fs-2"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-title">Signed</h6>
|
||||
<h3 class="mb-0" id="signed-count">0</h3>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="bi bi-check-circle fs-2"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-secondary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-title">Drafts</h6>
|
||||
<h3 class="mb-0" id="draft-count">0</h3>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<i class="bi bi-pencil fs-2"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Filters</h5>
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-funnel me-1"></i> Filters & Search
|
||||
</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse"
|
||||
data-bs-target="#filtersCollapse" aria-expanded="false" aria-controls="filtersCollapse">
|
||||
<i class="bi bi-chevron-down"></i> Toggle Filters
|
||||
</button>
|
||||
</div>
|
||||
<div class="collapse show" id="filtersCollapse">
|
||||
<div class="card-body">
|
||||
<form action="/contracts/my-contracts" method="get" class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label for="status" class="form-label">Status</label>
|
||||
<select class="form-select" id="status" name="status">
|
||||
<option value="">All Statuses</option>
|
||||
<option value="Draft">Draft</option>
|
||||
<option value="PendingSignatures">Pending Signatures</option>
|
||||
<option value="Signed">Signed</option>
|
||||
<option value="Expired">Expired</option>
|
||||
<option value="Cancelled">Cancelled</option>
|
||||
<option value="Draft" {% if current_status_filter=="Draft" %}selected{% endif %}>
|
||||
Draft</option>
|
||||
<option value="PendingSignatures" {% if current_status_filter=="PendingSignatures"
|
||||
%}selected{% endif %}>Pending Signatures</option>
|
||||
<option value="Signed" {% if current_status_filter=="Signed" %}selected{% endif %}>
|
||||
Signed</option>
|
||||
<option value="Active" {% if current_status_filter=="Active" %}selected{% endif %}>
|
||||
Active</option>
|
||||
<option value="Expired" {% if current_status_filter=="Expired" %}selected{% endif
|
||||
%}>Expired</option>
|
||||
<option value="Cancelled" {% if current_status_filter=="Cancelled" %}selected{%
|
||||
endif %}>Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-3">
|
||||
<label for="type" class="form-label">Contract Type</label>
|
||||
<select class="form-select" id="type" name="type">
|
||||
<option value="">All Types</option>
|
||||
<option value="Service">Service Agreement</option>
|
||||
<option value="Employment">Employment Contract</option>
|
||||
<option value="NDA">Non-Disclosure Agreement</option>
|
||||
<option value="SLA">Service Level Agreement</option>
|
||||
<option value="Other">Other</option>
|
||||
<option value="Service Agreement" {% if current_type_filter=="Service Agreement"
|
||||
%}selected{% endif %}>Service Agreement</option>
|
||||
<option value="Employment Contract" {% if current_type_filter=="Employment Contract"
|
||||
%}selected{% endif %}>Employment Contract</option>
|
||||
<option value="Non-Disclosure Agreement" {% if
|
||||
current_type_filter=="Non-Disclosure Agreement" %}selected{% endif %}>
|
||||
Non-Disclosure Agreement</option>
|
||||
<option value="Service Level Agreement" {% if
|
||||
current_type_filter=="Service Level Agreement" %}selected{% endif %}>Service
|
||||
Level Agreement</option>
|
||||
<option value="Partnership Agreement" {% if
|
||||
current_type_filter=="Partnership Agreement" %}selected{% endif %}>Partnership
|
||||
Agreement</option>
|
||||
<option value="Other" {% if current_type_filter=="Other" %}selected{% endif %}>Other
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4 d-flex align-items-end">
|
||||
<div class="col-md-3">
|
||||
<label for="search" class="form-label">Search</label>
|
||||
<input type="text" class="form-control" id="search" name="search"
|
||||
placeholder="Search by title or description"
|
||||
value="{{ current_search_filter | default(value='') }}">
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">Apply Filters</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -62,53 +159,128 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract List -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">My Contracts</h5>
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-earmark-text me-1"></i> My Contracts
|
||||
{% if contracts and contracts | length > 0 %}
|
||||
<span class="badge bg-primary ms-2">{{ contracts|length }}</span>
|
||||
{% endif %}
|
||||
</h5>
|
||||
<div class="btn-group">
|
||||
<a href="/contracts/statistics" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="bi bi-graph-up me-1"></i> Statistics
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contracts and contracts | length > 0 %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Contract Title</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>Signers</th>
|
||||
<th>Created</th>
|
||||
<th>Updated</th>
|
||||
<th>Actions</th>
|
||||
<th scope="col">
|
||||
<div class="d-flex align-items-center">
|
||||
Contract Title
|
||||
<i class="bi bi-arrow-down-up ms-1 text-muted" style="cursor: pointer;"
|
||||
onclick="sortTable(0)"></i>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col">Type</th>
|
||||
<th scope="col">Status</th>
|
||||
<th scope="col">Progress</th>
|
||||
<th scope="col">
|
||||
<div class="d-flex align-items-center">
|
||||
Created
|
||||
<i class="bi bi-arrow-down-up ms-1 text-muted" style="cursor: pointer;"
|
||||
onclick="sortTable(4)"></i>
|
||||
</div>
|
||||
</th>
|
||||
<th scope="col">Last Updated</th>
|
||||
<th scope="col" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for contract in contracts %}
|
||||
<tr>
|
||||
<tr
|
||||
class="{% if contract.status == 'Expired' %}table-danger{% elif contract.status == 'PendingSignatures' %}table-warning{% elif contract.status == 'Signed' %}table-success{% endif %}">
|
||||
<td>
|
||||
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
|
||||
<div>
|
||||
<a href="/contracts/{{ contract.id }}" class="fw-bold text-decoration-none">
|
||||
{{ contract.title }}
|
||||
</a>
|
||||
{% if contract.description %}
|
||||
<div class="small text-muted">{{ contract.description }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ contract.contract_type }}</td>
|
||||
<td>
|
||||
<span class="badge {% if contract.status == 'Signed' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% else %}bg-dark{% endif %}">
|
||||
<span class="badge bg-light text-dark">{{ contract.contract_type }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
class="badge {% if contract.status == 'Signed' or contract.status == 'Active' %}bg-success{% elif contract.status == 'PendingSignatures' %}bg-warning text-dark{% elif contract.status == 'Draft' %}bg-secondary{% elif contract.status == 'Expired' %}bg-danger{% elif contract.status == 'Cancelled' %}bg-dark{% else %}bg-info{% endif %}">
|
||||
{% if contract.status == 'PendingSignatures' %}
|
||||
<i class="bi bi-clock me-1"></i>
|
||||
{% elif contract.status == 'Signed' %}
|
||||
<i class="bi bi-check-circle me-1"></i>
|
||||
{% elif contract.status == 'Draft' %}
|
||||
<i class="bi bi-pencil me-1"></i>
|
||||
{% elif contract.status == 'Expired' %}
|
||||
<i class="bi bi-exclamation-triangle me-1"></i>
|
||||
{% endif %}
|
||||
{{ contract.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ contract.signed_signers }}/{{ contract.signers|length }}</td>
|
||||
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>{{ contract.updated_at | date(format="%Y-%m-%d") }}</td>
|
||||
<td>
|
||||
{% if contract.signers|length > 0 %}
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="progress me-2" style="width: 60px; height: 8px;">
|
||||
<div class="progress-bar bg-success" role="progressbar"
|
||||
style="width: 0%" data-contract-id="{{ contract.id }}">
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ contract.signed_signers }}/{{
|
||||
contract.signers|length }}</small>
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="text-muted small">No signers</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="small">
|
||||
{{ contract.created_at | date(format="%b %d, %Y") }}
|
||||
<div class="text-muted">{{ contract.created_at | date(format="%I:%M %p") }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="small">
|
||||
{{ contract.updated_at | date(format="%b %d, %Y") }}
|
||||
<div class="text-muted">{{ contract.updated_at | date(format="%I:%M %p") }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group">
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary"
|
||||
title="View Details">
|
||||
<i class="bi bi-eye"></i>
|
||||
</a>
|
||||
{% if contract.status == 'Draft' %}
|
||||
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
|
||||
<a href="/contracts/{{ contract.id }}/edit"
|
||||
class="btn btn-sm btn-outline-secondary" title="Edit Contract">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-danger" title="Delete Contract"
|
||||
onclick="deleteContract('{{ contract.id }}', '{{ contract.title }}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@ -119,11 +291,20 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="bi bi-file-earmark-text fs-1 text-muted"></i>
|
||||
<p class="mt-3 text-muted">You don't have any contracts yet</p>
|
||||
<a href="/contracts/create" class="btn btn-primary mt-2">
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-file-earmark-text display-1 text-muted"></i>
|
||||
</div>
|
||||
<h4 class="text-muted mb-3">No Contracts Found</h4>
|
||||
<p class="text-muted mb-4">You haven't created any contracts yet. Get started by creating your
|
||||
first contract.</p>
|
||||
<div class="d-flex justify-content-center gap-2">
|
||||
<a href="/contracts/create" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle me-1"></i> Create Your First Contract
|
||||
</a>
|
||||
<a href="/contracts" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -131,4 +312,166 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete Confirmation Modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteModalLabel">Delete Contract</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-danger">
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Warning:</strong> This action cannot be undone!
|
||||
</div>
|
||||
<p>Are you sure you want to delete the contract "<strong id="contractTitle"></strong>"?</p>
|
||||
<p>This will permanently remove:</p>
|
||||
<ul>
|
||||
<li>The contract document and all its content</li>
|
||||
<li>All signers and their signatures</li>
|
||||
<li>All revisions and history</li>
|
||||
<li>Any associated files or attachments</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" id="confirmDeleteBtn">
|
||||
<i class="bi bi-trash me-1"></i> Delete Contract
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
console.log('My Contracts page scripts loading...');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Delete contract functionality using Bootstrap modal
|
||||
window.deleteContract = function (contractId, contractTitle) {
|
||||
console.log('Delete contract called:', contractId, contractTitle);
|
||||
|
||||
// Set the contract title in the modal
|
||||
document.getElementById('contractTitle').textContent = contractTitle;
|
||||
|
||||
// Store the contract ID for later use
|
||||
window.currentDeleteContractId = contractId;
|
||||
|
||||
// Show the modal
|
||||
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
||||
deleteModal.show();
|
||||
};
|
||||
|
||||
// Simple table sorting functionality
|
||||
window.sortTable = function (columnIndex) {
|
||||
console.log('Sorting table by column:', columnIndex);
|
||||
const table = document.querySelector('.table tbody');
|
||||
const rows = Array.from(table.querySelectorAll('tr'));
|
||||
|
||||
// Toggle sort direction
|
||||
const isAscending = table.dataset.sortDirection !== 'asc';
|
||||
table.dataset.sortDirection = isAscending ? 'asc' : 'desc';
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aText = a.cells[columnIndex].textContent.trim();
|
||||
const bText = b.cells[columnIndex].textContent.trim();
|
||||
|
||||
// Handle date sorting for created/updated columns
|
||||
if (columnIndex === 4 || columnIndex === 5) {
|
||||
const aDate = new Date(aText);
|
||||
const bDate = new Date(bText);
|
||||
return isAscending ? aDate - bDate : bDate - aDate;
|
||||
}
|
||||
|
||||
// Handle text sorting
|
||||
return isAscending ? aText.localeCompare(bText) : bText.localeCompare(aText);
|
||||
});
|
||||
|
||||
// Re-append sorted rows
|
||||
rows.forEach(row => table.appendChild(row));
|
||||
|
||||
// Update sort indicators
|
||||
document.querySelectorAll('.bi-arrow-down-up').forEach(icon => {
|
||||
icon.className = 'bi bi-arrow-down-up ms-1 text-muted';
|
||||
});
|
||||
|
||||
const currentIcon = document.querySelectorAll('.bi-arrow-down-up')[columnIndex === 4 ? 1 : 0];
|
||||
if (currentIcon) {
|
||||
currentIcon.className = `bi ${isAscending ? 'bi-arrow-up' : 'bi-arrow-down'} ms-1 text-primary`;
|
||||
}
|
||||
};
|
||||
|
||||
// Calculate statistics and update progress bars
|
||||
function updateStatistics() {
|
||||
const rows = document.querySelectorAll('.table tbody tr');
|
||||
let totalContracts = rows.length;
|
||||
let pendingCount = 0;
|
||||
let signedCount = 0;
|
||||
let draftCount = 0;
|
||||
|
||||
rows.forEach(row => {
|
||||
const statusCell = row.cells[2];
|
||||
const statusText = statusCell.textContent.trim();
|
||||
|
||||
if (statusText.includes('PendingSignatures') || statusText.includes('Pending')) {
|
||||
pendingCount++;
|
||||
} else if (statusText.includes('Signed')) {
|
||||
signedCount++;
|
||||
} else if (statusText.includes('Draft')) {
|
||||
draftCount++;
|
||||
}
|
||||
|
||||
// Update progress bars
|
||||
const progressBar = row.querySelector('.progress-bar');
|
||||
if (progressBar) {
|
||||
const signersText = row.cells[3].textContent.trim();
|
||||
if (signersText !== 'No signers') {
|
||||
const [signed, total] = signersText.split('/').map(n => parseInt(n));
|
||||
const percentage = total > 0 ? Math.round((signed / total) * 100) : 0;
|
||||
progressBar.style.width = percentage + '%';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Update statistics cards
|
||||
document.getElementById('pending-count').textContent = pendingCount;
|
||||
document.getElementById('signed-count').textContent = signedCount;
|
||||
document.getElementById('draft-count').textContent = draftCount;
|
||||
|
||||
// Update total count badge
|
||||
const badge = document.querySelector('.badge.bg-primary');
|
||||
if (badge) {
|
||||
badge.textContent = totalContracts;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Calculate initial statistics
|
||||
updateStatistics();
|
||||
|
||||
// Handle confirm delete button click
|
||||
document.getElementById('confirmDeleteBtn').addEventListener('click', function () {
|
||||
console.log('User confirmed deletion, submitting form...');
|
||||
|
||||
// Create and submit form
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/contracts/' + window.currentDeleteContractId + '/delete';
|
||||
form.style.display = 'none';
|
||||
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
});
|
||||
});
|
||||
|
||||
console.log('My Contracts page scripts loaded successfully');
|
||||
</script>
|
||||
{% endblock %}
|
370
actix_mvc_app/src/views/contracts/signed_document.html
Normal file
370
actix_mvc_app/src/views/contracts/signed_document.html
Normal file
@ -0,0 +1,370 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ contract.title }} - Signed Contract{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Action Bar (hidden in print) -->
|
||||
<div class="row mb-4 no-print">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h4 mb-1">
|
||||
<i class="bi bi-file-earmark-check text-success me-2"></i>
|
||||
Signed Contract Document
|
||||
</h1>
|
||||
<p class="text-muted mb-0">Official digitally signed copy</p>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<a href="/contracts/{{ contract.id }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Back to Contract
|
||||
</a>
|
||||
<button class="btn btn-primary" onclick="window.print()">
|
||||
<i class="bi bi-printer me-1"></i> Print Document
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary" id="copyContentBtn"
|
||||
title="Copy contract content to clipboard">
|
||||
<i class="bi bi-clipboard" id="copyIcon"></i>
|
||||
<div class="spinner-border spinner-border-sm d-none" id="copySpinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Signature Verification Banner -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="alert alert-success border-success">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-1 text-center">
|
||||
<i class="bi bi-shield-check text-success" style="font-size: 2rem;"></i>
|
||||
</div>
|
||||
<div class="col-md-11">
|
||||
<h5 class="alert-heading mb-2">
|
||||
<i class="bi bi-check-circle me-2"></i>Digitally Signed Document
|
||||
</h5>
|
||||
<p class="mb-1">
|
||||
<strong>{{ signer.name }}</strong> ({{ signer.email }}) digitally signed this contract on
|
||||
<strong>{{ signer.signed_at }}</strong>
|
||||
</p>
|
||||
{% if signer.comments %}
|
||||
<p class="mb-0">
|
||||
<strong>Signer Comments:</strong> {{ signer.comments }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-info-circle me-2"></i>Contract Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<p><strong>Contract ID:</strong> {{ contract.contract_id }}</p>
|
||||
<p><strong>Title:</strong> {{ contract.title }}</p>
|
||||
<p><strong>Type:</strong> {{ contract.contract_type }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p><strong>Status:</strong>
|
||||
<span class="badge bg-success">{{ contract.status }}</span>
|
||||
</p>
|
||||
<p><strong>Created:</strong> {{ contract.created_at }}</p>
|
||||
<p><strong>Version:</strong> {{ contract.current_version }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if contract.description %}
|
||||
<p><strong>Description:</strong> {{ contract.description }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-person-check me-2"></i>Signer Information
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p><strong>Name:</strong> {{ signer.name }}</p>
|
||||
<p><strong>Email:</strong> {{ signer.email }}</p>
|
||||
<p><strong>Status:</strong>
|
||||
<span class="badge bg-success">{{ signer.status }}</span>
|
||||
</p>
|
||||
<p><strong>Signed At:</strong> {{ signer.signed_at }}</p>
|
||||
{% if signer.comments %}
|
||||
<p><strong>Comments:</strong></p>
|
||||
<div class="bg-light p-2 rounded">
|
||||
{{ signer.comments }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Display Saved Signature -->
|
||||
{% if signer.signature_data %}
|
||||
<div class="mt-3">
|
||||
<p><strong>Digital Signature:</strong></p>
|
||||
<div class="signature-display bg-white border rounded p-3 text-center">
|
||||
<img src="{{ signer.signature_data }}" alt="Digital Signature" class="signature-image" />
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contract Content -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="bi bi-file-text me-2"></i>Contract Terms & Conditions
|
||||
</h5>
|
||||
<div>
|
||||
<button class="btn btn-outline-secondary btn-sm" id="copyContentBtn"
|
||||
title="Copy contract content to clipboard">
|
||||
<i class="bi bi-clipboard" id="copyIcon"></i>
|
||||
<div class="spinner-border spinner-border-sm d-none" id="copySpinner" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if contract_content_html %}
|
||||
<!-- Hidden element containing raw markdown content for copying -->
|
||||
<div id="rawContractContent" class="d-none">{{ contract.terms_and_conditions }}</div>
|
||||
<div class="contract-content bg-white p-4 border rounded">
|
||||
{{ contract_content_html | safe }}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center py-5">
|
||||
<i class="bi bi-file-text text-muted" style="font-size: 3rem;"></i>
|
||||
<h5 class="mt-3">No Content Available</h5>
|
||||
<p class="text-muted">This contract doesn't have any content.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Digital Signature Footer -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-success">
|
||||
<div class="card-body text-center">
|
||||
<h6 class="text-success mb-2">
|
||||
<i class="bi bi-shield-check me-2"></i>Digital Signature Verification
|
||||
</h6>
|
||||
<p class="small text-muted mb-0">
|
||||
This document has been digitally signed by {{ signer.name }} on {{ signer.signed_at }}.
|
||||
The digital signature ensures the authenticity and integrity of this contract.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Print styles */
|
||||
@media print {
|
||||
|
||||
.btn,
|
||||
.card-header .btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border: 2px solid #28a745 !important;
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #dee2e6 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.bg-light {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Markdown Content Styles */
|
||||
.contract-content h1,
|
||||
.contract-content h2,
|
||||
.contract-content h3,
|
||||
.contract-content h4,
|
||||
.contract-content h5,
|
||||
.contract-content h6 {
|
||||
margin-top: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.contract-content h1 {
|
||||
font-size: 2rem;
|
||||
border-bottom: 2px solid #e9ecef;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.contract-content h2 {
|
||||
font-size: 1.5rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
padding-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.contract-content p {
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.contract-content ul,
|
||||
.contract-content ol {
|
||||
margin-bottom: 1rem;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
.contract-content table {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.contract-content table th,
|
||||
.contract-content table td {
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #dee2e6;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.contract-content table th {
|
||||
background-color: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Signature Display Styles */
|
||||
.signature-display {
|
||||
min-height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.signature-image {
|
||||
max-width: 100%;
|
||||
max-height: 60px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
/* Copy button styles */
|
||||
#copyContentBtn {
|
||||
position: relative;
|
||||
min-width: 40px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
#copyContentBtn:disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
#copySpinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Copy contract content functionality
|
||||
const copyContentBtn = document.getElementById('copyContentBtn');
|
||||
const copyIcon = document.getElementById('copyIcon');
|
||||
const copySpinner = document.getElementById('copySpinner');
|
||||
|
||||
if (copyContentBtn) {
|
||||
copyContentBtn.addEventListener('click', async function () {
|
||||
const rawContent = document.getElementById('rawContractContent');
|
||||
|
||||
if (!rawContent) {
|
||||
alert('No contract content available to copy.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
copyIcon.classList.add('d-none');
|
||||
copySpinner.classList.remove('d-none');
|
||||
copyContentBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// Copy to clipboard
|
||||
await navigator.clipboard.writeText(rawContent.textContent);
|
||||
|
||||
// Show success state
|
||||
copySpinner.classList.add('d-none');
|
||||
copyIcon.classList.remove('d-none');
|
||||
copyIcon.className = 'bi bi-check-circle text-success';
|
||||
|
||||
// Initialize tooltip
|
||||
const tooltip = new bootstrap.Tooltip(copyContentBtn, {
|
||||
title: 'Contract content copied to clipboard!',
|
||||
placement: 'top',
|
||||
trigger: 'manual'
|
||||
});
|
||||
|
||||
// Show tooltip
|
||||
tooltip.show();
|
||||
|
||||
// Hide tooltip and reset icon after 2 seconds
|
||||
setTimeout(() => {
|
||||
tooltip.hide();
|
||||
copyIcon.className = 'bi bi-clipboard';
|
||||
copyContentBtn.disabled = false;
|
||||
|
||||
// Dispose tooltip to prevent memory leaks
|
||||
setTimeout(() => {
|
||||
tooltip.dispose();
|
||||
}, 300);
|
||||
}, 2000);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to copy content: ', err);
|
||||
|
||||
// Show error state
|
||||
copySpinner.classList.add('d-none');
|
||||
copyIcon.classList.remove('d-none');
|
||||
copyIcon.className = 'bi bi-x-circle text-danger';
|
||||
|
||||
alert('Failed to copy content to clipboard. Please try again.');
|
||||
|
||||
// Reset icon after 2 seconds
|
||||
setTimeout(() => {
|
||||
copyIcon.className = 'bi bi-clipboard';
|
||||
copyContentBtn.disabled = false;
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
125
actix_mvc_app/src/views/errors/404.html
Normal file
125
actix_mvc_app/src/views/errors/404.html
Normal file
@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Page Not Found - Zanzibar Digital Freezone</title>
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
<div class="container-fluid min-vh-100 d-flex align-items-center justify-content-center">
|
||||
<div class="container-fluid">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8 col-lg-6">
|
||||
<div class="text-center py-5">
|
||||
<!-- 404 Icon -->
|
||||
<div class="mb-4">
|
||||
<i class="bi bi-exclamation-triangle display-1 text-warning"></i>
|
||||
</div>
|
||||
|
||||
<!-- Error Code -->
|
||||
<h1 class="display-1 fw-bold text-muted">404</h1>
|
||||
|
||||
<!-- Error Message -->
|
||||
<h2 class="mb-3">{% if error_title %}{{ error_title }}{% else %}Page Not Found{% endif %}</h2>
|
||||
<p class="lead text-muted mb-4">
|
||||
{% if error_message %}{{ error_message }}{% else %}The page you're looking for doesn't exist
|
||||
or has
|
||||
been moved.{% endif %}
|
||||
</p>
|
||||
|
||||
<!-- Suggestions -->
|
||||
<div class="card bg-light border-0 mb-4">
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">What can you do?</h6>
|
||||
<ul class="list-unstyled mb-0">
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-arrow-left text-primary me-2"></i>
|
||||
Go back to the previous page
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-house text-primary me-2"></i>
|
||||
Visit our homepage
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-search text-primary me-2"></i>
|
||||
Check the URL for typos
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<i class="bi bi-arrow-clockwise text-primary me-2"></i>
|
||||
Try refreshing the page
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center">
|
||||
<button onclick="history.back()" class="btn btn-outline-primary">
|
||||
<i class="bi bi-arrow-left me-1"></i> Go Back
|
||||
</button>
|
||||
<a href="/" class="btn btn-primary">
|
||||
<i class="bi bi-house me-1"></i> Go Home
|
||||
</a>
|
||||
{% if return_url %}
|
||||
<a href="{{ return_url }}" class="btn btn-outline-secondary">
|
||||
<i class="bi bi-arrow-return-left me-1"></i> {% if return_text %}{{ return_text }}{%
|
||||
else
|
||||
%}Return{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Contact Support -->
|
||||
<div class="mt-5 pt-4 border-top">
|
||||
<p class="text-muted small">
|
||||
Still having trouble?
|
||||
<a href="/support" class="text-decoration-none">Contact Support</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Auto-redirect after 10 seconds if no user interaction
|
||||
let redirectTimer;
|
||||
let countdown = 10;
|
||||
|
||||
function startAutoRedirect() {
|
||||
redirectTimer = setInterval(() => {
|
||||
countdown--;
|
||||
if (countdown <= 0) {
|
||||
clearInterval(redirectTimer);
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
// Cancel auto-redirect on any user interaction
|
||||
function cancelAutoRedirect() {
|
||||
if (redirectTimer) {
|
||||
clearInterval(redirectTimer);
|
||||
redirectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Start auto-redirect after 5 seconds of no interaction
|
||||
setTimeout(startAutoRedirect, 5000);
|
||||
|
||||
// Cancel auto-redirect on mouse movement, clicks, or key presses
|
||||
document.addEventListener('mousemove', cancelAutoRedirect);
|
||||
document.addEventListener('click', cancelAutoRedirect);
|
||||
document.addEventListener('keydown', cancelAutoRedirect);
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user