Add share note functionality with multiselect field support and adjust layout spacing
- Added share note input field with comma-separated name parsing - Integrated shared_with and shared boolean fields to note payload - Reduced form spacing (space-y-4 to space-y-2.5) to accommodate new field - Adjusted container positioning: noteContainer translateY(-33px), notesListContainer translateY(-24px) - Share functionality now working with PocketBase multiselect field validation - Note cards will display green background when shared=true
This commit is contained in:
+50
-95
@@ -369,7 +369,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notes List Container -->
|
<!-- Notes List Container -->
|
||||||
<div id="notesListContainer" class="hidden flex flex-col bg-purple-100 rounded-2xl shadow-2xl border border-purple-200 max-w-5xl w-full p-6 gap-4 max-h-[calc(100vh-136px)] overflow-hidden mt-6 mb-3">
|
<div id="notesListContainer" class="hidden flex flex-col bg-purple-100 rounded-2xl shadow-2xl border border-purple-200 max-w-5xl w-full p-6 gap-4 max-h-[calc(100vh-136px)] overflow-hidden mb-3" style="transform: translateY(-24px);">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<div class="flex flex-row items-center justify-between gap-3">
|
<div class="flex flex-row items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -439,12 +439,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note + Audio Container -->
|
<!-- Note + Audio Container -->
|
||||||
<div id="noteContainer" class="hidden self-center bg-white rounded-2xl shadow-2xl max-w-xl w-full py-[8px] px-8 space-y-6 mt-4">
|
<div id="noteContainer" class="hidden self-center bg-white rounded-2xl shadow-2xl max-w-xl w-full py-[8px] px-8 space-y-2 mt-4" style="transform: translateY(-33px);">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
<button id="backToListBtn" class="px-2 sm:px-3 py-1 sm:py-2 text-xs sm:text-sm rounded border border-gray-200 text-gray-700 hover:bg-gray-100">Back to Notes</button>
|
<button id="backToListBtn" class="px-2 sm:px-3 py-1 sm:py-2 text-xs sm:text-sm rounded border border-gray-200 text-gray-700 hover:bg-gray-100">Back to Notes</button>
|
||||||
<h2 class="text-lg sm:text-2xl font-bold text-gray-800">New Note</h2>
|
<h2 class="text-lg sm:text-2xl font-bold text-gray-800">New Note</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-2.5">
|
||||||
<div>
|
<div>
|
||||||
<label for="noteTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
<label for="noteTitle" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -478,12 +478,9 @@
|
|||||||
<input type="text" id="jobNumber" class="w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Enter job number (required)">
|
<input type="text" id="jobNumber" class="w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Enter job number (required)">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Share Note</label>
|
<label for="shareNoteInput" class="block text-sm font-medium text-gray-700 mb-2">Share Note (comma-separated names)</label>
|
||||||
<div class="flex gap-2 mb-3">
|
<input type="text" id="shareNoteInput" class="w-full rounded-lg border border-gray-300 p-3 focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Enter names separated by commas (e.g., John, Sarah, Mike)">
|
||||||
<input type="text" id="userSearchInput" class="flex-1 rounded-lg border border-gray-300 p-2 text-sm focus:outline-none focus:ring-2 focus:ring-primary" placeholder="Search users by name...">
|
<div id="selectedUsersContainer" class="flex flex-wrap gap-2 mt-3">
|
||||||
<div id="userSuggestions" class="hidden absolute top-full left-0 right-0 mt-1 bg-white border border-gray-300 rounded-lg shadow-lg max-h-48 overflow-y-auto z-20"></div>
|
|
||||||
</div>
|
|
||||||
<div id="selectedUsersContainer" class="flex flex-wrap gap-2 mb-3">
|
|
||||||
<!-- Selected user capsules will appear here -->
|
<!-- Selected user capsules will appear here -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1811,7 +1808,7 @@
|
|||||||
document.getElementById('noteType').value = 'personal';
|
document.getElementById('noteType').value = 'personal';
|
||||||
document.getElementById('jobNumber').value = '';
|
document.getElementById('jobNumber').value = '';
|
||||||
document.getElementById('jobNumberContainer').classList.add('hidden');
|
document.getElementById('jobNumberContainer').classList.add('hidden');
|
||||||
document.getElementById('userSearchInput').value = '';
|
document.getElementById('shareNoteInput').value = '';
|
||||||
selectedUsers = [];
|
selectedUsers = [];
|
||||||
renderSelectedUsers();
|
renderSelectedUsers();
|
||||||
clearAudio();
|
clearAudio();
|
||||||
@@ -1886,24 +1883,30 @@
|
|||||||
.map(iconBtnHtml)
|
.map(iconBtnHtml)
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
// Create type indicator badge with matching card background colors
|
// Only create type indicator badge for shared notes
|
||||||
const typeColorMap = {
|
let typeBadge = '';
|
||||||
personal: { bg: 'bg-amber-200', border: 'border-amber-400', text: 'text-amber-900' },
|
if (note.shared) {
|
||||||
manager: { bg: 'bg-orange-300', border: 'border-orange-500', text: 'text-orange-900' },
|
const typeColorMap = {
|
||||||
job: { bg: 'bg-pink-300', border: 'border-pink-500', text: 'text-pink-900' },
|
personal: { bg: 'bg-amber-200', border: 'border-amber-400', text: 'text-amber-900' },
|
||||||
};
|
manager: { bg: 'bg-orange-300', border: 'border-orange-500', text: 'text-orange-900' },
|
||||||
const typeColor = typeColorMap[type] || { bg: 'bg-gray-200', border: 'border-gray-400', text: 'text-gray-900' };
|
job: { bg: 'bg-pink-300', border: 'border-pink-500', text: 'text-pink-900' },
|
||||||
const typeBadge = `<span class="inline-block px-2 py-1 rounded-full text-xs font-semibold ${typeColor.bg} ${typeColor.border} border ${typeColor.text}">${escapeHtml(type)}</span>`;
|
};
|
||||||
|
const typeColor = typeColorMap[type] || { bg: 'bg-gray-200', border: 'border-gray-400', text: 'text-gray-900' };
|
||||||
|
typeBadge = `<span class="inline-block px-2 py-1 rounded-full text-xs font-semibold ${typeColor.bg} ${typeColor.border} border ${typeColor.text}">${escapeHtml(type)}</span>`;
|
||||||
|
}
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<div class="font-semibold text-gray-900 truncate">${escapeHtml(title)}</div>
|
<div class="font-semibold text-gray-900 truncate">${escapeHtml(title)}</div>
|
||||||
<div class="text-xs text-gray-500">${formatDateShort(note.created)}</div>
|
<div class="text-xs text-gray-500">${formatDateShort(note.created)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-gray-700 line-clamp-2 flex-1">${escapeHtml(snippet || '(empty)')}</div>
|
<div class="text-sm text-gray-700 line-clamp-3 flex-1">${escapeHtml(snippet || '(empty)')}</div>
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between text-xs text-gray-600">
|
||||||
<div class="flex items-center gap-1">${attachmentIcons}</div>
|
<span>${escapeHtml(meta)}</span>
|
||||||
${typeBadge}
|
<div class="flex items-center gap-1">
|
||||||
|
${attachmentIcons}
|
||||||
|
${typeBadge}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
frag.appendChild(card);
|
frag.appendChild(card);
|
||||||
@@ -2541,97 +2544,45 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- Share Note Logic ---
|
// --- Share Note Logic ---
|
||||||
let selectedUsers = []; // { id, displayName, firstName }
|
let selectedUsers = []; // { name }
|
||||||
const userSearchInput = document.getElementById('userSearchInput');
|
const shareNoteInput = document.getElementById('shareNoteInput');
|
||||||
const userSuggestions = document.getElementById('userSuggestions');
|
|
||||||
const selectedUsersContainer = document.getElementById('selectedUsersContainer');
|
const selectedUsersContainer = document.getElementById('selectedUsersContainer');
|
||||||
|
|
||||||
userSearchInput.addEventListener('input', async (e) => {
|
shareNoteInput.addEventListener('input', (e) => {
|
||||||
const query = e.target.value.trim().toLowerCase();
|
const input = e.target.value;
|
||||||
userSuggestions.innerHTML = '';
|
const names = input.split(',')
|
||||||
|
.map(name => name.trim())
|
||||||
|
.filter(name => name.length > 0);
|
||||||
|
|
||||||
if (!query) {
|
selectedUsers = names.map(name => ({
|
||||||
userSuggestions.classList.add('hidden');
|
name: name,
|
||||||
return;
|
firstName: name.split(' ')[0]
|
||||||
}
|
}));
|
||||||
|
|
||||||
try {
|
renderSelectedUsers();
|
||||||
const allUsers = await pb.collection('users').getFullList({
|
|
||||||
sort: 'displayName'
|
|
||||||
});
|
|
||||||
|
|
||||||
const matching = allUsers.filter(u =>
|
|
||||||
u.displayName?.toLowerCase().includes(query) ||
|
|
||||||
u.firstName?.toLowerCase().includes(query) ||
|
|
||||||
u.email?.toLowerCase().includes(query)
|
|
||||||
).filter(u => !selectedUsers.find(su => su.id === u.id));
|
|
||||||
|
|
||||||
if (matching.length === 0) {
|
|
||||||
userSuggestions.innerHTML = '<div class="px-4 py-2 text-sm text-gray-500">No users found</div>';
|
|
||||||
} else {
|
|
||||||
matching.forEach(user => {
|
|
||||||
const option = document.createElement('button');
|
|
||||||
option.type = 'button';
|
|
||||||
option.className = 'w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition';
|
|
||||||
option.innerHTML = `<div class="font-medium">${escapeHtml(user.displayName || user.email)}</div><div class="text-xs text-gray-500">${escapeHtml(user.email)}</div>`;
|
|
||||||
option.addEventListener('click', (ev) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
selectUser(user);
|
|
||||||
userSearchInput.value = '';
|
|
||||||
userSuggestions.classList.add('hidden');
|
|
||||||
});
|
|
||||||
userSuggestions.appendChild(option);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
userSuggestions.classList.remove('hidden');
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load users:', err);
|
|
||||||
userSuggestions.innerHTML = '<div class="px-4 py-2 text-sm text-red-500">Failed to load users</div>';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function selectUser(user) {
|
|
||||||
if (selectedUsers.find(u => u.id === user.id)) return;
|
|
||||||
|
|
||||||
selectedUsers.push({
|
|
||||||
id: user.id,
|
|
||||||
displayName: user.displayName || user.email,
|
|
||||||
firstName: user.firstName || (user.displayName?.split(' ')[0] || 'User')
|
|
||||||
});
|
|
||||||
|
|
||||||
renderSelectedUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeUser(userId) {
|
|
||||||
selectedUsers = selectedUsers.filter(u => u.id !== userId);
|
|
||||||
renderSelectedUsers();
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSelectedUsers() {
|
function renderSelectedUsers() {
|
||||||
selectedUsersContainer.innerHTML = '';
|
selectedUsersContainer.innerHTML = '';
|
||||||
selectedUsers.forEach(user => {
|
selectedUsers.forEach((user, index) => {
|
||||||
const capsule = document.createElement('div');
|
const capsule = document.createElement('div');
|
||||||
capsule.className = 'inline-flex items-center gap-2 px-3 py-1 rounded-full bg-green-100 border border-green-300 text-sm text-gray-800';
|
capsule.className = 'inline-flex items-center gap-2 px-3 py-1 rounded-full bg-green-100 border border-green-300 text-sm text-gray-800';
|
||||||
capsule.innerHTML = `
|
capsule.innerHTML = `
|
||||||
<span>${escapeHtml(user.firstName)}</span>
|
<span>${escapeHtml(user.firstName)}</span>
|
||||||
<button type="button" class="text-gray-600 hover:text-gray-800 font-bold" data-remove-user="${user.id}">×</button>
|
<button type="button" class="text-gray-600 hover:text-gray-800 font-bold" data-remove-user="${index}">×</button>
|
||||||
`;
|
`;
|
||||||
const removeBtn = capsule.querySelector('[data-remove-user]');
|
const removeBtn = capsule.querySelector('[data-remove-user]');
|
||||||
removeBtn.addEventListener('click', (e) => {
|
removeBtn.addEventListener('click', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
removeUser(user.id);
|
selectedUsers = selectedUsers.filter((_, i) => i !== index);
|
||||||
|
const remaining = selectedUsers.map(u => u.name).join(', ');
|
||||||
|
shareNoteInput.value = remaining;
|
||||||
|
renderSelectedUsers();
|
||||||
});
|
});
|
||||||
selectedUsersContainer.appendChild(capsule);
|
selectedUsersContainer.appendChild(capsule);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close suggestions when clicking outside
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
if (!e.target.closest('#userSearchInput') && !e.target.closest('#userSuggestions')) {
|
|
||||||
userSuggestions.classList.add('hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --- Submit note ---
|
// --- Submit note ---
|
||||||
async function submitNote() {
|
async function submitNote() {
|
||||||
const statusEl = document.getElementById('submitStatus');
|
const statusEl = document.getElementById('submitStatus');
|
||||||
@@ -2681,8 +2632,8 @@
|
|||||||
job_note: jobNoteValue === 'yes',
|
job_note: jobNoteValue === 'yes',
|
||||||
type: noteType,
|
type: noteType,
|
||||||
userId: pb.authStore.model?.id || pb.authStore.record?.id,
|
userId: pb.authStore.model?.id || pb.authStore.record?.id,
|
||||||
shared: selectedUsers.length > 0,
|
shared: selectedUsers.length > 0 ? true : false,
|
||||||
shared_with: selectedUsers.map(u => u.id),
|
shared_with: selectedUsers.map(u => u.name),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include job_number if it has a value
|
// Only include job_number if it has a value
|
||||||
@@ -2747,7 +2698,11 @@
|
|||||||
loadNotesList();
|
loadNotesList();
|
||||||
showNotesList();
|
showNotesList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error('Error creating note:', e);
|
||||||
|
console.error('Full error:', JSON.stringify(e, null, 2));
|
||||||
|
if (e.response) {
|
||||||
|
console.error('Response data:', e.response);
|
||||||
|
}
|
||||||
statusEl.textContent = `Error saving: ${e.message}`;
|
statusEl.textContent = `Error saving: ${e.message}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user