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:
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useCallback, useRef, useEffect, Suspense } from "react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags, useStatement, useCreateRule, useParticipants, useRecordPayment, useCurrentUser } from "@/lib/hooks";
|
||||
import { useTransactions, useBanks, useUpdateTransaction, useBulkAction, useTags, useStatement, useCreateRule, useParticipants, useRecordPayment, useCurrentUser, useTrips, useAssignTransactionsToTrip } from "@/lib/hooks";
|
||||
import { CATEGORIES, formatCategory } from "@/lib/categories";
|
||||
import { SplitModal } from "@/components/split-modal";
|
||||
import { TagPicker } from "@/components/tag-picker";
|
||||
@@ -477,6 +477,7 @@ function TransactionsContent() {
|
||||
amount_min: undefined as number | undefined,
|
||||
amount_max: undefined as number | undefined,
|
||||
has_split: "" as string,
|
||||
trip_id: "" as string,
|
||||
});
|
||||
const [queryInput, setQueryInput] = useState("");
|
||||
const [queryTokens, setQueryTokens] = useState<QueryToken[]>([]);
|
||||
@@ -506,6 +507,7 @@ function TransactionsContent() {
|
||||
const [selected, setSelected] = useState<Set<number>>(new Set());
|
||||
const [bulkCategory, setBulkCategory] = useState("");
|
||||
const [bulkTagId, setBulkTagId] = useState("");
|
||||
const [bulkTripId, setBulkTripId] = useState("");
|
||||
const [splitModal, setSplitModal] = useState<{ transactionId?: number; transactionIds?: number[]; amount?: number; description: string; merchant?: string } | null>(null);
|
||||
const [addModal, setAddModal] = useState<{ prefill?: Parameters<typeof AddTransactionModal>[0]["prefill"]; title?: string } | null>(null);
|
||||
const [editModal, setEditModal] = useState<TransactionRow | null>(null);
|
||||
@@ -524,6 +526,8 @@ function TransactionsContent() {
|
||||
const { data: statementInfo } = useStatement(parseInt(filters.statement_id) || 0);
|
||||
const updateTxn = useUpdateTransaction();
|
||||
const bulkAction = useBulkAction();
|
||||
const { data: trips = [] } = useTrips();
|
||||
const assignToTrip = useAssignTransactionsToTrip();
|
||||
|
||||
const toggleSelect = useCallback((id: number) => {
|
||||
setSelected((prev) => {
|
||||
@@ -687,6 +691,17 @@ function TransactionsContent() {
|
||||
<option value="yes">Split only</option>
|
||||
<option value="no">Unsplit only</option>
|
||||
</select>
|
||||
<select
|
||||
value={filters.trip_id}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, trip_id: e.target.value, offset: 0 }))}
|
||||
className="bg-zinc-900 border border-zinc-700 rounded px-3 py-1.5 text-sm text-zinc-300"
|
||||
>
|
||||
<option value="">All Trips</option>
|
||||
<option value="unassigned">No Trip</option>
|
||||
{trips.map((t) => (
|
||||
<option key={t.id} value={String(t.id)}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Bulk action bar */}
|
||||
@@ -748,6 +763,30 @@ function TransactionsContent() {
|
||||
>
|
||||
Tag
|
||||
</button>
|
||||
<select
|
||||
value={bulkTripId}
|
||||
onChange={(e) => setBulkTripId(e.target.value)}
|
||||
className="bg-zinc-800 border border-zinc-600 rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="">Assign trip…</option>
|
||||
<option value="remove">Remove from trip</option>
|
||||
{trips.filter((t) => !t.archived).map((t) => (
|
||||
<option key={t.id} value={String(t.id)}>{t.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
disabled={!bulkTripId || assignToTrip.isPending}
|
||||
onClick={() => {
|
||||
const tripId = bulkTripId === "remove" ? null : Number(bulkTripId);
|
||||
assignToTrip.mutate(
|
||||
{ tripId, transactionIds: Array.from(selected) },
|
||||
{ onSuccess: () => { setSelected(new Set()); setBulkTripId(""); } }
|
||||
);
|
||||
}}
|
||||
className="px-3 py-1 bg-emerald-700 hover:bg-emerald-600 disabled:opacity-50 rounded text-sm"
|
||||
>
|
||||
{bulkTripId === "remove" ? "Remove" : "Assign"}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelected(new Set())}
|
||||
className="px-3 py-1 text-zinc-400 hover:text-white text-sm"
|
||||
|
||||
Reference in New Issue
Block a user