- Add .env.example file for environment variable setup - Add .gitignore to manage sensitive files and directories - Add Dockerfile.prod for production-ready Docker image - Add PRODUCTION_CHECKLIST.md for pre/post deployment steps - Add PRODUCTION_DEPLOYMENT.md for deployment instructions - Add STRIPE_SETUP.md for Stripe payment configuration - Add config/default.toml for default configuration settings - Add config/local.toml.example for local configuration template
417 lines
18 KiB
HTML
417 lines
18 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ company.name }} - Document Management{% endblock %}
|
|
|
|
{% block head %}
|
|
{{ super() }}
|
|
<style>
|
|
.document-card {
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.document-card:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
|
|
.file-icon {
|
|
font-size: 2rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.upload-area {
|
|
border: 2px dashed #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 2rem;
|
|
text-align: center;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.upload-area:hover {
|
|
border-color: #0d6efd;
|
|
}
|
|
|
|
.upload-area.dragover {
|
|
border-color: #0d6efd;
|
|
background-color: #f8f9fa;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<h2><i class="bi bi-folder me-2"></i>{{ company.name }} - Documents</h2>
|
|
<div>
|
|
<a href="/company/view/{{ company.base_data.id }}" class="btn btn-outline-secondary me-2">
|
|
<i class="bi bi-arrow-left me-1"></i>Back to Company
|
|
</a>
|
|
<a href="/company" class="btn btn-outline-secondary">
|
|
<i class="bi bi-building me-1"></i>All Companies
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success/Error Messages -->
|
|
{% if success %}
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-check-circle me-2"></i>{{ success }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if error %}
|
|
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
|
<i class="bi bi-exclamation-triangle me-2"></i>{{ error }}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Document Statistics -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<i class="bi bi-files text-primary" style="font-size: 2rem;"></i>
|
|
<h4 class="mt-2">{{ stats.total_documents }}</h4>
|
|
<p class="text-muted mb-0">Total Documents</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<i class="bi bi-hdd text-info" style="font-size: 2rem;"></i>
|
|
<h4 class="mt-2">{{ stats.formatted_total_size }}</h4>
|
|
<p class="text-muted mb-0">Total Size</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<i class="bi bi-upload text-success" style="font-size: 2rem;"></i>
|
|
<h4 class="mt-2">{{ stats.recent_uploads }}</h4>
|
|
<p class="text-muted mb-0">Recent Uploads</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card text-center">
|
|
<div class="card-body">
|
|
<i class="bi bi-folder-plus text-warning" style="font-size: 2rem;"></i>
|
|
<h4 class="mt-2">{{ stats.by_type | length }}</h4>
|
|
<p class="text-muted mb-0">Document Types</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Upload Section -->
|
|
<div class="card mb-4">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0"><i class="bi bi-cloud-upload me-2"></i>Upload Documents</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form action="/company/documents/{{ company_id }}/upload" method="post" enctype="multipart/form-data"
|
|
id="uploadForm">
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="document_type" class="form-label">Document Type</label>
|
|
<select class="form-select" id="document_type" name="document_type" required>
|
|
<option value="">Select document type...</option>
|
|
{% for doc_type in document_types %}
|
|
<option value="{{ doc_type.0 }}">{{ doc_type.1 }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="description" class="form-label">Description (Optional)</label>
|
|
<textarea class="form-control" id="description" name="description" rows="3"
|
|
placeholder="Enter document description..."></textarea>
|
|
</div>
|
|
<div class="mb-3 form-check">
|
|
<input type="checkbox" class="form-check-input" id="is_public" name="is_public">
|
|
<label class="form-check-label" for="is_public">
|
|
Make document publicly accessible
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label for="documents" class="form-label">Select Files</label>
|
|
<div class="upload-area" id="uploadArea">
|
|
<i class="bi bi-cloud-upload file-icon text-muted"></i>
|
|
<p class="mb-2">Drag and drop files here or click to browse</p>
|
|
<input type="file" class="form-control" id="documents" name="documents" multiple
|
|
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.txt" style="display: none;">
|
|
<button type="button" class="btn btn-outline-primary"
|
|
onclick="document.getElementById('documents').click()">
|
|
<i class="bi bi-folder2-open me-1"></i>Browse Files
|
|
</button>
|
|
<div id="fileList" class="mt-3"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="d-flex justify-content-end">
|
|
<button type="submit" class="btn btn-primary" id="uploadBtn">
|
|
<i class="bi bi-upload me-1"></i>Upload Documents
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Documents List -->
|
|
<div class="card">
|
|
<div class="card-header bg-light">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0"><i class="bi bi-files me-2"></i>Documents ({{ documents | length }})</h5>
|
|
<div class="input-group" style="width: 300px;">
|
|
<input type="text" class="form-control" id="searchInput" placeholder="Search documents...">
|
|
<button class="btn btn-outline-secondary" type="button" id="searchBtn">
|
|
<i class="bi bi-search"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if documents and documents | length > 0 %}
|
|
<div class="row" id="documentsGrid">
|
|
{% for document in documents %}
|
|
<div class="col-md-4 mb-3 document-item" data-name="{{ document.name | lower }}"
|
|
data-type="{{ document.document_type_str | lower }}">
|
|
<div class="card document-card h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
<div class="file-icon">
|
|
{% if document.is_pdf %}
|
|
<i class="bi bi-file-earmark-pdf text-danger"></i>
|
|
{% elif document.is_image %}
|
|
<i class="bi bi-file-earmark-image text-success"></i>
|
|
{% elif document.mime_type == "application/msword" %}
|
|
<i class="bi bi-file-earmark-word text-primary"></i>
|
|
{% else %}
|
|
<i class="bi bi-file-earmark text-secondary"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="dropdown">
|
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
|
data-bs-toggle="dropdown">
|
|
<i class="bi bi-three-dots"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="#"
|
|
onclick="downloadDocument({{ document.id }})">
|
|
<i class="bi bi-download me-1"></i>Download
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="editDocument({{ document.id }})">
|
|
<i class="bi bi-pencil me-1"></i>Edit
|
|
</a></li>
|
|
<li>
|
|
<hr class="dropdown-divider">
|
|
</li>
|
|
<li><a class="dropdown-item text-danger" href="#"
|
|
onclick="deleteDocument({{ document.id }}, '{{ document.name }}')">
|
|
<i class="bi bi-trash me-1"></i>Delete
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<h6 class="card-title text-truncate" title="{{ document.name }}">{{ document.name }}</h6>
|
|
<p class="card-text">
|
|
<small class="text-muted">
|
|
<span class="badge bg-secondary mb-1">{{ document.document_type_str }}</span><br>
|
|
Size: {{ document.formatted_file_size }}<br>
|
|
Uploaded: {{ document.formatted_upload_date }}<br>
|
|
By: {{ document.uploaded_by }}
|
|
{% if document.is_public %}
|
|
<br><span class="badge bg-success">Public</span>
|
|
{% endif %}
|
|
</small>
|
|
</p>
|
|
{% if document.description %}
|
|
<p class="card-text"><small>{{ document.description }}</small></p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="bi bi-folder-x text-muted" style="font-size: 4rem;"></i>
|
|
<h4 class="text-muted mt-3">No Documents Found</h4>
|
|
<p class="text-muted">Upload your first document using the form above.</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Delete Confirmation Modal -->
|
|
<div class="modal fade" id="deleteModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Confirm Delete</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p>Are you sure you want to delete the document "<span id="deleteDocumentName"></span>"?</p>
|
|
<p class="text-danger"><small>This action cannot be undone.</small></p>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<a href="#" class="btn btn-danger" id="confirmDeleteBtn">Delete Document</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
{{ super() }}
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const uploadArea = document.getElementById('uploadArea');
|
|
const fileInput = document.getElementById('documents');
|
|
const fileList = document.getElementById('fileList');
|
|
const uploadBtn = document.getElementById('uploadBtn');
|
|
const searchInput = document.getElementById('searchInput');
|
|
|
|
// File upload handling
|
|
fileInput.addEventListener('change', function () {
|
|
console.log('Files selected:', this.files.length);
|
|
updateFileList();
|
|
updateUploadButton();
|
|
});
|
|
|
|
// Drag and drop
|
|
uploadArea.addEventListener('dragover', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
uploadArea.classList.add('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('dragleave', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
uploadArea.classList.remove('dragover');
|
|
});
|
|
|
|
uploadArea.addEventListener('drop', function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
uploadArea.classList.remove('dragover');
|
|
|
|
const files = e.dataTransfer.files;
|
|
console.log('Files dropped:', files.length);
|
|
|
|
// Create a new DataTransfer object and assign to input
|
|
const dt = new DataTransfer();
|
|
for (let i = 0; i < files.length; i++) {
|
|
dt.items.add(files[i]);
|
|
}
|
|
fileInput.files = dt.files;
|
|
|
|
updateFileList();
|
|
updateUploadButton();
|
|
});
|
|
|
|
// Click to upload area
|
|
uploadArea.addEventListener('click', function (e) {
|
|
if (e.target.tagName !== 'BUTTON' && e.target.tagName !== 'INPUT') {
|
|
fileInput.click();
|
|
}
|
|
});
|
|
|
|
// Search functionality
|
|
searchInput.addEventListener('input', function () {
|
|
const searchTerm = this.value.toLowerCase();
|
|
const documentItems = document.querySelectorAll('.document-item');
|
|
|
|
documentItems.forEach(function (item) {
|
|
const name = item.dataset.name;
|
|
const type = item.dataset.type;
|
|
const matches = name.includes(searchTerm) || type.includes(searchTerm);
|
|
item.style.display = matches ? 'block' : 'none';
|
|
});
|
|
});
|
|
|
|
function updateFileList() {
|
|
const files = Array.from(fileInput.files);
|
|
if (files.length === 0) {
|
|
fileList.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const listHtml = files.map(file =>
|
|
`<div class="d-flex justify-content-between align-items-center p-2 border rounded mb-1">
|
|
<span class="text-truncate">${file.name}</span>
|
|
<small class="text-muted">${formatFileSize(file.size)}</small>
|
|
</div>`
|
|
).join('');
|
|
|
|
fileList.innerHTML = `<div class="mt-2"><strong>Selected files:</strong>${listHtml}</div>`;
|
|
}
|
|
|
|
function updateUploadButton() {
|
|
const hasFiles = fileInput.files.length > 0;
|
|
const hasDocumentType = document.getElementById('document_type').value !== '';
|
|
uploadBtn.disabled = !hasFiles || !hasDocumentType;
|
|
|
|
console.log('Update upload button - Files:', hasFiles, 'DocType:', hasDocumentType);
|
|
}
|
|
|
|
// Also update button when document type changes
|
|
document.getElementById('document_type').addEventListener('change', function () {
|
|
updateUploadButton();
|
|
});
|
|
|
|
// Add form submission debugging
|
|
document.getElementById('uploadForm').addEventListener('submit', function (e) {
|
|
console.log('Form submitted');
|
|
console.log('Files:', fileInput.files.length);
|
|
console.log('Document type:', document.getElementById('document_type').value);
|
|
|
|
if (fileInput.files.length === 0) {
|
|
e.preventDefault();
|
|
alert('Please select at least one file to upload.');
|
|
return false;
|
|
}
|
|
|
|
if (document.getElementById('document_type').value === '') {
|
|
e.preventDefault();
|
|
alert('Please select a document type.');
|
|
return false;
|
|
}
|
|
});
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
}
|
|
});
|
|
|
|
function deleteDocument(documentId, documentName) {
|
|
document.getElementById('deleteDocumentName').textContent = documentName;
|
|
document.getElementById('confirmDeleteBtn').href = `/company/documents/{{ company_id }}/delete/${documentId}`;
|
|
new bootstrap.Modal(document.getElementById('deleteModal')).show();
|
|
}
|
|
|
|
function downloadDocument(documentId) {
|
|
// TODO: Implement download functionality
|
|
alert('Download functionality will be implemented soon');
|
|
}
|
|
|
|
function editDocument(documentId) {
|
|
// TODO: Implement edit functionality
|
|
alert('Edit functionality will be implemented soon');
|
|
}
|
|
</script>
|
|
{% endblock %} |