@@ -1797,6 +1842,7 @@
let notesCache = {};
let notesList = [];
let searchQuery = '';
+ let currentFilter = 'all'; // 'all', 'myshared', 'sharedwithme', 'job', 'manager', 'personal'
let searchRecognition = null;
let isSearchRecognizing = false;
@@ -1822,6 +1868,169 @@
document.getElementById('noteContainer').classList.add('hidden');
document.getElementById('conversationsContainer').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: '
',
+ video: '
',
+ audio: '
',
+ file: '
',
+ };
+ return `
`;
+ }
+ 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 = `
`;
+
+ 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 `
${escapeHtml(firstName)}`;
+ }).join('');
+ sharedWithDisplay = `
${capsules}
`;
+ }
+ }
+
+ card.innerHTML = `
+
+
${escapeHtml(title)}
+
+
${formatDateShort(note.created)}
+
+
+
+
+
${escapeHtml(snippet || '(empty)')}
+ ${note.shared ? `
${typeBubble}
` : ''}
+
+
+
+ ${attachmentIcons}
+
+
+ ${note.shared ? sharedWithDisplay : ''}
+
+
+ `;
+
+ // 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() {
@@ -1830,6 +2039,7 @@
document.getElementById('noteContainer').classList.remove('hidden');
document.getElementById('conversationsContainer').classList.add('hidden');
document.getElementById('conversationContainer').classList.add('hidden');
+ document.getElementById('hiddenNotesContainer').classList.add('hidden');
// Initialize email lookup for sharing
if (window.setupUserEmailLookup) {
window.setupUserEmailLookup(pb, '#selectedUsersContainer');
@@ -1988,11 +2198,16 @@
card.dataset.noteId = note.id;
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;
- 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';
} 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',
@@ -2047,7 +2262,10 @@
card.innerHTML = `
${escapeHtml(title)}
-
${formatDateShort(note.created)}
+
+
${formatDateShort(note.created)}
+
+
${escapeHtml(snippet || '(empty)')}
@@ -2062,6 +2280,19 @@
`;
+
+ // 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);
});
grid.appendChild(frag);
@@ -2074,6 +2305,33 @@
try {
const res = await pb.collection(NOTES_COLLECTION).getList(1, 50, { sort: '-created' });
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
const attachmentsByNote = {};
@@ -2107,11 +2365,34 @@
function applySearch() {
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) {
- renderNotesList(notesList);
+ renderNotesList(visibleNotes);
return;
}
- const filtered = notesList.filter((n) => {
+ const filtered = visibleNotes.filter((n) => {
const hay = [
n.title,
n[NOTE_BODY_PLAIN_FIELD],
@@ -2128,12 +2409,38 @@
if (!note) return;
document.getElementById('notesListContainer').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('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]) : '');
- 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 audioStatus = document.getElementById('noteDetailAudioStatus');
@@ -2279,6 +2586,57 @@
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', () => {
showConversations();
});
@@ -2309,6 +2667,91 @@
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
const attachmentMenuBtn = document.getElementById('attachmentMenuBtn');
const attachmentMenu = document.getElementById('attachmentMenu');
diff --git a/logs/SESSION_LOG.txt b/logs/SESSION_LOG.txt
index 2f9bf6b..e3f14f8 100644
--- a/logs/SESSION_LOG.txt
+++ b/logs/SESSION_LOG.txt
@@ -202,5 +202,47 @@ Project history, decisions, and architectural milestones
* Works for self-sharing (user shares note with self)
- 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
+
================================================================================