- 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.
477 lines
24 KiB
HTML
477 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}My Contracts{% 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 active" aria-current="page">My Contracts</li>
|
|
</ol>
|
|
</nav>
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h1 class="display-5 mb-0">My Contracts</h1>
|
|
<p class="text-muted mb-0">Manage and track your personal contracts</p>
|
|
</div>
|
|
<div class="btn-group">
|
|
<a href="/contracts/create" class="btn btn-primary">
|
|
<i class="bi bi-plus-circle me-1"></i> Create New Contract
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</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 d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="bi bi-funnel me-1"></i> Filters & Search
|
|
</h5>
|
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse"
|
|
data-bs-target="#filtersCollapse" aria-expanded="false" aria-controls="filtersCollapse">
|
|
<i class="bi bi-chevron-down"></i> Toggle Filters
|
|
</button>
|
|
</div>
|
|
<div class="collapse show" id="filtersCollapse">
|
|
<div class="card-body">
|
|
<form action="/contracts/my-contracts" method="get" class="row g-3">
|
|
<div class="col-md-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>
|
|
</div>
|
|
|
|
<!-- Contract List -->
|
|
<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-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 align-middle">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<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
|
|
class="{% if contract.status == 'Expired' %}table-danger{% elif contract.status == 'PendingSignatures' %}table-warning{% elif contract.status == 'Signed' %}table-success{% endif %}">
|
|
<td>
|
|
<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>
|
|
<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>
|
|
{% 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"
|
|
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" 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>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<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>
|
|
</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:</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 %} |