Skip to content
Merged
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
4 changes: 3 additions & 1 deletion ui/src/components/MobileNav.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { NavLink } from 'react-router-dom';
import { cn } from '../lib/utils';
import { LayoutDashboard, Zap, Receipt, ShieldAlert, Wallet, Settings } from 'lucide-react';
import { LayoutDashboard, Zap, Receipt, ShieldAlert, Wallet, Scale, ListChecks, Settings } from 'lucide-react';

const navItems = [
{ path: '/', label: 'Home', icon: LayoutDashboard },
{ path: '/queue', label: 'Queue', icon: Zap },
{ path: '/bills', label: 'Bills', icon: Receipt },
{ path: '/disputes', label: 'Disputes', icon: ShieldAlert },
{ path: '/legal', label: 'Legal', icon: Scale },
{ path: '/tasks', label: 'Tasks', icon: ListChecks },
{ path: '/accounts', label: 'Accounts', icon: Wallet },
{ path: '/settings', label: 'Settings', icon: Settings },
];
Expand Down
4 changes: 3 additions & 1 deletion ui/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { NavLink } from 'react-router-dom';
import { cn } from '../lib/utils';
import {
LayoutDashboard, Zap, Receipt, ShieldAlert, Wallet, Scale, Gavel,
Lightbulb, TrendingUp, Upload, Settings, LogOut,
Lightbulb, TrendingUp, Upload, Settings, LogOut, BookOpen, ListChecks,
} from 'lucide-react';
import { logout, getUser } from '../lib/auth';

Expand Down Expand Up @@ -31,13 +31,15 @@ const navGroups: NavGroup[] = [
{ path: '/disputes', label: 'Active Disputes', icon: ShieldAlert },
{ path: '/legal', label: 'Legal Deadlines', icon: Scale },
{ path: '/litigation', label: 'Litigation AI', icon: Gavel },
{ path: '/evidence', label: 'Evidence Timeline', icon: BookOpen },
],
},
{
label: 'Intelligence',
items: [
{ path: '/queue', label: 'Action Queue', icon: Zap },
{ path: '/recommendations', label: 'AI Recs', icon: Lightbulb },
{ path: '/tasks', label: 'Task Board', icon: ListChecks },
],
},
{
Expand Down
173 changes: 165 additions & 8 deletions ui/src/components/planner/RevenueSources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@ import { api, type RevenueSource } from '../../lib/api';
import { formatCurrency } from '../../lib/utils';
import { Card } from '../ui/Card';
import { ActionButton } from '../ui/ActionButton';
import { useToast } from '../../lib/toast';
import { Plus, X, Pencil, Check } from 'lucide-react';

export function RevenueSources() {
const [sources, setSources] = useState<RevenueSource[]>([]);
const [summary, setSummary] = useState({ count: 0, total_monthly: 0, weighted_monthly: 0 });
const [discovering, setDiscovering] = useState(false);
const [error, setError] = useState<string | null>(null);
const [showAdd, setShowAdd] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
const toast = useToast();

const emptyForm = { description: '', amount: '', recurrence: 'monthly', source: 'manual', confidence: '0.8' };
const [form, setForm] = useState(emptyForm);

const load = useCallback(async () => {
try {
Expand Down Expand Up @@ -38,6 +47,63 @@ export function RevenueSources() {
}
};

const handleAdd = async () => {
if (!form.description || !form.amount) return;
setSaving(true);
try {
await api.addRevenueSource({
description: form.description,
amount: form.amount,
recurrence: form.recurrence,
source: form.source,
confidence: form.confidence,
status: 'active',
});
setShowAdd(false);
setForm(emptyForm);
await load();
toast.success('Revenue source added', form.description);
} catch (e: unknown) {
toast.error('Add failed', e instanceof Error ? e.message : 'Unknown error');
} finally {
setSaving(false);
}
};

const startEdit = (src: RevenueSource) => {
setEditingId(src.id);
setForm({
description: src.description,
amount: src.amount,
recurrence: src.recurrence || 'monthly',
source: src.source,
confidence: src.confidence,
});
};

const handleUpdate = async () => {
if (!editingId || !form.description || !form.amount) return;
setSaving(true);
try {
await api.updateRevenueSource(editingId, {
description: form.description,
amount: form.amount,
recurrence: form.recurrence,
confidence: form.confidence,
});
setEditingId(null);
setForm(emptyForm);
await load();
toast.success('Revenue source updated', form.description);
} catch (e: unknown) {
toast.error('Update failed', e instanceof Error ? e.message : 'Unknown error');
} finally {
setSaving(false);
}
};

const recurrenceOptions = ['weekly', 'biweekly', 'monthly', 'quarterly', 'annually', 'one-time'];

return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
Expand All @@ -47,21 +113,105 @@ export function RevenueSources() {
{summary.count} sources | {formatCurrency(summary.weighted_monthly)}/mo (confidence-weighted)
</p>
</div>
<ActionButton
label={discovering ? 'Discovering...' : 'Discover from History'}
onClick={discover}
loading={discovering}
/>
<div className="flex gap-2">
<ActionButton
label={showAdd ? 'Cancel' : 'Add Source'}
variant={showAdd ? 'secondary' : 'primary'}
onClick={() => { setShowAdd(!showAdd); setEditingId(null); setForm(emptyForm); }}
/>
<ActionButton
label={discovering ? 'Discovering...' : 'Auto-Discover'}
variant="secondary"
onClick={discover}
loading={discovering}
/>
</div>
</div>

{error && (
<p className="text-urgency-amber text-sm">{error}</p>
{error && <p className="text-urgency-amber text-sm">{error}</p>}

{/* Add / Edit form */}
{(showAdd || editingId) && (
<Card>
<div className="space-y-3">
<h3 className="font-semibold text-card-text text-sm flex items-center gap-2">
{editingId ? <Pencil size={14} /> : <Plus size={14} />}
{editingId ? 'Edit Revenue Source' : 'New Revenue Source'}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div className="sm:col-span-2">
<label className="block text-xs font-medium text-card-muted mb-1">Description *</label>
<input
value={form.description}
onChange={(e) => setForm(f => ({ ...f, description: e.target.value }))}
placeholder="e.g. Rental income - 550 W Surf"
className="w-full px-3 py-2 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-sm focus:outline-none focus:ring-2 focus:ring-chitty-500/50"
/>
</div>
<div>
<label className="block text-xs font-medium text-card-muted mb-1">Amount *</label>
<input
type="number"
step="0.01"
value={form.amount}
onChange={(e) => setForm(f => ({ ...f, amount: e.target.value }))}
placeholder="0.00"
className="w-full px-3 py-2 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-sm focus:outline-none focus:ring-2 focus:ring-chitty-500/50"
/>
</div>
<div>
<label className="block text-xs font-medium text-card-muted mb-1">Recurrence</label>
<select
value={form.recurrence}
onChange={(e) => setForm(f => ({ ...f, recurrence: e.target.value }))}
className="w-full px-3 py-2 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-sm focus:outline-none"
>
{recurrenceOptions.map(r => <option key={r} value={r}>{r}</option>)}
</select>
</div>
<div>
<label className="block text-xs font-medium text-card-muted mb-1">Source</label>
<input
value={form.source}
onChange={(e) => setForm(f => ({ ...f, source: e.target.value }))}
placeholder="e.g. manual, plaid"
className="w-full px-3 py-2 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-sm focus:outline-none"
/>
</div>
<div>
<label className="block text-xs font-medium text-card-muted mb-1">Confidence (0-1)</label>
<input
type="number"
step="0.1"
min="0"
max="1"
value={form.confidence}
onChange={(e) => setForm(f => ({ ...f, confidence: e.target.value }))}
className="w-full px-3 py-2 rounded-lg bg-slate-50 border border-slate-200 text-card-text text-sm focus:outline-none"
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-1">
<ActionButton
label="Cancel"
variant="secondary"
onClick={() => { setShowAdd(false); setEditingId(null); setForm(emptyForm); }}
/>
<ActionButton
label={saving ? 'Saving...' : editingId ? 'Update' : 'Add'}
onClick={editingId ? handleUpdate : handleAdd}
loading={saving}
disabled={!form.description || !form.amount}
/>
</div>
</div>
</Card>
)}

{sources.length === 0 ? (
<Card className="text-center py-6">
<p className="text-card-muted">No revenue sources discovered yet.</p>
<p className="text-card-muted text-sm mt-1">Click "Discover from History" to scan transaction data.</p>
<p className="text-card-muted text-sm mt-1">Click "Auto-Discover" to scan transaction data or "Add Source" to enter manually.</p>
</Card>
) : (
<div className="space-y-2">
Expand Down Expand Up @@ -94,6 +244,13 @@ export function RevenueSources() {
: parseFloat(src.confidence) >= 0.6 ? 'bg-amber-500'
: 'bg-red-500'
}`} title={`${Math.round(parseFloat(src.confidence) * 100)}% confidence`} />
<button
onClick={() => startEdit(src)}
className="text-card-muted hover:text-chitty-500 transition-colors p-1"
title="Edit"
>
<Pencil size={14} />
</button>
</div>
</div>
</Card>
Expand Down
81 changes: 81 additions & 0 deletions ui/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,39 @@ export const api = {
spawnRecommendation: (id: string, data: { rec_type: string; priority?: number; action_type?: string; estimated_savings?: number }) =>
request<{ task_id: string; recommendation_id: string }>(`/tasks/${id}/spawn-recommendation`, { method: 'POST', body: JSON.stringify(data) }),

// Legal CRUD
createLegalDeadline: (data: {
case_ref: string;
deadline_type: string;
title: string;
deadline_date: string;
description?: string;
case_system?: string;
status?: string;
urgency_score?: number;
}) =>
request<LegalDeadline>('/legal', { method: 'POST', body: JSON.stringify(data) }),
updateLegalDeadline: (id: string, data: Partial<{
status: string;
deadline_date: string;
urgency_score: number;
}>) =>
request<LegalDeadline>(`/legal/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),

// Evidence Timeline
getCaseTimeline: (caseId: string, params?: { start?: string; end?: string }) => {
const qs = new URLSearchParams(
Object.fromEntries(Object.entries(params || {}).filter(([, v]) => v != null)),
).toString();
return request<TimelineResponse>(`/cases/${caseId}/timeline${qs ? '?' + qs : ''}`);
},
getCaseFacts: (caseId: string) =>
request<{ caseId: string; facts: TimelineFact[] }>(`/cases/${caseId}/facts`),
getCaseContradictions: (caseId: string) =>
request<{ caseId: string; contradictions: Contradiction[] }>(`/cases/${caseId}/contradictions`),
getPendingFacts: (caseId: string, limit?: number) =>
request<{ caseId: string; pending: TimelineFact[] }>(`/cases/${caseId}/pending-facts${limit ? '?limit=' + limit : ''}`),

// Litigation Assistant
litigationSynthesize: (data: { rawNotes: string; property?: string; caseNumber?: string }) =>
request<{ synthesis: string }>('/litigation/synthesize', { method: 'POST', body: JSON.stringify(data) }),
Expand Down Expand Up @@ -755,3 +788,51 @@ export interface TaskAction {
status: string;
executed_at: string;
}

// ── Evidence Timeline Types ─────────────────────────────────

export interface TimelineEvent {
id: string;
date: string;
type: 'fact' | 'deadline' | 'dispute' | 'document';
title: string;
description?: string;
source: string;
metadata?: Record<string, unknown>;
}

export interface TimelineResponse {
caseId: string;
eventCount: number;
dateRange: { earliest: string | null; latest: string | null };
sources: { facts: number; deadlines: number; disputes: number; documents: number };
events: TimelineEvent[];
warnings?: string[];
partial?: boolean;
}

export interface TimelineFact {
id: string;
case_id: string;
fact_text: string;
fact_type?: string;
fact_date?: string;
confidence?: number;
verification_status?: string;
source_quote?: string;
document_id?: string;
entities?: { name: string; entity_type: string; role?: string }[];
amounts?: { amount_value: number; currency: string; description?: string }[];
}

export interface Contradiction {
id: string;
fact_a_id: string;
fact_b_id: string;
fact_a_text: string;
fact_b_text: string;
contradiction_type: string;
severity: string;
resolution_status?: string;
explanation?: string;
}
4 changes: 4 additions & 0 deletions ui/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { CashFlow } from './pages/CashFlow';
import { Recommendations } from './pages/Recommendations';
import { Settings } from './pages/Settings';
import { ActionQueue } from './pages/ActionQueue';
import { Evidence } from './pages/Evidence';
import { Tasks } from './pages/Tasks';
import { Login } from './pages/Login';
import { isAuthenticated } from './lib/auth';
import { ToastProvider } from './lib/toast';
Expand Down Expand Up @@ -41,6 +43,8 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
<Route path="/litigation" element={<LitigationAssistant />} />
<Route path="/recommendations" element={<Recommendations />} />
<Route path="/cashflow" element={<CashFlow />} />
<Route path="/evidence" element={<Evidence />} />
<Route path="/tasks" element={<Tasks />} />
<Route path="/upload" element={<Upload />} />
<Route path="/settings" element={<Settings />} />
</Route>
Expand Down
Loading
Loading