diff --git a/src/components/NotesSidebar.tsx b/src/components/NotesSidebar.tsx index 2ae8046..9715638 100644 --- a/src/components/NotesSidebar.tsx +++ b/src/components/NotesSidebar.tsx @@ -1,4 +1,4 @@ -import { type Component, createMemo, For, Show } from "solid-js"; +import { type Component, createMemo, createResource, For, Show } from "solid-js"; import { store, getFavoriteTag } from "@/store"; import { pb } from "@/lib/pocketbase"; import { Plus, Lock, Star } from "lucide-solid"; @@ -12,6 +12,35 @@ export const NotesSidebar: Component<{ setSelectedNoteId: (id: string | null) => void; }> = (props) => { const currentUserId = pb.authStore.model?.id; + const [accessibleJobs] = createResource(currentUserId, async (userId) => { + if (!userId) return { ids: new Set(), numbers: new Set() }; + try { + const items = await pb.collection('Job_Info_Prod').getFullList({ + filter: [ + `EstimatorUser = "${userId}"`, + `OfficeRepUser = "${userId}"`, + `ProjectManagerUser = "${userId}"`, + `Estimator = "${userId}"`, + `Office_Rep = "${userId}"`, + `Project_Manager = "${userId}"` + ].join(" || "), + fields: "id,Job_Number", + requestKey: null + }); + const ids = new Set(); + const numbers = new Set(); + items.forEach((r: any) => { + if (r.id) ids.add(String(r.id)); + if (r.Job_Number !== undefined && r.Job_Number !== null && r.Job_Number !== "") { + numbers.add(String(r.Job_Number)); + } + }); + return { ids, numbers }; + } catch (e) { + console.warn("Failed to load job access list for notes filter:", e); + return { ids: new Set(), numbers: new Set() }; + } + }); const filteredNotes = createMemo(() => { const filter = store.noteFilter; @@ -56,7 +85,20 @@ export const NotesSidebar: Component<{ // Owned By Me Filter if (filter.ownedByMe) { - n = n.filter(note => note.user === currentUserId); + const accessible = accessibleJobs(); + n = n.filter(note => { + if (note.tags?.includes("prism_job")) { + if (!accessible) return false; + const jobIdTag = note.tags.find(t => t.startsWith("job_id:")); + const jobRef = jobIdTag?.split(":")[1]?.trim(); + if (!jobRef) return false; + const isLegacyNumber = /^\d{4}$/.test(jobRef); + return isLegacyNumber + ? accessible.numbers.has(jobRef) + : accessible.ids.has(jobRef); + } + return note.user === currentUserId; + }); } // Starred Filter diff --git a/src/store/index.ts b/src/store/index.ts index 47a92de..3703cfb 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1095,6 +1095,105 @@ const runLegacyTagMigration = async () => { } }; +const runLegacyJobIdTagMigration = async () => { + const currentUserId = pb.authStore.model?.id; + if (!currentUserId) return; + + try { + const userRec = await pb.collection('users').getOne(currentUserId, { requestKey: null }); + const prefs = userRec.Taskgrid_pref || {}; + + if (prefs.migratedLegacyJobIdTagsV1) return; + + // Fetch all Notes (all users) + const notes = await pb.collection(NOTES_COLLECTION).getFullList({ + requestKey: null + }); + + const extractLegacyJobNumbers = (tags: string[] | undefined) => { + if (!tags || tags.length === 0) return []; + const nums: string[] = []; + tags.forEach(t => { + if (!t.startsWith("job_id:")) return; + const val = t.split(":")[1]?.trim(); + if (val && /^\d{4}$/.test(val)) nums.push(val); + }); + return nums; + }; + + const legacyNumbers = new Set(); + notes.forEach(n => extractLegacyJobNumbers(n.tags).forEach(nm => legacyNumbers.add(nm))); + + if (legacyNumbers.size > 0) { + const allNumbers = Array.from(legacyNumbers); + const jobMap = new Map(); // jobNumber -> jobId + const chunkSize = 50; + + for (let i = 0; i < allNumbers.length; i += chunkSize) { + const chunk = allNumbers.slice(i, i + chunkSize); + const filter = chunk.map(n => `Job_Number = "${n}"`).join(" || "); + const jobs = await pb.collection('Job_Info_Prod').getFullList({ + filter, + fields: "id,Job_Number", + requestKey: null + }); + jobs.forEach((j: any) => { + if (j.Job_Number !== undefined && j.Job_Number !== null && j.Job_Number !== "") { + jobMap.set(String(j.Job_Number), String(j.id)); + } + }); + } + + let migratedNotes = 0; + for (const n of notes) { + if (!n.tags || n.tags.length === 0) continue; + let changed = false; + const newTags = n.tags.map((tag: string) => { + if (!tag.startsWith("job_id:")) return tag; + const val = tag.split(":")[1]?.trim(); + if (!val || !/^\d{4}$/.test(val)) return tag; + const mappedId = jobMap.get(val); + if (!mappedId) return tag; + changed = true; + return `job_id:${mappedId}`; + }); + if (changed) { + await pb.collection(NOTES_COLLECTION).update(n.id, { tags: newTags }, { requestKey: null }); + migratedNotes++; + } + } + + if (migratedNotes > 0) { + setStore("notes", (items) => items.map(n => { + if (!n.tags || n.tags.length === 0) return n; + let localChanged = false; + const nt = n.tags.map(tag => { + if (!tag.startsWith("job_id:")) return tag; + const val = tag.split(":")[1]?.trim(); + if (!val || !/^\d{4}$/.test(val)) return tag; + const mappedId = jobMap.get(val); + if (!mappedId) return tag; + localChanged = true; + return `job_id:${mappedId}`; + }); + return localChanged ? { ...n, tags: nt } : n; + })); + } + + console.log(`[MIGRATION] Job tag migration complete. Updated ${migratedNotes} notes.`); + } + + prefs.migratedLegacyJobIdTagsV1 = true; + await pb.collection('users').update(currentUserId, { Taskgrid_pref: prefs }, { requestKey: null }); + + if (pb.authStore.model) { + pb.authStore.model.Taskgrid_pref = prefs; + } + } catch (err) { + console.error("[MIGRATION] Job tag migration failed:", err); + } +}; + export const initStore = async () => { if (!pb.authStore.isValid || store.isInitializing) return; setStore("isInitializing", true); @@ -1438,8 +1537,9 @@ export const initStore = async () => { // Fire off background sync await backgroundSync(); - // Run one-time migration for legacy tags + // Run one-time migrations await runLegacyTagMigration(); + await runLegacyJobIdTagMigration(); // Separate Templates load (less critical) try {