From 8ad5499f06adb3a581559ac08b8a5b2de11eb7e0 Mon Sep 17 00:00:00 2001 From: Timothy Cardoza Date: Fri, 13 Mar 2026 11:05:33 -0500 Subject: [PATCH] filtering by status added --- src/components/FilterBar.tsx | 181 +++++++++++++++++++++++++++++++- src/components/NotesSidebar.tsx | 65 ++++++++++++ src/store/index.ts | 18 +++- 3 files changed, 259 insertions(+), 5 deletions(-) diff --git a/src/components/FilterBar.tsx b/src/components/FilterBar.tsx index e8e8b29..f1b5501 100644 --- a/src/components/FilterBar.tsx +++ b/src/components/FilterBar.tsx @@ -17,6 +17,8 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr const [tagSearch, setTagSearch] = createSignal(""); const [newTemplateName, setNewTemplateName] = createSignal(""); const [isSaving, setIsSaving] = createSignal(false); + const [isJobStatusOpen, setIsJobStatusOpen] = createSignal(false); + const [isJobDivisionOpen, setIsJobDivisionOpen] = createSignal(false); const mode = () => props.mode || 'tasks'; @@ -30,13 +32,36 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr const hasActiveFilters = () => { const f = currentFilter(); const baseActive = (f.query || "") !== "" || (f.tags || []).length > 0 || f.editedToday || f.ownedByMe || f.starred; + const jobActive = (f.jobStatus?.length || 0) > 0 || (f.jobDivision?.length || 0) > 0; if (mode() === 'tasks') { return baseActive || f.priorityMin !== 1 || f.priorityMax !== 10 || f.urgencyMin !== 1 || f.urgencyMax !== 10; } - return baseActive; + return baseActive || jobActive; }; const allTags = () => store.tagDefinitions.map(d => d.name).sort(); + const JOB_STATUSES = [ + "Estimating", + "Est Sent", + "Est Hold", + "Follow Up", + "Awarded", + "In Progress", + "On Hold", + "Billed (In Progress)", + "Billed (Closed)", + "Archive", + "Not Awarded", + "Not Bidding" + ]; + const JOB_DIVISIONS = ["C#", "R#", "S#"]; + const normalizeFilterTags = (items: Array | undefined) => { + if (!items) return [] as { name: string; excluded: boolean }[]; + return items.map((i: any) => { + if (typeof i === "string") return { name: i, excluded: false }; + return { name: i.name, excluded: !!i.excluded }; + }); + }; return (
@@ -240,6 +265,160 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
+ +
+
+ +
+ + {(status) => ( + +
{ + const current = normalizeFilterTags(currentFilter().jobStatus as any); + const next = current.map(s => + s.name === status.name ? { ...s, excluded: !s.excluded } : s + ); + currentSetFilter({ jobStatus: next as any }); + }} + > + }> + + + {status.name} +
+ +
+ )} +
+ + + + Status + + + + + + No status found. + + + {(status) => ( + { + const current = normalizeFilterTags(currentFilter().jobStatus as any); + if (!current.some(s => s.name === status)) { + currentSetFilter({ jobStatus: [...current, { name: status, excluded: false }] as any }); + } + setIsJobStatusOpen(false); + }} + > + {status} + + )} + + + + + + +
+
+
+ +
+ + {(division) => ( + +
{ + const current = normalizeFilterTags(currentFilter().jobDivision as any); + const next = current.map(d => + d.name === division.name ? { ...d, excluded: !d.excluded } : d + ); + currentSetFilter({ jobDivision: next as any }); + }} + > + }> + + + {division.name} +
+ +
+ )} +
+ + + + Division + + + + + + No division found. + + + {(division) => ( + { + const current = normalizeFilterTags(currentFilter().jobDivision as any); + if (!current.some(d => d.name === division)) { + currentSetFilter({ jobDivision: [...current, { name: division, excluded: false }] as any }); + } + setIsJobDivisionOpen(false); + }} + > + {division} + + )} + + + + + + +
+
+
+
{/* Tags Multi-select */}
diff --git a/src/components/NotesSidebar.tsx b/src/components/NotesSidebar.tsx index 0ef82f5..5669cee 100644 --- a/src/components/NotesSidebar.tsx +++ b/src/components/NotesSidebar.tsx @@ -33,6 +33,58 @@ export const NotesSidebar: Component<{ return new Set(); } }); + const [jobFilterIds] = createResource( + () => { + const f = store.noteFilter; + const normalize = (items: Array | undefined) => { + if (!items) return [] as { name: string; excluded: boolean }[]; + return items.map((i: any) => (typeof i === "string" ? { name: i, excluded: false } : { name: i.name, excluded: !!i.excluded })); + }; + const status = normalize(f.jobStatus as any).sort((a, b) => a.name.localeCompare(b.name)); + const division = normalize(f.jobDivision as any).sort((a, b) => a.name.localeCompare(b.name)); + const key = JSON.stringify({ status, division }); + return key; + }, + async (key) => { + if (!key) return null; + const parsed = JSON.parse(key) as { status: Array<{ name: string; excluded: boolean }>; division: Array<{ name: string; excluded: boolean }> }; + const status = parsed.status || []; + const division = parsed.division || []; + if (status.length === 0 && division.length === 0) return null; + + const parts: string[] = []; + const statusInclude = status.filter(s => !s.excluded).map(s => s.name); + const statusExclude = status.filter(s => s.excluded).map(s => s.name); + const divisionInclude = division.filter(d => !d.excluded).map(d => d.name); + const divisionExclude = division.filter(d => d.excluded).map(d => d.name); + + if (statusInclude.length > 0) { + parts.push(`(${statusInclude.map(s => `Job_Status = "${s}"`).join(" || ")})`); + } + if (statusExclude.length > 0) { + parts.push(`(${statusExclude.map(s => `Job_Status != "${s}"`).join(" && ")})`); + } + if (divisionInclude.length > 0) { + parts.push(`(${divisionInclude.map(d => `Job_Division = "${d}"`).join(" || ")})`); + } + if (divisionExclude.length > 0) { + parts.push(`(${divisionExclude.map(d => `Job_Division != "${d}"`).join(" && ")})`); + } + + const filter = parts.join(" && "); + try { + const items = await pb.collection('Job_Info_Prod').getFullList({ + filter, + fields: "id", + requestKey: null + }); + return new Set(items.map((r: any) => String(r.id))); + } catch (e) { + console.warn("Failed to load job filter list for notes:", e); + return new Set(); + } + } + ); const filteredNotes = createMemo(() => { const filter = store.noteFilter; @@ -90,6 +142,19 @@ export const NotesSidebar: Component<{ }); } + // Job Status / Division Filter (notes only) + if ((filter.jobStatus?.length || 0) > 0 || (filter.jobDivision?.length || 0) > 0) { + const allowedJobIds = jobFilterIds(); + n = n.filter(note => { + if (!note.tags?.includes("prism_job")) return false; + if (!allowedJobIds) return false; + const jobIdTag = note.tags.find(t => t.startsWith("job_id:")); + const jobId = jobIdTag?.split(":")[1]?.trim(); + if (!jobId) return false; + return allowedJobIds.has(jobId); + }); + } + // Starred Filter if (filter.starred) { n = n.filter(note => note.tags?.includes(getFavoriteTag())); diff --git a/src/store/index.ts b/src/store/index.ts index c2b191e..732e377 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -171,6 +171,8 @@ export interface Filter { editedToday: boolean; ownedByMe: boolean; starred: boolean; + jobStatus: FilterTag[]; + jobDivision: FilterTag[]; } // Background sync for Context Switcher (polling fallback for realtime) @@ -298,7 +300,9 @@ export const [store, setStore] = createStore({ urgencyMax: 10, editedToday: false, ownedByMe: false, - starred: false + starred: false, + jobStatus: [], + jobDivision: [] }, shareRules: [], filterTemplates: [], @@ -316,7 +320,9 @@ export const [store, setStore] = createStore({ urgencyMax: 10, editedToday: false, ownedByMe: false, - starred: false + starred: false, + jobStatus: [], + jobDivision: [] } }); @@ -2736,7 +2742,9 @@ export const clearFilter = () => { urgencyMax: 10, editedToday: false, ownedByMe: false, - starred: false + starred: false, + jobStatus: [], + jobDivision: [] }); }; @@ -2818,7 +2826,9 @@ export const clearNoteFilter = () => { urgencyMax: 10, editedToday: false, ownedByMe: false, - starred: false + starred: false, + jobStatus: [], + jobDivision: [] }); syncPreferences(); };