botui/ui/suite/people/people.html
Rodrigo Rodriguez (Pragmatismo) 80c91f6304 Redesign home page with beautiful layout, add People/Contacts, rename Tools to Compliance
- Complete home page redesign with large icons, full descriptions, recent documents
- Add People (Contacts) menu item and page with contacts management
- Move Paper right after Chat in menu order
- Rename Tools to Compliance with shield icon
- Settings moved to end of menu
- Logo click now shows home page
- Add Project, Canvas, Goals, Player, Workspace, Video, Learn to menu
- New CSS for home page with modern card layout
2026-01-09 20:56:59 -03:00

527 lines
22 KiB
HTML

<link rel="stylesheet" href="people/people.css" />
<div class="people-container">
<!-- Header -->
<header class="people-header">
<div class="header-left">
<h1 data-i18n="people-title">People</h1>
<p class="header-subtitle" data-i18n="people-subtitle">
Contacts, Groups & Directory
</p>
</div>
<div class="header-actions">
<div class="search-box">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="text"
placeholder="Search contacts..."
data-i18n-placeholder="people-search"
id="people-search"
/>
</div>
<button class="btn btn-primary" onclick="openAddContact()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="8.5" cy="7" r="4"/>
<line x1="20" y1="8" x2="20" y2="14"/>
<line x1="23" y1="11" x2="17" y2="11"/>
</svg>
<span data-i18n="people-add">Add Contact</span>
</button>
</div>
</header>
<!-- Tab Navigation -->
<nav class="tab-nav" role="tablist">
<button class="tab-btn active" role="tab" aria-selected="true" onclick="showTab('contacts', this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
</svg>
<span data-i18n="people-tab-contacts">Contacts</span>
</button>
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('groups', this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<span data-i18n="people-tab-groups">Groups</span>
</button>
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('directory', this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>
</svg>
<span data-i18n="people-tab-directory">Directory</span>
</button>
<button class="tab-btn" role="tab" aria-selected="false" onclick="showTab('recent', this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
</svg>
<span data-i18n="people-tab-recent">Recent</span>
</button>
</nav>
<!-- Main Content -->
<div class="people-content">
<!-- Contacts Tab -->
<div id="contacts-tab" class="tab-content active">
<!-- Alphabet Filter -->
<div class="alphabet-filter">
<button class="alpha-btn active" onclick="filterByLetter('all', this)">All</button>
<button class="alpha-btn" onclick="filterByLetter('A', this)">A</button>
<button class="alpha-btn" onclick="filterByLetter('B', this)">B</button>
<button class="alpha-btn" onclick="filterByLetter('C', this)">C</button>
<button class="alpha-btn" onclick="filterByLetter('D', this)">D</button>
<button class="alpha-btn" onclick="filterByLetter('E', this)">E</button>
<button class="alpha-btn" onclick="filterByLetter('F', this)">F</button>
<button class="alpha-btn" onclick="filterByLetter('G', this)">G</button>
<button class="alpha-btn" onclick="filterByLetter('H', this)">H</button>
<button class="alpha-btn" onclick="filterByLetter('I', this)">I</button>
<button class="alpha-btn" onclick="filterByLetter('J', this)">J</button>
<button class="alpha-btn" onclick="filterByLetter('K', this)">K</button>
<button class="alpha-btn" onclick="filterByLetter('L', this)">L</button>
<button class="alpha-btn" onclick="filterByLetter('M', this)">M</button>
<button class="alpha-btn" onclick="filterByLetter('N', this)">N</button>
<button class="alpha-btn" onclick="filterByLetter('O', this)">O</button>
<button class="alpha-btn" onclick="filterByLetter('P', this)">P</button>
<button class="alpha-btn" onclick="filterByLetter('Q', this)">Q</button>
<button class="alpha-btn" onclick="filterByLetter('R', this)">R</button>
<button class="alpha-btn" onclick="filterByLetter('S', this)">S</button>
<button class="alpha-btn" onclick="filterByLetter('T', this)">T</button>
<button class="alpha-btn" onclick="filterByLetter('U', this)">U</button>
<button class="alpha-btn" onclick="filterByLetter('V', this)">V</button>
<button class="alpha-btn" onclick="filterByLetter('W', this)">W</button>
<button class="alpha-btn" onclick="filterByLetter('X', this)">X</button>
<button class="alpha-btn" onclick="filterByLetter('Y', this)">Y</button>
<button class="alpha-btn" onclick="filterByLetter('Z', this)">Z</button>
</div>
<!-- Contacts List -->
<div class="contacts-list" id="contacts-list">
<!-- Contacts will be loaded here -->
<div class="loading-state">
<div class="spinner"></div>
<p data-i18n="people-loading">Loading contacts...</p>
</div>
</div>
</div>
<!-- Groups Tab -->
<div id="groups-tab" class="tab-content">
<div class="groups-grid" id="groups-list">
<!-- Groups will be loaded here -->
</div>
</div>
<!-- Directory Tab -->
<div id="directory-tab" class="tab-content">
<div class="directory-tree" id="directory-tree">
<!-- Organization directory will be loaded here -->
</div>
</div>
<!-- Recent Tab -->
<div id="recent-tab" class="tab-content">
<div class="recent-list" id="recent-list">
<!-- Recent contacts will be loaded here -->
</div>
</div>
</div>
<!-- Contact Detail Panel (slides in from right) -->
<div class="contact-panel" id="contact-panel">
<div class="panel-header">
<button class="close-btn" onclick="closeContactPanel()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
<div class="panel-actions">
<button class="icon-btn" title="Edit" onclick="editContact()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
</button>
<button class="icon-btn" title="Delete" onclick="deleteContact()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</div>
<div class="panel-content" id="contact-detail">
<!-- Contact details will be loaded here -->
</div>
</div>
</div>
<!-- Add/Edit Contact Modal -->
<dialog id="contact-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title" data-i18n="people-add-contact">Add Contact</h2>
<button class="close-btn" onclick="closeModal()">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<form id="contact-form" onsubmit="saveContact(event)">
<div class="form-row">
<div class="form-group">
<label data-i18n="people-first-name">First Name</label>
<input type="text" name="firstName" required />
</div>
<div class="form-group">
<label data-i18n="people-last-name">Last Name</label>
<input type="text" name="lastName" required />
</div>
</div>
<div class="form-group">
<label data-i18n="people-email">Email</label>
<input type="email" name="email" />
</div>
<div class="form-group">
<label data-i18n="people-phone">Phone</label>
<input type="tel" name="phone" />
</div>
<div class="form-group">
<label data-i18n="people-company">Company</label>
<input type="text" name="company" />
</div>
<div class="form-group">
<label data-i18n="people-title">Title</label>
<input type="text" name="title" />
</div>
<div class="form-group">
<label data-i18n="people-notes">Notes</label>
<textarea name="notes" rows="3"></textarea>
</div>
<div class="form-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()" data-i18n="cancel">Cancel</button>
<button type="submit" class="btn btn-primary" data-i18n="save">Save</button>
</div>
</form>
</div>
</dialog>
<script>
let currentContact = null;
let contacts = [];
document.addEventListener('DOMContentLoaded', () => {
loadContacts();
});
async function loadContacts() {
try {
const response = await fetch('/api/contacts');
if (response.ok) {
contacts = await response.json();
renderContacts(contacts);
} else {
renderEmptyState();
}
} catch (error) {
console.error('Failed to load contacts:', error);
renderEmptyState();
}
}
function renderContacts(contactsList) {
const container = document.getElementById('contacts-list');
if (!contactsList || contactsList.length === 0) {
renderEmptyState();
return;
}
const grouped = groupByLetter(contactsList);
let html = '';
for (const [letter, group] of Object.entries(grouped)) {
html += `<div class="contact-group" data-letter="${letter}">
<div class="group-header">${letter}</div>
<div class="group-contacts">`;
for (const contact of group) {
html += renderContactCard(contact);
}
html += '</div></div>';
}
container.innerHTML = html;
}
function renderContactCard(contact) {
const initials = getInitials(contact.firstName, contact.lastName);
const name = `${contact.firstName} ${contact.lastName}`;
return `<div class="contact-card" onclick="showContact('${contact.id}')">
<div class="contact-avatar" style="background: ${getAvatarColor(name)}">${initials}</div>
<div class="contact-info">
<div class="contact-name">${name}</div>
<div class="contact-detail">${contact.email || contact.phone || ''}</div>
</div>
<div class="contact-actions">
<button class="icon-btn small" onclick="event.stopPropagation(); startChat('${contact.id}')" title="Chat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
</button>
<button class="icon-btn small" onclick="event.stopPropagation(); sendEmail('${contact.email}')" title="Email">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
</button>
</div>
</div>`;
}
function renderEmptyState() {
document.getElementById('contacts-list').innerHTML = `
<div class="empty-state">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<h3 data-i18n="people-empty-title">No contacts yet</h3>
<p data-i18n="people-empty-desc">Add your first contact to get started</p>
<button class="btn btn-primary" onclick="openAddContact()">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"/>
<line x1="5" y1="12" x2="19" y2="12"/>
</svg>
<span data-i18n="people-add">Add Contact</span>
</button>
</div>
`;
}
function groupByLetter(contactsList) {
const grouped = {};
for (const contact of contactsList) {
const letter = (contact.lastName || contact.firstName || '#').charAt(0).toUpperCase();
if (!grouped[letter]) grouped[letter] = [];
grouped[letter].push(contact);
}
return Object.fromEntries(Object.entries(grouped).sort());
}
function getInitials(firstName, lastName) {
return ((firstName?.charAt(0) || '') + (lastName?.charAt(0) || '')).toUpperCase() || '?';
}
function getAvatarColor(name) {
const colors = ['#6366f1', '#8b5cf6', '#ec4899', '#ef4444', '#f97316', '#eab308', '#22c55e', '#14b8a6', '#06b6d4', '#3b82f6'];
let hash = 0;
for (let i = 0; i < name.length; i++) {
hash = name.charCodeAt(i) + ((hash << 5) - hash);
}
return colors[Math.abs(hash) % colors.length];
}
function showTab(tabId, btn) {
document.querySelectorAll('.tab-content').forEach(tab => tab.classList.remove('active'));
document.querySelectorAll('.tab-btn').forEach(b => {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
document.getElementById(tabId + '-tab').classList.add('active');
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
}
function filterByLetter(letter, btn) {
document.querySelectorAll('.alpha-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.contact-group').forEach(group => {
if (letter === 'all' || group.dataset.letter === letter) {
group.style.display = '';
} else {
group.style.display = 'none';
}
});
}
function showContact(id) {
currentContact = contacts.find(c => c.id === id);
if (!currentContact) return;
const panel = document.getElementById('contact-panel');
const detail = document.getElementById('contact-detail');
detail.innerHTML = `
<div class="contact-header">
<div class="contact-avatar large" style="background: ${getAvatarColor(currentContact.firstName + ' ' + currentContact.lastName)}">
${getInitials(currentContact.firstName, currentContact.lastName)}
</div>
<h2>${currentContact.firstName} ${currentContact.lastName}</h2>
${currentContact.title ? `<p class="contact-title">${currentContact.title}</p>` : ''}
${currentContact.company ? `<p class="contact-company">${currentContact.company}</p>` : ''}
</div>
<div class="contact-fields">
${currentContact.email ? `
<div class="field">
<label>Email</label>
<a href="mailto:${currentContact.email}">${currentContact.email}</a>
</div>
` : ''}
${currentContact.phone ? `
<div class="field">
<label>Phone</label>
<a href="tel:${currentContact.phone}">${currentContact.phone}</a>
</div>
` : ''}
${currentContact.notes ? `
<div class="field">
<label>Notes</label>
<p>${currentContact.notes}</p>
</div>
` : ''}
</div>
<div class="contact-quick-actions">
<button class="action-btn" onclick="startChat('${currentContact.id}')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
</svg>
Chat
</button>
<button class="action-btn" onclick="sendEmail('${currentContact.email}')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
<polyline points="22,6 12,13 2,6"/>
</svg>
Email
</button>
<button class="action-btn" onclick="scheduleMeeting('${currentContact.id}')">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
Meeting
</button>
</div>
`;
panel.classList.add('open');
}
function closeContactPanel() {
document.getElementById('contact-panel').classList.remove('open');
currentContact = null;
}
function openAddContact() {
currentContact = null;
document.getElementById('modal-title').textContent = 'Add Contact';
document.getElementById('contact-form').reset();
document.getElementById('contact-modal').showModal();
}
function editContact() {
if (!currentContact) return;
document.getElementById('modal-title').textContent = 'Edit Contact';
const form = document.getElementById('contact-form');
form.firstName.value = currentContact.firstName || '';
form.lastName.value = currentContact.lastName || '';
form.email.value = currentContact.email || '';
form.phone.value = currentContact.phone || '';
form.company.value = currentContact.company || '';
form.title.value = currentContact.title || '';
form.notes.value = currentContact.notes || '';
document.getElementById('contact-modal').showModal();
}
function closeModal() {
document.getElementById('contact-modal').close();
}
async function saveContact(event) {
event.preventDefault();
const form = event.target;
const data = {
firstName: form.firstName.value,
lastName: form.lastName.value,
email: form.email.value,
phone: form.phone.value,
company: form.company.value,
title: form.title.value,
notes: form.notes.value
};
try {
const url = currentContact ? `/api/contacts/${currentContact.id}` : '/api/contacts';
const method = currentContact ? 'PUT' : 'POST';
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.ok) {
closeModal();
loadContacts();
if (currentContact) closeContactPanel();
}
} catch (error) {
console.error('Failed to save contact:', error);
}
}
async function deleteContact() {
if (!currentContact || !confirm('Delete this contact?')) return;
try {
const response = await fetch(`/api/contacts/${currentContact.id}`, { method: 'DELETE' });
if (response.ok) {
closeContactPanel();
loadContacts();
}
} catch (error) {
console.error('Failed to delete contact:', error);
}
}
function startChat(contactId) {
window.location.href = `/#chat?contact=${contactId}`;
}
function sendEmail(email) {
if (email) window.location.href = `mailto:${email}`;
}
function scheduleMeeting(contactId) {
window.location.href = `/#calendar?new=meeting&contact=${contactId}`;
}
// Search functionality
document.getElementById('people-search')?.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = contacts.filter(c =>
(c.firstName + ' ' + c.lastName).toLowerCase().includes(query) ||
(c.email || '').toLowerCase().includes(query) ||
(c.company || '').toLowerCase().includes(query)
);
renderContacts(filtered);
});
</script>