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:
2026-01-16 02:28:48 +00:00
parent 95888562a0
commit 1b751742dc
2 changed files with 496 additions and 11 deletions
+454 -11
View File
@@ -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);
@@ -2074,6 +2305,33 @@
try { try {
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 = {};
@@ -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');
+42
View File
@@ -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
================================================================================ ================================================================================