first
This commit is contained in:
164
templates/base.html
Normal file
164
templates/base.html
Normal file
@@ -0,0 +1,164 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}DocuPulse{% endblock %}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #16767b;
|
||||
--secondary-color: #741b5f;
|
||||
--primary-light: #1a8a90;
|
||||
--secondary-light: #8a2170;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: white;
|
||||
min-height: calc(100vh - 56px);
|
||||
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar .nav-link {
|
||||
color: #333;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
margin: 0.2rem 0;
|
||||
}
|
||||
|
||||
.sidebar .nav-link:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sidebar .nav-link i {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.document-card {
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.document-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
</style>
|
||||
{% block extra_css %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('main.dashboard') }}">DocuPulse</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link flex items-center justify-center" href="#">
|
||||
<i class="fas fa-bell text-xl" style="width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle flex items-center gap-2" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown">
|
||||
<img src="{{ url_for('profile_pic', filename=current_user.profile_picture) if current_user.profile_picture else url_for('static', filename='default-avatar.png') }}"
|
||||
alt="Profile Picture"
|
||||
class="w-8 h-8 rounded-full object-cover border-2 border-white shadow"
|
||||
style="display: inline-block; vertical-align: middle;">
|
||||
<span class="text-white font-medium">{{ current_user.username }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li><a class="dropdown-item" href="{{ url_for('main.profile') }}"><i class="fas fa-user"></i> Profile</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-cog"></i> Settings</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}"><i class="fas fa-sign-out-alt"></i> Logout</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Sidebar -->
|
||||
<div class="col-md-3 col-lg-2 px-0 sidebar">
|
||||
<div class="p-3">
|
||||
<ul class="nav flex-column">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'main.dashboard' %}active{% endif %}" href="{{ url_for('main.dashboard') }}">
|
||||
<i class="fas fa-home"></i> Dashboard
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'rooms.rooms' %}active{% endif %}" href="{{ url_for('rooms.rooms') }}">
|
||||
<i class="fas fa-door-open"></i> Rooms
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
{% if current_user.is_admin %}
|
||||
<a class="nav-link {% if request.endpoint == 'contacts.contacts_list' %}active{% endif %}" href="{{ url_for('contacts.contacts_list') }}">
|
||||
<i class="fas fa-address-book"></i> Contacts
|
||||
</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'main.starred' %}active{% endif %}" href="{{ url_for('main.starred') }}">
|
||||
<i class="fas fa-star"></i> Starred
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'main.trash' %}active{% endif %}" href="{{ url_for('main.trash') }}">
|
||||
<i class="fas fa-trash"></i> Trash
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="col-md-9 col-lg-10 main-content">
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
8
templates/contacts.html
Normal file
8
templates/contacts.html
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Contacts - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Contacts</h2>
|
||||
<p class="text-muted">Your contacts will appear here.</p>
|
||||
{% endblock %}
|
||||
196
templates/contacts/form.html
Normal file
196
templates/contacts/form.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-2xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
{% if title %}
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">{{ title }}</h1>
|
||||
{% else %}
|
||||
<h1 class="text-2xl font-bold text-gray-800 mb-6">User Form</h1>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('contacts.contacts_list') }}"
|
||||
class="text-gray-600 hover:text-gray-900">
|
||||
← Back to Contacts
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<form method="POST" class="space-y-6" enctype="multipart/form-data">
|
||||
{{ form.hidden_tag() }}
|
||||
|
||||
<!-- Profile Picture Upload (matches profile page style) -->
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<div class="relative group flex flex-col items-center">
|
||||
<label for="profile_picture" class="cursor-pointer">
|
||||
<img id="avatarPreview" src="{{ url_for('profile_pic', filename=form.profile_picture.data or user.profile_picture) if (form.profile_picture.data or (user and user.profile_picture)) else url_for('static', filename='default-avatar.png') }}" alt="Profile Picture" class="w-32 h-32 rounded-full object-cover border-4 border-gray-200 mb-0 transition duration-200 group-hover:opacity-80 group-hover:ring-4 group-hover:ring-primary-200 shadow-sm">
|
||||
<input id="profile_picture" type="file" name="profile_picture" accept="image/*" class="hidden" onchange="previewAvatar(event)" />
|
||||
</label>
|
||||
{% if user and user.profile_picture %}
|
||||
<button type="submit" name="remove_picture" value="1" class="mt-2 mb-2 text-xs px-3 py-1 rounded bg-red-100 text-red-700 border border-red-200 hover:bg-red-200 transition">Remove Picture</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
{{ form.first_name.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.first_name(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% if form.first_name.errors %}
|
||||
{% for error in form.first_name.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.last_name.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.last_name(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% if form.last_name.errors %}
|
||||
{% for error in form.last_name.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.email.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.email(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% if form.email.errors %}
|
||||
{% for error in form.email.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<div class="relative">
|
||||
{{ form.new_password.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.new_password(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10", autocomplete="new-password", id="new_password") }}
|
||||
<button type="button" tabindex="-1" class="absolute right-2 top-9 text-gray-500" style="background: none; border: none;" onmousedown="showPwd('new_password')" onmouseup="hidePwd('new_password')" onmouseleave="hidePwd('new_password')">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="relative">
|
||||
{{ form.confirm_password.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.confirm_password(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 pr-10", autocomplete="new-password", id="confirm_password") }}
|
||||
<button type="button" tabindex="-1" class="absolute right-2 top-9 text-gray-500" style="background: none; border: none;" onmousedown="showPwd('confirm_password')" onmouseup="hidePwd('confirm_password')" onmouseleave="hidePwd('confirm_password')">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
{% if form.confirm_password.errors %}
|
||||
{% for error in form.confirm_password.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
function showPwd(id) {
|
||||
document.getElementById(id).type = 'text';
|
||||
}
|
||||
function hidePwd(id) {
|
||||
document.getElementById(id).type = 'password';
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
{{ form.phone.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.phone(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% if form.phone.errors %}
|
||||
{% for error in form.phone.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.company.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.company(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% if form.company.errors %}
|
||||
{% for error in form.company.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.position.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.position(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500") }}
|
||||
{% if form.position.errors %}
|
||||
{% for error in form.position.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{ form.notes.label(class="block text-sm font-medium text-gray-700 mb-1") }}
|
||||
{{ form.notes(class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500", rows="4") }}
|
||||
{% if form.notes.errors %}
|
||||
{% for error in form.notes.errors %}
|
||||
<p class="mt-1 text-sm text-red-600">{{ error }}</p>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="flex items-center">
|
||||
{{ form.is_active(class="h-4 w-4 focus:ring-blue-500 border-gray-300 rounded", style="accent-color: #16767b;") }}
|
||||
{{ form.is_active.label(class="ml-2 block text-sm text-gray-900") }}
|
||||
</div>
|
||||
<div class="flex items-center relative group">
|
||||
{% set is_last_admin = current_user.is_admin and total_admins <= 1 %}
|
||||
{{ form.is_admin(
|
||||
class="h-4 w-4 focus:ring-blue-500 border-gray-300 rounded",
|
||||
style="accent-color: #16767b;",
|
||||
disabled=is_last_admin and form.is_admin.data
|
||||
) }}
|
||||
{{ form.is_admin.label(class="ml-2 block text-sm text-gray-900") }}
|
||||
{% if is_last_admin and form.is_admin.data %}
|
||||
<input type="hidden" name="is_admin" value="y">
|
||||
{% endif %}
|
||||
<div class="absolute left-0 bottom-full mb-2 hidden group-hover:block bg-gray-800 text-white text-xs rounded py-1 px-2 w-48">
|
||||
Admin users have full access to manage contacts and system settings.
|
||||
</div>
|
||||
{% if is_last_admin and form.is_admin.data %}
|
||||
<div class="ml-2 text-sm text-amber-600">
|
||||
<i class="fas fa-exclamation-circle"></i>
|
||||
You are the only admin
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if form.is_admin.errors %}
|
||||
<div class="p-3 bg-red-50 border border-red-200 rounded-lg">
|
||||
{% for error in form.is_admin.errors %}
|
||||
<p class="text-sm text-red-700">{{ error }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
{{ form.submit(class="text-white px-6 py-2 rounded-lg transition duration-200", style="background-color: #16767b; border: 1px solid #16767b;", onmouseover="this.style.backgroundColor='#1a8a90'", onmouseout="this.style.backgroundColor='#16767b'") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function previewAvatar(event) {
|
||||
const [file] = event.target.files;
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('avatarPreview').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
247
templates/contacts/list.html
Normal file
247
templates/contacts/list.html
Normal file
@@ -0,0 +1,247 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block head %}
|
||||
{{ super() }}
|
||||
<style>
|
||||
body {
|
||||
background: #f7f9fb;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">Contacts</h1>
|
||||
<a href="{{ url_for('contacts.new_contact') }}"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-white text-sm font-semibold transition-all duration-200 hover:-translate-y-0.5 hover:shadow-lg no-underline"
|
||||
style="background-color: #16767b; border: 1px solid #16767b;"
|
||||
onmouseover="this.style.backgroundColor='#1a8a90'"
|
||||
onmouseout="this.style.backgroundColor='#16767b'">
|
||||
<i class="fas fa-plus"></i>
|
||||
Add New Contact
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<form method="GET" class="flex flex-col md:flex-row gap-4" id="filterForm">
|
||||
<div class="flex-1">
|
||||
<input type="text" name="search" placeholder="Search contacts..."
|
||||
value="{{ request.args.get('search', '') }}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-200">
|
||||
</div>
|
||||
<div class="flex gap-4 items-center">
|
||||
<select name="status" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-200">
|
||||
<option value="">All Status</option>
|
||||
<option value="active" {% if request.args.get('status') == 'active' %}selected{% endif %}>Active</option>
|
||||
<option value="inactive" {% if request.args.get('status') == 'inactive' %}selected{% endif %}>Inactive</option>
|
||||
</select>
|
||||
<select name="role" class="px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-200">
|
||||
<option value="">All Roles</option>
|
||||
<option value="admin" {% if request.args.get('role') == 'admin' %}selected{% endif %}>Admin</option>
|
||||
<option value="user" {% if request.args.get('role') == 'user' %}selected{% endif %}>User</option>
|
||||
</select>
|
||||
<button type="button" id="clearFilters" class="px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200"
|
||||
style="background-color: #16767b; border: 1px solid #16767b;"
|
||||
onmouseover="this.style.backgroundColor='#1a8a90'"
|
||||
onmouseout="this.style.backgroundColor='#16767b'">
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
// Auto-submit the form on select change
|
||||
document.querySelectorAll('#filterForm select').forEach(function(el) {
|
||||
el.addEventListener('change', function() {
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
});
|
||||
// Debounced submit for search input, keep cursor after reload
|
||||
const searchInput = document.querySelector('#filterForm input[name="search"]');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('input', debounce(function() {
|
||||
// Save value and cursor position
|
||||
sessionStorage.setItem('searchFocus', '1');
|
||||
sessionStorage.setItem('searchValue', searchInput.value);
|
||||
sessionStorage.setItem('searchPos', searchInput.selectionStart);
|
||||
document.getElementById('filterForm').submit();
|
||||
}, 300));
|
||||
// On page load, restore focus and cursor position if needed
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
if (sessionStorage.getItem('searchFocus') === '1') {
|
||||
searchInput.focus();
|
||||
const val = sessionStorage.getItem('searchValue') || '';
|
||||
const pos = parseInt(sessionStorage.getItem('searchPos')) || val.length;
|
||||
searchInput.value = val;
|
||||
searchInput.setSelectionRange(pos, pos);
|
||||
// Clean up
|
||||
sessionStorage.removeItem('searchFocus');
|
||||
sessionStorage.removeItem('searchValue');
|
||||
sessionStorage.removeItem('searchPos');
|
||||
}
|
||||
});
|
||||
}
|
||||
// Clear button resets all filters and submits the form
|
||||
document.getElementById('clearFilters').addEventListener('click', function() {
|
||||
document.querySelector('#filterForm input[name="search"]').value = '';
|
||||
document.querySelector('#filterForm select[name="status"]').selectedIndex = 0;
|
||||
document.querySelector('#filterForm select[name="role"]').selectedIndex = 0;
|
||||
document.getElementById('filterForm').submit();
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<!-- Contacts List -->
|
||||
<div class="bg-white rounded-lg shadow-sm overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Contact Info</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Company</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Role</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for user in users %}
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-shrink-0 h-10 w-10">
|
||||
<img class="h-10 w-10 rounded-full object-cover"
|
||||
src="{{ url_for('profile_pic', filename=user.profile_picture) if user.profile_picture else url_for('static', filename='default-avatar.png') }}"
|
||||
alt="{{ user.username }}">
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<div class="text-sm font-medium text-gray-900">{{ user.username }} {{ user.last_name }}</div>
|
||||
<div class="text-sm text-gray-500">{{ user.position or 'No position' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex flex-row flex-wrap gap-1.5">
|
||||
<a href="mailto:{{ user.email }}"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm no-underline transition-colors duration-200"
|
||||
style="background-color: rgba(22,118,123,0.08); color: #16767b;">
|
||||
<i class="fas fa-envelope" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
{{ user.email }}
|
||||
</a>
|
||||
{% if user.phone %}
|
||||
<a href="tel:{{ user.phone }}"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm no-underline transition-colors duration-200"
|
||||
style="background-color: rgba(22,118,123,0.08); color: #16767b;">
|
||||
<i class="fas fa-phone" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
{{ user.phone }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ user.company or 'No company' }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex flex-row flex-wrap gap-1.5">
|
||||
{% if user.email != current_user.email and not user.is_admin %}
|
||||
<form method="POST" action="{{ url_for('contacts.toggle_active', id=user.id) }}" class="inline">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium transition-colors duration-200 cursor-pointer"
|
||||
style="background-color: {% if user.is_active %}rgba(34,197,94,0.1){% else %}rgba(239,68,68,0.1){% endif %}; color: {% if user.is_active %}#15803d{% else %}#b91c1c{% endif %};">
|
||||
<i class="fas fa-{% if user.is_active %}check-circle{% else %}times-circle{% endif %}" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
{{ 'Active' if user.is_active else 'Inactive' }}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium"
|
||||
style="background-color: {% if user.is_active %}rgba(34,197,94,0.1){% else %}rgba(239,68,68,0.1){% endif %}; color: {% if user.is_active %}#15803d{% else %}#b91c1c{% endif %};">
|
||||
<i class="fas fa-{% if user.is_active %}check-circle{% else %}times-circle{% endif %}" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
{{ 'Active' if user.is_active else 'Inactive' }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex flex-row flex-wrap gap-1.5">
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm font-medium"
|
||||
style="background-color: {% if user.is_admin %}rgba(147,51,234,0.1){% else %}rgba(107,114,128,0.1){% endif %}; color: {% if user.is_admin %}#7e22ce{% else %}#374151{% endif %};">
|
||||
<i class="fas fa-{% if user.is_admin %}shield-alt{% else %}user{% endif %}" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
{{ 'Admin' if user.is_admin else 'User' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end gap-1.5">
|
||||
<a href="{{ url_for('contacts.edit_contact', id=user.id) }}"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm no-underline transition-colors duration-200"
|
||||
style="background-color: rgba(22,118,123,0.08); color: #16767b;">
|
||||
<i class="fas fa-edit" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
Edit
|
||||
</a>
|
||||
{% if user.email != current_user.email %}
|
||||
<form method="POST" action="{{ url_for('contacts.delete_contact', id=user.id) }}" class="inline"
|
||||
onsubmit="return confirm('Are you sure you want to delete this contact?');">
|
||||
<button type="submit"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-sm no-underline transition-colors duration-200"
|
||||
style="background-color: rgba(239,68,68,0.1); color: #b91c1c;">
|
||||
<i class="fas fa-trash" style="font-size: 0.85em; opacity: 0.7;"></i>
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if pagination and pagination.pages > 1 %}
|
||||
<div class="mt-6 flex justify-center">
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
{% if pagination.has_prev %}
|
||||
<a href="{{ url_for('contacts.contacts_list', page=pagination.prev_num, **request.args) }}"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<span class="sr-only">Previous</span>
|
||||
<i class="fas fa-chevron-left"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
{% for page in pagination.iter_pages() %}
|
||||
{% if page %}
|
||||
<a href="{{ url_for('contacts.contacts_list', page=page, **request.args) }}"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium {% if page == pagination.page %}text-[#16767b] bg-[#16767b]/10{% else %}text-gray-700 hover:bg-gray-50{% endif %}">
|
||||
{{ page }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
|
||||
...
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if pagination.has_next %}
|
||||
<a href="{{ url_for('contacts.contacts_list', page=pagination.next_num, **request.args) }}"
|
||||
class="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
|
||||
<span class="sr-only">Next</span>
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
48
templates/create_room.html
Normal file
48
templates/create_room.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Create Room - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title mb-0">Create New Room</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('rooms.create_room') }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ form.name.label(class="form-label") }}
|
||||
{{ form.name(class="form-control") }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger">
|
||||
{% for error in form.name.errors %}
|
||||
<small>{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id }}" class="form-label">Description (Optional)</label>
|
||||
{{ form.description(class="form-control", rows="3", placeholder="Enter a description (optional)") }}
|
||||
{% if form.description.errors %}
|
||||
<div class="text-danger">
|
||||
{% for error in form.description.errors %}
|
||||
<small>{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('rooms.rooms') }}" class="btn btn-secondary">Cancel</a>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
471
templates/dashboard.html
Normal file
471
templates/dashboard.html
Normal file
@@ -0,0 +1,471 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2>Welcome back, {{ current_user.username }}!</h2>
|
||||
<div class="input-group" style="max-width: 300px;">
|
||||
<input type="text" class="form-control" placeholder="Search documents...">
|
||||
<button class="btn btn-outline-secondary" type="button">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% macro format_size(size) %}
|
||||
{% if size < 1024 %}
|
||||
{{ size }} B
|
||||
{% elif size < 1024 * 1024 %}
|
||||
{{ (size / 1024)|round(1) }} KB
|
||||
{% elif size < 1024 * 1024 * 1024 %}
|
||||
{{ (size / (1024 * 1024))|round(1) }} MB
|
||||
{% else %}
|
||||
{{ (size / (1024 * 1024 * 1024))|round(1) }} GB
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
<style>
|
||||
.masonry {
|
||||
column-count: 1;
|
||||
column-gap: 1.5rem;
|
||||
}
|
||||
@media (min-width: 768px) {
|
||||
.masonry { column-count: 2; }
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.masonry { column-count: 3; }
|
||||
}
|
||||
.masonry-card {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="masonry">
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-database me-2" style="color:#16767b;"></i>Storage Overview</h5>
|
||||
<a href="{{ url_for('rooms.rooms') }}" class="btn btn-sm text-white" style="background-color:#16767b;">Browse</a>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-door-open me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Rooms:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ room_count }}</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-file me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Files:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ file_count }}</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-folder me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Folders:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ folder_count }}</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-hdd me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Total Size:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ format_size(total_size) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="masonry-card">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-white border-0">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-history me-2" style="color:#16767b;"></i>Recent Activity</h5>
|
||||
<a href="{{ url_for('rooms.rooms') }}" class="btn btn-sm text-white" style="background-color:#16767b;">View All</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for activity in recent_activity %}
|
||||
<div class="list-group-item px-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="flex-shrink-0">
|
||||
{% if activity.type == 'folder' %}
|
||||
<i class="fas fa-folder me-2" style="color:#16767b;"></i>
|
||||
{% else %}
|
||||
<i class="fas fa-file me-2" style="color:#16767b;"></i>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex-grow-1 ms-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="d-flex align-items-center mb-1">
|
||||
<h6 class="mb-0">{{ activity.name }}</h6>
|
||||
{% if activity.is_starred %}
|
||||
<span class="badge bg-warning text-dark ms-2" style="background-color: rgba(255,215,0,0.15) !important; color: #ffd700 !important;">
|
||||
<i class="fas fa-star me-1"></i>Starred
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if activity.is_deleted %}
|
||||
<span class="badge bg-danger ms-2" style="background-color: rgba(220,53,69,0.15) !important; color: #dc3545 !important;">
|
||||
<i class="fas fa-trash me-1"></i>Trash
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="text-muted">
|
||||
{{ activity.room.name }} •
|
||||
{{ activity.uploader.username }} {{ activity.uploader.last_name }} •
|
||||
{% if activity.uploaded_at %}{{ activity.uploaded_at|timeago }}{% else %}Unknown{% endif %}
|
||||
</small>
|
||||
</div>
|
||||
{% if activity.type == 'file' and activity.can_download %}
|
||||
<a href="{{ url_for('room_files.download_room_file', room_id=activity.room.id, filename=activity.name, path=activity.path) }}"
|
||||
class="btn btn-sm text-white ms-2" style="background-color:#16767b; border-radius:6px; border:none; box-shadow:0 1px 2px rgba(22,118,123,0.08);" title="Download">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-chart-pie me-2" style="color:#16767b;"></i>Storage Usage</h5>
|
||||
<a href="{{ url_for('rooms.rooms') }}" class="btn btn-sm text-white" style="background-color:#16767b;">View Details</a>
|
||||
</div>
|
||||
{% if storage_by_type %}
|
||||
<div class="position-relative" style="height: 200px;">
|
||||
<canvas id="storageChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
{% for type in storage_by_type %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-file me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">{{ type.extension|upper }}:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ format_size(type.total_size) }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted small">No storage data available</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-address-book me-2" style="color:#16767b;"></i>Recent Contacts</h5>
|
||||
<div>
|
||||
<a href="{{ url_for('contacts.contacts_list') }}" class="btn btn-sm text-white me-2" style="background-color:#16767b;">View All</a>
|
||||
<a href="{{ url_for('contacts.new_contact') }}" class="btn btn-sm text-white" style="background-color:#16767b;">+ Add</a>
|
||||
</div>
|
||||
</div>
|
||||
{% if recent_contacts %}
|
||||
<ul class="list-unstyled mb-3">
|
||||
{% for contact in recent_contacts %}
|
||||
<li class="mb-2">
|
||||
<div class="fw-semibold">{{ contact.first_name }} {{ contact.last_name }}</div>
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
<a href="mailto:{{ contact.email }}"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-sm font-normal transition duration-200 no-underline"
|
||||
style="background-color: rgba(22,118,123,0.08); color: #16767b;">
|
||||
<i class="fas fa-envelope mr-1" style="font-size: 0.85em; opacity: 0.7;"></i>{{ contact.email }}
|
||||
</a>
|
||||
{% if contact.phone %}
|
||||
<a href="tel:{{ contact.phone }}"
|
||||
class="inline-flex items-center px-2 py-0.5 rounded text-sm font-normal transition duration-200 no-underline"
|
||||
style="background-color: rgba(22,118,123,0.08); color: #16767b;">
|
||||
<i class="fas fa-phone mr-1" style="font-size: 0.85em; opacity: 0.7;"></i>{{ contact.phone }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<div class="text-muted small">No contacts yet.</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-chart-pie me-2" style="color:#16767b;"></i>Contact Status</h5>
|
||||
<div>
|
||||
<a href="{{ url_for('contacts.contacts_list') }}" class="btn btn-sm text-white me-2" style="background-color:#16767b;">View All</a>
|
||||
<a href="{{ url_for('contacts.new_contact') }}" class="btn btn-sm text-white" style="background-color:#16767b;">+ Add</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-relative" style="height: 200px;">
|
||||
<canvas id="statusChart"></canvas>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center gap-4 mt-3">
|
||||
<div class="text-center">
|
||||
<div class="fw-bold" style="color:#16767b; font-size:1.5rem;">{{ active_count }}</div>
|
||||
<div class="text-muted small">Active</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="fw-bold" style="color:#741b5f; font-size:1.5rem;">{{ inactive_count }}</div>
|
||||
<div class="text-muted small">Inactive</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-star me-2" style="color:#16767b;"></i>Starred Files</h5>
|
||||
<a href="{{ url_for('main.starred') }}" class="btn btn-sm text-white" style="background-color:#16767b;">View All</a>
|
||||
</div>
|
||||
<div class="position-relative" style="height: 200px;">
|
||||
<canvas id="starredChart"></canvas>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center gap-4 mt-3">
|
||||
<div class="text-center">
|
||||
<div class="fw-bold" style="color:#ffd700; font-size:1.5rem;">{{ starred_count }}</div>
|
||||
<div class="text-muted small">Starred</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<div class="fw-bold" style="color:#16767b; font-size:1.5rem;">{{ file_count - starred_count }}</div>
|
||||
<div class="text-muted small">Unstarred</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-trash me-2" style="color:#16767b;"></i>Trash</h5>
|
||||
<a href="{{ url_for('main.trash') }}" class="btn btn-sm text-white" style="background-color:#16767b;">View All</a>
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-file me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Files in trash:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ trash_count }}</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-exclamation-triangle me-2" style="color:#dc3545;"></i>
|
||||
<span class="text-muted">Deleting in 7 days:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#dc3545;">{{ pending_deletion }}</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-clock me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Oldest deletion:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ oldest_trash_date|default('N/A') }}</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-hdd me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">Storage used:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ format_size(trash_size) }}</div>
|
||||
</div>
|
||||
<div class="alert alert-info mb-0">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
<div>
|
||||
<div class="fw-bold mb-1">Files will be permanently deleted after 30 days</div>
|
||||
<div class="small">You can restore files before they are permanently deleted</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="masonry-card">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="card-title mb-0"><i class="fas fa-file-alt me-2" style="color:#16767b;"></i>Trash by Type</h5>
|
||||
<a href="{{ url_for('main.trash') }}" class="btn btn-sm text-white" style="background-color:#16767b;">View All</a>
|
||||
</div>
|
||||
{% if trash_by_type %}
|
||||
<div class="position-relative" style="height: 200px;">
|
||||
<canvas id="trashTypeChart"></canvas>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
{% for type in trash_by_type %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-file me-2" style="color:#16767b;"></i>
|
||||
<span class="text-muted">{{ type.extension|upper }}:</span>
|
||||
</div>
|
||||
<div class="fw-bold" style="color:#16767b;">{{ type.count }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-muted small">No files in trash</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Contact Status Chart
|
||||
const statusCtx = document.getElementById('statusChart');
|
||||
if (statusCtx) {
|
||||
new Chart(statusCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Active', 'Inactive'],
|
||||
datasets: [{
|
||||
data: [{{ active_count }}, {{ inactive_count }}],
|
||||
backgroundColor: ['#16767b', '#741b5f'],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Starred Files Chart
|
||||
const starredCtx = document.getElementById('starredChart');
|
||||
if (starredCtx) {
|
||||
new Chart(starredCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['Starred', 'Unstarred'],
|
||||
datasets: [{
|
||||
data: [{{ starred_count }}, {{ file_count - starred_count }}],
|
||||
backgroundColor: ['#ffd700', '#16767b'],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Storage Usage Chart
|
||||
const storageCtx = document.getElementById('storageChart');
|
||||
if (storageCtx) {
|
||||
const storageData = {
|
||||
labels: [{% for type in storage_by_type %}'{{ type.extension|upper }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
datasets: [{
|
||||
data: [{% for type in storage_by_type %}{{ type.total_size }}{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
backgroundColor: [
|
||||
'#16767b', '#2c9da9', '#43c4d3', '#5ad9e8', '#71eefd',
|
||||
'#741b5f', '#8a2b73', '#a03b87', '#b64b9b', '#cc5baf'
|
||||
],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart(storageCtx, {
|
||||
type: 'doughnut',
|
||||
data: storageData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Trash Type Chart
|
||||
const trashTypeCtx = document.getElementById('trashTypeChart');
|
||||
if (trashTypeCtx) {
|
||||
const trashTypeData = {
|
||||
labels: [{% for type in trash_by_type %}'{{ type.extension|upper }}'{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
datasets: [{
|
||||
data: [{% for type in trash_by_type %}{{ type.count }}{% if not loop.last %}, {% endif %}{% endfor %}],
|
||||
backgroundColor: [
|
||||
'#16767b', '#2c9da9', '#43c4d3', '#5ad9e8', '#71eefd',
|
||||
'#741b5f', '#8a2b73', '#a03b87', '#b64b9b', '#cc5baf'
|
||||
],
|
||||
borderWidth: 0,
|
||||
hoverOffset: 4
|
||||
}]
|
||||
};
|
||||
|
||||
new Chart(trashTypeCtx, {
|
||||
type: 'doughnut',
|
||||
data: trashTypeData,
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
},
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
48
templates/edit_room.html
Normal file
48
templates/edit_room.html
Normal file
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Edit Room - {{ room.name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title mb-0">Edit Room</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ url_for('rooms.edit_room', room_id=room.id) }}">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ form.name.label(class="form-label") }}
|
||||
{{ form.name(class="form-control") }}
|
||||
{% if form.name.errors %}
|
||||
<div class="text-danger">
|
||||
{% for error in form.name.errors %}
|
||||
<small>{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id }}" class="form-label">Description (Optional)</label>
|
||||
{{ form.description(class="form-control", rows="3", placeholder="Enter a description (optional)") }}
|
||||
{% if form.description.errors %}
|
||||
<div class="text-danger">
|
||||
{% for error in form.description.errors %}
|
||||
<small>{{ error }}</small>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="d-flex justify-content-between">
|
||||
<a href="{{ url_for('rooms.rooms') }}" class="btn btn-secondary">Cancel</a>
|
||||
{{ form.submit(class="btn btn-primary", value="Update Room") }}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
148
templates/home.html
Normal file
148
templates/home.html
Normal file
@@ -0,0 +1,148 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DocuPulse - Legal Document Management</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #16767b;
|
||||
--secondary-color: #741b5f;
|
||||
--primary-light: #1a8a90;
|
||||
--secondary-light: #8a2170;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.hero-section {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
color: white;
|
||||
padding: 100px 0;
|
||||
}
|
||||
|
||||
.feature-card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
transition: transform 0.3s;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
font-size: 2.5rem;
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-outline-primary:hover {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: var(--primary-color) !important;
|
||||
}
|
||||
|
||||
.nav-link:hover {
|
||||
color: var(--secondary-color) !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">DocuPulse</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{{ url_for('auth.register') }}">Register</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="hero-section">
|
||||
<div class="container text-center">
|
||||
<h1 class="display-4 mb-4">Streamline Your Legal Document Management</h1>
|
||||
<p class="lead mb-5">Secure, efficient, and intelligent document handling for modern law practices</p>
|
||||
<a href="{{ url_for('auth.register') }}" class="btn btn-light btn-lg">Get Started</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Features Section -->
|
||||
<section class="py-5">
|
||||
<div class="container">
|
||||
<h2 class="text-center mb-5">Key Features</h2>
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div class="card feature-card h-100 p-4">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-shield-alt feature-icon"></i>
|
||||
<h3 class="h5">Secure Storage</h3>
|
||||
<p class="text-muted">Bank-level encryption and secure cloud storage for your sensitive legal documents</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card feature-card h-100 p-4">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-search feature-icon"></i>
|
||||
<h3 class="h5">Smart Search</h3>
|
||||
<p class="text-muted">Advanced search capabilities to find any document in seconds</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card feature-card h-100 p-4">
|
||||
<div class="card-body text-center">
|
||||
<i class="fas fa-users feature-icon"></i>
|
||||
<h3 class="h5">Collaboration</h3>
|
||||
<p class="text-muted">Seamless collaboration tools for legal teams and clients</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="text-light py-4">
|
||||
<div class="container text-center">
|
||||
<p class="mb-0">© 2024 DocuPulse. All rights reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
12
templates/index.html
Normal file
12
templates/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Flask App</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to Flask App</h1>
|
||||
<p>Your application is running successfully!</p>
|
||||
</body>
|
||||
</html>
|
||||
105
templates/login.html
Normal file
105
templates/login.html
Normal file
@@ -0,0 +1,105 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Login - DocuPulse</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #16767b;
|
||||
--secondary-color: #741b5f;
|
||||
--primary-light: #1a8a90;
|
||||
--secondary-light: #8a2170;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
max-width: 400px;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.login-header {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(22, 118, 123, 0.25);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="login-container">
|
||||
<div class="card login-card">
|
||||
<div class="login-header text-center">
|
||||
<h2>Welcome Back</h2>
|
||||
<p class="mb-0">Sign in to your account</p>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
{% if category in ['error', 'danger', 'login'] %}
|
||||
<div class="alert alert-{{ 'danger' if category in ['error', 'danger'] else 'info' }}">{{ message }}</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.login') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" id="remember" name="remember">
|
||||
<label class="form-check-label" for="remember">Remember me</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Sign In</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<p class="mb-0">Don't have an account? <a href="{{ url_for('auth.register') }}" class="text-decoration-none">Register</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
115
templates/profile.html
Normal file
115
templates/profile.html
Normal file
@@ -0,0 +1,115 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Profile - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h1 class="text-2xl font-bold text-gray-800">My Profile</h1>
|
||||
</div>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="mb-4 p-4 rounded-lg {% if category == 'error' %}bg-red-100 text-red-700{% else %}bg-green-100 text-green-700{% endif %}">
|
||||
{{ message }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||
<form method="POST" action="{{ url_for('main.profile') }}" class="p-6" enctype="multipart/form-data">
|
||||
<div class="flex flex-col items-center mb-6">
|
||||
<div class="relative group flex flex-col items-center">
|
||||
<label for="profile_picture" class="cursor-pointer">
|
||||
<img id="avatarPreview" src="{{ url_for('profile_pic', filename=current_user.profile_picture) if current_user.profile_picture else url_for('static', filename='default-avatar.png') }}" alt="Profile Picture" class="w-32 h-32 rounded-full object-cover border-4 border-gray-200 mb-0 transition duration-200 group-hover:opacity-80 group-hover:ring-4 group-hover:ring-primary-200 shadow-sm">
|
||||
<input id="profile_picture" type="file" name="profile_picture" accept="image/*" class="hidden" onchange="previewAvatar(event)" />
|
||||
</label>
|
||||
{% if current_user.profile_picture %}
|
||||
<button type="submit" name="remove_picture" value="1" class="mt-2 text-xs px-3 py-1 rounded bg-red-100 text-red-700 border border-red-200 hover:bg-red-200 transition">Remove Picture</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">First Name</label>
|
||||
<input type="text" name="first_name" value="{{ current_user.username }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Last Name</label>
|
||||
<input type="text" name="last_name" value="{{ current_user.last_name }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Email</label>
|
||||
<input type="email" name="email" value="{{ current_user.email }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Phone</label>
|
||||
<input type="tel" name="phone" value="{{ current_user.phone or '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Company</label>
|
||||
<input type="text" name="company" value="{{ current_user.company or '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Position</label>
|
||||
<input type="text" name="position" value="{{ current_user.position or '' }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Notes</label>
|
||||
<textarea name="notes" rows="4"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">{{ current_user.notes or '' }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 mb-4">Change Password</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">New Password</label>
|
||||
<input type="password" name="new_password"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Confirm New Password</label>
|
||||
<input type="password" name="confirm_password"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end">
|
||||
<button type="submit"
|
||||
class="text-white px-6 py-2 rounded-lg transition duration-200"
|
||||
style="background-color: #16767b; border: 1px solid #16767b;"
|
||||
onmouseover="this.style.backgroundColor='#1a8a90'"
|
||||
onmouseout="this.style.backgroundColor='#16767b'">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function previewAvatar(event) {
|
||||
const [file] = event.target.files;
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById('avatarPreview').src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
102
templates/register.html
Normal file
102
templates/register.html
Normal file
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Register - DocuPulse</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<style>
|
||||
:root {
|
||||
--primary-color: #16767b;
|
||||
--secondary-color: #741b5f;
|
||||
--primary-light: #1a8a90;
|
||||
--secondary-light: #8a2170;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.register-container {
|
||||
max-width: 400px;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.register-card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.register-header {
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
border-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background-color: var(--primary-light);
|
||||
border-color: var(--primary-light);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 0.2rem rgba(22, 118, 123, 0.25);
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="register-container">
|
||||
<div class="card register-card">
|
||||
<div class="register-header text-center">
|
||||
<h2>Create Account</h2>
|
||||
<p class="mb-0">Join DocuPulse today</p>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('auth.register') }}">
|
||||
<div class="mb-3">
|
||||
<label for="username" class="form-label">Username</label>
|
||||
<input type="text" class="form-control" id="username" name="username" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<input type="email" class="form-control" id="email" name="email" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">Create Account</button>
|
||||
</form>
|
||||
|
||||
<div class="text-center mt-3">
|
||||
<p class="mb-0">Already have an account? <a href="{{ url_for('auth.login') }}" class="text-decoration-none">Sign In</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1819
templates/room.html
Normal file
1819
templates/room.html
Normal file
File diff suppressed because it is too large
Load Diff
223
templates/room_members.html
Normal file
223
templates/room_members.html
Normal file
@@ -0,0 +1,223 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Room Members - {{ room.name }}{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
||||
<link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
.badge.bg-primary.rounded-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||
font-size: 1em;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
padding: 0 18px;
|
||||
}
|
||||
.btn-sm.member-action {
|
||||
min-width: 70px;
|
||||
height: 32px;
|
||||
font-size: 1em;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
.form-check-inline {
|
||||
margin-right: 8px;
|
||||
}
|
||||
.member-row {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.badge.creator-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.02em;
|
||||
font-size: 1em;
|
||||
height: 32px;
|
||||
border-radius: 5px;
|
||||
padding: 0 18px;
|
||||
background-color: rgba(22,118,123,0.08);
|
||||
color: #16767b;
|
||||
border: 1px solid #16767b22;
|
||||
}
|
||||
.btn-save-member {
|
||||
background-color: #16767b;
|
||||
color: #fff;
|
||||
border: 1px solid #16767b;
|
||||
min-width: 70px;
|
||||
height: 32px;
|
||||
font-size: 1em;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
transition: background 0.2s, border 0.2s;
|
||||
}
|
||||
.btn-save-member:hover {
|
||||
background-color: #1a8a90;
|
||||
border-color: #1a8a90;
|
||||
}
|
||||
.btn-remove-member {
|
||||
background-color: rgba(239,68,68,0.1);
|
||||
color: #b91c1c;
|
||||
border: 1px solid #b91c1c22;
|
||||
min-width: 70px;
|
||||
height: 32px;
|
||||
font-size: 1em;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 16px;
|
||||
transition: background 0.2s, color 0.2s;
|
||||
}
|
||||
.btn-remove-member:hover {
|
||||
background-color: #b91c1c;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2>Room Members - {{ room.name }}</h2>
|
||||
<p class="text-muted">Manage who has access to this room.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="card-title mb-0">Current Members</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if room.member_permissions %}
|
||||
<div class="list-group">
|
||||
{% for perm in room.member_permissions %}
|
||||
{% set member = perm.user %}
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center member-row">
|
||||
<div class="d-flex align-items-center">
|
||||
<img src="{{ url_for('profile_pic', filename=member.profile_picture) if member.profile_picture else url_for('static', filename='default-avatar.png') }}"
|
||||
alt="{{ member.username }}"
|
||||
class="rounded-circle me-3"
|
||||
style="width: 40px; height: 40px; object-fit: cover;">
|
||||
<div>
|
||||
<h6 class="mb-0">{{ member.username }} {{ member.last_name }}</h6>
|
||||
<small class="text-muted">{{ member.email }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
{% if member.id != room.created_by %}
|
||||
<form action="{{ url_for('rooms.update_member_permissions', room_id=room.id, user_id=member.id) }}" method="POST" class="d-flex align-items-center gap-2 auto-save-perms-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_view" id="can_view_{{ member.id }}" checked disabled>
|
||||
<input type="hidden" name="can_view" value="1">
|
||||
<label class="form-check-label" for="can_view_{{ member.id }}" title="View permission is always required"><i class="fas fa-eye"></i></label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_download" id="can_download_{{ member.id }}" {% if perm.can_download %}checked{% endif %}>
|
||||
<label class="form-check-label" for="can_download_{{ member.id }}" title="Can Download Files"><i class="fas fa-download"></i></label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_upload" id="can_upload_{{ member.id }}" {% if perm.can_upload %}checked{% endif %}>
|
||||
<label class="form-check-label" for="can_upload_{{ member.id }}" title="Can Upload Files"><i class="fas fa-upload"></i></label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_delete" id="can_delete_{{ member.id }}" {% if perm.can_delete %}checked{% endif %}>
|
||||
<label class="form-check-label" for="can_delete_{{ member.id }}" title="Can Delete Files"><i class="fas fa-trash"></i></label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_rename" id="can_rename_{{ member.id }}" {% if perm.can_rename %}checked{% endif %}>
|
||||
<label class="form-check-label" for="can_rename_{{ member.id }}" title="Can Rename Files"><i class="fas fa-i-cursor"></i></label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_move" id="can_move_{{ member.id }}" {% if perm.can_move %}checked{% endif %}>
|
||||
<label class="form-check-label" for="can_move_{{ member.id }}" title="Can Move Files"><i class="fas fa-arrows-alt"></i></label>
|
||||
</div>
|
||||
<div class="form-check form-check-inline mb-0">
|
||||
<input class="form-check-input" type="checkbox" name="can_share" id="can_share_{{ member.id }}" {% if perm.can_share %}checked{% endif %}>
|
||||
<label class="form-check-label" for="can_share_{{ member.id }}" title="Can Share Files"><i class="fas fa-share-alt"></i></label>
|
||||
</div>
|
||||
</form>
|
||||
<form action="{{ url_for('rooms.remove_member', room_id=room.id, user_id=member.id) }}" method="POST" class="d-inline ms-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-remove-member px-3 d-flex align-items-center gap-1">
|
||||
<i class="fas fa-user-minus"></i> Remove
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<span class="badge creator-badge px-3 py-2 d-flex align-items-center gap-1" style="height: 32px;">
|
||||
<i class="fas fa-user"></i> Creator
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">No members in this room yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header bg-white">
|
||||
<h5 class="card-title mb-0">Add New Member</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ url_for('rooms.add_member', room_id=room.id) }}" method="POST">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="mb-3">
|
||||
<label for="user_id" class="form-label">Select User</label>
|
||||
<select class="form-select select2" id="user_id" name="user_id" required>
|
||||
<option value="">Search for a user...</option>
|
||||
{% for user in available_users %}
|
||||
<option value="{{ user.id }}">{{ user.username }} {{ user.last_name }} ({{ user.email }})</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-user-plus me-2"></i>Add Member
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.select2').select2({
|
||||
theme: 'bootstrap-5',
|
||||
width: '100%',
|
||||
placeholder: 'Search for a user...',
|
||||
allowClear: true
|
||||
});
|
||||
// Auto-submit permission form on checkbox change
|
||||
document.querySelectorAll('.auto-save-perms-form input[type="checkbox"]').forEach(function(checkbox) {
|
||||
checkbox.addEventListener('change', function() {
|
||||
this.closest('form').submit();
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
196
templates/rooms.html
Normal file
196
templates/rooms.html
Normal file
@@ -0,0 +1,196 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Rooms - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2>Rooms</h2>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<a href="{{ url_for('rooms.create_room') }}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> New Room
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div class="bg-white rounded-lg shadow-sm p-4 mb-4">
|
||||
<form method="GET" class="d-flex align-items-center w-100 justify-content-between" id="roomFilterForm" style="gap: 1rem;">
|
||||
<input type="text" name="search" placeholder="Search rooms..." value="{{ search }}" class="form-control flex-grow-1" id="roomSearchInput" autocomplete="off" style="min-width: 0;" />
|
||||
<button type="button" id="clearRoomsFilter" class="px-4 py-2 rounded-lg text-white font-medium transition-colors duration-200 ms-2 flex-shrink-0"
|
||||
style="background-color: #16767b; border: 1px solid #16767b;"
|
||||
onmouseover="this.style.backgroundColor='#1a8a90'"
|
||||
onmouseout="this.style.backgroundColor='#16767b'">
|
||||
Clear
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
||||
{% for room in rooms %}
|
||||
<div class="col">
|
||||
<div class="card h-100 shadow-sm hover-shadow">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||
<div>
|
||||
<h5 class="card-title mb-0">{{ room.name }}</h5>
|
||||
<div class="text-muted small mt-1">Created on {{ room.created_at.strftime('%b %d, %Y') }}</div>
|
||||
</div>
|
||||
<span class="badge bg-light text-dark">
|
||||
<i class="fas fa-users"></i> {{ room.member_permissions|length }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="card-text text-muted">{{ room.description or 'No description' }}</p>
|
||||
<div class="d-flex align-items-center mt-3">
|
||||
<img src="{{ url_for('profile_pic', filename=room.creator.profile_picture) if room.creator.profile_picture else url_for('static', filename='default-avatar.png') }}"
|
||||
alt="Creator"
|
||||
class="rounded-circle me-2"
|
||||
style="width: 32px; height: 32px; object-fit: cover;">
|
||||
<div>
|
||||
<small class="text-muted">Created by</small>
|
||||
<div class="fw-medium">{{ room.creator.username }} {{ room.creator.last_name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-transparent border-top-0">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ url_for('rooms.room', room_id=room.id) }}" class="btn btn-primary flex-grow-1">
|
||||
<i class="fas fa-door-open me-2"></i>Open Room
|
||||
</a>
|
||||
{% if current_user.is_admin %}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" id="roomActions{{ room.id }}" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="roomActions{{ room.id }}">
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('rooms.edit_room', room_id=room.id) }}">
|
||||
<i class="fas fa-edit me-2"></i>Edit Room
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ url_for('rooms.room_members', room_id=room.id) }}">
|
||||
<i class="fas fa-users me-2"></i>Manage Members
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#deleteRoomModal{{ room.id }}">
|
||||
<i class="fas fa-trash me-2"></i>Delete Room
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.is_admin %}
|
||||
<!-- Delete Room Modal -->
|
||||
<div class="modal fade" id="deleteRoomModal{{ room.id }}" tabindex="-1" aria-labelledby="deleteRoomModalLabel{{ room.id }}" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="deleteRoomModalLabel{{ room.id }}">Move to Trash</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<i class="fas fa-trash text-danger" style="font-size: 2rem;"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">Move to Trash</h6>
|
||||
<p class="text-muted mb-0" id="deleteFileName{{ room.id }}">{{ room.name }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
This item will be moved to trash. You can restore it from the trash page within 30 days.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<form action="{{ url_for('rooms.delete_room', room_id=room.id) }}" method="POST" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<i class="fas fa-trash me-2"></i>Move to Trash
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.hover-shadow {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hover-shadow:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.btn {
|
||||
border-radius: 5px;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn i {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Debounce function
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const searchInput = document.getElementById('roomSearchInput');
|
||||
const form = document.getElementById('roomFilterForm');
|
||||
if (searchInput && form) {
|
||||
searchInput.addEventListener('input', debounce(function() {
|
||||
form.submit();
|
||||
}, 300));
|
||||
}
|
||||
// Clear button logic
|
||||
const clearBtn = document.getElementById('clearRoomsFilter');
|
||||
if (clearBtn && searchInput) {
|
||||
clearBtn.addEventListener('click', function() {
|
||||
searchInput.value = '';
|
||||
form.submit();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
468
templates/starred.html
Normal file
468
templates/starred.html
Normal file
@@ -0,0 +1,468 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Starred - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<div class="container mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2>Starred Items</h2>
|
||||
<div class="text-muted">Your starred files and folders from all rooms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="btn-group btn-group-sm" role="group" style="margin-right: 0.5rem;">
|
||||
<button type="button" id="gridViewBtn" class="btn btn-outline-secondary active" onclick="toggleView('grid')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button type="button" id="listViewBtn" class="btn btn-outline-secondary" onclick="toggleView('list')">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
<h5 class="mb-0">Files</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="ms-auto" style="max-width: 300px; position: relative;">
|
||||
<input type="text" id="quickSearchInput" class="form-control form-control-sm" placeholder="Quick search files..." autocomplete="off" style="padding-right: 2rem;" />
|
||||
<button id="clearSearchBtn" type="button" style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: none; border: none; background: transparent; font-size: 1.2rem; color: #888; cursor: pointer; z-index: 2;">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileGrid" class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
|
||||
<div id="fileError" class="text-danger mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal -->
|
||||
<div id="detailsModal" class="modal fade" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="detailsModalLabel">Item Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="detailsModalBody">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentView = 'grid';
|
||||
let lastSelectedIndex = -1;
|
||||
let sortColumn = 'name'; // Set default sort column to name
|
||||
let sortDirection = 1; // 1 for ascending, -1 for descending
|
||||
let batchDeleteItems = null;
|
||||
let currentFiles = [];
|
||||
|
||||
// Initialize the view and fetch files
|
||||
async function initializeView() {
|
||||
try {
|
||||
const response = await fetch('/api/user/preferred_view');
|
||||
const data = await response.json();
|
||||
currentView = data.preferred_view || 'grid';
|
||||
} catch (error) {
|
||||
console.error('Error fetching preferred view:', error);
|
||||
currentView = 'grid';
|
||||
}
|
||||
|
||||
// First fetch files
|
||||
await fetchFiles();
|
||||
|
||||
// Then toggle view after files are loaded
|
||||
toggleView(currentView);
|
||||
|
||||
// Sort files by name by default
|
||||
sortFiles('name');
|
||||
}
|
||||
|
||||
function formatDate(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts * 1000);
|
||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
|
||||
function toggleView(view) {
|
||||
currentView = view;
|
||||
const grid = document.getElementById('fileGrid');
|
||||
const gridBtn = document.getElementById('gridViewBtn');
|
||||
const listBtn = document.getElementById('listViewBtn');
|
||||
if (view === 'grid') {
|
||||
grid.classList.remove('table-mode');
|
||||
gridBtn.classList.add('active');
|
||||
listBtn.classList.remove('active');
|
||||
} else {
|
||||
grid.classList.add('table-mode');
|
||||
gridBtn.classList.remove('active');
|
||||
listBtn.classList.add('active');
|
||||
}
|
||||
renderFiles(currentFiles);
|
||||
// Save the new preference
|
||||
fetch('/api/user/preferred_view', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ preferred_view: view })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Preferred view saved:', data);
|
||||
})
|
||||
.catch(error => console.error('Error saving preferred view:', error));
|
||||
}
|
||||
|
||||
function sortFiles(column) {
|
||||
if (sortColumn === column) {
|
||||
sortDirection *= -1; // Toggle direction
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortDirection = 1;
|
||||
}
|
||||
currentFiles.sort((a, b) => {
|
||||
let valA = a[column];
|
||||
let valB = b[column];
|
||||
// For size, convert to number
|
||||
if (column === 'size') {
|
||||
valA = typeof valA === 'number' ? valA : 0;
|
||||
valB = typeof valB === 'number' ? valB : 0;
|
||||
}
|
||||
// For name/type, compare as strings
|
||||
if (typeof valA === 'string' && typeof valB === 'string') {
|
||||
return valA.localeCompare(valB) * sortDirection;
|
||||
}
|
||||
// For date (modified), compare as numbers
|
||||
return (valA - valB) * sortDirection;
|
||||
});
|
||||
renderFiles(currentFiles);
|
||||
}
|
||||
|
||||
function renderFiles(files) {
|
||||
if (!files) return;
|
||||
currentFiles = files;
|
||||
const grid = document.getElementById('fileGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (!files.length) {
|
||||
grid.innerHTML = '<div class="col"><div class="text-muted">No starred items yet.</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === 'list') {
|
||||
let table = `<table><thead><tr>
|
||||
<th></th>
|
||||
<th>Room</th>
|
||||
<th onclick="sortFiles('name')" style="cursor:pointer;">Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th onclick="sortFiles('modified')" style="cursor:pointer;">Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th onclick="sortFiles('type')" style="cursor:pointer;">Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th onclick="sortFiles('size')" style="cursor:pointer;">Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th class='file-actions'></th>
|
||||
</tr></thead><tbody>`;
|
||||
files.forEach((file, idx) => {
|
||||
let icon = file.type === 'folder'
|
||||
? `<i class='fas fa-folder' style='font-size:1.5rem;color:#16767b;'></i>`
|
||||
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:#741b5f;'></i>`;
|
||||
let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-';
|
||||
let actionsArr = [];
|
||||
let dblClickAction = '';
|
||||
if (file.type === 'folder') {
|
||||
dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
|
||||
} else {
|
||||
dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
|
||||
}
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' onclick='event.stopPropagation();toggleStar("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-star'></i></button>`);
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
|
||||
const actions = actionsArr.join('');
|
||||
table += `<tr ${dblClickAction} onclick='navigateToFile(${file.room_id}, "${file.name}", "${file.path}", "${file.type}")'>
|
||||
<td class='file-icon'><span style="display:inline-flex;align-items:center;gap:1.2rem;">${icon}</span></td>
|
||||
<td class='room-name'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></td>
|
||||
<td class='file-name' title='${file.name}'>${file.name}</td>
|
||||
<td class='file-date'>${formatDate(file.modified)}</td>
|
||||
<td class='file-type'>${file.type}</td>
|
||||
<td class='file-size'>${size}</td>
|
||||
<td class='file-actions'>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
table += '</tbody></table>';
|
||||
grid.innerHTML = table;
|
||||
} else {
|
||||
files.forEach((file, idx) => {
|
||||
let icon = file.type === 'folder'
|
||||
? `<i class='fas fa-folder' style='font-size:2.5rem;color:#16767b;'></i>`
|
||||
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:#741b5f;'></i>`;
|
||||
let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-';
|
||||
let actionsArr = [];
|
||||
let dblClickAction = '';
|
||||
if (file.type === 'folder') {
|
||||
dblClickAction = `ondblclick=\"window.location.href='/room/${file.room_id}?path=${encodeURIComponent(file.path ? file.path + '/' + file.name : file.name)}'\"`;
|
||||
} else {
|
||||
dblClickAction = `ondblclick=\"window.location.href='/api/rooms/${file.room_id}/files/${encodeURIComponent(file.name)}?path=${encodeURIComponent(file.path)}'\"`;
|
||||
}
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='${file.starred ? 'Unstar' : 'Star'}' style='background-color:${file.starred ? 'rgba(255,215,0,0.15)' : 'rgba(22,118,123,0.08)'};color:${file.starred ? '#ffd700' : '#16767b'};' onclick='event.stopPropagation();toggleStar("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-star'></i></button>`);
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Details' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();showDetailsModal(${idx})'><i class='fas fa-info-circle'></i></button>`);
|
||||
const actions = actionsArr.join('');
|
||||
grid.innerHTML += `
|
||||
<div class='col'>
|
||||
<div class='card file-card h-100 border-0 shadow-sm position-relative' ${dblClickAction} onclick='navigateToFile(${file.room_id}, "${file.name}", "${file.path}", "${file.type}")'>
|
||||
<div class='card-body d-flex flex-column align-items-center justify-content-center text-center'>
|
||||
<div class='mb-2'>${icon}</div>
|
||||
<div class='fw-semibold file-name-ellipsis' title='${file.name}'>${file.name}</div>
|
||||
<div class='text-muted small'>${formatDate(file.modified)}</div>
|
||||
<div class='text-muted small'>${size}</div>
|
||||
<div class='mt-2'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='event.stopPropagation();window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></div>
|
||||
</div>
|
||||
<div class='card-footer bg-white border-0 d-flex justify-content-center gap-2'>${actions}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFiles() {
|
||||
fetch('/api/rooms/starred')
|
||||
.then(r => r.json())
|
||||
.then(files => {
|
||||
if (files) {
|
||||
window.currentFiles = files;
|
||||
// Sort files by name by default
|
||||
window.currentFiles.sort((a, b) => {
|
||||
if (a.name < b.name) return -1;
|
||||
if (a.name > b.name) return 1;
|
||||
return 0;
|
||||
});
|
||||
renderFiles(files);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading files:', error);
|
||||
document.getElementById('fileGrid').innerHTML = '<div class="col"><div class="text-danger">Failed to load files. Please try refreshing the page.</div></div>';
|
||||
});
|
||||
}
|
||||
|
||||
function toggleStar(filename, path = '', roomId) {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch(`/api/rooms/${roomId}/star`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: filename,
|
||||
path: path
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
// Remove the file from the current view since it's no longer starred
|
||||
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
||||
renderFiles(currentFiles);
|
||||
} else {
|
||||
console.error('Failed to toggle star:', res.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error toggling star:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function navigateToFile(roomId, filename, path, type) {
|
||||
if (file.type === 'folder') {
|
||||
window.location.href = `/room/${roomId}?path=${encodeURIComponent(path ? path + '/' + filename : filename)}`;
|
||||
} else {
|
||||
window.location.href = `/api/rooms/${roomId}/files/${encodeURIComponent(filename)}?path=${encodeURIComponent(path)}`;
|
||||
}
|
||||
}
|
||||
|
||||
function showDetailsModal(idx) {
|
||||
const item = currentFiles[idx];
|
||||
const icon = item.type === 'folder'
|
||||
? `<i class='fas fa-folder' style='font-size:2.2rem;color:#16767b;'></i>`
|
||||
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:#741b5f;'></i>`;
|
||||
const uploaderPic = item.uploader_profile_pic
|
||||
? `/uploads/profile_pics/${item.uploader_profile_pic}`
|
||||
: '/static/default-avatar.png';
|
||||
const detailsHtml = `
|
||||
<div class='d-flex align-items-center gap-3 mb-3'>
|
||||
<div>${icon}</div>
|
||||
<div style='min-width:0;'>
|
||||
<div class='fw-bold' style='font-size:1.1rem;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' title='${item.name}'>${item.name}</div>
|
||||
<div class='text-muted small'>${item.type === 'folder' ? 'Folder' : 'File'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='mb-2 d-flex align-items-center gap-2'>
|
||||
<img src='${uploaderPic}' alt='Profile Picture' class='rounded-circle border' style='width:28px;height:28px;object-fit:cover;'>
|
||||
<span class='fw-semibold' style='font-size:0.98rem;'>${item.uploaded_by || '-'}</span>
|
||||
</div>
|
||||
<div class='mb-2 text-muted' style='font-size:0.92rem;'><i class='far fa-clock me-1'></i>${formatDate(item.modified)}</div>
|
||||
<hr style='margin:0.7rem 0 0.5rem 0; border-color:#e6f3f4;'>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Room:</strong> <button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href=\"/room/${item.room_id}\"'><i class='fas fa-door-open me-1'></i>${item.room_name || 'Room ' + item.room_id}</button></div>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Size:</strong> ${item.size === '-' ? '-' : (item.size > 0 ? (item.size < 1024*1024 ? (item.size/1024).toFixed(1)+' KB' : (item.size/1024/1024).toFixed(2)+' MB') : '0 KB')}</div>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
|
||||
`;
|
||||
document.getElementById('detailsModalBody').innerHTML = detailsHtml;
|
||||
var modal = new bootstrap.Modal(document.getElementById('detailsModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Live search
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeView();
|
||||
|
||||
const quickSearchInput = document.getElementById('quickSearchInput');
|
||||
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
||||
let searchTimeout = null;
|
||||
|
||||
quickSearchInput.addEventListener('input', function() {
|
||||
const query = quickSearchInput.value.trim().toLowerCase();
|
||||
clearSearchBtn.style.display = query.length > 0 ? 'block' : 'none';
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (query.length === 0) {
|
||||
fetchFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredFiles = currentFiles.filter(file =>
|
||||
file.name.toLowerCase().includes(query)
|
||||
);
|
||||
renderFiles(filteredFiles);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
clearSearchBtn.addEventListener('click', function() {
|
||||
quickSearchInput.value = '';
|
||||
clearSearchBtn.style.display = 'none';
|
||||
fetchFiles();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.file-name-ellipsis {
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.file-name-ellipsis { max-width: 180px; }
|
||||
}
|
||||
.file-action-btn {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.card:hover > .card-footer .file-action-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.card-footer.bg-white.border-0.d-flex.justify-content-center.gap-2 {
|
||||
min-height: 40px;
|
||||
}
|
||||
.card.file-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card.file-card .file-action-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
#fileGrid.table-mode {
|
||||
padding: 0;
|
||||
}
|
||||
#fileGrid.table-mode table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
}
|
||||
#fileGrid.table-mode th, #fileGrid.table-mode td {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
text-align: left;
|
||||
font-size: 0.95rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#fileGrid.table-mode th {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
#fileGrid.table-mode tr:hover td {
|
||||
background-color: rgba(22, 118, 123, 0.08);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#fileGrid.table-mode .file-icon {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
#fileGrid.table-mode .file-actions {
|
||||
min-width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
#fileGrid.table-mode .file-action-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
/* Disable text selection for file grid and table rows/cards */
|
||||
#fileGrid, #fileGrid * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
#fileGrid .card.file-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
#fileGrid.table-mode tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
.btn-group.btn-group-sm .btn {
|
||||
background-color: #fff;
|
||||
border-color: #e9ecef;
|
||||
color: #6c757d;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
}
|
||||
.btn-group.btn-group-sm .btn.active, .btn-group.btn-group-sm .btn:active {
|
||||
background-color: #e6f3f4 !important;
|
||||
color: #16767b !important;
|
||||
border-color: #16767b !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-group.btn-group-sm .btn:focus {
|
||||
box-shadow: 0 0 0 0.1rem #16767b33;
|
||||
}
|
||||
.btn-group.btn-group-sm .btn:hover:not(.active) {
|
||||
background-color: #f8f9fa;
|
||||
color: #16767b;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
643
templates/trash.html
Normal file
643
templates/trash.html
Normal file
@@ -0,0 +1,643 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Trash - DocuPulse{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
<div class="container mt-4">
|
||||
<div class="row mb-4">
|
||||
<div class="col">
|
||||
<h2>Trash</h2>
|
||||
<div class="text-muted">Your deleted files and folders from all rooms</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header d-flex justify-content-between align-items-center bg-white">
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<div class="btn-group btn-group-sm" role="group" style="margin-right: 0.5rem;">
|
||||
<button type="button" id="gridViewBtn" class="btn btn-outline-secondary active" onclick="toggleView('grid')">
|
||||
<i class="fas fa-th-large"></i>
|
||||
</button>
|
||||
<button type="button" id="listViewBtn" class="btn btn-outline-secondary" onclick="toggleView('list')">
|
||||
<i class="fas fa-list"></i>
|
||||
</button>
|
||||
</div>
|
||||
<h5 class="mb-0">Files</h5>
|
||||
</div>
|
||||
{% if current_user.is_admin %}
|
||||
<button type="button" id="emptyTrashBtn" class="btn btn-danger btn-sm" onclick="showEmptyTrashModal()">
|
||||
<i class="fas fa-trash me-1"></i>Empty Trash
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="d-flex align-items-center gap-2 mb-3">
|
||||
<div class="ms-auto" style="max-width: 300px; position: relative;">
|
||||
<input type="text" id="quickSearchInput" class="form-control form-control-sm" placeholder="Quick search files..." autocomplete="off" style="padding-right: 2rem;" />
|
||||
<button id="clearSearchBtn" type="button" style="position: absolute; right: 0.25rem; top: 50%; transform: translateY(-50%); display: none; border: none; background: transparent; font-size: 1.2rem; color: #888; cursor: pointer; z-index: 2;">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="fileGrid" class="row row-cols-1 row-cols-md-2 row-cols-lg-4 g-4"></div>
|
||||
<div id="fileError" class="text-danger mt-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Modal -->
|
||||
<div id="detailsModal" class="modal fade" tabindex="-1" aria-labelledby="detailsModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="detailsModalLabel">Item Details</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="detailsModalBody">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Permanent Delete Confirmation Modal -->
|
||||
<div id="permanentDeleteModal" class="modal fade" tabindex="-1" aria-labelledby="permanentDeleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="permanentDeleteModalLabel">Permanently Delete Item</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 2rem;"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">Are you sure you want to permanently delete this item?</h6>
|
||||
<p class="text-muted mb-0" id="permanentDeleteItemName"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
This action cannot be undone. The item will be permanently removed from the system.
|
||||
</div>
|
||||
</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="confirmPermanentDelete">
|
||||
<i class="fas fa-trash me-1"></i>Delete Permanently
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty Trash Confirmation Modal -->
|
||||
<div id="emptyTrashModal" class="modal fade" tabindex="-1" aria-labelledby="emptyTrashModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="emptyTrashModalLabel">Empty Trash</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="d-flex align-items-center gap-3 mb-3">
|
||||
<i class="fas fa-exclamation-triangle text-danger" style="font-size: 2rem;"></i>
|
||||
<div>
|
||||
<h6 class="mb-1">Are you sure you want to empty the trash?</h6>
|
||||
<p class="text-muted mb-0">This will permanently delete all items in the trash.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
This action cannot be undone. All items will be permanently removed from the system.
|
||||
</div>
|
||||
</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="confirmEmptyTrash">
|
||||
<i class="fas fa-trash me-1"></i>Empty Trash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentView = 'grid';
|
||||
let lastSelectedIndex = -1;
|
||||
let sortColumn = 'auto_delete'; // Set default sort column to auto_delete
|
||||
let sortDirection = 1; // 1 for ascending, -1 for descending
|
||||
let batchDeleteItems = null;
|
||||
let currentFiles = [];
|
||||
let fileToDelete = null;
|
||||
window.isAdmin = {{ 'true' if current_user.is_admin else 'false' }};
|
||||
|
||||
// Initialize the view and fetch files
|
||||
async function initializeView() {
|
||||
try {
|
||||
const response = await fetch('/api/user/preferred_view');
|
||||
const data = await response.json();
|
||||
currentView = data.preferred_view || 'grid';
|
||||
} catch (error) {
|
||||
console.error('Error fetching preferred view:', error);
|
||||
currentView = 'grid';
|
||||
}
|
||||
|
||||
// First fetch files
|
||||
await fetchFiles();
|
||||
|
||||
// Then toggle view after files are loaded
|
||||
toggleView(currentView);
|
||||
|
||||
// Sort files by auto_delete by default
|
||||
sortFiles('auto_delete');
|
||||
}
|
||||
|
||||
function formatDate(ts) {
|
||||
if (!ts) return '';
|
||||
const d = new Date(ts * 1000);
|
||||
return d.toLocaleDateString() + ' ' + d.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
|
||||
}
|
||||
|
||||
function toggleView(view) {
|
||||
currentView = view;
|
||||
const grid = document.getElementById('fileGrid');
|
||||
const gridBtn = document.getElementById('gridViewBtn');
|
||||
const listBtn = document.getElementById('listViewBtn');
|
||||
if (view === 'grid') {
|
||||
grid.classList.remove('table-mode');
|
||||
gridBtn.classList.add('active');
|
||||
listBtn.classList.remove('active');
|
||||
} else {
|
||||
grid.classList.add('table-mode');
|
||||
gridBtn.classList.remove('active');
|
||||
listBtn.classList.add('active');
|
||||
}
|
||||
renderFiles(currentFiles);
|
||||
// Save the new preference
|
||||
fetch('/api/user/preferred_view', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
body: JSON.stringify({ preferred_view: view })
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Preferred view saved:', data);
|
||||
})
|
||||
.catch(error => console.error('Error saving preferred view:', error));
|
||||
}
|
||||
|
||||
function sortFiles(column) {
|
||||
if (sortColumn === column) {
|
||||
sortDirection *= -1; // Toggle direction
|
||||
} else {
|
||||
sortColumn = column;
|
||||
sortDirection = 1;
|
||||
}
|
||||
currentFiles.sort((a, b) => {
|
||||
let valA = a[column];
|
||||
let valB = b[column];
|
||||
|
||||
// Special handling for auto_delete column
|
||||
if (column === 'auto_delete') {
|
||||
valA = valA ? new Date(valA).getTime() : Infinity;
|
||||
valB = valB ? new Date(valB).getTime() : Infinity;
|
||||
return (valA - valB) * sortDirection;
|
||||
}
|
||||
|
||||
// For size, convert to number
|
||||
if (column === 'size') {
|
||||
valA = typeof valA === 'number' ? valA : 0;
|
||||
valB = typeof valB === 'number' ? valB : 0;
|
||||
}
|
||||
// For name/type, compare as strings
|
||||
if (typeof valA === 'string' && typeof valB === 'string') {
|
||||
return valA.localeCompare(valB) * sortDirection;
|
||||
}
|
||||
// For date (modified), compare as numbers
|
||||
return (valA - valB) * sortDirection;
|
||||
});
|
||||
renderFiles(currentFiles);
|
||||
}
|
||||
|
||||
function renderFiles(files) {
|
||||
if (!files) return;
|
||||
currentFiles = files;
|
||||
const grid = document.getElementById('fileGrid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
if (!files.length) {
|
||||
grid.innerHTML = '<div class="col"><div class="text-muted">No deleted items yet.</div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentView === 'list') {
|
||||
let table = `<table><thead><tr>
|
||||
<th></th>
|
||||
<th>Room</th>
|
||||
<th onclick="sortFiles('name')" style="cursor:pointer;">Name ${(sortColumn==='name') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th onclick="sortFiles('modified')" style="cursor:pointer;">Date Modified ${(sortColumn==='modified') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th onclick="sortFiles('type')" style="cursor:pointer;">Type ${(sortColumn==='type') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th onclick="sortFiles('size')" style="cursor:pointer;">Size ${(sortColumn==='size') ? (sortDirection===1?'▲':'▼') : ''}</th>
|
||||
<th>Deleted By</th>
|
||||
<th>Deleted At</th>
|
||||
<th>Auto-Delete</th>
|
||||
<th class='file-actions'></th>
|
||||
</tr></thead><tbody>`;
|
||||
files.forEach((file, idx) => {
|
||||
let icon = file.type === 'folder'
|
||||
? `<i class='fas fa-folder' style='font-size:1.5rem;color:#16767b;'></i>`
|
||||
: `<i class='fas fa-file-alt' style='font-size:1.5rem;color:#741b5f;'></i>`;
|
||||
let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-';
|
||||
let actionsArr = [];
|
||||
if (file.can_restore) {
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='restoreFile("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-undo'></i></button>`);
|
||||
}
|
||||
if (window.isAdmin) {
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete Permanently' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='deletePermanently("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-trash'></i></button>`);
|
||||
}
|
||||
const actions = actionsArr.join('');
|
||||
|
||||
// Calculate days until auto-deletion
|
||||
const deletedAt = new Date(file.deleted_at);
|
||||
const now = new Date();
|
||||
const daysUntilDeletion = 30 - Math.floor((now - deletedAt) / (1000 * 60 * 60 * 24));
|
||||
let autoDeleteStatus = '';
|
||||
if (daysUntilDeletion <= 0) {
|
||||
autoDeleteStatus = '<span class="badge bg-danger">Deleting soon</span>';
|
||||
} else if (daysUntilDeletion <= 7) {
|
||||
autoDeleteStatus = `<span class="badge bg-warning text-dark">${daysUntilDeletion} days left</span>`;
|
||||
} else {
|
||||
autoDeleteStatus = `<span class="badge bg-info text-dark">${daysUntilDeletion} days left</span>`;
|
||||
}
|
||||
|
||||
table += `<tr>
|
||||
<td class='file-icon'><span style="display:inline-flex;align-items:center;gap:1.2rem;">${icon}</span></td>
|
||||
<td class='room-name'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></td>
|
||||
<td class='file-name' title='${file.name}'>${file.name}</td>
|
||||
<td class='file-date'>${formatDate(file.modified)}</td>
|
||||
<td class='file-type'>${file.type}</td>
|
||||
<td class='file-size'>${size}</td>
|
||||
<td class='deleted-by'>${file.deleted_by || '-'}</td>
|
||||
<td class='deleted-at'>${file.deleted_at ? new Date(file.deleted_at).toLocaleString() : '-'}</td>
|
||||
<td class='auto-delete'>${autoDeleteStatus}</td>
|
||||
<td class='file-actions'>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
table += '</tbody></table>';
|
||||
grid.innerHTML = table;
|
||||
} else {
|
||||
files.forEach((file, idx) => {
|
||||
let icon = file.type === 'folder'
|
||||
? `<i class='fas fa-folder' style='font-size:2.5rem;color:#16767b;'></i>`
|
||||
: `<i class='fas fa-file-alt' style='font-size:2.5rem;color:#741b5f;'></i>`;
|
||||
let size = file.size !== '-' ? (file.size > 0 ? (file.size < 1024*1024 ? (file.size/1024).toFixed(1)+' KB' : (file.size/1024/1024).toFixed(2)+' MB') : '0 KB') : '-';
|
||||
let actionsArr = [];
|
||||
if (file.can_restore) {
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Restore' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='restoreFile("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-undo'></i></button>`);
|
||||
}
|
||||
if (window.isAdmin) {
|
||||
actionsArr.push(`<button class='btn btn-sm file-action-btn' title='Delete Permanently' style='background-color:rgba(239,68,68,0.1);color:#b91c1c;' onclick='deletePermanently("${file.name}", "${file.path}", ${file.room_id})'><i class='fas fa-trash'></i></button>`);
|
||||
}
|
||||
const actions = actionsArr.join('');
|
||||
|
||||
// Calculate days until auto-deletion
|
||||
const deletedAt = new Date(file.deleted_at);
|
||||
const now = new Date();
|
||||
const daysUntilDeletion = 30 - Math.floor((now - deletedAt) / (1000 * 60 * 60 * 24));
|
||||
let autoDeleteStatus = '';
|
||||
if (daysUntilDeletion <= 0) {
|
||||
autoDeleteStatus = '<span class="badge bg-danger">Deleting soon</span>';
|
||||
} else if (daysUntilDeletion <= 7) {
|
||||
autoDeleteStatus = `<span class="badge bg-warning text-dark">${daysUntilDeletion} days left</span>`;
|
||||
} else {
|
||||
autoDeleteStatus = `<span class="badge bg-info text-dark">${daysUntilDeletion} days left</span>`;
|
||||
}
|
||||
|
||||
grid.innerHTML += `
|
||||
<div class='col'>
|
||||
<div class='card file-card h-100 border-0 shadow-sm position-relative'>
|
||||
<div class='card-body d-flex flex-column align-items-center justify-content-center text-center'>
|
||||
<div class='mb-2'>${icon}</div>
|
||||
<div class='fw-semibold file-name-ellipsis' title='${file.name}'>${file.name}</div>
|
||||
<div class='text-muted small'>${formatDate(file.modified)}</div>
|
||||
<div class='text-muted small'>${size}</div>
|
||||
<div class='mt-2'><button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href="/rooms/${file.room_id}"'><i class='fas fa-door-open me-1'></i>${file.room_name || 'Room ' + file.room_id}</button></div>
|
||||
<div class='text-muted small mt-1'>Deleted by: ${file.deleted_by || '-'}</div>
|
||||
<div class='text-muted small'>${file.deleted_at ? new Date(file.deleted_at).toLocaleString() : '-'}</div>
|
||||
<div class='mt-2'>${autoDeleteStatus}</div>
|
||||
</div>
|
||||
<div class='card-footer bg-white border-0 d-flex justify-content-center gap-2'>${actions}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function fetchFiles() {
|
||||
fetch('/api/rooms/trash')
|
||||
.then(r => r.json())
|
||||
.then(files => {
|
||||
if (files) {
|
||||
window.currentFiles = files;
|
||||
// Sort files by auto_delete by default
|
||||
window.currentFiles.sort((a, b) => {
|
||||
const timeA = a.auto_delete ? new Date(a.auto_delete).getTime() : Infinity;
|
||||
const timeB = b.auto_delete ? new Date(b.auto_delete).getTime() : Infinity;
|
||||
return timeA - timeB;
|
||||
});
|
||||
renderFiles(files);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error loading files:', error);
|
||||
document.getElementById('fileGrid').innerHTML = '<div class="col"><div class="text-danger">Failed to load files. Please try refreshing the page.</div></div>';
|
||||
});
|
||||
}
|
||||
|
||||
function restoreFile(filename, path, roomId) {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
fetch(`/api/rooms/${roomId}/restore`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: filename,
|
||||
path: path
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
// Remove the file from the current view since it's been restored
|
||||
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
||||
renderFiles(currentFiles);
|
||||
} else {
|
||||
console.error('Failed to restore file:', res.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error restoring file:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function deletePermanently(filename, path, roomId) {
|
||||
fileToDelete = { filename, path, roomId };
|
||||
const modal = new bootstrap.Modal(document.getElementById('permanentDeleteModal'));
|
||||
document.getElementById('permanentDeleteItemName').textContent = filename;
|
||||
modal.show();
|
||||
}
|
||||
|
||||
document.getElementById('confirmPermanentDelete').addEventListener('click', function() {
|
||||
if (!fileToDelete) return;
|
||||
|
||||
const { filename, path, roomId } = fileToDelete;
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
fetch(`/api/rooms/${roomId}/delete-permanent`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: filename,
|
||||
path: path
|
||||
})
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(res => {
|
||||
if (res.success) {
|
||||
// Remove the file from the current view since it's been permanently deleted
|
||||
currentFiles = currentFiles.filter(f => !(f.name === filename && f.path === path && f.room_id === roomId));
|
||||
renderFiles(currentFiles);
|
||||
// Close the modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('permanentDeleteModal')).hide();
|
||||
} else {
|
||||
console.error('Failed to delete file permanently:', res.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting file permanently:', error);
|
||||
})
|
||||
.finally(() => {
|
||||
fileToDelete = null;
|
||||
});
|
||||
});
|
||||
|
||||
function showDetailsModal(idx) {
|
||||
const item = currentFiles[idx];
|
||||
const icon = item.type === 'folder'
|
||||
? `<i class='fas fa-folder' style='font-size:2.2rem;color:#16767b;'></i>`
|
||||
: `<i class='fas fa-file-alt' style='font-size:2.2rem;color:#741b5f;'></i>`;
|
||||
const uploaderPic = item.uploader_profile_pic
|
||||
? `/uploads/profile_pics/${item.uploader_profile_pic}`
|
||||
: '/static/default-avatar.png';
|
||||
const detailsHtml = `
|
||||
<div class='d-flex align-items-center gap-3 mb-3'>
|
||||
<div>${icon}</div>
|
||||
<div style='min-width:0;'>
|
||||
<div class='fw-bold' style='font-size:1.1rem;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' title='${item.name}'>${item.name}</div>
|
||||
<div class='text-muted small'>${item.type === 'folder' ? 'Folder' : 'File'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='mb-2 d-flex align-items-center gap-2'>
|
||||
<img src='${uploaderPic}' alt='Profile Picture' class='rounded-circle border' style='width:28px;height:28px;object-fit:cover;'>
|
||||
<span class='fw-semibold' style='font-size:0.98rem;'>${item.uploaded_by || '-'}</span>
|
||||
</div>
|
||||
<div class='mb-2 text-muted' style='font-size:0.92rem;'><i class='far fa-clock me-1'></i>${formatDate(item.modified)}</div>
|
||||
<hr style='margin:0.7rem 0 0.5rem 0; border-color:#e6f3f4;'>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Room:</strong> <button class='btn btn-sm' style='background-color:rgba(22,118,123,0.08);color:#16767b;' onclick='window.location.href=\"/room/${item.room_id}\"'><i class='fas fa-door-open me-1'></i>${item.room_name || 'Room ' + item.room_id}</button></div>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Path:</strong> <span style='word-break:break-all;'>${(item.path ? item.path + '/' : '') + item.name}</span></div>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Size:</strong> ${item.size === '-' ? '-' : (item.size > 0 ? (item.size < 1024*1024 ? (item.size/1024).toFixed(1)+' KB' : (item.size/1024/1024).toFixed(2)+' MB') : '0 KB')}</div>
|
||||
<div style='font-size:0.91rem;color:#555;'><strong style='color:#16767b;'>Uploaded at:</strong> ${item.uploaded_at ? new Date(item.uploaded_at).toLocaleString() : '-'}</div>
|
||||
`;
|
||||
document.getElementById('detailsModalBody').innerHTML = detailsHtml;
|
||||
var modal = new bootstrap.Modal(document.getElementById('detailsModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
// Live search
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeView();
|
||||
|
||||
const quickSearchInput = document.getElementById('quickSearchInput');
|
||||
const clearSearchBtn = document.getElementById('clearSearchBtn');
|
||||
let searchTimeout = null;
|
||||
|
||||
quickSearchInput.addEventListener('input', function() {
|
||||
const query = quickSearchInput.value.trim().toLowerCase();
|
||||
clearSearchBtn.style.display = query.length > 0 ? 'block' : 'none';
|
||||
|
||||
if (searchTimeout) clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (query.length === 0) {
|
||||
fetchFiles();
|
||||
return;
|
||||
}
|
||||
|
||||
const filteredFiles = currentFiles.filter(file =>
|
||||
file.name.toLowerCase().includes(query)
|
||||
);
|
||||
renderFiles(filteredFiles);
|
||||
}, 200);
|
||||
});
|
||||
|
||||
clearSearchBtn.addEventListener('click', function() {
|
||||
quickSearchInput.value = '';
|
||||
clearSearchBtn.style.display = 'none';
|
||||
fetchFiles();
|
||||
});
|
||||
});
|
||||
|
||||
function showEmptyTrashModal() {
|
||||
const modal = new bootstrap.Modal(document.getElementById('emptyTrashModal'));
|
||||
modal.show();
|
||||
}
|
||||
|
||||
document.getElementById('confirmEmptyTrash').addEventListener('click', function() {
|
||||
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
||||
|
||||
// Get all unique room IDs from current files
|
||||
const roomIds = [...new Set(currentFiles.map(file => file.room_id))];
|
||||
|
||||
// Create an array of promises for emptying trash in each room
|
||||
const emptyPromises = roomIds.map(roomId =>
|
||||
fetch(`/api/rooms/${roomId}/delete-permanent`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: '*', // Special value to indicate all files
|
||||
path: '' // Empty path to indicate root
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
// Execute all promises
|
||||
Promise.all(emptyPromises)
|
||||
.then(responses => {
|
||||
// Check if all responses were successful
|
||||
const allSuccessful = responses.every(response => response.ok);
|
||||
if (allSuccessful) {
|
||||
// Clear the current files array and re-render
|
||||
currentFiles = [];
|
||||
renderFiles(currentFiles);
|
||||
// Close the modal
|
||||
bootstrap.Modal.getInstance(document.getElementById('emptyTrashModal')).hide();
|
||||
// Refresh the files list to ensure we have the latest state
|
||||
fetchFiles();
|
||||
} else {
|
||||
console.error('Failed to empty trash in some rooms');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error emptying trash:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.file-name-ellipsis {
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
@media (min-width: 992px) {
|
||||
.file-name-ellipsis { max-width: 180px; }
|
||||
}
|
||||
.file-action-btn {
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.card:hover > .card-footer .file-action-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.card-footer.bg-white.border-0.d-flex.justify-content-center.gap-2 {
|
||||
min-height: 40px;
|
||||
}
|
||||
.card.file-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
.card.file-card .file-action-btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
#fileGrid.table-mode {
|
||||
padding: 0;
|
||||
}
|
||||
#fileGrid.table-mode table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: #fff;
|
||||
}
|
||||
#fileGrid.table-mode th, #fileGrid.table-mode td {
|
||||
padding: 0.5rem 1rem;
|
||||
border-bottom: 1px solid #e9ecef;
|
||||
text-align: left;
|
||||
font-size: 0.95rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
#fileGrid.table-mode th {
|
||||
background: #f8f9fa;
|
||||
color: #6c757d;
|
||||
font-weight: 500;
|
||||
}
|
||||
#fileGrid.table-mode tr:hover td {
|
||||
background-color: rgba(22, 118, 123, 0.08);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
#fileGrid.table-mode .file-icon {
|
||||
width: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
#fileGrid.table-mode .file-actions {
|
||||
min-width: 90px;
|
||||
text-align: right;
|
||||
}
|
||||
#fileGrid.table-mode .file-action-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
font-size: 0.875rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
/* Disable text selection for file grid and table rows/cards */
|
||||
#fileGrid, #fileGrid * {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
#fileGrid .card.file-card {
|
||||
cursor: pointer;
|
||||
}
|
||||
#fileGrid.table-mode tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user