/* Tables, filters, charts and main screens for Kisongi Farm Tracker. */ const PALETTE = ["#2E7D32", "#FBC02D", "#558B2F", "#8D6E63", "#0288D1", "#7B1FA2", "#C2185B", "#F57C00", "#00897B", "#5D4037", "#9E9D24"]; /* ---------------- Filter bar ---------------- */ function FilterBar({ kind, filters, setFilters, options, onReset }) { const isExpense = kind === "expense"; return (
setFilters({ ...filters, from: e.target.value })} aria-label="From"/> setFilters({ ...filters, to: e.target.value })} aria-label="To"/>
setFilters({ ...filters, cats: v })} options={options} label={isExpense ? "Categories" : "Sources"}/> } placeholder="Search description or notes…" value={filters.q || ""} onChange={(e) => setFilters({ ...filters, q: e.target.value })}/>
); } /* ---------------- Records table ---------------- */ function RecordsTable({ kind, rows, role, onEdit, onDelete, onView, loading }) { const isExpense = kind === "expense"; const labelCol = isExpense ? "Category" : "Source"; const colKey = isExpense ? "category" : "source"; const sorted = useMemo(() => [...rows].sort((a,b) => { const ad = a.date || ""; const bd = b.date || ""; return ad.localeCompare(bd) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0); }), [rows]); let running = 0; const withRunning = sorted.map(r => { running += (r.amount || 0); return { ...r, _running: running }; }); const total = running; const [swipeId, setSwipeId] = useState(null); const swipeStart = useRef({ x: 0, id: null }); const onTouchStart = (e, id) => { if (role !== "admin") return; swipeStart.current = { x: e.touches[0].clientX, id }; }; const onTouchMove = (e, id) => { if (role !== "admin" || swipeStart.current.id !== id) return; const dx = e.touches[0].clientX - swipeStart.current.x; if (dx < -40) setSwipeId(id); else if (dx > 10) setSwipeId(null); }; return (
{loading && Array.from({length: 6}).map((_, i) => )} {!loading && withRunning.map((r, i) => ( ))} {!loading && rows.length > 0 && ( )}
# Date {labelCol} Description Amount Running Notes
{i + 1} {formatDate(r.date)} {r[colKey]} {r.description} {formatUGX(r.amount, { bare: true })} {formatUGX(r._running, { bare: true })} {r.notes || "—"}
{role === "admin" ? ( <> ) : ( )}
Total {formatUGX(total, { bare: true })}
); } function hashIdx(s) { let h = 0; for (let i = 0; i < (s || "").length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return Math.abs(h) % PALETTE.length; } /* ---------------- Empty states ---------------- */ function EmptyState({ kind, filtered, role, onAdd, onReset }) { if (filtered) { return (

No {kind} match your filters.

); } return (

No {kind} yet.

{role === "admin" ? `Tap "Add ${kind === "expenses" ? "expense" : "income"}" to log your first one.` : "Ask the admin to add the first entry."}

{role === "admin" && ( )}
); } /* ---------------- Add / Edit form (controlled by parent via formRef) ---------------- */ function RecordForm({ kind, initial, formRef, onDeleteRequest }) { const isExpense = kind === "expense"; const [date, setDate] = useState(initial?.date || todayISO()); const [amount, setAmount] = useState(initial?.amount?.toString() || ""); const [description, setDescription] = useState(initial?.description || ""); const [category, setCategory] = useState(initial?.[isExpense ? "category" : "source"] || ""); const [notes, setNotes] = useState(initial?.notes || ""); const options = isExpense ? window.CATEGORIES : window.SOURCES; // Expose collect() to parent via ref useEffect(() => { if (!formRef) return; formRef.current = () => { if (!date || !description || !category || amount === "") return null; return { ...(initial || {}), id: initial?.id || ("n" + Math.random().toString(36).slice(2, 8)), date, amount: parseFloat(amount) || 0, description, [isExpense ? "category" : "source"]: category, notes, }; }; }, [date, amount, description, category, notes, initial, isExpense, formRef]); const suggestion = useMemo(() => { if (!isExpense || !description || category) return null; const d = description.toLowerCase(); const map = [ ["transport", "Transport"], ["taxi", "Transport"], ["fuel", "Transport"], ["lunch", "Meals"], ["meal", "Meals"], ["breakfast", "Meals"], ["food", "Meals"], ["seedling", "Seedlings"], ["sucker", "Seedlings"], ["hoe", "Tools"], ["panga", "Tools"], ["tool", "Tools"], ["dig", "Land Preparation"], ["tractor", "Land Preparation"], ["land", "Land Preparation"], ["chairman", "Community/Admin"], ["lc", "Community/Admin"], ["agronomist", "Labor"], ["labor", "Labor"], ["worker", "Labor"], ["padlock", "Supplies"], ["supplies", "Supplies"], ["string", "Supplies"], ["measure", "Supplies"], ["boot", "PPE/Equipment"], ["glove", "PPE/Equipment"], ["goodwill", "Goodwill"], ]; for (const [k, v] of map) if (d.includes(k)) return v; return null; }, [description, category, isExpense]); return (
setDate(e.target.value)} className="mt-1"/>
setAmount(e.target.value)} placeholder="0" className="mt-1 h-14 w-full rounded-lg border border-stone-200 bg-white px-3 text-2xl font-mono tabular-nums focus:outline-none focus:ring-2 focus:ring-emerald-600/30 focus:border-emerald-600/40" />
setDescription(e.target.value)} placeholder={isExpense ? "e.g. Transport to Kisongi" : "e.g. Sale of trees"} className="mt-1"/> {suggestion && ( )}