import React, { useState, useEffect } from 'react'; import { Group, Member, Subscription, OneOffExpense, SettlementRecord, MemberBalance, DebtTransaction, } from './types'; import { INITIAL_GROUPS, INITIAL_MEMBERS, INITIAL_SUBSCRIPTIONS, INITIAL_ONE_OFF_EXPENSES, INITIAL_SETTLEMENTS, } from './data/initialData'; import { calculateAllMemberBalances, calculateSimplifiedDebts, getMonthlyCost } from './utils/splitCalculator'; import { Header } from './components/Header'; import { Dashboard } from './components/Dashboard'; import { SubscriptionList } from './components/SubscriptionList'; import { SubscriptionModal } from './components/SubscriptionModal'; import { OneOffExpenseList } from './components/OneOffExpenseList'; import { OneOffExpenseModal } from './components/OneOffExpenseModal'; import { GroupMemberManager } from './components/GroupMemberManager'; import { SettlementModal } from './components/SettlementModal'; import { SettlementLog } from './components/SettlementLog'; import { BreakdownModal } from './components/BreakdownModal'; import { MemberBreakdownModal } from './components/MemberBreakdownModal'; import { ShareReportModal } from './components/ShareReportModal'; export default function App() { // Persistence Keys const STORAGE_KEY_GROUPS = 'subsplit_groups_v1'; const STORAGE_KEY_MEMBERS = 'subsplit_members_v1'; const STORAGE_KEY_SUBS = 'subsplit_subs_v1'; const STORAGE_KEY_ONE_OFF = 'subsplit_one_off_v1'; const STORAGE_KEY_SETTLEMENTS = 'subsplit_settlements_v1'; // State const [groups, setGroups] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY_GROUPS); return saved ? JSON.parse(saved) : INITIAL_GROUPS; }); const [members, setMembers] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY_MEMBERS); return saved ? JSON.parse(saved) : INITIAL_MEMBERS; }); const [subscriptions, setSubscriptions] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY_SUBS); return saved ? JSON.parse(saved) : INITIAL_SUBSCRIPTIONS; }); const [oneOffExpenses, setOneOffExpenses] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY_ONE_OFF); return saved ? JSON.parse(saved) : INITIAL_ONE_OFF_EXPENSES; }); const [settlements, setSettlements] = useState(() => { const saved = localStorage.getItem(STORAGE_KEY_SETTLEMENTS); return saved ? JSON.parse(saved) : INITIAL_SETTLEMENTS; }); // Save to LocalStorage useEffect(() => { localStorage.setItem(STORAGE_KEY_GROUPS, JSON.stringify(groups)); }, [groups]); useEffect(() => { localStorage.setItem(STORAGE_KEY_MEMBERS, JSON.stringify(members)); }, [members]); useEffect(() => { localStorage.setItem(STORAGE_KEY_SUBS, JSON.stringify(subscriptions)); }, [subscriptions]); useEffect(() => { localStorage.setItem(STORAGE_KEY_ONE_OFF, JSON.stringify(oneOffExpenses)); }, [oneOffExpenses]); useEffect(() => { localStorage.setItem(STORAGE_KEY_SETTLEMENTS, JSON.stringify(settlements)); }, [settlements]); // Tab & Modal Navigation const [activeTab, setActiveTab] = useState<'dashboard' | 'subscriptions' | 'one-off' | 'members' | 'settlements'>('dashboard'); // Modal States const [isSubModalOpen, setIsSubModalOpen] = useState(false); const [editingSub, setEditingSub] = useState(null); const [isOneOffModalOpen, setIsOneOffModalOpen] = useState(false); const [editingOneOff, setEditingOneOff] = useState(null); const [isSettleModalOpen, setIsSettleModalOpen] = useState(false); const [settleFromId, setSettleFromId] = useState(); const [settleToId, setSettleToId] = useState(); const [settleDefaultAmount, setSettleDefaultAmount] = useState(); const [breakdownSub, setBreakdownSub] = useState(null); const [selectedMemberBalance, setSelectedMemberBalance] = useState(null); const [isShareReportOpen, setIsShareReportOpen] = useState(false); // Computed Balances & Debts const balances: MemberBalance[] = calculateAllMemberBalances(subscriptions, oneOffExpenses, members, groups, settlements); const debts: DebtTransaction[] = calculateSimplifiedDebts(balances); const activeSubs = subscriptions.filter((s) => s.status === 'active'); const totalMonthlyCost = activeSubs.reduce( (acc, s) => acc + getMonthlyCost(s.cost, s.billingCycle), 0 ); // Handlers for Subscriptions const handleSaveSub = (sub: Subscription) => { const exists = subscriptions.some((s) => s.id === sub.id); if (exists) { setSubscriptions(subscriptions.map((s) => (s.id === sub.id ? sub : s))); } else { setSubscriptions([...subscriptions, sub]); } }; const handleDeleteSub = (subId: string) => { if (window.confirm('Are you sure you want to delete this subscription?')) { setSubscriptions(subscriptions.filter((s) => s.id !== subId)); } }; const handleToggleSubStatus = (subId: string) => { setSubscriptions( subscriptions.map((s) => s.id === subId ? { ...s, status: s.status === 'active' ? 'paused' : 'active' } : s ) ); }; // Handlers for One-Off Expenses const handleSaveOneOff = (expense: OneOffExpense) => { const exists = oneOffExpenses.some((e) => e.id === expense.id); if (exists) { setOneOffExpenses(oneOffExpenses.map((e) => (e.id === expense.id ? expense : e))); } else { setOneOffExpenses([expense, ...oneOffExpenses]); } }; const handleDeleteOneOff = (expenseId: string) => { if (window.confirm('Delete this one-off shared charge?')) { setOneOffExpenses(oneOffExpenses.filter((e) => e.id !== expenseId)); } }; // Handlers for Members & Groups const handleAddMember = (m: Omit) => { const newMember: Member = { ...m, id: `mem-${Date.now()}` }; setMembers([...members, newMember]); }; const handleEditMember = (m: Member) => { setMembers(members.map((item) => (item.id === m.id ? m : item))); }; const handleDeleteMember = (memberId: string) => { if (window.confirm('Remove this member?')) { setMembers(members.filter((m) => m.id !== memberId)); } }; const handleAddGroup = (g: Omit) => { const newGroup: Group = { ...g, id: `grp-${Date.now()}` }; setGroups([...groups, newGroup]); }; const handleEditGroup = (g: Group) => { setGroups(groups.map((item) => (item.id === g.id ? g : item))); }; const handleDeleteGroup = (groupId: string) => { if (window.confirm('Delete this family group? Members in this group will become individual.')) { setGroups(groups.filter((g) => g.id !== groupId)); setMembers(members.map((m) => (m.groupId === groupId ? { ...m, groupId: undefined } : m))); } }; // Settlement Handlers const handleOpenSettleModal = (fromId?: string, toId?: string, defaultAmt?: number) => { setSettleFromId(fromId); setSettleToId(toId); setSettleDefaultAmount(defaultAmt); setIsSettleModalOpen(true); }; const handleSaveSettlement = (record: Omit) => { const newRecord: SettlementRecord = { ...record, id: `settle-${Date.now()}` }; setSettlements([newRecord, ...settlements]); }; const handleDeleteSettlement = (id: string) => { setSettlements(settlements.filter((s) => s.id !== id)); }; const handleResetData = () => { if (window.confirm('Reset all subscriptions, one-off charges, members, and settlement records to initial seed data?')) { setGroups(INITIAL_GROUPS); setMembers(INITIAL_MEMBERS); setSubscriptions(INITIAL_SUBSCRIPTIONS); setOneOffExpenses(INITIAL_ONE_OFF_EXPENSES); setSettlements(INITIAL_SETTLEMENTS); } }; return (
{/* Top Header & Navigation */}
{ setEditingSub(null); setIsSubModalOpen(true); }} onOpenAddExpense={() => { setEditingOneOff(null); setIsOneOffModalOpen(true); }} onOpenRecordSettlement={() => handleOpenSettleModal()} onOpenShareReport={() => setIsShareReportOpen(true)} onResetData={handleResetData} /> {/* Main Container */}
{activeTab === 'dashboard' && ( setBreakdownSub(sub)} onOpenMemberBreakdownModal={(bal) => setSelectedMemberBalance(bal)} onOpenAddExpense={() => { setEditingOneOff(null); setIsOneOffModalOpen(true); }} onOpenAddSub={() => { setEditingSub(null); setIsSubModalOpen(true); }} /> )} {activeTab === 'subscriptions' && ( { setEditingSub(null); setIsSubModalOpen(true); }} onEditSub={(sub) => { setEditingSub(sub); setIsSubModalOpen(true); }} onDeleteSub={handleDeleteSub} onToggleStatus={handleToggleSubStatus} onOpenBreakdownModal={(sub) => setBreakdownSub(sub)} /> )} {activeTab === 'one-off' && ( { setEditingOneOff(null); setIsOneOffModalOpen(true); }} onEditExpense={(exp) => { setEditingOneOff(exp); setIsOneOffModalOpen(true); }} onDeleteExpense={handleDeleteOneOff} /> )} {activeTab === 'members' && ( )} {activeTab === 'settlements' && ( handleOpenSettleModal()} onDeleteSettlement={handleDeleteSettlement} /> )}
{/* Subscription Modal (Create / Edit) */} setIsSubModalOpen(false)} onSave={handleSaveSub} initialData={editingSub} members={members} groups={groups} /> {/* One-Off Expense Modal (Create / Edit) */} setIsOneOffModalOpen(false)} onSave={handleSaveOneOff} initialData={editingOneOff} members={members} groups={groups} /> {/* Settlement Payment Modal */} setIsSettleModalOpen(false)} onSaveSettlement={handleSaveSettlement} members={members} defaultFromId={settleFromId} defaultToId={settleToId} defaultAmount={settleDefaultAmount} /> {/* Subscription Math Breakdown Modal */} setBreakdownSub(null)} members={members} groups={groups} /> {/* Member Balance Statement Modal */} setSelectedMemberBalance(null)} members={members} groups={groups} /> {/* Share/Export Chat Report Modal */} setIsShareReportOpen(false)} balances={balances} debts={debts} subscriptions={subscriptions} oneOffExpenses={oneOffExpenses} />
); }