feat(trips): trip tracking with analytics, tag conversion, and transaction assignment
Adds trips table usage across API and UI: trip CRUD, per-trip analytics (category/daily/merchant/tag/participant breakdowns), tag-to-trip conversion, trip assignment via transaction overrides, and trip filter in the transactions view. Recovered from working tree after local git corruption; feature was already live via host-context Docker builds.
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
useAddTransactionTag,
|
||||
useRemoveTransactionTag,
|
||||
useTransactionSplits,
|
||||
useTrips,
|
||||
} from "@/lib/hooks";
|
||||
import { SplitModal } from "./split-modal";
|
||||
import { CATEGORIES, formatCategory } from "@/lib/categories";
|
||||
@@ -93,6 +94,7 @@ export function EditTransactionModal({
|
||||
}) {
|
||||
const isManual = !transaction.statement_id;
|
||||
const updateTxn = useUpdateTransaction();
|
||||
const { data: trips = [] } = useTrips();
|
||||
|
||||
// Editable override fields
|
||||
const [merchant, setMerchant] = useState(transaction.merchant_override ?? transaction.merchant_normalized ?? "");
|
||||
@@ -105,6 +107,8 @@ export function EditTransactionModal({
|
||||
const [description, setDescription] = useState(transaction.description);
|
||||
const [amount, setAmount] = useState(String(transaction.amount));
|
||||
|
||||
const [tripId, setTripId] = useState<number | null>(transaction.trip_id ?? null);
|
||||
|
||||
// Splits — live via hook so they refresh after SplitModal saves
|
||||
const { data: liveSplits = [] } = useTransactionSplits(transaction.id);
|
||||
|
||||
@@ -136,6 +140,9 @@ export function EditTransactionModal({
|
||||
patch.amount = parseFloat(amount);
|
||||
}
|
||||
|
||||
if (tripId !== (transaction.trip_id ?? null))
|
||||
patch.trip_id = tripId;
|
||||
|
||||
await updateTxn.mutateAsync(patch);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
@@ -255,6 +262,21 @@ export function EditTransactionModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Trip */}
|
||||
<div>
|
||||
<label className="block text-xs text-zinc-500 mb-1">Trip</label>
|
||||
<select
|
||||
value={tripId ?? ""}
|
||||
onChange={(e) => setTripId(e.target.value ? Number(e.target.value) : null)}
|
||||
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1.5 text-sm"
|
||||
>
|
||||
<option value="">— No Trip —</option>
|
||||
{trips.filter((t) => !t.archived).map((t) => (
|
||||
<option key={t.id} value={t.id}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
<div>
|
||||
<p className="text-xs text-zinc-500 mb-1.5">Tags</p>
|
||||
|
||||
Reference in New Issue
Block a user