108 lines
3.9 KiB
JavaScript
108 lines
3.9 KiB
JavaScript
import { useCallback, useEffect, useState } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import { ArrowLeft, Link2, RefreshCw } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { api } from '@/api';
|
|
import { Button } from '@/components/ui/button';
|
|
import BillModal from '@/components/BillModal';
|
|
import SubscriptionCatalogSection from '@/components/SubscriptionCatalogSection';
|
|
|
|
export default function SubscriptionCatalogPage() {
|
|
const [catalogKey, setCatalogKey] = useState(0);
|
|
const [subscriptions, setSubscriptions] = useState([]);
|
|
const [categories, setCategories] = useState([]);
|
|
const [modal, setModal] = useState(null);
|
|
|
|
const refreshSubscriptions = useCallback(async () => {
|
|
try {
|
|
const [subscriptionData, categoryData] = await Promise.all([
|
|
api.subscriptions(),
|
|
api.categories(),
|
|
]);
|
|
setSubscriptions(subscriptionData?.subscriptions || []);
|
|
setCategories(Array.isArray(categoryData) ? categoryData : []);
|
|
} catch (err) {
|
|
toast.error(err.message || 'Subscriptions could not be refreshed.');
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refreshSubscriptions();
|
|
}, [refreshSubscriptions]);
|
|
|
|
function openBillEditor(billId) {
|
|
const bill = subscriptions.find(item => item.id === billId) || { id: billId };
|
|
setModal({ bill });
|
|
}
|
|
|
|
return (
|
|
<div className="mx-auto w-full max-w-6xl space-y-6">
|
|
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
|
<div className="min-w-0">
|
|
<p className="mb-1 text-[11px] font-semibold uppercase tracking-[0.15em] text-muted-foreground">
|
|
Matching Tools
|
|
</p>
|
|
<h1 className="text-3xl font-semibold tracking-tight">Service Catalog</h1>
|
|
<p className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
|
Link tracked subscriptions to known services and tune bank descriptors so future recommendations are more accurate.
|
|
</p>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
|
|
<Button asChild type="button" variant="outline" className="gap-2">
|
|
<Link to="/subscriptions">
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Subscriptions
|
|
</Link>
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="gap-2"
|
|
onClick={() => setCatalogKey(key => key + 1)}
|
|
>
|
|
<RefreshCw className="h-4 w-4" />
|
|
Refresh
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="rounded-lg border border-border/70 bg-card/80 p-4">
|
|
<div className="flex items-start gap-3">
|
|
<span className="mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
|
|
<Link2 className="h-4 w-4" />
|
|
</span>
|
|
<div className="min-w-0">
|
|
<p className="text-sm font-semibold text-foreground">This page improves matching, not discovery.</p>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Recommendations on the Subscriptions page come from bank transactions. Use this catalog when a service needs a better descriptor or an existing bill should be linked to a known service.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<SubscriptionCatalogSection
|
|
key={catalogKey}
|
|
onEditBill={openBillEditor}
|
|
onTrackComplete={async () => {
|
|
await refreshSubscriptions();
|
|
setCatalogKey(key => key + 1);
|
|
}}
|
|
/>
|
|
|
|
{modal && (
|
|
<BillModal
|
|
key={modal.bill?.id ? `catalog-edit-${modal.bill.id}` : 'catalog-edit'}
|
|
bill={modal.bill}
|
|
categories={categories}
|
|
onClose={() => setModal(null)}
|
|
onSave={async () => {
|
|
setModal(null);
|
|
await refreshSubscriptions();
|
|
setCatalogKey(key => key + 1);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|