@fayz-ai/plugin-financial 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +3 -3
  2. package/dist/components/PersonStatementWidget.d.ts +1 -1
  3. package/dist/components/PersonStatementWidget.d.ts.map +1 -1
  4. package/dist/{FinancialContext.d.ts → context.d.ts} +4 -2
  5. package/dist/context.d.ts.map +1 -0
  6. package/dist/data/registries.d.ts.map +1 -0
  7. package/dist/index.d.ts +13 -4
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1426 -1426
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/stripe/connectorDef.d.ts +14 -0
  12. package/dist/integrations/stripe/connectorDef.d.ts.map +1 -0
  13. package/dist/integrations/stripe/data.d.ts +43 -0
  14. package/dist/integrations/stripe/data.d.ts.map +1 -0
  15. package/dist/lib/message-events.d.ts +12 -0
  16. package/dist/lib/message-events.d.ts.map +1 -0
  17. package/dist/{quick-add.d.ts → lib/quick-add.d.ts} +1 -1
  18. package/dist/lib/quick-add.d.ts.map +1 -0
  19. package/dist/locales/en.d.ts.map +1 -1
  20. package/dist/locales/pt-BR.d.ts.map +1 -1
  21. package/dist/{FinancialPage.d.ts → views/FinancialPage.d.ts} +3 -3
  22. package/dist/views/FinancialPage.d.ts.map +1 -0
  23. package/dist/views/InvoiceDetailView.d.ts.map +1 -1
  24. package/dist/views/InvoiceFormView.d.ts.map +1 -1
  25. package/dist/views/InvoiceListView.d.ts.map +1 -1
  26. package/dist/views/PayablesView.d.ts +1 -1
  27. package/dist/views/PayablesView.d.ts.map +1 -1
  28. package/dist/views/ReceivablesView.d.ts +1 -1
  29. package/dist/views/ReceivablesView.d.ts.map +1 -1
  30. package/dist/views/StatementsView.d.ts.map +1 -1
  31. package/dist/views/dashboardWidgets.d.ts +1 -1
  32. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  33. package/package.json +12 -14
  34. package/dist/FinancialContext.d.ts.map +0 -1
  35. package/dist/FinancialPage.d.ts.map +0 -1
  36. package/dist/contract.test.d.ts +0 -2
  37. package/dist/contract.test.d.ts.map +0 -1
  38. package/dist/quick-add.d.ts.map +0 -1
  39. package/dist/registries.d.ts.map +0 -1
  40. package/src/FinancialContext.tsx +0 -59
  41. package/src/FinancialPage.tsx +0 -413
  42. package/src/README.md +0 -155
  43. package/src/components/FinancialGeneralSettings.tsx +0 -39
  44. package/src/components/FinancialOnboarding.tsx +0 -87
  45. package/src/components/FinancialSettings.tsx +0 -31
  46. package/src/components/PaymentModal.tsx +0 -369
  47. package/src/components/PersonStatementWidget.tsx +0 -133
  48. package/src/components/TransferModal.tsx +0 -150
  49. package/src/contract.test.ts +0 -58
  50. package/src/data/capability.test.ts +0 -135
  51. package/src/data/index.ts +0 -3
  52. package/src/data/mock.ts +0 -842
  53. package/src/data/supabase.ts +0 -968
  54. package/src/data/tables.ts +0 -13
  55. package/src/data/types.ts +0 -86
  56. package/src/index.ts +0 -487
  57. package/src/lib/tenant.ts +0 -3
  58. package/src/locales/en.ts +0 -493
  59. package/src/locales/index.ts +0 -7
  60. package/src/locales/pt-BR.ts +0 -493
  61. package/src/migrations/000_plg_rename.sql +0 -30
  62. package/src/migrations/001_financial_base.sql +0 -109
  63. package/src/migrations/002_chart_of_accounts.sql +0 -28
  64. package/src/migrations/003_card_brands.sql +0 -12
  65. package/src/migrations/004_order_to_cash.sql +0 -178
  66. package/src/migrations/005_seed_defaults.sql +0 -38
  67. package/src/migrations/006_rls_policies.sql +0 -37
  68. package/src/migrations/006b_extract_fee_amount.sql +0 -20
  69. package/src/migrations/007_reconciliation.sql +0 -32
  70. package/src/migrations/007b_movement_payment_method_type.sql +0 -6
  71. package/src/migrations/008_split_payment_movements.sql +0 -29
  72. package/src/migrations/009_agent_rpcs.sql +0 -165
  73. package/src/migrations/010_invoice_internal.sql +0 -133
  74. package/src/migrations/011_nfe.sql +0 -58
  75. package/src/migrations/index.ts +0 -923
  76. package/src/quick-add.ts +0 -58
  77. package/src/registries.ts +0 -308
  78. package/src/store.ts +0 -329
  79. package/src/types.ts +0 -592
  80. package/src/views/CardsView.tsx +0 -213
  81. package/src/views/CashRegistersView.tsx +0 -597
  82. package/src/views/CommissionsView.tsx +0 -18
  83. package/src/views/InvoiceDetailView.tsx +0 -616
  84. package/src/views/InvoiceFormView.tsx +0 -945
  85. package/src/views/InvoiceListView.tsx +0 -323
  86. package/src/views/NotasFiscaisView.tsx +0 -209
  87. package/src/views/PayablesView.tsx +0 -48
  88. package/src/views/QuickTransactionForm.tsx +0 -548
  89. package/src/views/ReceivablesView.tsx +0 -48
  90. package/src/views/ReconciliationView.tsx +0 -211
  91. package/src/views/StatementsView.tsx +0 -335
  92. package/src/views/SummaryView.tsx +0 -203
  93. package/src/views/dashboardWidgets.tsx +0 -437
  94. package/src/views/navReferrer.ts +0 -24
  95. /package/dist/{registries.d.ts → data/registries.d.ts} +0 -0
@@ -1,548 +0,0 @@
1
- import React, { useEffect, useMemo, useRef, useState } from 'react'
2
- import {
3
- ArrowUpCircle, ArrowDownCircle, ArrowLeftRight, Check, ChevronDown,
4
- Tag, Wallet, Repeat, Paperclip, CalendarDays, X, Camera,
5
- } from 'lucide-react'
6
- import {
7
- Modal, ModalContent, Button, DatePicker, toast,
8
- } from '@fayz-ai/ui'
9
- import { useTranslation } from '@fayz-ai/core'
10
- import { useLimitGuard, invalidateLimit, usePermissionOptional } from '@fayz-ai/saas'
11
- import { useFinancialConfig, useFinancialProvider, useFinancialStore, formatCurrency } from '../FinancialContext'
12
- import type { QuickTransactionType } from '../types'
13
- import type { ChartOfAccountsNode, BankAccount } from '../types'
14
-
15
- // ---------------------------------------------------------------------------
16
- // Type theme — the header is context-colored so the user always knows what
17
- // kind of money they're logging (Mobills: red expense / green income / neutral
18
- // transfer).
19
- // ---------------------------------------------------------------------------
20
-
21
- interface TypeTheme {
22
- header: string // header band background + text
23
- ring: string // focus/selected accent
24
- icon: React.ElementType
25
- }
26
-
27
- function typeTheme(type: QuickTransactionType): TypeTheme {
28
- switch (type) {
29
- case 'income':
30
- return { header: 'bg-success text-success-foreground', ring: 'ring-success', icon: ArrowDownCircle }
31
- case 'transfer':
32
- return { header: 'bg-muted text-foreground', ring: 'ring-ring', icon: ArrowLeftRight }
33
- case 'expense':
34
- default:
35
- return { header: 'bg-destructive text-destructive-foreground', ring: 'ring-destructive', icon: ArrowUpCircle }
36
- }
37
- }
38
-
39
- // ---------------------------------------------------------------------------
40
- // Small popover-chip select (category / account). Thumb-friendly rows.
41
- // ---------------------------------------------------------------------------
42
-
43
- function ChipSelect({ icon: Icon, label, value, options, onChange, emptyLabel }: {
44
- icon: React.ElementType
45
- label: string
46
- value?: string
47
- options: Array<{ id: string; name: string; hint?: string }>
48
- onChange: (id: string | undefined) => void
49
- emptyLabel: string
50
- }) {
51
- const [open, setOpen] = useState(false)
52
- const ref = useRef<HTMLDivElement>(null)
53
-
54
- useEffect(() => {
55
- if (!open) return
56
- function onDoc(e: MouseEvent) {
57
- if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
58
- }
59
- document.addEventListener('mousedown', onDoc)
60
- return () => document.removeEventListener('mousedown', onDoc)
61
- }, [open])
62
-
63
- const selected = options.find((o) => o.id === value)
64
-
65
- return (
66
- <div ref={ref} className="relative">
67
- <span className="text-xs font-medium text-muted-foreground">{label}</span>
68
- <button
69
- type="button"
70
- onClick={() => setOpen((v) => !v)}
71
- className="mt-1 flex w-full items-center gap-2 rounded-input border border-input bg-card px-3 py-2.5 text-left text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] transition-colors hover:bg-muted/30"
72
- >
73
- <Icon className="h-4 w-4 shrink-0 text-muted-foreground" />
74
- <span className={`flex-1 truncate ${selected ? '' : 'text-muted-foreground'}`}>
75
- {selected?.name ?? emptyLabel}
76
- </span>
77
- <ChevronDown className={`h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? 'rotate-180' : ''}`} />
78
- </button>
79
-
80
- {open && (
81
- <div className="fayz-glass-surface absolute bottom-full left-0 right-0 z-50 mb-1 max-h-56 overflow-auto rounded-lg border bg-popover py-1 shadow-lg">
82
- {options.length === 0 ? (
83
- <p className="px-3 py-4 text-center text-xs text-muted-foreground">{emptyLabel}</p>
84
- ) : (
85
- <>
86
- {value != null && (
87
- <button
88
- onClick={() => { onChange(undefined); setOpen(false) }}
89
- className="flex w-full items-center gap-2 px-3 py-2.5 text-left text-xs text-muted-foreground hover:bg-muted/50"
90
- >
91
- <X className="h-3.5 w-3.5" /> {emptyLabel}
92
- </button>
93
- )}
94
- {options.map((o) => (
95
- <button
96
- key={o.id}
97
- onClick={() => { onChange(o.id); setOpen(false) }}
98
- className={`flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm hover:bg-muted/50 ${o.id === value ? 'bg-muted/30' : ''}`}
99
- >
100
- <span className="flex-1 truncate">{o.name}</span>
101
- {o.hint && <span className="text-[10px] text-muted-foreground">{o.hint}</span>}
102
- {o.id === value && <Check className="h-3.5 w-3.5 text-primary" />}
103
- </button>
104
- ))}
105
- </>
106
- )}
107
- </div>
108
- )}
109
- </div>
110
- )
111
- }
112
-
113
- // ---------------------------------------------------------------------------
114
- // Toggle pill
115
- // ---------------------------------------------------------------------------
116
-
117
- function TogglePill({ icon: Icon, label, checked, onChange, accent }: {
118
- icon: React.ElementType
119
- label: string
120
- checked: boolean
121
- onChange: (v: boolean) => void
122
- accent?: string
123
- }) {
124
- return (
125
- <button
126
- type="button"
127
- onClick={() => onChange(!checked)}
128
- className={`flex items-center gap-2 rounded-full border px-3 py-2 text-xs font-medium transition-colors ${
129
- checked
130
- ? `border-transparent ${accent ?? 'bg-primary text-primary-foreground'}`
131
- : 'border-input bg-card text-muted-foreground hover:bg-muted/40'
132
- }`}
133
- >
134
- <Icon className="h-3.5 w-3.5" />
135
- {label}
136
- </button>
137
- )
138
- }
139
-
140
- // ---------------------------------------------------------------------------
141
- // Quick transaction form — responsive (mobile fullscreen sheet / desktop modal)
142
- // ---------------------------------------------------------------------------
143
-
144
- export function QuickTransactionForm({ open, onOpenChange, defaultType = 'expense', defaultAccountId, focusReceipt = false }: {
145
- open: boolean
146
- onOpenChange: (open: boolean) => void
147
- defaultType?: QuickTransactionType
148
- /** Preselect an account (e.g. opening from a specific card tile → log an expense against it). */
149
- defaultAccountId?: string
150
- /**
151
- * FAY-1226 "snap a receipt": open with the attachment step foregrounded — a
152
- * prominent capture card at the top of the sheet + auto-open the image picker
153
- * (mobile offers the camera). Used by the "Enviar recibo" entry point.
154
- */
155
- focusReceipt?: boolean
156
- }) {
157
- const t = useTranslation()
158
- const { currency } = useFinancialConfig()
159
- const provider = useFinancialProvider()
160
- const createQuickTransaction = useFinancialStore((s) => s.createQuickTransaction)
161
- const guardMovements = useLimitGuard('movements_month')
162
- const can = usePermissionOptional()
163
-
164
- const [type, setType] = useState<QuickTransactionType>(defaultType)
165
- const [amount, setAmount] = useState(0)
166
- const [amountStr, setAmountStr] = useState('')
167
- const [paid, setPaid] = useState(true)
168
- const [date, setDate] = useState(() => new Date().toISOString().slice(0, 10))
169
- const [description, setDescription] = useState('')
170
- const [categoryId, setCategoryId] = useState<string | undefined>()
171
- const [accountId, setAccountId] = useState<string | undefined>()
172
- const [toAccountId, setToAccountId] = useState<string | undefined>()
173
- const [recurring, setRecurring] = useState(false)
174
- const [saving, setSaving] = useState(false)
175
- // FAY-1226: the snapped receipt image, held in-memory as a data URL.
176
- const [receiptUrl, setReceiptUrl] = useState<string | undefined>()
177
- const fileInputRef = useRef<HTMLInputElement>(null)
178
-
179
- const [accounts, setAccounts] = useState<BankAccount[]>([])
180
- const [categories, setCategories] = useState<ChartOfAccountsNode[]>([])
181
-
182
- // Reset to a clean slate each time the sheet opens.
183
- useEffect(() => {
184
- if (!open) return
185
- setType(defaultType)
186
- setAmount(0)
187
- setAmountStr('')
188
- setPaid(true)
189
- setDate(new Date().toISOString().slice(0, 10))
190
- setDescription('')
191
- setCategoryId(undefined)
192
- setAccountId(defaultAccountId)
193
- setToAccountId(undefined)
194
- setRecurring(false)
195
- setReceiptUrl(undefined)
196
- }, [open, defaultType, defaultAccountId])
197
-
198
- // "Enviar recibo" flow: auto-open the picker so the user goes straight to the
199
- // camera/gallery. Fires once per open; harmless if the browser needs a tap.
200
- useEffect(() => {
201
- if (open && focusReceipt) {
202
- const id = window.setTimeout(() => fileInputRef.current?.click(), 250)
203
- return () => window.clearTimeout(id)
204
- }
205
- }, [open, focusReceipt])
206
-
207
- // Read the selected image into a data URL (mock has no Supabase bucket).
208
- function onPickReceipt(e: React.ChangeEvent<HTMLInputElement>) {
209
- const file = e.target.files?.[0]
210
- // Reset the input so re-picking the same file still fires onChange.
211
- e.target.value = ''
212
- if (!file) return
213
- if (!file.type.startsWith('image/')) {
214
- toast.error(t('financial.quickTx.receiptInvalid'))
215
- return
216
- }
217
- const reader = new FileReader()
218
- reader.onload = () => {
219
- setReceiptUrl(typeof reader.result === 'string' ? reader.result : undefined)
220
- // TODO(FAY-1230): Claude-vision OCR auto-fill from the receipt image —
221
- // prefill amount / merchant (description) / category from `reader.result`.
222
- }
223
- reader.onerror = () => toast.error(t('financial.quickTx.receiptInvalid'))
224
- reader.readAsDataURL(file)
225
- }
226
-
227
- // Load pickers once when opened.
228
- useEffect(() => {
229
- if (!open) return
230
- let cancelled = false
231
- void (async () => {
232
- const [accs, coa] = await Promise.all([
233
- provider.getBankAccounts(),
234
- provider.getChartOfAccounts().catch(() => [] as ChartOfAccountsNode[]),
235
- ])
236
- if (cancelled) return
237
- setAccounts(accs)
238
- setCategories(coa)
239
- setAccountId((prev) => prev ?? accs[0]?.id)
240
- })()
241
- return () => { cancelled = true }
242
- }, [open, provider])
243
-
244
- const theme = typeTheme(type)
245
- const HeaderIcon = theme.icon
246
-
247
- const accountOptions = useMemo(
248
- () => accounts.map((a) => ({ id: a.id, name: a.name, hint: a.accountType === 'credit_card' ? t('financial.quickTx.card') : undefined })),
249
- [accounts, t],
250
- )
251
- const categoryOptions = useMemo(
252
- () => categories.filter((c) => c.isActive).map((c) => ({ id: c.id, name: c.name, hint: c.code })),
253
- [categories],
254
- )
255
-
256
- function onAmountChange(raw: string) {
257
- // Keep digits + a single separator; store the numeric value.
258
- const cleaned = raw.replace(/[^\d.,]/g, '').replace(',', '.')
259
- setAmountStr(raw.replace(/[^\d.,]/g, ''))
260
- const n = Number.parseFloat(cleaned)
261
- setAmount(Number.isFinite(n) ? n : 0)
262
- }
263
-
264
- const canSave =
265
- amount > 0 &&
266
- !saving &&
267
- (type === 'transfer'
268
- ? !!accountId && !!toAccountId && accountId !== toAccountId
269
- : true)
270
-
271
- async function handleSave() {
272
- // Defense in depth: never persist for a role without `create`, even if a
273
- // stray affordance slipped through.
274
- if (!can('financial', 'create')) return
275
- if (!canSave) {
276
- if (type === 'transfer' && accountId && accountId === toAccountId) {
277
- toast.error(t('financial.transfer.sameAccount'))
278
- }
279
- return
280
- }
281
- // Plan quantity guard (client-side): a transfer writes two movements (from +
282
- // to), other kinds one. Guard that many against the monthly cap before the
283
- // store call — the guard opens the global UpgradeModal when it would exceed.
284
- if ((await guardMovements(type === 'transfer' ? 2 : 1)) === 'blocked') return
285
- setSaving(true)
286
- try {
287
- await createQuickTransaction({
288
- type,
289
- amount,
290
- date,
291
- paid: type === 'transfer' ? true : paid,
292
- description: description || undefined,
293
- categoryId: type === 'transfer' ? undefined : categoryId,
294
- bankAccountId: accountId,
295
- toAccountId: type === 'transfer' ? toAccountId : undefined,
296
- recurring: type === 'transfer' ? false : recurring,
297
- receiptUrl: type === 'transfer' ? undefined : receiptUrl,
298
- })
299
- invalidateLimit('movements_month')
300
- onOpenChange(false)
301
- } catch {
302
- // toast handled by store
303
- } finally {
304
- setSaving(false)
305
- }
306
- }
307
-
308
- const TYPES: Array<{ id: QuickTransactionType; label: string }> = [
309
- { id: 'expense', label: t('financial.quickTx.expense') },
310
- { id: 'income', label: t('financial.quickTx.income') },
311
- { id: 'transfer', label: t('financial.quickTx.transfer') },
312
- ]
313
-
314
- return (
315
- <Modal open={open} onOpenChange={onOpenChange}>
316
- <ModalContent size="md" noPadding hideClose className="overflow-hidden">
317
- {/* Context-colored header */}
318
- <div className={`${theme.header} shrink-0 px-5 pb-5 pt-5`}>
319
- <div className="flex items-center justify-between">
320
- <div className="flex items-center gap-2">
321
- <HeaderIcon className="h-5 w-5" />
322
- <h2 className="text-sm font-semibold">{t('financial.quickTx.title')}</h2>
323
- </div>
324
- <button
325
- onClick={() => onOpenChange(false)}
326
- className="flex h-7 w-7 items-center justify-center rounded-full opacity-80 transition-colors hover:bg-black/10 hover:opacity-100"
327
- aria-label={t('financial.quickTx.close')}
328
- >
329
- <X className="h-4 w-4" />
330
- </button>
331
- </div>
332
-
333
- {/* Type segmented control */}
334
- <div className="mt-4 flex gap-1 rounded-full bg-black/15 p-1">
335
- {TYPES.map((ty) => (
336
- <button
337
- key={ty.id}
338
- onClick={() => setType(ty.id)}
339
- className={`flex-1 rounded-full px-2 py-1.5 text-xs font-medium transition-colors ${
340
- type === ty.id ? 'bg-white/90 text-foreground shadow-sm' : 'opacity-90 hover:bg-white/10'
341
- }`}
342
- >
343
- {ty.label}
344
- </button>
345
- ))}
346
- </div>
347
-
348
- {/* Big amount input */}
349
- <div className="mt-5 flex items-baseline justify-center gap-2">
350
- <span className="text-xl font-medium opacity-90">{currency.symbol}</span>
351
- <input
352
- type="text"
353
- inputMode="decimal"
354
- autoFocus
355
- value={amountStr}
356
- onChange={(e) => onAmountChange(e.target.value)}
357
- onKeyDown={(e) => { if (e.key === 'Enter') void handleSave() }}
358
- placeholder="0,00"
359
- className="w-full max-w-[220px] bg-transparent text-center text-4xl font-bold tracking-tight outline-none placeholder:opacity-40"
360
- />
361
- </div>
362
- </div>
363
-
364
- {/* Body */}
365
- <div className="flex-1 space-y-4 overflow-y-auto px-5 py-5">
366
- {/* Paid / not paid (hidden for transfers — always settled) */}
367
- {type !== 'transfer' && (
368
- <div className="flex items-center justify-between rounded-input border border-input bg-card px-3 py-2.5 shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)]">
369
- <span className="text-sm font-medium">
370
- {type === 'income' ? t('financial.quickTx.received') : t('financial.quickTx.paid')}
371
- </span>
372
- <div className="flex gap-1">
373
- <button
374
- onClick={() => setPaid(true)}
375
- className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${paid ? 'bg-success text-success-foreground' : 'bg-muted text-muted-foreground'}`}
376
- >
377
- {t('financial.quickTx.yes')}
378
- </button>
379
- <button
380
- onClick={() => setPaid(false)}
381
- className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${!paid ? 'bg-warning text-warning-foreground' : 'bg-muted text-muted-foreground'}`}
382
- >
383
- {t('financial.quickTx.no')}
384
- </button>
385
- </div>
386
- </div>
387
- )}
388
-
389
- {/* Date */}
390
- <div>
391
- <span className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
392
- <CalendarDays className="h-3.5 w-3.5" /> {t('financial.invoiceForm.date')}
393
- </span>
394
- <DatePicker value={date} onChange={setDate} />
395
- </div>
396
-
397
- {/* Description */}
398
- <div>
399
- <span className="text-xs font-medium text-muted-foreground">{t('financial.invoiceForm.description')}</span>
400
- <input
401
- type="text"
402
- value={description}
403
- onChange={(e) => setDescription(e.target.value)}
404
- onKeyDown={(e) => { if (e.key === 'Enter') void handleSave() }}
405
- placeholder={t('financial.quickTx.descriptionPlaceholder')}
406
- className="mt-1 w-full rounded-input border border-input bg-card px-3 py-2.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] outline-none focus:ring-2 focus:ring-ring"
407
- />
408
- </div>
409
-
410
- {/* FAY-1226 receipt attachment — snap/attach an image (data URL in mock).
411
- Hidden picker; `capture=environment` makes mobile offer the camera. */}
412
- {type !== 'transfer' && (
413
- <div>
414
- <span className="mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
415
- <Paperclip className="h-3.5 w-3.5" /> {t('financial.quickTx.receipt')}
416
- </span>
417
- <input
418
- ref={fileInputRef}
419
- type="file"
420
- accept="image/*"
421
- capture="environment"
422
- className="hidden"
423
- onChange={onPickReceipt}
424
- />
425
- {receiptUrl ? (
426
- <div className="flex items-center gap-3 rounded-input border border-input bg-card p-2 shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)]">
427
- <img
428
- src={receiptUrl}
429
- alt={t('financial.quickTx.receipt')}
430
- className="h-16 w-16 shrink-0 rounded-md border object-cover"
431
- />
432
- <div className="min-w-0 flex-1">
433
- <p className="truncate text-sm font-medium">{t('financial.quickTx.receiptAttached')}</p>
434
- <button
435
- type="button"
436
- onClick={() => fileInputRef.current?.click()}
437
- className="text-xs text-primary hover:underline"
438
- >
439
- {t('financial.quickTx.receiptReplace')}
440
- </button>
441
- </div>
442
- <button
443
- type="button"
444
- onClick={() => setReceiptUrl(undefined)}
445
- aria-label={t('financial.quickTx.receiptRemove')}
446
- className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted"
447
- >
448
- <X className="h-4 w-4" />
449
- </button>
450
- </div>
451
- ) : (
452
- <button
453
- type="button"
454
- onClick={() => fileInputRef.current?.click()}
455
- className={`flex w-full items-center justify-center gap-2 rounded-input border border-dashed px-3 py-3 text-sm font-medium transition-colors hover:bg-muted/30 ${
456
- focusReceipt ? 'border-primary text-primary ring-1 ring-primary/30' : 'border-input text-muted-foreground'
457
- }`}
458
- >
459
- <Camera className="h-4 w-4" /> {t('financial.quickTx.receiptCapture')}
460
- </button>
461
- )}
462
- </div>
463
- )}
464
-
465
- {type === 'transfer' ? (
466
- <div className="grid grid-cols-1 gap-3">
467
- <ChipSelect
468
- icon={Wallet}
469
- label={t('financial.quickTx.fromAccount')}
470
- value={accountId}
471
- options={accountOptions}
472
- onChange={setAccountId}
473
- emptyLabel={t('financial.quickTx.selectAccount')}
474
- />
475
- <ChipSelect
476
- icon={Wallet}
477
- label={t('financial.quickTx.toAccount')}
478
- value={toAccountId}
479
- options={accountOptions.filter((o) => o.id !== accountId)}
480
- onChange={setToAccountId}
481
- emptyLabel={t('financial.quickTx.selectAccount')}
482
- />
483
- </div>
484
- ) : (
485
- <div className="grid grid-cols-1 gap-3">
486
- <ChipSelect
487
- icon={Tag}
488
- label={t('financial.quickTx.category')}
489
- value={categoryId}
490
- options={categoryOptions}
491
- onChange={setCategoryId}
492
- emptyLabel={t('financial.quickTx.selectCategory')}
493
- />
494
- <ChipSelect
495
- icon={Wallet}
496
- label={t('financial.quickTx.account')}
497
- value={accountId}
498
- options={accountOptions}
499
- onChange={setAccountId}
500
- emptyLabel={t('financial.quickTx.selectAccount')}
501
- />
502
- </div>
503
- )}
504
-
505
- {/* Recurring + attachment (Anexo now opens the receipt picker — FAY-1226) */}
506
- {type !== 'transfer' && (
507
- <div className="flex flex-wrap gap-2">
508
- <TogglePill
509
- icon={Repeat}
510
- label={t('financial.quickTx.recurring')}
511
- checked={recurring}
512
- onChange={setRecurring}
513
- />
514
- <button
515
- type="button"
516
- onClick={() => fileInputRef.current?.click()}
517
- title={t('financial.quickTx.attachment')}
518
- className={`flex items-center gap-2 rounded-full border px-3 py-2 text-xs font-medium transition-colors ${
519
- receiptUrl
520
- ? 'border-transparent bg-primary text-primary-foreground'
521
- : 'border-input bg-card text-muted-foreground hover:bg-muted/40'
522
- }`}
523
- >
524
- <Paperclip className="h-3.5 w-3.5" /> {t('financial.quickTx.attachment')}
525
- {receiptUrl && <Check className="h-3.5 w-3.5" />}
526
- </button>
527
- </div>
528
- )}
529
- </div>
530
-
531
- {/* Footer */}
532
- <div className="flex shrink-0 items-center gap-3 border-t px-5 py-3">
533
- <span className="text-xs text-muted-foreground">
534
- {amount > 0 ? formatCurrency(amount, currency) : t('financial.quickTx.enterAmount')}
535
- </span>
536
- <Button
537
- className="ml-auto"
538
- onClick={() => void handleSave()}
539
- disabled={!canSave}
540
- >
541
- <Check className="h-4 w-4" />
542
- {saving ? t('financial.quickTx.saving') : t('financial.quickTx.save')}
543
- </Button>
544
- </div>
545
- </ModalContent>
546
- </Modal>
547
- )
548
- }
@@ -1,48 +0,0 @@
1
- import React from 'react'
2
- import { useTranslation } from '@fayz-ai/core'
3
- import { InvoiceListView } from './InvoiceListView'
4
- import { InvoiceFormView } from './InvoiceFormView'
5
- import { InvoiceDetailView } from './InvoiceDetailView'
6
- import { referrerLabel } from './navReferrer'
7
- import type { ViewIntent } from '../FinancialPage'
8
-
9
- export function ReceivablesView({ intent, onNavigate, previousView, back }: {
10
- intent: ViewIntent
11
- onNavigate: (view: string) => void
12
- /** The in-module view the user came from — drives the context-aware back link. */
13
- previousView?: string | null
14
- /** Return to the actual previous page (falls back to the given view). */
15
- back?: (fallbackView?: string) => void
16
- }) {
17
- const t = useTranslation()
18
-
19
- if (intent.mode === 'new') {
20
- return <InvoiceFormView direction="credit" onSaved={(id) => onNavigate(id ? `receivables-detail:${id}` : 'receivables-list')} />
21
- }
22
-
23
- if (intent.mode === 'edit' && intent.editId) {
24
- return <InvoiceFormView direction="credit" editId={intent.editId} onSaved={() => onNavigate(`receivables-detail:${intent.editId}`)} />
25
- }
26
-
27
- if (intent.mode === 'detail' && intent.editId) {
28
- // Back to where the user actually came from (e.g. statements), else the list.
29
- const prevLabel = referrerLabel(previousView, t)
30
- return (
31
- <InvoiceDetailView
32
- invoiceId={intent.editId}
33
- direction="credit"
34
- backLabel={prevLabel ?? undefined}
35
- onBack={prevLabel && back ? () => back('receivables-list') : () => onNavigate('receivables-list')}
36
- onEdit={() => onNavigate(`receivables-edit:${intent.editId}`)}
37
- />
38
- )
39
- }
40
-
41
- return (
42
- <InvoiceListView
43
- direction="credit"
44
- onNew={() => onNavigate('receivables-new')}
45
- onEdit={(id) => onNavigate(`receivables-detail:${id}`)}
46
- />
47
- )
48
- }