Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@
## 2024-05-18 - [Extract Repeated array filter into O(N) reduce]
**Learning:** Multiple consecutive `.filter()` assignments on identical arrays (like `P.tickets`) in heavily invoked functions (`buildContext`, `generateLocalResponse`) degrade overall component efficiency by repeatedly traversing arrays O(N) over N times when a single loop suffices.
**Action:** Consolidate multiple independent `.filter()` passes and sum calculations (`.reduce`) into a single `.reduce()` step. By pushing matches to category-specific accumulator arrays and counting aggregate fields (like sums/counts) simultaneously, you accomplish what took ~5 traversals into one O(N) pass, reducing time complexity significantly (benchmarked from ~350ms to ~45ms for large arrays).
## 2024-05-19 - Avoid repeated Array.filter().length inside JSX loops
**Learning:** Found multiple instances where `.filter().length` was called directly inside render maps (e.g., in MaintenanceModule, ProjectsModule) combined with multiple separate filter and reduce calls (e.g., DocumentsModule). This causes multiple O(N) array traversals on every single render.
**Action:** Replaced direct `.filter().length` calls inside JSX loops with a single `useMemo` block using `.reduce()` to calculate all necessary counts in a single O(N) pass, and replaced multiple passes with a single loop mapping entries by category.
30 changes: 23 additions & 7 deletions src/components/DocumentsModule.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { cd, Badge } from "@/lib/data";
import { Modal } from "./Modal";

Expand All @@ -21,8 +21,27 @@ export default function Doc({ docs, setDocs, showToast }) {
const [form, setForm] = useState(null);
const [filter, setFilter] = useState("all");

const expired = docs.filter(d => d.expired || (d.expiry && daysUntil(d.expiry) < 0));
const expiring = docs.filter(d => !d.expired && d.expiry && daysUntil(d.expiry) >= 0 && daysUntil(d.expiry) <= 30);
// ⚡ Bolt Optimization: Memoize the derived document groups to avoid recalculating on every render
const { expired, expiring, flt } = useMemo(() => {
const expiredArr = [];
const expiringArr = [];
const typedArr = [];

for (const d of docs) {
const isExpired = d.expired || (d.expiry && daysUntil(d.expiry) < 0);
const isExpiring = !isExpired && d.expiry && daysUntil(d.expiry) >= 0 && daysUntil(d.expiry) <= 30;

if (isExpired) expiredArr.push(d);
if (isExpiring) expiringArr.push(d);

if (filter === "all") typedArr.push(d);
else if (filter === "expired" && isExpired) typedArr.push(d);
else if (filter === "expiring" && isExpiring) typedArr.push(d);
else if (filter !== "expired" && filter !== "expiring" && d.type === filter) typedArr.push(d);
}

return { expired: expiredArr, expiring: expiringArr, flt: typedArr };
}, [docs, filter]);

const handleAdd = () => {
setForm({ name: "", type: "policy", expiry: "", size: "", expired: false });
Expand Down Expand Up @@ -56,10 +75,7 @@ export default function Doc({ docs, setDocs, showToast }) {
showToast("Document deleted");
};

const flt = filter === "all" ? docs
: filter === "expired" ? docs.filter(d => d.expired || (d.expiry && daysUntil(d.expiry) < 0))
: filter === "expiring" ? docs.filter(d => !d.expired && d.expiry && daysUntil(d.expiry) >= 0 && daysUntil(d.expiry) <= 30)
: docs.filter(d => d.type === filter);


return (
<div>
Expand Down
27 changes: 20 additions & 7 deletions src/components/MaintenanceModule.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { cd, statusColor, SB, PD } from "@/lib/data";
import { Modal } from "./Modal";
import { api } from "@/lib/api";
Expand All @@ -19,11 +19,24 @@ export default function Mnt({ tasks, setTasks, showToast, staff, assets }) {
{ k: "completed", l: "Done" },
];

const flt = tasks.filter((t) => {
if (tab !== "all" && t.status !== tab) return false;
if (search && !t.asset.toLowerCase().includes(search.toLowerCase())) return false;
return true;
});
// ⚡ Bolt Optimization: Memoize filter and extract loop invariants
// Prevents re-evaluating search.toLowerCase() on every array element, and memoizes the filtered result.
const flt = useMemo(() => {
const searchLower = search ? search.toLowerCase() : "";
return tasks.filter((t) => {
if (tab !== "all" && t.status !== tab) return false;
if (searchLower && !t.asset.toLowerCase().includes(searchLower)) return false;
return true;
});
}, [tasks, tab, search]);

// ⚡ Bolt Optimization: Memoize status counts to prevent multiple O(N) passes on every render
const counts = useMemo(() => {
return tasks.reduce((acc, t) => {
acc[t.status] = (acc[t.status] || 0) + 1;
return acc;
}, {});
}, [tasks]);

const done = (id) => {
setTasks((p) => p.map((t) => (t.id === id ? { ...t, status: "completed", completedAt: new Date().toISOString().split("T")[0], remark } : t)));
Expand Down Expand Up @@ -88,7 +101,7 @@ export default function Mnt({ tasks, setTasks, showToast, staff, assets }) {
<input value={search} onChange={(e) => setSearch(e.target.value)} placeholder="🔍 Search tasks/assets..." style={{ width: "100%", border: "1px solid #e5e7eb", borderRadius: 12, padding: "10px 14px", fontSize: 13, outline: "none", marginBottom: 10, boxSizing: "border-box" }} />
<div style={{ display: "flex", gap: 6, overflowX: "auto", paddingBottom: 4, marginBottom: 12 }}>
{tabs.map((t) => {
const c = t.k === "all" ? tasks.length : tasks.filter((x) => x.status === t.k).length;
const c = t.k === "all" ? tasks.length : (counts[t.k] || 0);
return (
<button key={t.k} onClick={() => setTab(t.k)} style={{ padding: "5px 12px", borderRadius: 999, border: `1px solid ${tab === t.k ? "#4f46e5" : "#e5e7eb"}`, background: tab === t.k ? "#4f46e5" : "#fff", color: tab === t.k ? "#fff" : "#64748b", fontSize: 11, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap", flexShrink: 0 }}>
{t.l} ({c})
Expand Down
17 changes: 14 additions & 3 deletions src/components/ProjectsModule.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { cd, SB, Badge } from "@/lib/data";
import { Modal } from "./Modal";

Expand Down Expand Up @@ -40,7 +40,18 @@ export default function Prj({ projects, setProjects, showToast, staff }) {
showToast("Project deleted");
};

const flt = filter === "all" ? projects : projects.filter(p => p.status === filter);
// ⚡ Bolt Optimization: Memoize filter array
const flt = useMemo(() => {
return filter === "all" ? projects : projects.filter(p => p.status === filter);
}, [projects, filter]);

// ⚡ Bolt Optimization: Memoize status counts to prevent multiple O(n) passes on every render
const counts = useMemo(() => {
return projects.reduce((acc, p) => {
acc[p.status] = (acc[p.status] || 0) + 1;
return acc;
}, {});
}, [projects]);

return (
<div>
Expand All @@ -54,7 +65,7 @@ export default function Prj({ projects, setProjects, showToast, staff }) {
<div style={{ display: "flex", gap: 6, overflowX: "auto", marginBottom: 12 }}>
{[["all", "All"], ["planned", "Planned"], ["in-progress", "Active"], ["completed", "Done"], ["on-hold", "On Hold"]].map(([v, l]) => (
<button key={v} onClick={() => setFilter(v)} style={{ padding: "5px 12px", borderRadius: 999, border: `1px solid ${filter === v ? "#7c3aed" : "#e5e7eb"}`, background: filter === v ? "#7c3aed" : "#fff", color: filter === v ? "#fff" : "#64748b", fontSize: 11, fontWeight: 600, cursor: "pointer", whiteSpace: "nowrap" }}>
{l} ({v === "all" ? projects.length : projects.filter(p => p.status === v).length})
{l} ({v === "all" ? projects.length : counts[v] || 0})
</button>
))}
</div>
Expand Down