599 lines
26 KiB
Rust
599 lines
26 KiB
Rust
use yew::prelude::*;
|
|
use std::collections::HashMap;
|
|
use crate::routing::ViewContext;
|
|
use crate::components::ViewComponent;
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub enum ContractStatus {
|
|
Draft,
|
|
PendingSignatures,
|
|
Signed,
|
|
Active,
|
|
Expired,
|
|
Cancelled,
|
|
}
|
|
|
|
impl ContractStatus {
|
|
fn to_string(&self) -> &'static str {
|
|
match self {
|
|
ContractStatus::Draft => "Draft",
|
|
ContractStatus::PendingSignatures => "Pending Signatures",
|
|
ContractStatus::Signed => "Signed",
|
|
ContractStatus::Active => "Active",
|
|
ContractStatus::Expired => "Expired",
|
|
ContractStatus::Cancelled => "Cancelled",
|
|
}
|
|
}
|
|
|
|
fn badge_class(&self) -> &'static str {
|
|
match self {
|
|
ContractStatus::Draft => "bg-secondary",
|
|
ContractStatus::PendingSignatures => "bg-warning text-dark",
|
|
ContractStatus::Signed => "bg-success",
|
|
ContractStatus::Active => "bg-success",
|
|
ContractStatus::Expired => "bg-danger",
|
|
ContractStatus::Cancelled => "bg-dark",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub struct ContractSigner {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub email: String,
|
|
pub status: String, // "Pending", "Signed", "Rejected"
|
|
pub signed_at: Option<String>,
|
|
pub comments: Option<String>,
|
|
}
|
|
|
|
#[derive(Clone, PartialEq)]
|
|
pub struct Contract {
|
|
pub id: String,
|
|
pub title: String,
|
|
pub description: String,
|
|
pub contract_type: String,
|
|
pub status: ContractStatus,
|
|
pub created_by: String,
|
|
pub created_at: String,
|
|
pub updated_at: String,
|
|
pub signers: Vec<ContractSigner>,
|
|
pub terms_and_conditions: Option<String>,
|
|
pub effective_date: Option<String>,
|
|
pub expiration_date: Option<String>,
|
|
}
|
|
|
|
impl Contract {
|
|
fn signed_signers(&self) -> usize {
|
|
self.signers.iter().filter(|s| s.status == "Signed").count()
|
|
}
|
|
|
|
fn pending_signers(&self) -> usize {
|
|
self.signers.iter().filter(|s| s.status == "Pending").count()
|
|
}
|
|
}
|
|
|
|
#[derive(Properties, PartialEq)]
|
|
pub struct ContractsViewProps {
|
|
pub context: ViewContext,
|
|
}
|
|
|
|
pub enum ContractsMsg {
|
|
CreateContract,
|
|
EditContract(String),
|
|
DeleteContract(String),
|
|
ViewContract(String),
|
|
FilterByStatus(String),
|
|
FilterByType(String),
|
|
SearchContracts(String),
|
|
}
|
|
|
|
pub struct ContractsViewComponent {
|
|
contracts: Vec<Contract>,
|
|
filtered_contracts: Vec<Contract>,
|
|
status_filter: String,
|
|
type_filter: String,
|
|
search_filter: String,
|
|
}
|
|
|
|
impl Component for ContractsViewComponent {
|
|
type Message = ContractsMsg;
|
|
type Properties = ContractsViewProps;
|
|
|
|
fn create(_ctx: &Context<Self>) -> Self {
|
|
let contracts = Self::get_sample_contracts();
|
|
let filtered_contracts = contracts.clone();
|
|
|
|
Self {
|
|
contracts,
|
|
filtered_contracts,
|
|
status_filter: String::new(),
|
|
type_filter: String::new(),
|
|
search_filter: String::new(),
|
|
}
|
|
}
|
|
|
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
|
match msg {
|
|
ContractsMsg::CreateContract => {
|
|
// Handle create contract navigation
|
|
true
|
|
}
|
|
ContractsMsg::EditContract(id) => {
|
|
// Handle edit contract navigation
|
|
true
|
|
}
|
|
ContractsMsg::DeleteContract(id) => {
|
|
// Handle delete contract
|
|
self.contracts.retain(|c| c.id != id);
|
|
self.apply_filters();
|
|
true
|
|
}
|
|
ContractsMsg::ViewContract(id) => {
|
|
// Handle view contract navigation
|
|
true
|
|
}
|
|
ContractsMsg::FilterByStatus(status) => {
|
|
self.status_filter = status;
|
|
self.apply_filters();
|
|
true
|
|
}
|
|
ContractsMsg::FilterByType(contract_type) => {
|
|
self.type_filter = contract_type;
|
|
self.apply_filters();
|
|
true
|
|
}
|
|
ContractsMsg::SearchContracts(query) => {
|
|
self.search_filter = query;
|
|
self.apply_filters();
|
|
true
|
|
}
|
|
}
|
|
}
|
|
|
|
fn view(&self, ctx: &Context<Self>) -> Html {
|
|
let context = &ctx.props().context;
|
|
|
|
let (title, description) = match context {
|
|
ViewContext::Business => ("Legal Contracts", "Manage business agreements and legal documents"),
|
|
ViewContext::Person => ("Contracts", "Personal contracts and agreements"),
|
|
};
|
|
|
|
// Create tabs content
|
|
let mut tabs = HashMap::new();
|
|
|
|
// Contracts Tab
|
|
tabs.insert("Contracts".to_string(), self.render_contracts_tab(ctx));
|
|
|
|
// Create Contract Tab
|
|
tabs.insert("Create Contract".to_string(), self.render_create_contract_tab(ctx));
|
|
|
|
html! {
|
|
<ViewComponent
|
|
title={Some(title.to_string())}
|
|
description={Some(description.to_string())}
|
|
tabs={Some(tabs)}
|
|
default_tab={Some("Contracts".to_string())}
|
|
use_modern_header={true}
|
|
/>
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ContractsViewComponent {
|
|
fn get_sample_contracts() -> Vec<Contract> {
|
|
vec![
|
|
Contract {
|
|
id: "1".to_string(),
|
|
title: "Service Agreement - Web Development".to_string(),
|
|
description: "Development of company website and maintenance".to_string(),
|
|
contract_type: "Service Agreement".to_string(),
|
|
status: ContractStatus::PendingSignatures,
|
|
created_by: "John Smith".to_string(),
|
|
created_at: "2024-01-15".to_string(),
|
|
updated_at: "2024-01-16".to_string(),
|
|
signers: vec![
|
|
ContractSigner {
|
|
id: "s1".to_string(),
|
|
name: "Alice Johnson".to_string(),
|
|
email: "alice@example.com".to_string(),
|
|
status: "Signed".to_string(),
|
|
signed_at: Some("2024-01-16 10:30".to_string()),
|
|
comments: Some("Looks good!".to_string()),
|
|
},
|
|
ContractSigner {
|
|
id: "s2".to_string(),
|
|
name: "Bob Wilson".to_string(),
|
|
email: "bob@example.com".to_string(),
|
|
status: "Pending".to_string(),
|
|
signed_at: None,
|
|
comments: None,
|
|
},
|
|
],
|
|
terms_and_conditions: Some("# Service Agreement\n\nThis agreement outlines...".to_string()),
|
|
effective_date: Some("2024-02-01".to_string()),
|
|
expiration_date: Some("2024-12-31".to_string()),
|
|
},
|
|
Contract {
|
|
id: "2".to_string(),
|
|
title: "Non-Disclosure Agreement".to_string(),
|
|
description: "Confidentiality agreement for project collaboration".to_string(),
|
|
contract_type: "Non-Disclosure Agreement".to_string(),
|
|
status: ContractStatus::Signed,
|
|
created_by: "Sarah Davis".to_string(),
|
|
created_at: "2024-01-10".to_string(),
|
|
updated_at: "2024-01-12".to_string(),
|
|
signers: vec![
|
|
ContractSigner {
|
|
id: "s3".to_string(),
|
|
name: "Mike Brown".to_string(),
|
|
email: "mike@example.com".to_string(),
|
|
status: "Signed".to_string(),
|
|
signed_at: Some("2024-01-12 14:20".to_string()),
|
|
comments: None,
|
|
},
|
|
ContractSigner {
|
|
id: "s4".to_string(),
|
|
name: "Lisa Green".to_string(),
|
|
email: "lisa@example.com".to_string(),
|
|
status: "Signed".to_string(),
|
|
signed_at: Some("2024-01-12 16:45".to_string()),
|
|
comments: Some("Agreed to all terms".to_string()),
|
|
},
|
|
],
|
|
terms_and_conditions: Some("# Non-Disclosure Agreement\n\nThe parties agree...".to_string()),
|
|
effective_date: Some("2024-01-12".to_string()),
|
|
expiration_date: Some("2026-01-12".to_string()),
|
|
},
|
|
Contract {
|
|
id: "3".to_string(),
|
|
title: "Employment Contract - Software Engineer".to_string(),
|
|
description: "Full-time employment agreement".to_string(),
|
|
contract_type: "Employment Contract".to_string(),
|
|
status: ContractStatus::Draft,
|
|
created_by: "HR Department".to_string(),
|
|
created_at: "2024-01-20".to_string(),
|
|
updated_at: "2024-01-20".to_string(),
|
|
signers: vec![],
|
|
terms_and_conditions: Some("# Employment Contract\n\nPosition: Software Engineer...".to_string()),
|
|
effective_date: Some("2024-02-15".to_string()),
|
|
expiration_date: None,
|
|
},
|
|
]
|
|
}
|
|
|
|
fn apply_filters(&mut self) {
|
|
self.filtered_contracts = self.contracts
|
|
.iter()
|
|
.filter(|contract| {
|
|
// Status filter
|
|
if !self.status_filter.is_empty() && contract.status.to_string() != self.status_filter {
|
|
return false;
|
|
}
|
|
|
|
// Type filter
|
|
if !self.type_filter.is_empty() && contract.contract_type != self.type_filter {
|
|
return false;
|
|
}
|
|
|
|
// Search filter
|
|
if !self.search_filter.is_empty() {
|
|
let query = self.search_filter.to_lowercase();
|
|
if !contract.title.to_lowercase().contains(&query) &&
|
|
!contract.description.to_lowercase().contains(&query) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
true
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
}
|
|
|
|
fn render_contracts_tab(&self, _ctx: &Context<Self>) -> Html {
|
|
html! {
|
|
<div>
|
|
// Filters Section
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm" style="border: none;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
|
|
<i class="bi bi-funnel text-primary fs-5"></i>
|
|
</div>
|
|
<h5 class="mb-0">{"Filters"}</h5>
|
|
</div>
|
|
<div class="row g-3">
|
|
<div class="col-md-3">
|
|
<label for="status" class="form-label">{"Status"}</label>
|
|
<select class="form-select" id="status">
|
|
<option value="">{"All Statuses"}</option>
|
|
<option value="Draft">{"Draft"}</option>
|
|
<option value="Pending Signatures">{"Pending Signatures"}</option>
|
|
<option value="Signed">{"Signed"}</option>
|
|
<option value="Active">{"Active"}</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">
|
|
<option value="">{"All Types"}</option>
|
|
<option value="Service Agreement">{"Service Agreement"}</option>
|
|
<option value="Employment Contract">{"Employment Contract"}</option>
|
|
<option value="Non-Disclosure Agreement">{"Non-Disclosure Agreement"}</option>
|
|
<option value="Service Level Agreement">{"Service Level Agreement"}</option>
|
|
<option value="Other">{"Other"}</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label for="search" class="form-label">{"Search"}</label>
|
|
<input type="text" class="form-control" id="search"
|
|
placeholder="Search by title or description" />
|
|
</div>
|
|
<div class="col-md-2 d-flex align-items-end">
|
|
<a href="#" class="btn btn-primary w-100">
|
|
<i class="bi bi-plus-circle me-1"></i>{"Create New"}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
// Contracts Table
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm" style="border: none;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="bg-success bg-opacity-10 rounded-3 p-2 me-3">
|
|
<i class="bi bi-file-earmark-text text-success fs-5"></i>
|
|
</div>
|
|
<h5 class="mb-0">{"Contracts"}</h5>
|
|
</div>
|
|
{self.render_contracts_table(_ctx)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
fn render_contracts_table(&self, ctx: &Context<Self>) -> Html {
|
|
if self.filtered_contracts.is_empty() {
|
|
return html! {
|
|
<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="#" class="btn btn-primary mt-2">
|
|
<i class="bi bi-plus-circle me-1"></i>{"Create New Contract"}
|
|
</a>
|
|
</div>
|
|
};
|
|
}
|
|
|
|
html! {
|
|
<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 self.filtered_contracts.iter().map(|contract| self.render_contract_row(contract, ctx))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
fn render_contract_row(&self, contract: &Contract, _ctx: &Context<Self>) -> Html {
|
|
html! {
|
|
<tr>
|
|
<td>
|
|
<a href="#" class="text-decoration-none">
|
|
{&contract.title}
|
|
</a>
|
|
</td>
|
|
<td>{&contract.contract_type}</td>
|
|
<td>
|
|
<span class={format!("badge {}", contract.status.badge_class())}>
|
|
{contract.status.to_string()}
|
|
</span>
|
|
</td>
|
|
<td>{&contract.created_by}</td>
|
|
<td>{format!("{}/{}", contract.signed_signers(), contract.signers.len())}</td>
|
|
<td>{&contract.created_at}</td>
|
|
<td>{&contract.updated_at}</td>
|
|
<td>
|
|
<div class="btn-group">
|
|
<a href="#" class="btn btn-sm btn-primary" title="View">
|
|
<i class="bi bi-eye"></i>
|
|
</a>
|
|
{if matches!(contract.status, ContractStatus::Draft) {
|
|
html! {
|
|
<>
|
|
<a href="#" class="btn btn-sm btn-outline-secondary" title="Edit">
|
|
<i class="bi bi-pencil"></i>
|
|
</a>
|
|
<a href="#" class="btn btn-sm btn-outline-danger" title="Delete">
|
|
<i class="bi bi-trash"></i>
|
|
</a>
|
|
</>
|
|
}
|
|
} else {
|
|
html! {}
|
|
}}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}
|
|
|
|
fn render_create_contract_tab(&self, _ctx: &Context<Self>) -> Html {
|
|
|
|
html! {
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-sm" style="border: none;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center mb-4">
|
|
<div class="bg-primary bg-opacity-10 rounded-3 p-2 me-3">
|
|
<i class="bi bi-file-earmark-plus text-primary fs-5"></i>
|
|
</div>
|
|
<h5 class="mb-0">{"Contract Details"}</h5>
|
|
</div>
|
|
<form>
|
|
<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=true />
|
|
</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=true>
|
|
<option value="" selected=true disabled=true>{"Select a contract type"}</option>
|
|
<option value="Service Agreement">{"Service Agreement"}</option>
|
|
<option value="Employment Contract">{"Employment Contract"}</option>
|
|
<option value="Non-Disclosure Agreement">{"Non-Disclosure Agreement"}</option>
|
|
<option value="Service Level Agreement">{"Service Level Agreement"}</option>
|
|
<option value="Partnership Agreement">{"Partnership Agreement"}</option>
|
|
<option value="Other">{"Other"}</option>
|
|
</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=true></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<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."}
|
|
</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">
|
|
<button type="button" class="btn btn-outline-secondary me-md-2">{"Cancel"}</button>
|
|
<button type="submit" class="btn btn-primary">{"Create Contract"}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-lg-4">
|
|
<div class="card shadow-sm mb-4" style="border: none;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="bg-info bg-opacity-10 rounded-3 p-2 me-3">
|
|
<i class="bi bi-lightbulb text-info fs-5"></i>
|
|
</div>
|
|
<h5 class="mb-0">{"Tips"}</h5>
|
|
</div>
|
|
<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 shadow-sm" style="border: none;">
|
|
<div class="card-body p-4">
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div class="bg-warning bg-opacity-10 rounded-3 p-2 me-3">
|
|
<i class="bi bi-file-earmark-code text-warning fs-5"></i>
|
|
</div>
|
|
<h5 class="mb-0">{"Contract Templates"}</h5>
|
|
</div>
|
|
<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">
|
|
{"Non-Disclosure Agreement"}
|
|
</button>
|
|
<button type="button" class="list-group-item list-group-item-action">
|
|
{"Service Agreement"}
|
|
</button>
|
|
<button type="button" class="list-group-item list-group-item-action">
|
|
{"Employment Contract"}
|
|
</button>
|
|
<button type="button" class="list-group-item list-group-item-action">
|
|
{"Service Level Agreement"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
}
|
|
|
|
#[function_component(ContractsView)]
|
|
pub fn contracts_view(props: &ContractsViewProps) -> Html {
|
|
html! {
|
|
<ContractsViewComponent context={props.context.clone()} />
|
|
}
|
|
} |