filtering by status added
This commit is contained in:
@@ -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<string | { name: string; excluded: boolean }> | 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 (
|
||||
<div class={cn(mode() === 'tasks' ? "fixed top-4 right-5 sm:right-6 z-[60]" : "relative", props.class)}>
|
||||
@@ -240,6 +265,160 @@ export const FilterBar: Component<{ mode?: 'tasks' | 'notes', class?: string, tr
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={mode() === 'notes'}>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Job Status</label>
|
||||
<div class="flex flex-wrap gap-2 min-h-[44px] p-2 bg-muted/10 border border-dashed border-border/60 rounded-xl">
|
||||
<For each={normalizeFilterTags(currentFilter().jobStatus as any)}>
|
||||
{(status) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class={cn(
|
||||
"h-7 px-2.5 rounded-lg text-xs gap-2 transition-all border shadow-sm",
|
||||
status.excluded
|
||||
? "bg-red-500/10 text-red-600 border-red-500/20 hover:bg-red-500/20"
|
||||
: "bg-blue-500/10 text-blue-600 border-blue-500/20 hover:bg-blue-500/20"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 cursor-pointer h-full"
|
||||
onClick={() => {
|
||||
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 });
|
||||
}}
|
||||
>
|
||||
<Show when={status.excluded} fallback={<Tag size={12} class="opacity-70" />}>
|
||||
<Minus size={12} class="opacity-100" />
|
||||
</Show>
|
||||
<span class={cn(status.excluded && "line-through opacity-70")}>{status.name}</span>
|
||||
</div>
|
||||
<button
|
||||
class="p-0.5 hover:bg-foreground/10 rounded transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const next = normalizeFilterTags(currentFilter().jobStatus as any).filter(s => s.name !== status.name);
|
||||
currentSetFilter({ jobStatus: next as any });
|
||||
}}
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
<Popover open={isJobStatusOpen()} onOpenChange={setIsJobStatusOpen}>
|
||||
<PopoverTrigger class="h-7 px-3 bg-muted/40 border-none shadow-none focus:ring-0 text-[0.625rem] font-bold uppercase tracking-wider rounded-lg hover:bg-muted/60 transition-colors">
|
||||
<span>+ Status</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[220px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search status..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No status found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<For each={JOB_STATUSES}>
|
||||
{(status) => (
|
||||
<CommandItem
|
||||
value={status}
|
||||
onSelect={() => {
|
||||
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}
|
||||
</CommandItem>
|
||||
)}
|
||||
</For>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2.5">
|
||||
<label class="text-[0.625rem] font-black uppercase tracking-widest text-muted-foreground ml-1">Job Division</label>
|
||||
<div class="flex flex-wrap gap-2 min-h-[44px] p-2 bg-muted/10 border border-dashed border-border/60 rounded-xl">
|
||||
<For each={normalizeFilterTags(currentFilter().jobDivision as any)}>
|
||||
{(division) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
class={cn(
|
||||
"h-7 px-2.5 rounded-lg text-xs gap-2 transition-all border shadow-sm",
|
||||
division.excluded
|
||||
? "bg-red-500/10 text-red-600 border-red-500/20 hover:bg-red-500/20"
|
||||
: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20 hover:bg-emerald-500/20"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 cursor-pointer h-full"
|
||||
onClick={() => {
|
||||
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 });
|
||||
}}
|
||||
>
|
||||
<Show when={division.excluded} fallback={<Tag size={12} class="opacity-70" />}>
|
||||
<Minus size={12} class="opacity-100" />
|
||||
</Show>
|
||||
<span class={cn(division.excluded && "line-through opacity-70")}>{division.name}</span>
|
||||
</div>
|
||||
<button
|
||||
class="p-0.5 hover:bg-foreground/10 rounded transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const next = normalizeFilterTags(currentFilter().jobDivision as any).filter(d => d.name !== division.name);
|
||||
currentSetFilter({ jobDivision: next as any });
|
||||
}}
|
||||
>
|
||||
<X size={10} />
|
||||
</button>
|
||||
</Badge>
|
||||
)}
|
||||
</For>
|
||||
<Popover open={isJobDivisionOpen()} onOpenChange={setIsJobDivisionOpen}>
|
||||
<PopoverTrigger class="h-7 px-3 bg-muted/40 border-none shadow-none focus:ring-0 text-[0.625rem] font-bold uppercase tracking-wider rounded-lg hover:bg-muted/60 transition-colors">
|
||||
<span>+ Division</span>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search division..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No division found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
<For each={JOB_DIVISIONS}>
|
||||
{(division) => (
|
||||
<CommandItem
|
||||
value={division}
|
||||
onSelect={() => {
|
||||
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}
|
||||
</CommandItem>
|
||||
)}
|
||||
</For>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
{/* Tags Multi-select */}
|
||||
<div class="space-y-2.5">
|
||||
|
||||
@@ -33,6 +33,58 @@ export const NotesSidebar: Component<{
|
||||
return new Set<string>();
|
||||
}
|
||||
});
|
||||
const [jobFilterIds] = createResource(
|
||||
() => {
|
||||
const f = store.noteFilter;
|
||||
const normalize = (items: Array<string | { name: string; excluded: boolean }> | 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<string>();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
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()));
|
||||
|
||||
+14
-4
@@ -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<TaskStore>({
|
||||
urgencyMax: 10,
|
||||
editedToday: false,
|
||||
ownedByMe: false,
|
||||
starred: false
|
||||
starred: false,
|
||||
jobStatus: [],
|
||||
jobDivision: []
|
||||
},
|
||||
shareRules: [],
|
||||
filterTemplates: [],
|
||||
@@ -316,7 +320,9 @@ export const [store, setStore] = createStore<TaskStore>({
|
||||
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();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user