Tag progetto, @menzioni, appuntamenti da conversazioni
- Modello Tag con nome e colore, M2M su Conversazione - Modello Appuntamento con luogo, note, partecipanti, link a Conversazione - @menzioni nei commenti e aggiornamenti: @username → link al profilo - Autocomplete JS per @menzioni nelle textarea - Auto-data conversazioni (default=now) - CRUD completo appuntamenti con permessi autore - Appuntamenti in agenda, dashboard, dettaglio conversazione - Crea riunione direttamente da una conversazione (pre-compila titolo e partecipanti) - Admin: Tag, Appuntamento registrati
This commit is contained in:
@@ -327,3 +327,74 @@ textarea.form-control:focus, input.form-control:focus { border-color: var(--acce
|
||||
.card:hover .dropdown .btn-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* ── @Mentions ── */
|
||||
.mention-link {
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
background: var(--accent-soft);
|
||||
padding: .1em .35em;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.mention-link:hover {
|
||||
text-decoration: underline;
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
.mention-dropdown {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-bottom: 4px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,.15);
|
||||
z-index: 1050;
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
display: none;
|
||||
}
|
||||
.mention-dropdown.show { display: block; }
|
||||
.mention-item {
|
||||
padding: .5rem .75rem;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
font-size: .85rem;
|
||||
transition: background .1s;
|
||||
}
|
||||
.mention-item:hover,
|
||||
.mention-item.active {
|
||||
background: var(--accent-soft);
|
||||
}
|
||||
.mention-item small {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* ── Tag pills ── */
|
||||
.tag-pill {
|
||||
font-size: .68rem;
|
||||
font-weight: 600;
|
||||
padding: .2em .55em;
|
||||
border-radius: 20px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .25rem;
|
||||
border: 1.5px solid;
|
||||
}
|
||||
.tag-dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ── Appuntamento ── */
|
||||
.agenda-icon-appuntamento {
|
||||
background: #fef3c7; color: #d97706;
|
||||
}
|
||||
.appuntamento-card {
|
||||
border-left: 3px solid #d97706 !important;
|
||||
}
|
||||
|
||||
@@ -148,6 +148,102 @@ function initApp() {
|
||||
convertMessagesToToasts();
|
||||
initSliders();
|
||||
initLiveSearch();
|
||||
initMentions();
|
||||
}
|
||||
|
||||
// ── @Mentions autocomplete ──
|
||||
function initMentions() {
|
||||
document.querySelectorAll('textarea.form-control').forEach(textarea => {
|
||||
if (textarea._mentionInit) return;
|
||||
textarea._mentionInit = true;
|
||||
|
||||
let dropdown = textarea.parentElement.querySelector('.mention-dropdown');
|
||||
if (!dropdown) {
|
||||
dropdown = document.createElement('div');
|
||||
dropdown.className = 'mention-dropdown';
|
||||
textarea.parentElement.style.position = 'relative';
|
||||
textarea.parentElement.appendChild(dropdown);
|
||||
}
|
||||
|
||||
let mentionStart = -1;
|
||||
let timer = null;
|
||||
|
||||
textarea.addEventListener('input', () => {
|
||||
const pos = textarea.selectionStart;
|
||||
const text = textarea.value.substring(0, pos);
|
||||
const atMatch = text.match(/@(\w[\w.]*)$/);
|
||||
|
||||
if (!atMatch) {
|
||||
dropdown.classList.remove('show');
|
||||
mentionStart = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
mentionStart = pos - atMatch[0].length;
|
||||
const query = atMatch[1];
|
||||
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
fetch('/api/utenti/?q=' + encodeURIComponent(query))
|
||||
.then(r => r.json())
|
||||
.then(users => {
|
||||
if (!users.length) {
|
||||
dropdown.classList.remove('show');
|
||||
return;
|
||||
}
|
||||
dropdown.innerHTML = users.map(u =>
|
||||
`<div class="mention-item" data-username="${u.username}" data-nome="${u.nome}">
|
||||
<span class="avatar" style="width:22px;height:22px;font-size:.6rem;">${u.username.substring(0,2).toUpperCase()}</span>
|
||||
<span>${u.nome}</span>
|
||||
<small class="text-muted">@${u.username}</small>
|
||||
</div>`
|
||||
).join('');
|
||||
dropdown.classList.add('show');
|
||||
});
|
||||
}, 200);
|
||||
});
|
||||
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
const item = e.target.closest('.mention-item');
|
||||
if (!item) return;
|
||||
const username = item.dataset.username;
|
||||
const before = textarea.value.substring(0, mentionStart);
|
||||
const after = textarea.value.substring(textarea.selectionStart);
|
||||
textarea.value = before + '@' + username + ' ' + after;
|
||||
textarea.focus();
|
||||
const newPos = mentionStart + username.length + 2;
|
||||
textarea.setSelectionRange(newPos, newPos);
|
||||
dropdown.classList.remove('show');
|
||||
});
|
||||
|
||||
textarea.addEventListener('blur', () => {
|
||||
setTimeout(() => dropdown.classList.remove('show'), 200);
|
||||
});
|
||||
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
if (!dropdown.classList.contains('show')) return;
|
||||
const items = dropdown.querySelectorAll('.mention-item');
|
||||
const active = dropdown.querySelector('.mention-item.active');
|
||||
let idx = Array.from(items).indexOf(active);
|
||||
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
if (active) active.classList.remove('active');
|
||||
idx = (idx + 1) % items.length;
|
||||
items[idx].classList.add('active');
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
if (active) active.classList.remove('active');
|
||||
idx = idx <= 0 ? items.length - 1 : idx - 1;
|
||||
items[idx].classList.add('active');
|
||||
} else if (e.key === 'Enter' && active) {
|
||||
e.preventDefault();
|
||||
active.click();
|
||||
} else if (e.key === 'Escape') {
|
||||
dropdown.classList.remove('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run on initial page load
|
||||
|
||||
Reference in New Issue
Block a user