Skip to content

Commit 8acd28c

Browse files
committed
✨ [feat][frontend] Edit event
1 parent ca77b9c commit 8acd28c

4 files changed

Lines changed: 334 additions & 172 deletions

File tree

frontend/src/components/create-event-fab.tsx

Lines changed: 0 additions & 164 deletions
This file was deleted.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import {
2+
ArrowLeftRight,
3+
type LucideIcon,
4+
Repeat,
5+
TrendingDown,
6+
TrendingUp
7+
} from 'lucide-react'
8+
import { useState } from 'react'
9+
10+
import { FabForm } from '@/components/fab-form'
11+
import { FabSheet } from '@/components/fab-sheet'
12+
import { type SelectedTransaction } from '@/components/link-transactions-table'
13+
import { LinkedTransactionsField } from '@/components/linked-transactions-field'
14+
import { TimePicker } from '@/components/time-picker'
15+
import { Field, FieldDescription, FieldLabel } from '@/components/ui/field'
16+
import { Input } from '@/components/ui/input'
17+
import { SheetHeader, SheetTitle } from '@/components/ui/sheet'
18+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
19+
import {
20+
type AccountRead,
21+
type EventCreate,
22+
type EventReadDetailed,
23+
type EventType
24+
} from '@/lib/client'
25+
import type { Client } from '@/lib/client/client'
26+
import { CLIENT_TIMEZONE } from '@/lib/constants'
27+
import { eventTypeTabActiveClass } from '@/lib/event-types'
28+
import {
29+
cn,
30+
formatZonedDateTime,
31+
toLocalDateTimeInputValue,
32+
toZonedISOString
33+
} from '@/lib/utils'
34+
35+
const EVENT_TYPES: { value: EventType; icon: LucideIcon }[] = [
36+
{ value: 'Expense', icon: TrendingDown },
37+
{ value: 'Income', icon: TrendingUp },
38+
{ value: 'Transfer', icon: ArrowLeftRight },
39+
{ value: 'Exchange', icon: Repeat }
40+
]
41+
42+
interface EventFabProps {
43+
client: Client
44+
accounts: AccountRead[]
45+
open: boolean
46+
onOpenChange: (open: boolean) => void
47+
editingEvent: EventReadDetailed | null
48+
onSubmit: (body: EventCreate, linkedTransactionIds: number[]) => void
49+
isPending: boolean
50+
}
51+
52+
interface EventFabBodyProps {
53+
client: Client
54+
accounts: AccountRead[]
55+
editingEvent: EventReadDetailed | null
56+
onSubmit: (body: EventCreate, linkedTransactionIds: number[]) => void
57+
isPending: boolean
58+
}
59+
60+
export function EventFab({
61+
client,
62+
accounts,
63+
open,
64+
onOpenChange,
65+
editingEvent,
66+
onSubmit,
67+
isPending
68+
}: EventFabProps) {
69+
return (
70+
<FabSheet
71+
open={open}
72+
onOpenChange={onOpenChange}
73+
hotkey="n"
74+
label="New event"
75+
>
76+
<SheetHeader>
77+
<SheetTitle>{editingEvent ? 'Edit event' : 'New event'}</SheetTitle>
78+
</SheetHeader>
79+
{/* Keyed so the form re-initializes from the picked event. */}
80+
<EventFabBody
81+
key={editingEvent?.id ?? 'new'}
82+
client={client}
83+
accounts={accounts}
84+
editingEvent={editingEvent}
85+
onSubmit={onSubmit}
86+
isPending={isPending}
87+
/>
88+
</FabSheet>
89+
)
90+
}
91+
92+
function EventFabBody({
93+
client,
94+
accounts,
95+
editingEvent,
96+
onSubmit,
97+
isPending
98+
}: EventFabBodyProps) {
99+
const isEditing = editingEvent != null
100+
const [type, setType] = useState<EventType>(editingEvent?.type ?? 'Expense')
101+
const [description, setDescription] = useState(
102+
editingEvent?.description ?? ''
103+
)
104+
const [timestamp, setTimestamp] = useState(() =>
105+
isEditing ? new Date(editingEvent.timestamp) : new Date()
106+
)
107+
const [linkedTransactions, setLinkedTransactions] = useState<
108+
SelectedTransaction[]
109+
>(() => {
110+
if (!isEditing) return []
111+
const accountsById = new Map(
112+
accounts.map((account) => [account.id, account])
113+
)
114+
return editingEvent.transactions.flatMap((transaction) => {
115+
const account = accountsById.get(transaction.account_id)
116+
return account ? [{ transaction, account }] : []
117+
})
118+
})
119+
120+
const handleSubmit = () => {
121+
const body: EventCreate = isEditing
122+
? {
123+
type,
124+
timestamp: toZonedISOString(timestamp, editingEvent.timezone),
125+
timezone: editingEvent.timezone,
126+
description: description.trim() || null
127+
}
128+
: {
129+
type,
130+
timestamp: toLocalDateTimeInputValue(timestamp),
131+
timezone: CLIENT_TIMEZONE as EventCreate['timezone'],
132+
description: description.trim() || null
133+
}
134+
onSubmit(
135+
body,
136+
linkedTransactions.map((item) => item.transaction.id)
137+
)
138+
}
139+
140+
return (
141+
<FabForm
142+
onSubmit={handleSubmit}
143+
isPending={isPending}
144+
isEditing={isEditing}
145+
>
146+
<Field>
147+
<FieldLabel htmlFor="event-description">Description</FieldLabel>
148+
<Input
149+
id="event-description"
150+
placeholder="Optional"
151+
value={description}
152+
onChange={(e) => setDescription(e.target.value)}
153+
/>
154+
</Field>
155+
156+
<Field>
157+
<FieldLabel>Type</FieldLabel>
158+
<Tabs
159+
value={type}
160+
onValueChange={(value) => setType(value as EventType)}
161+
>
162+
<TabsList className="w-full">
163+
{EVENT_TYPES.map(({ value, icon: Icon }) => (
164+
<TabsTrigger
165+
key={value}
166+
value={value}
167+
className={cn(
168+
'gap-1 px-1 text-xs',
169+
eventTypeTabActiveClass[value]
170+
)}
171+
>
172+
<Icon />
173+
{value}
174+
</TabsTrigger>
175+
))}
176+
</TabsList>
177+
</Tabs>
178+
</Field>
179+
180+
<Field>
181+
<FieldLabel htmlFor="event-timestamp">Timestamp</FieldLabel>
182+
<TimePicker
183+
id="event-timestamp"
184+
value={timestamp}
185+
onChange={setTimestamp}
186+
/>
187+
{isEditing && editingEvent.timezone != CLIENT_TIMEZONE && (
188+
<FieldDescription>
189+
{`= ${formatZonedDateTime(timestamp, editingEvent.timezone)} in ${editingEvent.timezone}`}
190+
</FieldDescription>
191+
)}
192+
</Field>
193+
194+
<Field>
195+
<FieldLabel>Transactions</FieldLabel>
196+
<LinkedTransactionsField
197+
client={client}
198+
value={linkedTransactions}
199+
onChange={setLinkedTransactions}
200+
/>
201+
</Field>
202+
</FabForm>
203+
)
204+
}

0 commit comments

Comments
 (0)