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:
Mahmoud-Emad 2025-06-12 13:53:33 +03:00
parent 7e95391a9c
commit 464e253739
21 changed files with 6371 additions and 1173 deletions

File diff suppressed because it is too large Load Diff

View 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
}

View File

@ -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

View File

@ -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
}
};

View 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,
}

View File

@ -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))
}
}

View File

@ -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())

View File

@ -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,

View File

@ -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(

View File

@ -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

View 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 %}

View 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

View File

@ -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 %}

View File

@ -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 %}
@ -38,28 +40,59 @@
{% 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></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">
<label for="effective_date" class="form-label">Effective Date</label>
<input type="date" class="form-control" id="effective_date" name="effective_date">
</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">
</div>
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
<a href="/contracts" class="btn btn-outline-secondary me-md-2">Cancel</a>
<button type="submit" class="btn btn-primary">Create Contract</button>
@ -68,14 +101,15 @@
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card mb-4">
<div class="card-header">
<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>
@ -85,7 +119,7 @@
<p>The contract will be in <strong>Draft</strong> status until you send it for signatures.</p>
</div>
</div>
<div class="card">
<div class="card-header">
<h5 class="mb-0">Contract Templates</h5>
@ -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>
@ -121,19 +159,101 @@
let description = '';
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';
@ -148,19 +268,19 @@
content = 'This Service Level Agreement (the "SLA") is made and entered into as of [DATE] by and between [SERVICE PROVIDER] and [CLIENT].\n\n1. Service Levels\n2. Performance Metrics\n3. Remedies for Failure\n...';
break;
}
document.getElementById('title').value = title;
document.getElementById('description').value = description;
document.getElementById('content').value = content;
// 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;
}
}
}
</script>
{% endblock %}
{% endblock %}

View 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 %}

View File

@ -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 %}

View File

@ -13,7 +13,10 @@
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">My Contracts</h1>
<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,41 +26,136 @@
</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="card-body">
<form action="/contracts/my-contracts" method="get" class="row g-3">
<div class="col-md-4">
<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>
</select>
</div>
<div class="col-md-4">
<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>
</select>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="submit" class="btn btn-primary w-100">Apply Filters</button>
</div>
</form>
<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-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" {% 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-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 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-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>
</div>
</div>
</div>
</div>
@ -67,48 +165,122 @@
<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">
<i class="bi bi-plus-circle me-1"></i> Create Your First Contract
</a>
<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 %}

View 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 %}

View 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>

View File

@ -49,4 +49,5 @@ cd actix_mvc_app
# Run the application on the specified port
echo "Starting application on port $PORT..."
cargo run -- --port $PORT
echo "[Running with PORT=$PORT]"
PORT=$PORT cargo watch -x run