Skip to content
Closed
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
2 changes: 2 additions & 0 deletions apps/frontend/src/adapters/shared/alternative-assets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ export const updateAlternativeAssetMetadata = async (
metadata: Record<string, string>,
name?: string,
notes?: string | null,
accountId?: string | null,
): Promise<void> => {
return invoke<void>("update_alternative_asset_metadata", {
assetId,
name,
metadata,
notes,
accountId,
});
};

Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/lib/portfolio-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function performanceToValuations(metrics: SimplePerformanceMetrics[]): AccountVa
costBasis: 0,
netContribution: pm.totalValue ?? 0,
calculatedAt: new_date.toISOString(),
alternativeMarketValue: 0,
};
});
}
Expand Down
20 changes: 19 additions & 1 deletion apps/frontend/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,19 @@ export interface Lot {
acquisitionFees: number;
}

export interface LotView {
id: string;
accountId: string;
acquisitionDate: string;
originalQuantity: number;
remainingQuantity: number;
costPerUnit: number;
totalCostBasis: number;
fees: number;
isClosed: boolean;
closeDate?: string;
}

export interface Position {
id: string;
accountId: string;
Expand Down Expand Up @@ -565,7 +578,7 @@ export interface Holding {
assetKind?: AssetKind | null;
quantity: number;
openDate?: string | Date | null;
lots?: Lot[] | null;
lotDetails?: LotView[] | null;
localCurrency: string;
baseCurrency: string;
fxRate?: number | null;
Expand Down Expand Up @@ -784,6 +797,7 @@ export interface AccountValuation {
costBasis: number;
netContribution: number;
calculatedAt: string;
alternativeMarketValue: number;
}

export interface AccountSummaryView {
Expand Down Expand Up @@ -1092,6 +1106,8 @@ export interface CreateAlternativeAssetRequest {
metadata?: Record<string, string>;
/** For liabilities: optional ID of the financed asset (UI-only linking) */
linkedAssetId?: string;
/** Optional link to the account this asset belongs to */
accountId?: string;
}

/**
Expand Down Expand Up @@ -1267,6 +1283,8 @@ export interface AlternativeAssetHolding {
linkedAssetId?: string;
/** Asset notes */
notes?: string | null;
/** Optional linked account ID */
accountId?: string | null;
}

/**
Expand Down
3 changes: 3 additions & 0 deletions apps/frontend/src/pages/asset/alternative-asset-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -810,13 +810,15 @@ export function useAlternativeAssetActions({
metadata: Record<string, string>,
name?: string,
notes?: string | null,
accountId?: string | null,
) => {
if (!holding) return;
await updateMetadataMutation.mutateAsync({
assetId: holding.id,
metadata,
name,
notes,
accountId,
});
};

Expand Down Expand Up @@ -849,6 +851,7 @@ export function useAlternativeAssetActions({
currency: holding.currency,
metadata: holding.metadata,
notes: holding.notes,
accountId: holding.accountId,
}
: null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from "@wealthfolio/ui";
import { cn } from "@/lib/utils";
import { useSettingsContext } from "@/lib/settings-provider";
import { useAccounts } from "@/hooks/use-accounts";

import { METAL_TYPES, LIABILITY_TYPES, WEIGHT_UNITS } from "./alternative-asset-quick-add-schema";
import { useAlternativeAssetMutations } from "../hooks/use-alternative-asset-mutations";
Expand Down Expand Up @@ -117,6 +118,7 @@ interface FormData {
liabilityType?: string;
hasMortgage?: boolean;
linkedAssetId?: string;
accountId?: string;
}

interface AlternativeAssetQuickAddModalProps {
Expand Down Expand Up @@ -154,6 +156,15 @@ export function AlternativeAssetQuickAddModal({
}: AlternativeAssetQuickAddModalProps) {
const { settings } = useSettingsContext();
const baseCurrency = settings?.baseCurrency ?? "USD";
const { accounts } = useAccounts();
const accountOptions = useMemo(
() =>
(accounts ?? []).map((a) => ({
value: a.id,
label: a.name,
})),
[accounts],
);

const [step, setStep] = useState<1 | 2>(1);
const [hasMortgageChecked, setHasMortgageChecked] = useState(false);
Expand Down Expand Up @@ -273,6 +284,7 @@ export function AlternativeAssetQuickAddModal({
purchaseDate: formData.purchaseDate ? formatDateToISO(formData.purchaseDate) : undefined,
metadata: Object.keys(metadata).length > 0 ? metadata : undefined,
linkedAssetId: formData.linkedAssetId || undefined,
accountId: formData.accountId || undefined,
};

setHasMortgageChecked(formData.hasMortgage ?? false);
Expand Down Expand Up @@ -604,6 +616,23 @@ export function AlternativeAssetQuickAddModal({
/>
</div>
)}

{/* Account link - optional for all alternative asset types */}
{accountOptions.length > 0 && (
<div className="space-y-2">
<Label className="text-foreground text-sm font-medium">
Account (optional)
</Label>
<ResponsiveSelect
value={formData.accountId}
onValueChange={(v) => updateFormData("accountId", v)}
options={accountOptions}
placeholder="Select account (optional)"
sheetTitle="Link to Account"
sheetDescription="Associate this asset with an account"
/>
</div>
)}
</motion.div>
)}
</AnimatePresence>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useMemo, useState } from "react";
import { useForm, type Resolver } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { useAccounts } from "@/hooks/use-accounts";
import {
Sheet,
SheetContent,
Expand Down Expand Up @@ -58,6 +59,7 @@ export interface AssetDetailsSheetAsset {
currency: string;
metadata?: Record<string, unknown>;
notes?: string | null;
accountId?: string | null;
}

/** Represents a liability that can be linked from a property */
Expand All @@ -80,6 +82,7 @@ interface AssetDetailsSheetProps {
metadata: Record<string, string>,
name?: string,
notes?: string | null,
accountId?: string | null,
) => Promise<void>;
/** Optional: For displaying linked asset name for liabilities */
linkedAssetName?: string;
Expand Down Expand Up @@ -116,9 +119,19 @@ export function AssetDetailsSheet({
}: AssetDetailsSheetProps) {
// Use a fallback kind for the form when asset is null (form state won't be used anyway)
const assetKind = asset?.kind ?? AlternativeAssetKind.OTHER;
const isLiability = assetKind === AlternativeAssetKind.LIABILITY;
const assetName = asset?.name ?? "";
const assetMetadata = asset?.metadata;
const assetNotes = asset?.notes;
const [selectedAccountId, setSelectedAccountId] = useState<string>(asset?.accountId ?? "");
const { accounts } = useAccounts();
const accountOptions: ResponsiveSelectOption[] = useMemo(
() => [
{ value: "__none__", label: "None" },
...(accounts ?? []).map((a) => ({ value: a.id, label: a.name })),
],
[accounts],
);

const form = useForm<AssetDetailsFormValues>({
resolver: zodResolver(assetDetailsSchema) as Resolver<AssetDetailsFormValues>,
Expand All @@ -129,6 +142,7 @@ export function AssetDetailsSheet({
useEffect(() => {
if (open && asset) {
form.reset(getDefaultDetailsFormValues(asset.kind, asset.name, asset.metadata, asset.notes));
setSelectedAccountId(asset.accountId ?? "");
}
}, [open, asset, form]);

Expand All @@ -154,8 +168,15 @@ export function AssetDetailsSheet({
const metadata = formValuesToMetadata(values);
// Only pass name if it changed
const nameChanged = values.name !== asset.name ? values.name : undefined;
// Pass accountId: empty string to clear, value to set, undefined if unchanged
const accountIdValue =
selectedAccountId === "__none__"
? ""
: selectedAccountId !== (asset.accountId ?? "")
? selectedAccountId
: undefined;
// Pass notes separately (it goes to asset.notes, not metadata)
await onSave(asset.id, metadata, nameChanged, values.notes);
await onSave(asset.id, metadata, nameChanged, values.notes, accountIdValue);
toast({
title: "Details saved successfully",
variant: "success",
Expand Down Expand Up @@ -324,7 +345,10 @@ export function AssetDetailsSheet({

{/* Notes Section */}
<div className="space-y-4">
<SectionHeader title="Notes" description="Additional information about this asset" />
<SectionHeader
title="Notes"
description={`Additional information about this ${isLiability ? "liability" : "asset"}`}
/>

<FormField
control={form.control}
Expand All @@ -333,7 +357,7 @@ export function AssetDetailsSheet({
<FormItem>
<FormControl>
<Textarea
placeholder="Add any notes about this asset..."
placeholder={`Add any notes about this ${isLiability ? "liability" : "asset"}...`}
className="min-h-[100px] resize-none"
value={field.value ?? ""}
onChange={(e) => field.onChange(e.target.value || null)}
Expand All @@ -345,6 +369,24 @@ export function AssetDetailsSheet({
/>
</div>

{/* Account Link */}
{accountOptions.length > 1 && (
<div className="space-y-4">
<SectionHeader
title="Account"
description={`Associate this ${isLiability ? "liability" : "asset"} with an account`}
/>
<ResponsiveSelect
value={selectedAccountId || "__none__"}
onValueChange={(v) => setSelectedAccountId(v === "__none__" ? "" : v)}
options={accountOptions}
placeholder="Select account (optional)"
sheetTitle="Link to Account"
sheetDescription="Associate this asset with an account"
/>
</div>
)}

<SheetFooter className="gap-2 pt-4">
<Button
type="button"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,14 @@ export function useAlternativeAssetMutations(options: UseAlternativeAssetMutatio
metadata,
name,
notes,
accountId,
}: {
assetId: string;
metadata: Record<string, string>;
name?: string;
notes?: string | null;
}) => updateAlternativeAssetMetadata(assetId, metadata, name, notes),
accountId?: string | null;
}) => updateAlternativeAssetMetadata(assetId, metadata, name, notes, accountId),
onSuccess: () => {
invalidateQueries();
options.onMetadataUpdateSuccess?.();
Expand Down
Loading