implement contracts

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

View File

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

View File

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

View File

@ -0,0 +1,291 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Contract status enum
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractStatus {
Draft,
PendingSignatures,
Signed,
Expired,
Cancelled
}
impl ContractStatus {
pub fn as_str(&self) -> &str {
match self {
ContractStatus::Draft => "Draft",
ContractStatus::PendingSignatures => "Pending Signatures",
ContractStatus::Signed => "Signed",
ContractStatus::Expired => "Expired",
ContractStatus::Cancelled => "Cancelled",
}
}
}
/// Contract type enum
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum ContractType {
Service,
Employment,
NDA,
SLA,
Other
}
impl ContractType {
pub fn as_str(&self) -> &str {
match self {
ContractType::Service => "Service Agreement",
ContractType::Employment => "Employment Contract",
ContractType::NDA => "Non-Disclosure Agreement",
ContractType::SLA => "Service Level Agreement",
ContractType::Other => "Other",
}
}
}
/// Contract signer status
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum SignerStatus {
Pending,
Signed,
Rejected
}
impl SignerStatus {
pub fn as_str(&self) -> &str {
match self {
SignerStatus::Pending => "Pending",
SignerStatus::Signed => "Signed",
SignerStatus::Rejected => "Rejected",
}
}
}
/// Contract signer
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractSigner {
pub id: String,
pub name: String,
pub email: String,
pub status: SignerStatus,
pub signed_at: Option<DateTime<Utc>>,
pub comments: Option<String>,
}
impl ContractSigner {
/// Creates a new contract signer
pub fn new(name: String, email: String) -> Self {
Self {
id: Uuid::new_v4().to_string(),
name,
email,
status: SignerStatus::Pending,
signed_at: None,
comments: None,
}
}
/// Signs the contract
pub fn sign(&mut self, comments: Option<String>) {
self.status = SignerStatus::Signed;
self.signed_at = Some(Utc::now());
self.comments = comments;
}
/// Rejects the contract
pub fn reject(&mut self, comments: Option<String>) {
self.status = SignerStatus::Rejected;
self.signed_at = Some(Utc::now());
self.comments = comments;
}
}
/// Contract revision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractRevision {
pub version: u32,
pub content: String,
pub created_at: DateTime<Utc>,
pub created_by: String,
pub comments: Option<String>,
}
impl ContractRevision {
/// Creates a new contract revision
pub fn new(version: u32, content: String, created_by: String, comments: Option<String>) -> Self {
Self {
version,
content,
created_at: Utc::now(),
created_by,
comments,
}
}
}
/// Contract model
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
pub id: String,
pub title: String,
pub description: String,
pub contract_type: ContractType,
pub status: ContractStatus,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub created_by: String,
pub effective_date: Option<DateTime<Utc>>,
pub expiration_date: Option<DateTime<Utc>>,
pub signers: Vec<ContractSigner>,
pub revisions: Vec<ContractRevision>,
pub current_version: u32,
pub organization_id: Option<String>,
}
impl Contract {
/// Creates a new contract
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,
description,
contract_type,
status: ContractStatus::Draft,
created_at: Utc::now(),
updated_at: Utc::now(),
created_by,
effective_date: None,
expiration_date: None,
signers: Vec::new(),
revisions: Vec::new(),
current_version: 0,
organization_id,
}
}
/// Adds a signer to the contract
pub fn add_signer(&mut self, name: String, email: String) {
let signer = ContractSigner::new(name, email);
self.signers.push(signer);
self.updated_at = Utc::now();
}
/// Adds a revision to the contract
pub fn add_revision(&mut self, content: String, created_by: String, comments: Option<String>) {
let new_version = self.current_version + 1;
let revision = ContractRevision::new(new_version, content, created_by, comments);
self.revisions.push(revision);
self.current_version = new_version;
self.updated_at = Utc::now();
}
/// Sends the contract for signatures
pub fn send_for_signatures(&mut self) -> Result<(), String> {
if self.revisions.is_empty() {
return Err("Cannot send contract without content".to_string());
}
if self.signers.is_empty() {
return Err("Cannot send contract without signers".to_string());
}
self.status = ContractStatus::PendingSignatures;
self.updated_at = Utc::now();
Ok(())
}
/// Checks if all signers have signed
pub fn is_fully_signed(&self) -> bool {
if self.signers.is_empty() {
return false;
}
self.signers.iter().all(|signer| signer.status == SignerStatus::Signed)
}
/// Marks the contract as signed if all signers have signed
pub fn finalize_if_signed(&mut self) -> bool {
if self.is_fully_signed() {
self.status = ContractStatus::Signed;
self.updated_at = Utc::now();
true
} else {
false
}
}
/// Cancels the contract
pub fn cancel(&mut self) {
self.status = ContractStatus::Cancelled;
self.updated_at = Utc::now();
}
/// Gets the latest revision
pub fn latest_revision(&self) -> Option<&ContractRevision> {
self.revisions.last()
}
/// Gets a specific revision
pub fn get_revision(&self, version: u32) -> Option<&ContractRevision> {
self.revisions.iter().find(|r| r.version == version)
}
/// Gets the number of pending signers
pub fn pending_signers_count(&self) -> usize {
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()
}
/// Gets the number of rejected signers
pub fn rejected_signers_count(&self) -> usize {
self.signers.iter().filter(|s| s.status == SignerStatus::Rejected).count()
}
}
/// Contract filter for listing contracts
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractFilter {
pub status: Option<ContractStatus>,
pub contract_type: Option<ContractType>,
pub created_by: Option<String>,
pub organization_id: Option<String>,
}
/// Contract statistics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractStatistics {
pub total_contracts: usize,
pub draft_contracts: usize,
pub pending_signature_contracts: usize,
pub signed_contracts: usize,
pub expired_contracts: usize,
pub cancelled_contracts: usize,
}
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();
Self {
total_contracts,
draft_contracts,
pending_signature_contracts,
signed_contracts,
expired_contracts,
cancelled_contracts,
}
}
}

View File

@ -4,10 +4,12 @@ pub mod ticket;
pub mod calendar;
pub mod governance;
pub mod flow;
pub mod contract;
// Re-export models for easier imports
pub use user::User;
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority};
pub use calendar::{CalendarEvent, CalendarViewMode};
pub use governance::{Proposal, ProposalStatus, Vote, VoteType, VotingResults, ProposalFilter};
pub use flow::{Flow, FlowStep, FlowLog, FlowStatus, FlowType, StepStatus, FlowFilter, FlowStatistics};
pub use flow::{Flow, FlowStep, FlowLog, FlowStatus, FlowType, StepStatus, FlowFilter, FlowStatistics};
pub use contract::{Contract, ContractSigner, ContractRevision, ContractStatus, ContractType, SignerStatus, ContractFilter, ContractStatistics};

View File

@ -6,6 +6,7 @@ use crate::controllers::ticket::TicketController;
use crate::controllers::calendar::CalendarController;
use crate::controllers::governance::GovernanceController;
use crate::controllers::flow::FlowController;
use crate::controllers::contract::ContractController;
use crate::middleware::JwtAuth;
use crate::SESSION_KEY;
@ -77,6 +78,17 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/create", web::post().to(FlowController::create_flow))
.route("/my-flows", web::get().to(FlowController::my_flows))
)
// Contract routes
.service(
web::scope("/contracts")
.route("", web::get().to(ContractController::index))
.route("/list", web::get().to(ContractController::list))
.route("/my", web::get().to(ContractController::my_contracts))
.route("/{id}", web::get().to(ContractController::detail))
.route("/create", web::get().to(ContractController::create_form))
.route("/create", web::post().to(ContractController::create))
)
);
// Keep the /protected scope for any future routes that should be under that path

View File

@ -64,6 +64,11 @@
<i class="bi bi-diagram-3 me-2"></i> Flows
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'contracts' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/contracts">
<i class="bi bi-file-earmark-text me-2"></i> Contracts
</a>
</li>
<li class="nav-item">
<a class="nav-link d-flex align-items-center ps-3 py-2 {% if active_page == 'editor' %}active fw-bold border-start border-4 border-primary bg-light{% endif %}" href="/editor">
<i class="bi bi-markdown me-2"></i> Markdown Editor

View File

@ -0,0 +1,340 @@
{% extends "base.html" %}
{% block title %}Contract Details{% endblock %}
{% block content %}
<div class="container">
<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/list">All Contracts</a></li>
<li class="breadcrumb-item active" aria-current="page">Contract Details</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">{{ contract.title }}</h1>
<div class="btn-group">
<a href="/contracts/list" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Back to List
</a>
</div>
</div>
</div>
</div>
<!-- Contract Overview -->
<div class="row mb-4">
<div class="col-md-8">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">Contract Overview</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-3 fw-bold">Status:</div>
<div class="col-md-9">
<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>
</div>
</div>
<div class="row mb-3">
<div class="col-md-3 fw-bold">Type:</div>
<div class="col-md-9">{{ contract.contract_type }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 fw-bold">Created By:</div>
<div class="col-md-9">{{ contract.created_by }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 fw-bold">Created:</div>
<div class="col-md-9">{{ contract.created_at }}</div>
</div>
<div class="row mb-3">
<div class="col-md-3 fw-bold">Last Updated:</div>
<div class="col-md-9">{{ contract.updated_at }}</div>
</div>
{% if contract.effective_date %}
<div class="row mb-3">
<div class="col-md-3 fw-bold">Effective Date:</div>
<div class="col-md-9">{{ contract.effective_date }}</div>
</div>
{% endif %}
{% if contract.expiration_date %}
<div class="row mb-3">
<div class="col-md-3 fw-bold">Expiration Date:</div>
<div class="col-md-9">{{ contract.expiration_date }}</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-md-3 fw-bold">Version:</div>
<div class="col-md-9">{{ contract.current_version }}</div>
</div>
<div class="row">
<div class="col-md-3 fw-bold">Description:</div>
<div class="col-md-9">{{ contract.description }}</div>
</div>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">Actions</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-primary">
<i class="bi bi-pencil me-1"></i> Edit Contract
</a>
<a href="/contracts/{{ contract.id }}/send" class="btn btn-success">
<i class="bi bi-send me-1"></i> Send for Signatures
</a>
<button class="btn btn-danger">
<i class="bi bi-trash me-1"></i> Delete Contract
</button>
{% elif contract.status == 'PendingSignatures' %}
<button class="btn btn-success">
<i class="bi bi-pen me-1"></i> Sign Contract
</button>
<button class="btn btn-warning">
<i class="bi bi-x-circle me-1"></i> Reject Contract
</button>
<button class="btn btn-secondary">
<i class="bi bi-send me-1"></i> Resend Invitations
</button>
{% elif contract.status == 'Signed' %}
<button class="btn btn-primary">
<i class="bi bi-download me-1"></i> Download Contract
</button>
<button class="btn btn-outline-secondary">
<i class="bi bi-files me-1"></i> Clone Contract
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Contract Content -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Contract Content</h5>
{% if contract.revisions|length > 1 %}
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button" id="versionDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Version {{ contract.current_version }}
</button>
<ul class="dropdown-menu" aria-labelledby="versionDropdown">
{% for revision in contract.revisions %}
<li><a class="dropdown-item" href="/contracts/{{ contract.id }}?version={{ revision.version }}">Version {{ revision.version }}</a></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
<div class="card-body">
{% if contract.revisions|length > 0 %}
{% set latest_revision = contract.latest_revision %}
<div class="contract-content border p-3 bg-light">
{{ latest_revision.content|safe }}
</div>
<div class="mt-3 text-muted small">
<p>Last updated by {{ latest_revision.created_by }} on {{ latest_revision.created_at }}</p>
{% if latest_revision.comments %}
<p><strong>Comments:</strong> {{ latest_revision.comments }}</p>
{% endif %}
</div>
{% else %}
<div class="text-center py-3 text-muted">
<p>No content has been added to this contract yet.</p>
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-primary">
<i class="bi bi-pencil me-1"></i> Add Content
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Signers -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Signers</h5>
{% if contract.status == 'Draft' %}
<button class="btn btn-sm btn-primary">
<i class="bi bi-plus me-1"></i> Add Signer
</button>
{% endif %}
</div>
<div class="card-body">
{% if contract.signers|length > 0 %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Status</th>
<th>Signed Date</th>
<th>Comments</th>
{% if contract.status == 'Draft' %}
<th>Actions</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for signer in contract.signers %}
<tr>
<td>{{ signer.name }}</td>
<td>{{ signer.email }}</td>
<td>
<span class="badge {% if signer.status == 'Signed' %}bg-success{% elif signer.status == 'Rejected' %}bg-danger{% else %}bg-warning text-dark{% endif %}">
{{ signer.status }}
</span>
</td>
<td>
{% if signer.signed_at %}
{{ signer.signed_at }}
{% else %}
-
{% endif %}
</td>
<td>
{% if signer.comments %}
{{ signer.comments }}
{% else %}
-
{% endif %}
</td>
{% if contract.status == 'Draft' %}
<td>
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-3 text-muted">
<p>No signers have been added to this contract yet.</p>
{% if contract.status == 'Draft' %}
<button class="btn btn-sm btn-primary">
<i class="bi bi-plus me-1"></i> Add Signer
</button>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Contract Revisions -->
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0">Contract Revisions</h5>
</div>
<div class="card-body">
{% if contract.revisions|length > 0 %}
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Version</th>
<th>Date</th>
<th>Notes</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for revision in contract.revisions %}
<tr>
<td>{{ revision.version }}</td>
<td>{{ revision.created_at }}</td>
<td>{{ revision.notes }}</td>
<td>
<a href="/contracts/{{ contract.id }}/revisions/{{ revision.version }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> View
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-muted">No revisions available.</p>
{% endif %}
</div>
</div>
<!-- Revision History -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Revision History</h5>
</div>
<div class="card-body">
{% if contract.revisions|length > 0 %}
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Version</th>
<th>Created By</th>
<th>Created Date</th>
<th>Comments</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for revision in contract.revisions|reverse %}
<tr>
<td>{{ revision.version }}</td>
<td>{{ revision.created_by }}</td>
<td>{{ revision.created_at }}</td>
<td>
{% if revision.comments %}
{{ revision.comments }}
{% else %}
-
{% endif %}
</td>
<td>
<a href="/contracts/{{ contract.id }}?version={{ revision.version }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-3 text-muted">
<p>No revisions have been made to this contract yet.</p>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,140 @@
{% extends "base.html" %}
{% block title %}All Contracts{% endblock %}
{% block content %}
<div class="container">
<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 active" aria-current="page">All Contracts</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">All Contracts</h1>
<div class="btn-group">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</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>
<div class="card-body">
<form action="/contracts/list" 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">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-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>
</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">
</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>
<!-- Contract List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Contracts</h5>
</div>
<div class="card-body">
{% if contracts and contracts | length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Status</th>
<th>Created By</th>
<th>Signers</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</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 %}">
{{ contract.status }}
</span>
</td>
<td>{{ contract.created_by }}</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>
<div class="btn-group">
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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">No contracts found</p>
<a href="/contracts/create" class="btn btn-primary mt-2">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,166 @@
{% extends "base.html" %}
{% block title %}Create New Contract{% endblock %}
{% block content %}
<div class="container">
<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 active" aria-current="page">Create New Contract</li>
</ol>
</nav>
<h1 class="display-5 mb-3">Create New 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/create" 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" 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>
<option value="" selected disabled>Select a contract type</option>
{% for type in contract_types %}
<option value="{{ type }}">{{ 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></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>
</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>
</div>
</form>
</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>
<ul>
<li>Add signers who need to approve the contract</li>
<li>Edit the contract content</li>
<li>Send the contract for signatures</li>
<li>Track the signing progress</li>
</ul>
<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>
</div>
<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')">
Non-Disclosure Agreement
</button>
<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')">
Employment Contract
</button>
<button type="button" class="list-group-item list-group-item-action" onclick="loadTemplate('sla')">
Service Level Agreement
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function loadTemplate(type) {
// In a real application, this would load template content from the server
let title = '';
let description = '';
let content = '';
let contractType = '';
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...';
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...';
break;
case 'employment':
title = 'Employment Contract';
description = 'Standard employment agreement';
contractType = 'Employment Contract';
content = 'This Employment Agreement (the "Agreement") is made and entered into as of [DATE] by and between [EMPLOYER] and [EMPLOYEE].\n\n1. Position and Duties\n2. Compensation and Benefits\n3. Term and Termination\n...';
break;
case 'sla':
title = 'Service Level Agreement';
description = 'Agreement defining service levels and metrics';
contractType = 'Service Level Agreement';
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) {
selectElement.selectedIndex = i;
break;
}
}
}
</script>
{% endblock %}

View File

@ -0,0 +1,187 @@
{% extends "base.html" %}
{% block title %}Contracts Dashboard{% endblock %}
{% block content %}
<div class="container">
<div class="row mb-4">
<div class="col-12">
<h1 class="display-5 mb-3">Contracts Dashboard</h1>
<p class="lead">Manage legal agreements and contracts across your organization.</p>
</div>
</div>
<!-- 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>
</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>
</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>
</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>
</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>
</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>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Quick Actions</h5>
</div>
<div class="card-body">
<div class="d-flex flex-wrap gap-2">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
<a href="/contracts/list" class="btn btn-outline-secondary">
<i class="bi bi-list me-1"></i> View All Contracts
</a>
<a href="/contracts/my-contracts" class="btn btn-outline-secondary">
<i class="bi bi-person me-1"></i> My Contracts
</a>
</div>
</div>
</div>
</div>
</div>
<!-- Pending Signature Contracts -->
{% if pending_signature_contracts and pending_signature_contracts | length > 0 %}
<div class="row mb-4">
<div class="col-12">
<div class="card border-warning">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0">Pending Signature ({{ pending_signature_contracts|length }})</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Created By</th>
<th>Pending Signers</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in pending_signature_contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.pending_signers }} of {{ contract.signers|length }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Draft Contracts -->
{% if draft_contracts and draft_contracts | length > 0 %}
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">Draft Contracts</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Created By</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in draft_contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</td>
<td>{{ contract.contract_type }}</td>
<td>{{ contract.created_by }}</td>
<td>{{ contract.created_at | date(format="%Y-%m-%d") }}</td>
<td>
<div class="btn-group">
<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">
<i class="bi bi-pencil"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -0,0 +1,134 @@
{% extends "base.html" %}
{% block title %}My Contracts{% endblock %}
{% block content %}
<div class="container">
<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 active" aria-current="page">My Contracts</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center">
<h1 class="display-5 mb-0">My Contracts</h1>
<div class="btn-group">
<a href="/contracts/create" class="btn btn-primary">
<i class="bi bi-plus-circle me-1"></i> Create New Contract
</a>
</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>
<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>
</div>
</div>
</div>
<!-- Contract List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0">My Contracts</h5>
</div>
<div class="card-body">
{% if contracts and contracts | length > 0 %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Contract Title</th>
<th>Type</th>
<th>Status</th>
<th>Signers</th>
<th>Created</th>
<th>Updated</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for contract in contracts %}
<tr>
<td>
<a href="/contracts/{{ contract.id }}">{{ contract.title }}</a>
</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 %}">
{{ 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>
<div class="btn-group">
<a href="/contracts/{{ contract.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i>
</a>
{% if contract.status == 'Draft' %}
<a href="/contracts/{{ contract.id }}/edit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</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>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}