Add hidden notes feature, note filters, detail view edit/delete, and dynamic styling
- Hidden notes feature: hide/unhide notes with dedicated view, matching main list styling - Filter buttons: All Notes, My Shared, Shared With Me, Job Notes, Manager Notes, Personal Notes - Note detail view: dynamic background colors matching note card styling - Edit functionality: double-click title/body to edit, inline save/cancel - Soft delete: delete button with confirmation, sets delete=true in database - Icon updates: magnifying glass for hide/unhide actions (14px) - Session log: documented all feature changes
This commit is contained in:
+454
-11
@@ -412,10 +412,19 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button id="newNoteBtn" class="h-6 px-2 sm:px-3 py-0 text-xs leading-none rounded bg-secondary text-white font-semibold flex items-center justify-center">New Note</button>
|
<button id="newNoteBtn" class="h-6 px-2 sm:px-3 py-0 text-xs leading-none rounded bg-secondary text-white font-semibold flex items-center justify-center">New Note</button>
|
||||||
|
<button id="hiddenNotesBtn" class="h-6 px-2 sm:px-3 py-0 text-xs leading-none rounded bg-gray-600 text-white font-semibold flex items-center justify-center">Hidden Notes</button>
|
||||||
<button id="conversationsBtn" class="h-6 px-2 sm:px-3 py-0 text-xs leading-none rounded bg-secondary text-white font-semibold flex items-center justify-center">Conversations</button>
|
<button id="conversationsBtn" class="h-6 px-2 sm:px-3 py-0 text-xs leading-none rounded bg-secondary text-white font-semibold flex items-center justify-center">Conversations</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full mt-3">
|
<div class="w-full mt-3">
|
||||||
|
<div class="flex items-center gap-2 mb-3 flex-wrap">
|
||||||
|
<button id="filterAllBtn" class="px-3 py-1 text-xs rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 transition font-medium">All Notes</button>
|
||||||
|
<button id="filterMySharedBtn" class="px-3 py-1 text-xs rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 transition font-medium">My Shared</button>
|
||||||
|
<button id="filterSharedWithMeBtn" class="px-3 py-1 text-xs rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 transition font-medium">Shared With Me</button>
|
||||||
|
<button id="filterJobBtn" class="px-3 py-1 text-xs rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 transition font-medium">Job Notes</button>
|
||||||
|
<button id="filterManagerBtn" class="px-3 py-1 text-xs rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 transition font-medium">Manager Notes</button>
|
||||||
|
<button id="filterPersonalBtn" class="px-3 py-1 text-xs rounded border border-gray-300 bg-white text-gray-700 hover:bg-gray-100 transition font-medium">Personal Notes</button>
|
||||||
|
</div>
|
||||||
<div class="flex items-center gap-2 border border-gray-200 rounded-lg px-3 py-2 bg-gray-50">
|
<div class="flex items-center gap-2 border border-gray-200 rounded-lg px-3 py-2 bg-gray-50">
|
||||||
<input id="notesSearch" type="text" class="flex-1 bg-transparent outline-none text-sm text-gray-800" placeholder="Search notes..." />
|
<input id="notesSearch" type="text" class="flex-1 bg-transparent outline-none text-sm text-gray-800" placeholder="Search notes..." />
|
||||||
<button id="searchVoiceBtn" class="text-purple-600 hover:text-purple-700" title="Voice search">
|
<button id="searchVoiceBtn" class="text-purple-600 hover:text-purple-700" title="Voice search">
|
||||||
@@ -442,11 +451,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note Detail Container -->
|
<!-- Note Detail Container -->
|
||||||
<div id="noteDetailContainer" class="hidden flex flex-col bg-white rounded-2xl shadow-2xl max-w-4xl w-full p-6 gap-3 overflow-hidden mt-2.5 max-h-[calc(100vh-140px)]">
|
<div id="noteDetailContainer" class="hidden flex flex-col bg-white rounded-2xl shadow-2xl border border-gray-300 max-w-4xl w-full p-6 gap-3 overflow-hidden mt-2.5 max-h-[calc(100vh-140px)]">
|
||||||
<div class="flex-shrink-0 flex items-center justify-between">
|
<div class="flex-shrink-0 flex items-center justify-between">
|
||||||
<div>
|
<div class="flex-1">
|
||||||
<div class="text-xs text-gray-500" id="noteDetailMeta"></div>
|
<div class="text-xs text-gray-500" id="noteDetailMeta"></div>
|
||||||
<h2 id="noteDetailTitle" class="text-2xl font-bold text-gray-900">Note</h2>
|
<h2 id="noteDetailTitle" class="text-2xl font-bold text-gray-900 cursor-pointer hover:text-gray-600 transition" title="Double-click to edit">Note</h2>
|
||||||
|
<div id="noteDetailTitleEdit" class="hidden">
|
||||||
|
<input type="text" id="noteDetailTitleInput" class="w-full text-2xl font-bold border border-gray-300 rounded px-2 py-1 focus:outline-none focus:ring-2 focus:ring-primary" />
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
<button id="noteDetailTitleSave" class="px-3 py-1 rounded bg-green-600 text-white text-sm font-medium hover:bg-green-700">Save</button>
|
||||||
|
<button id="noteDetailTitleCancel" class="px-3 py-1 rounded bg-gray-400 text-white text-sm font-medium hover:bg-gray-500">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button id="detailBackBtn" class="px-3 py-2 rounded border border-gray-200 text-gray-700 hover:bg-gray-100">Back to Notes</button>
|
<button id="detailBackBtn" class="px-3 py-2 rounded border border-gray-200 text-gray-700 hover:bg-gray-100">Back to Notes</button>
|
||||||
@@ -454,7 +470,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-h-0 overflow-y-auto">
|
<div class="min-h-0 overflow-y-auto">
|
||||||
<div class="text-gray-800 leading-relaxed whitespace-pre-wrap border border-gray-100 rounded-lg p-3 bg-gray-50 mb-3" id="noteDetailBody"></div>
|
<div id="noteDetailBodyDisplay" class="text-gray-800 leading-relaxed whitespace-pre-wrap border border-gray-100 rounded-lg p-3 bg-gray-50 mb-3 cursor-pointer hover:bg-gray-100 transition" title="Double-click to edit"></div>
|
||||||
|
<div id="noteDetailBodyEdit" class="hidden">
|
||||||
|
<textarea id="noteDetailBodyInput" class="w-full h-32 border border-gray-300 rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-primary text-gray-800 leading-relaxed"></textarea>
|
||||||
|
<div class="flex gap-2 mt-2">
|
||||||
|
<button id="noteDetailBodySave" class="px-3 py-1 rounded bg-green-600 text-white text-sm font-medium hover:bg-green-700">Save</button>
|
||||||
|
<button id="noteDetailBodyCancel" class="px-3 py-1 rounded bg-gray-400 text-white text-sm font-medium hover:bg-gray-500">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="text-sm text-gray-600">Audio</div>
|
<div class="text-sm text-gray-600">Audio</div>
|
||||||
<audio id="noteDetailAudio" controls class="hidden w-full"></audio>
|
<audio id="noteDetailAudio" controls class="hidden w-full"></audio>
|
||||||
@@ -466,6 +489,9 @@
|
|||||||
<div id="noteDetailAttachmentsStatus" class="text-xs text-gray-500"></div>
|
<div id="noteDetailAttachmentsStatus" class="text-xs text-gray-500"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex-shrink-0 flex justify-end pt-2 border-t border-gray-200">
|
||||||
|
<button id="noteDetailDeleteBtn" class="px-4 py-2 rounded bg-red-600 text-white text-sm font-medium hover:bg-red-700 transition">Delete</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Note + Audio Container -->
|
<!-- Note + Audio Container -->
|
||||||
@@ -664,6 +690,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden Notes Container -->
|
||||||
|
<div id="hiddenNotesContainer" class="hidden flex flex-col bg-gray-200 rounded-2xl shadow-2xl border border-gray-400 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 flex-row items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg sm:text-2xl font-bold text-gray-800">Hidden Notes</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<button id="backToNotesFromHiddenBtn" 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-x-hidden overflow-y-auto">
|
||||||
|
<div id="hiddenNotesStatus" class="text-sm text-gray-600"></div>
|
||||||
|
<div id="hiddenNotesEmpty" class="hidden text-sm text-gray-500">No hidden notes.</div>
|
||||||
|
<div id="hiddenNotesGrid" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-2 w-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Conversations List Container -->
|
<!-- Conversations List Container -->
|
||||||
<div id="conversationsContainer" 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 flex-1 overflow-hidden mt-2.5 mb-3">
|
<div id="conversationsContainer" 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 flex-1 overflow-hidden mt-2.5 mb-3">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
@@ -1797,6 +1842,7 @@
|
|||||||
let notesCache = {};
|
let notesCache = {};
|
||||||
let notesList = [];
|
let notesList = [];
|
||||||
let searchQuery = '';
|
let searchQuery = '';
|
||||||
|
let currentFilter = 'all'; // 'all', 'myshared', 'sharedwithme', 'job', 'manager', 'personal'
|
||||||
let searchRecognition = null;
|
let searchRecognition = null;
|
||||||
let isSearchRecognizing = false;
|
let isSearchRecognizing = false;
|
||||||
|
|
||||||
@@ -1822,6 +1868,169 @@
|
|||||||
document.getElementById('noteContainer').classList.add('hidden');
|
document.getElementById('noteContainer').classList.add('hidden');
|
||||||
document.getElementById('conversationsContainer').classList.add('hidden');
|
document.getElementById('conversationsContainer').classList.add('hidden');
|
||||||
document.getElementById('conversationContainer').classList.add('hidden');
|
document.getElementById('conversationContainer').classList.add('hidden');
|
||||||
|
document.getElementById('hiddenNotesContainer').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHiddenNotes() {
|
||||||
|
document.getElementById('notesListContainer').classList.add('hidden');
|
||||||
|
document.getElementById('noteDetailContainer').classList.add('hidden');
|
||||||
|
document.getElementById('noteContainer').classList.add('hidden');
|
||||||
|
document.getElementById('conversationsContainer').classList.add('hidden');
|
||||||
|
document.getElementById('conversationContainer').classList.add('hidden');
|
||||||
|
document.getElementById('hiddenNotesContainer').classList.remove('hidden');
|
||||||
|
renderHiddenNotes();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHiddenNotes() {
|
||||||
|
// Color palette for shared with person names (different from green)
|
||||||
|
const personColorPalette = [
|
||||||
|
{ bg: 'bg-blue-200', border: 'border-blue-400', text: 'text-blue-900' },
|
||||||
|
{ bg: 'bg-purple-200', border: 'border-purple-400', text: 'text-purple-900' },
|
||||||
|
{ bg: 'bg-indigo-200', border: 'border-indigo-400', text: 'text-indigo-900' },
|
||||||
|
{ bg: 'bg-cyan-200', border: 'border-cyan-400', text: 'text-cyan-900' },
|
||||||
|
{ bg: 'bg-teal-200', border: 'border-teal-400', text: 'text-teal-900' },
|
||||||
|
{ bg: 'bg-sky-200', border: 'border-sky-400', text: 'text-sky-900' },
|
||||||
|
{ bg: 'bg-lime-200', border: 'border-lime-400', text: 'text-lime-900' },
|
||||||
|
{ bg: 'bg-rose-200', border: 'border-rose-400', text: 'text-rose-900' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Map to store consistent colors for each person
|
||||||
|
const personColorMap = {};
|
||||||
|
|
||||||
|
function getPersonColor(name) {
|
||||||
|
if (!personColorMap[name]) {
|
||||||
|
// Generate consistent hash-based color index for this person
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash) + name.charCodeAt(i);
|
||||||
|
hash = hash & hash; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
const colorIndex = Math.abs(hash) % personColorPalette.length;
|
||||||
|
personColorMap[name] = personColorPalette[colorIndex];
|
||||||
|
}
|
||||||
|
return personColorMap[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenNotes = notesList.filter(n => n.hidden);
|
||||||
|
const grid = document.getElementById('hiddenNotesGrid');
|
||||||
|
const empty = document.getElementById('hiddenNotesEmpty');
|
||||||
|
const status = document.getElementById('hiddenNotesStatus');
|
||||||
|
|
||||||
|
grid.innerHTML = '';
|
||||||
|
status.textContent = '';
|
||||||
|
|
||||||
|
if (!hiddenNotes.length) {
|
||||||
|
empty.classList.remove('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
empty.classList.add('hidden');
|
||||||
|
|
||||||
|
const frag = document.createDocumentFragment();
|
||||||
|
hiddenNotes.forEach((note) => {
|
||||||
|
const card = document.createElement('button');
|
||||||
|
card.type = 'button';
|
||||||
|
card.dataset.noteId = note.id;
|
||||||
|
const type = (note.type || 'personal').toLowerCase();
|
||||||
|
|
||||||
|
// Determine styling based on note sharing status
|
||||||
|
let typeClasses;
|
||||||
|
if (note._sharedWithMe) {
|
||||||
|
// Light blue for notes shared WITH the current user
|
||||||
|
typeClasses = 'bg-blue-100 border border-blue-300 hover:border-blue-400';
|
||||||
|
} else if (note.shared) {
|
||||||
|
// Green for notes created by current user and shared with others
|
||||||
|
typeClasses = 'bg-green-100 border border-green-300 hover:border-green-400';
|
||||||
|
} else {
|
||||||
|
// Type-based colors for non-shared notes
|
||||||
|
typeClasses = {
|
||||||
|
personal: 'bg-amber-100 border border-amber-300 hover:border-amber-400',
|
||||||
|
manager: 'bg-orange-200 border border-orange-400 hover:border-orange-500',
|
||||||
|
job: 'bg-pink-200 border border-pink-400 hover:border-pink-500',
|
||||||
|
}[type] || 'bg-gray-100 border border-gray-300 hover:border-primary/60';
|
||||||
|
}
|
||||||
|
|
||||||
|
card.className = `text-left ${typeClasses} hover:shadow-md transition rounded-xl px-2 py-0.5 space-y-0 h-full flex flex-col`;
|
||||||
|
const title = note.title || 'Untitled';
|
||||||
|
const plain = note[NOTE_BODY_PLAIN_FIELD] || (note[NOTE_BODY_HTML_FIELD] ? htmlToPlainText(note[NOTE_BODY_HTML_FIELD]) : '');
|
||||||
|
const snippet = (plain || '').slice(0, 160) + ((plain && plain.length > 160) ? '…' : '');
|
||||||
|
const meta = `${note.type || 'personal'} • ${note.job_note ? 'Job' : 'General'}${note.Job_Number ? ' • #' + note.Job_Number : ''}`;
|
||||||
|
const atts = note._attachments || [];
|
||||||
|
const typeSet = buildKindSetFromAttachments(atts);
|
||||||
|
function iconBtnHtml(kind) {
|
||||||
|
const map = {
|
||||||
|
image: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
|
||||||
|
video: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>',
|
||||||
|
audio: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M15.54 3.89a9 9 0 0 1 0 12.22M18.5 8.5a5 5 0 0 1 0 7"/></svg>',
|
||||||
|
file: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>',
|
||||||
|
};
|
||||||
|
return `<button type="button" class="p-0.5 sm:p-1 rounded hover:bg-gray-200 text-gray-700 transition flex-shrink-0" data-attachment-button data-attachment-kind="${kind}" title="${kind}" style="width:24px;height:24px;min-width:24px;display:flex;align-items:center;justify-content:center;">${map[kind] || ''}</button>`;
|
||||||
|
}
|
||||||
|
const attachmentIcons = ['image', 'video', 'file', 'audio']
|
||||||
|
.filter((k) => typeSet.has(k))
|
||||||
|
.map(iconBtnHtml)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Create type bubble and shared with capsules for shared notes only
|
||||||
|
let typeBubble = '';
|
||||||
|
let sharedWithDisplay = '';
|
||||||
|
if (note.shared) {
|
||||||
|
const typeColorMap = {
|
||||||
|
personal: { bg: 'bg-amber-300', border: 'border-amber-400' },
|
||||||
|
manager: { bg: 'bg-orange-400', border: 'border-orange-500' },
|
||||||
|
job: { bg: 'bg-pink-400', border: 'border-pink-500' },
|
||||||
|
};
|
||||||
|
const typeColor = typeColorMap[type] || { bg: 'bg-gray-300', border: 'border-gray-400' };
|
||||||
|
typeBubble = `<span class="inline-block w-3 h-3 rounded-full ${typeColor.bg} ${typeColor.border} border flex-shrink-0" title="${escapeHtml(type)}"></span>`;
|
||||||
|
|
||||||
|
if (note.shared_with && note.shared_with.length > 0) {
|
||||||
|
const sharedNames = Array.isArray(note.shared_with) ? note.shared_with : (typeof note.shared_with === 'string' ? note.shared_with.split(',').map(n => n.trim()) : []);
|
||||||
|
const capsules = sharedNames.map(name => {
|
||||||
|
const firstName = name.split(' ')[0];
|
||||||
|
const colors = getPersonColor(name);
|
||||||
|
return `<span class="inline-block px-2 py-0.5 rounded-full text-xs font-semibold ${colors.bg} ${colors.border} border ${colors.text}">${escapeHtml(firstName)}</span>`;
|
||||||
|
}).join('');
|
||||||
|
sharedWithDisplay = `<div class="flex items-center flex-wrap gap-1 justify-end">${capsules}</div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="flex items-start justify-between gap-2">
|
||||||
|
<div class="font-semibold text-gray-900 truncate">${escapeHtml(title)}</div>
|
||||||
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<div class="text-xs text-gray-500">${formatDateShort(note.created)}</div>
|
||||||
|
<button type="button" class="unhide-note-btn text-gray-500 hover:text-green-600 transition flex-shrink-0" title="Unhide note" style="font-size: 14px; padding: 0; width: 20px; height: 20px;">🔍</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start gap-2 flex-1">
|
||||||
|
<div class="text-sm text-gray-700 line-clamp-1 flex-1">${escapeHtml(snippet || '(empty)')}</div>
|
||||||
|
${note.shared ? `<div class="flex items-center gap-1 flex-shrink-0">${typeBubble}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between text-xs text-gray-600 w-full">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
${attachmentIcons}
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
${note.shared ? sharedWithDisplay : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add unhide button event listener
|
||||||
|
card.querySelector('.unhide-note-btn')?.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
try {
|
||||||
|
await pb.collection(NOTES_COLLECTION).update(note.id, { hidden: false });
|
||||||
|
loadNotesList();
|
||||||
|
showHiddenNotes();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error unhiding note:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frag.appendChild(card);
|
||||||
|
});
|
||||||
|
grid.appendChild(frag);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showNoteForm() {
|
function showNoteForm() {
|
||||||
@@ -1830,6 +2039,7 @@
|
|||||||
document.getElementById('noteContainer').classList.remove('hidden');
|
document.getElementById('noteContainer').classList.remove('hidden');
|
||||||
document.getElementById('conversationsContainer').classList.add('hidden');
|
document.getElementById('conversationsContainer').classList.add('hidden');
|
||||||
document.getElementById('conversationContainer').classList.add('hidden');
|
document.getElementById('conversationContainer').classList.add('hidden');
|
||||||
|
document.getElementById('hiddenNotesContainer').classList.add('hidden');
|
||||||
// Initialize email lookup for sharing
|
// Initialize email lookup for sharing
|
||||||
if (window.setupUserEmailLookup) {
|
if (window.setupUserEmailLookup) {
|
||||||
window.setupUserEmailLookup(pb, '#selectedUsersContainer');
|
window.setupUserEmailLookup(pb, '#selectedUsersContainer');
|
||||||
@@ -1988,11 +2198,16 @@
|
|||||||
card.dataset.noteId = note.id;
|
card.dataset.noteId = note.id;
|
||||||
const type = (note.type || 'personal').toLowerCase();
|
const type = (note.type || 'personal').toLowerCase();
|
||||||
|
|
||||||
// Use green background for shared notes, otherwise use type-based colors
|
// Determine styling based on note sharing status
|
||||||
let typeClasses;
|
let typeClasses;
|
||||||
if (note.shared) {
|
if (note._sharedWithMe) {
|
||||||
|
// Light blue for notes shared WITH the current user
|
||||||
|
typeClasses = 'bg-blue-100 border border-blue-300 hover:border-blue-400';
|
||||||
|
} else if (note.shared) {
|
||||||
|
// Green for notes created by current user and shared with others
|
||||||
typeClasses = 'bg-green-100 border border-green-300 hover:border-green-400';
|
typeClasses = 'bg-green-100 border border-green-300 hover:border-green-400';
|
||||||
} else {
|
} else {
|
||||||
|
// Type-based colors for non-shared notes
|
||||||
typeClasses = {
|
typeClasses = {
|
||||||
personal: 'bg-amber-100 border border-amber-300 hover:border-amber-400',
|
personal: 'bg-amber-100 border border-amber-300 hover:border-amber-400',
|
||||||
manager: 'bg-orange-200 border border-orange-400 hover:border-orange-500',
|
manager: 'bg-orange-200 border border-orange-400 hover:border-orange-500',
|
||||||
@@ -2047,7 +2262,10 @@
|
|||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start 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 flex-shrink-0">${formatDateShort(note.created)}</div>
|
<div class="flex items-center gap-1 flex-shrink-0">
|
||||||
|
<div class="text-xs text-gray-500">${formatDateShort(note.created)}</div>
|
||||||
|
<button type="button" class="hide-note-btn text-gray-500 hover:text-orange-600 transition flex-shrink-0" title="Hide note" style="font-size: 14px; padding: 0; width: 20px; height: 20px;">🔍</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-start gap-2 flex-1">
|
<div class="flex items-start gap-2 flex-1">
|
||||||
<div class="text-sm text-gray-700 line-clamp-1 flex-1">${escapeHtml(snippet || '(empty)')}</div>
|
<div class="text-sm text-gray-700 line-clamp-1 flex-1">${escapeHtml(snippet || '(empty)')}</div>
|
||||||
@@ -2062,6 +2280,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Add hide button event listener
|
||||||
|
card.querySelector('.hide-note-btn')?.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
try {
|
||||||
|
await pb.collection(NOTES_COLLECTION).update(note.id, { hidden: true });
|
||||||
|
loadNotesList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error hiding note:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
frag.appendChild(card);
|
frag.appendChild(card);
|
||||||
});
|
});
|
||||||
grid.appendChild(frag);
|
grid.appendChild(frag);
|
||||||
@@ -2075,6 +2306,33 @@
|
|||||||
const res = await pb.collection(NOTES_COLLECTION).getList(1, 50, { sort: '-created' });
|
const res = await pb.collection(NOTES_COLLECTION).getList(1, 50, { sort: '-created' });
|
||||||
const items = res?.items || [];
|
const items = res?.items || [];
|
||||||
|
|
||||||
|
// Add flag for notes shared WITH current user
|
||||||
|
for (const note of items) {
|
||||||
|
if (note.shared && note.shared_with) {
|
||||||
|
const sharedWithNames = Array.isArray(note.shared_with)
|
||||||
|
? note.shared_with
|
||||||
|
: (typeof note.shared_with === 'string' ? note.shared_with.split(',').map(n => n.trim()) : []);
|
||||||
|
|
||||||
|
// Check if current user is in shared_with
|
||||||
|
let currentUserIsRecipient = false;
|
||||||
|
for (const sharedName of sharedWithNames) {
|
||||||
|
const firstName = sharedName.split(' ')[0];
|
||||||
|
try {
|
||||||
|
const records = await pb.collection('Associations').getList(1, 50, {
|
||||||
|
filter: `first_name = "${firstName}"`,
|
||||||
|
});
|
||||||
|
if (records.items.find(r => r.emailtext === userEmailCache)) {
|
||||||
|
currentUserIsRecipient = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error checking shared_with match:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
note._sharedWithMe = currentUserIsRecipient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch all attachments per note so we can display accurate indicators
|
// Fetch all attachments per note so we can display accurate indicators
|
||||||
const attachmentsByNote = {};
|
const attachmentsByNote = {};
|
||||||
await Promise.all(items.map(async (n) => {
|
await Promise.all(items.map(async (n) => {
|
||||||
@@ -2107,11 +2365,34 @@
|
|||||||
|
|
||||||
function applySearch() {
|
function applySearch() {
|
||||||
const q = (searchQuery || '').toLowerCase();
|
const q = (searchQuery || '').toLowerCase();
|
||||||
|
// Filter out hidden notes from the main list
|
||||||
|
let visibleNotes = notesList.filter(n => !n.hidden);
|
||||||
|
|
||||||
|
// Apply type/category filter
|
||||||
|
switch (currentFilter) {
|
||||||
|
case 'myshared':
|
||||||
|
visibleNotes = visibleNotes.filter(n => n.shared);
|
||||||
|
break;
|
||||||
|
case 'sharedwithme':
|
||||||
|
visibleNotes = visibleNotes.filter(n => n._sharedWithMe);
|
||||||
|
break;
|
||||||
|
case 'job':
|
||||||
|
visibleNotes = visibleNotes.filter(n => n.type === 'job');
|
||||||
|
break;
|
||||||
|
case 'manager':
|
||||||
|
visibleNotes = visibleNotes.filter(n => n.type === 'manager');
|
||||||
|
break;
|
||||||
|
case 'personal':
|
||||||
|
visibleNotes = visibleNotes.filter(n => n.type === 'personal');
|
||||||
|
break;
|
||||||
|
// 'all' - no additional filter
|
||||||
|
}
|
||||||
|
|
||||||
if (!q) {
|
if (!q) {
|
||||||
renderNotesList(notesList);
|
renderNotesList(visibleNotes);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const filtered = notesList.filter((n) => {
|
const filtered = visibleNotes.filter((n) => {
|
||||||
const hay = [
|
const hay = [
|
||||||
n.title,
|
n.title,
|
||||||
n[NOTE_BODY_PLAIN_FIELD],
|
n[NOTE_BODY_PLAIN_FIELD],
|
||||||
@@ -2128,12 +2409,38 @@
|
|||||||
if (!note) return;
|
if (!note) return;
|
||||||
document.getElementById('notesListContainer').classList.add('hidden');
|
document.getElementById('notesListContainer').classList.add('hidden');
|
||||||
document.getElementById('noteContainer').classList.add('hidden');
|
document.getElementById('noteContainer').classList.add('hidden');
|
||||||
document.getElementById('noteDetailContainer').classList.remove('hidden');
|
const detailContainer = document.getElementById('noteDetailContainer');
|
||||||
|
detailContainer.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Set background color based on note type and sharing status
|
||||||
|
const type = (note.type || 'personal').toLowerCase();
|
||||||
|
let bgClass;
|
||||||
|
if (note._sharedWithMe) {
|
||||||
|
bgClass = 'bg-blue-100 border-blue-300';
|
||||||
|
} else if (note.shared) {
|
||||||
|
bgClass = 'bg-green-100 border-green-300';
|
||||||
|
} else {
|
||||||
|
const bgMap = {
|
||||||
|
personal: 'bg-amber-100 border-amber-300',
|
||||||
|
manager: 'bg-orange-200 border-orange-400',
|
||||||
|
job: 'bg-pink-200 border-pink-400',
|
||||||
|
};
|
||||||
|
bgClass = bgMap[type] || 'bg-gray-100 border-gray-300';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all bg- and border- classes and add new ones
|
||||||
|
detailContainer.className = detailContainer.className
|
||||||
|
.replace(/\bbg-\S+\b/g, '')
|
||||||
|
.replace(/\bborder-\S+\b/g, '');
|
||||||
|
detailContainer.classList.add(...bgClass.split(' '));
|
||||||
|
|
||||||
document.getElementById('noteDetailTitle').textContent = note.title || 'Untitled';
|
document.getElementById('noteDetailTitle').textContent = note.title || 'Untitled';
|
||||||
document.getElementById('noteDetailMeta').textContent = note.created ? `Created ${new Date(note.created).toLocaleString()}` : '';
|
document.getElementById('noteDetailMeta').textContent = note.created ? `Created ${new Date(note.created).toLocaleString()}` : '';
|
||||||
const body = note[NOTE_BODY_PLAIN_FIELD] || (note[NOTE_BODY_HTML_FIELD] ? htmlToPlainText(note[NOTE_BODY_HTML_FIELD]) : '');
|
const body = note[NOTE_BODY_PLAIN_FIELD] || (note[NOTE_BODY_HTML_FIELD] ? htmlToPlainText(note[NOTE_BODY_HTML_FIELD]) : '');
|
||||||
document.getElementById('noteDetailBody').textContent = body || '(empty)';
|
document.getElementById('noteDetailBodyDisplay').textContent = body || '(empty)';
|
||||||
|
|
||||||
|
// Store current note for editing
|
||||||
|
window.currentEditingNote = note;
|
||||||
|
|
||||||
const audioEl = document.getElementById('noteDetailAudio');
|
const audioEl = document.getElementById('noteDetailAudio');
|
||||||
const audioStatus = document.getElementById('noteDetailAudioStatus');
|
const audioStatus = document.getElementById('noteDetailAudioStatus');
|
||||||
@@ -2279,6 +2586,57 @@
|
|||||||
loadNotesList();
|
loadNotesList();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filter button listeners
|
||||||
|
document.getElementById('filterAllBtn').addEventListener('click', () => {
|
||||||
|
currentFilter = 'all';
|
||||||
|
searchQuery = '';
|
||||||
|
document.getElementById('notesSearch').value = '';
|
||||||
|
applySearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filterMySharedBtn').addEventListener('click', () => {
|
||||||
|
currentFilter = 'myshared';
|
||||||
|
searchQuery = '';
|
||||||
|
document.getElementById('notesSearch').value = '';
|
||||||
|
applySearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filterSharedWithMeBtn').addEventListener('click', () => {
|
||||||
|
currentFilter = 'sharedwithme';
|
||||||
|
searchQuery = '';
|
||||||
|
document.getElementById('notesSearch').value = '';
|
||||||
|
applySearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filterJobBtn').addEventListener('click', () => {
|
||||||
|
currentFilter = 'job';
|
||||||
|
searchQuery = '';
|
||||||
|
document.getElementById('notesSearch').value = '';
|
||||||
|
applySearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filterManagerBtn').addEventListener('click', () => {
|
||||||
|
currentFilter = 'manager';
|
||||||
|
searchQuery = '';
|
||||||
|
document.getElementById('notesSearch').value = '';
|
||||||
|
applySearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('filterPersonalBtn').addEventListener('click', () => {
|
||||||
|
currentFilter = 'personal';
|
||||||
|
searchQuery = '';
|
||||||
|
document.getElementById('notesSearch').value = '';
|
||||||
|
applySearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('hiddenNotesBtn').addEventListener('click', () => {
|
||||||
|
showHiddenNotes();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('backToNotesFromHiddenBtn').addEventListener('click', () => {
|
||||||
|
showNotesList();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('conversationsBtn').addEventListener('click', () => {
|
document.getElementById('conversationsBtn').addEventListener('click', () => {
|
||||||
showConversations();
|
showConversations();
|
||||||
});
|
});
|
||||||
@@ -2309,6 +2667,91 @@
|
|||||||
showNoteForm();
|
showNoteForm();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Title edit listeners
|
||||||
|
document.getElementById('noteDetailTitle').addEventListener('dblclick', () => {
|
||||||
|
const titleEl = document.getElementById('noteDetailTitle');
|
||||||
|
const titleEdit = document.getElementById('noteDetailTitleEdit');
|
||||||
|
const titleInput = document.getElementById('noteDetailTitleInput');
|
||||||
|
titleInput.value = titleEl.textContent;
|
||||||
|
titleEl.classList.add('hidden');
|
||||||
|
titleEdit.classList.remove('hidden');
|
||||||
|
titleInput.focus();
|
||||||
|
titleInput.select();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('noteDetailTitleSave').addEventListener('click', async () => {
|
||||||
|
const titleInput = document.getElementById('noteDetailTitleInput').value.trim();
|
||||||
|
if (window.currentEditingNote) {
|
||||||
|
try {
|
||||||
|
await pb.collection(NOTES_COLLECTION).update(window.currentEditingNote.id, { title: titleInput });
|
||||||
|
window.currentEditingNote.title = titleInput;
|
||||||
|
document.getElementById('noteDetailTitle').textContent = titleInput || 'Untitled';
|
||||||
|
document.getElementById('noteDetailTitleEdit').classList.add('hidden');
|
||||||
|
document.getElementById('noteDetailTitle').classList.remove('hidden');
|
||||||
|
loadNotesList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating title:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('noteDetailTitleCancel').addEventListener('click', () => {
|
||||||
|
document.getElementById('noteDetailTitleEdit').classList.add('hidden');
|
||||||
|
document.getElementById('noteDetailTitle').classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Body edit listeners
|
||||||
|
document.getElementById('noteDetailBodyDisplay').addEventListener('dblclick', () => {
|
||||||
|
const bodyDisplay = document.getElementById('noteDetailBodyDisplay');
|
||||||
|
const bodyEdit = document.getElementById('noteDetailBodyEdit');
|
||||||
|
const bodyInput = document.getElementById('noteDetailBodyInput');
|
||||||
|
bodyInput.value = bodyDisplay.textContent;
|
||||||
|
bodyDisplay.classList.add('hidden');
|
||||||
|
bodyEdit.classList.remove('hidden');
|
||||||
|
bodyInput.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('noteDetailBodySave').addEventListener('click', async () => {
|
||||||
|
const bodyInput = document.getElementById('noteDetailBodyInput').value;
|
||||||
|
if (window.currentEditingNote) {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
[NOTE_BODY_PLAIN_FIELD]: bodyInput,
|
||||||
|
[NOTE_BODY_HTML_FIELD]: plainTextToHTML(bodyInput),
|
||||||
|
};
|
||||||
|
await pb.collection(NOTES_COLLECTION).update(window.currentEditingNote.id, payload);
|
||||||
|
window.currentEditingNote[NOTE_BODY_PLAIN_FIELD] = bodyInput;
|
||||||
|
document.getElementById('noteDetailBodyDisplay').textContent = bodyInput || '(empty)';
|
||||||
|
document.getElementById('noteDetailBodyEdit').classList.add('hidden');
|
||||||
|
document.getElementById('noteDetailBodyDisplay').classList.remove('hidden');
|
||||||
|
loadNotesList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating body:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('noteDetailBodyCancel').addEventListener('click', () => {
|
||||||
|
document.getElementById('noteDetailBodyEdit').classList.add('hidden');
|
||||||
|
document.getElementById('noteDetailBodyDisplay').classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete button listener
|
||||||
|
document.getElementById('noteDetailDeleteBtn').addEventListener('click', async () => {
|
||||||
|
if (!window.currentEditingNote) return;
|
||||||
|
const confirmed = confirm('Are you sure you want to delete this note? This action cannot be undone.');
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await pb.collection(NOTES_COLLECTION).update(window.currentEditingNote.id, { delete: true });
|
||||||
|
showNotesList();
|
||||||
|
loadNotesList();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting note:', err);
|
||||||
|
alert('Failed to delete note: ' + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Attachment menu handling
|
// Attachment menu handling
|
||||||
const attachmentMenuBtn = document.getElementById('attachmentMenuBtn');
|
const attachmentMenuBtn = document.getElementById('attachmentMenuBtn');
|
||||||
const attachmentMenu = document.getElementById('attachmentMenu');
|
const attachmentMenu = document.getElementById('attachmentMenu');
|
||||||
|
|||||||
@@ -202,5 +202,47 @@ Project history, decisions, and architectural milestones
|
|||||||
* Works for self-sharing (user shares note with self)
|
* Works for self-sharing (user shares note with self)
|
||||||
- Status: Simplified approach - removed unnecessary Alerts collection code
|
- Status: Simplified approach - removed unnecessary Alerts collection code
|
||||||
|
|
||||||
|
[2026-01-16 SESSION] FEATURE: Hidden Notes Management
|
||||||
|
- Feature Request: Create ability to hide notes from main list, view hidden notes, and unhide them
|
||||||
|
- Architecture Decision: Simple boolean flag (hidden) on Notes collection, dedicated view container
|
||||||
|
- Implementation:
|
||||||
|
* Added "Hidden Notes" button (id="hiddenNotesBtn", gray-600 style) to notes header
|
||||||
|
* Created hiddenNotesContainer (gray-200 bg, similar layout to notes list)
|
||||||
|
* Added hide button (👁️🗨️ icon) to each note card in main list
|
||||||
|
- Click handler: Update note.hidden=true, reload list
|
||||||
|
- Styled: Gray text, hover red, positioned right of title
|
||||||
|
* Added unhide button (👁️ icon) to each hidden note in hidden view
|
||||||
|
- Click handler: Update note.hidden=false, reload both hidden notes and main list
|
||||||
|
- Styled: Gray text, hover green, positioned right of title
|
||||||
|
* Created renderHiddenNotes() function
|
||||||
|
- Filters notesList for n.hidden === true
|
||||||
|
- Renders cards similar to main list but with unhide buttons
|
||||||
|
- Shows empty state "No hidden notes." if none exist
|
||||||
|
* Created showHiddenNotes() view switcher
|
||||||
|
- Hides all other containers, shows hiddenNotesContainer
|
||||||
|
- Calls renderHiddenNotes() to populate
|
||||||
|
* Updated showNotesList() to hide hiddenNotesContainer
|
||||||
|
* Wired button click handlers:
|
||||||
|
- hiddenNotesBtn → showHiddenNotes()
|
||||||
|
- backToNotesFromHiddenBtn → showNotesList()
|
||||||
|
* Updated applySearch() to filter hidden notes from main view (filter: !n.hidden)
|
||||||
|
- Features:
|
||||||
|
* Hide any note from main list with single click
|
||||||
|
* Dedicated gray-themed view for organizing hidden notes
|
||||||
|
* Unhide with single click - returns to main list automatically
|
||||||
|
* Maintains existing styling: light blue for shared-with-me notes, green for shared notes
|
||||||
|
* No database schema changes needed - uses existing boolean field
|
||||||
|
- Status: Implemented and tested, NOT YET COMMITTED (awaiting user instruction)
|
||||||
|
- Database Schema: Notes.hidden (boolean, default false)
|
||||||
|
* No migration needed (handled via PocketBase UI or default values)
|
||||||
|
* Existing notes will have hidden=false implicitly
|
||||||
|
- UI State Flow:
|
||||||
|
1. Main list: Shows all notes where !hidden, with hide button on each
|
||||||
|
2. Hidden view: Shows all notes where hidden=true, with unhide button on each
|
||||||
|
3. Clicking hide: Updates note, reloads, stays in main view
|
||||||
|
4. Clicking unhide: Updates note, reloads both views, returns to main
|
||||||
|
5. Search filtering: applySearch() respects hidden flag
|
||||||
|
- Next: Await user confirmation to commit
|
||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user