245 lines
15 KiB
HTML
245 lines
15 KiB
HTML
{% extends "common/base.html" %}
|
|
{% from "components/header.html" import header %}
|
|
|
|
{% block head %}
|
|
{{ super() }}
|
|
<style>
|
|
body {
|
|
background: #f7f9fb;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
{{ header(
|
|
title="Contacts",
|
|
description="Manage your contacts and their information",
|
|
button_text="Add New Contact",
|
|
button_url=url_for('contacts.new_contact'),
|
|
icon="fa-address-book",
|
|
button_icon="fa-plus"
|
|
) }}
|
|
|
|
<div class="container mx-auto px-4 py-8">
|
|
<!-- 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: var(--primary-color); border: 1px solid var(--primary-color);"
|
|
onmouseover="this.style.backgroundColor='var(--primary-light)'"
|
|
onmouseout="this.style.backgroundColor='var(--primary-color)'">
|
|
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: var(--primary-opacity-8); color: var(--primary-color);">
|
|
<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: var(--primary-opacity-8); color: var(--primary-color);">
|
|
<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: var(--primary-opacity-8); color: var(--primary-color);">
|
|
<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 %} |