@fayz-ai/plugin-financial 0.1.6 → 0.2.0

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 (50) hide show
  1. package/dist/FinancialContext.d.ts +2 -0
  2. package/dist/FinancialContext.d.ts.map +1 -1
  3. package/dist/FinancialPage.d.ts.map +1 -1
  4. package/dist/data/index.d.ts +1 -0
  5. package/dist/data/index.d.ts.map +1 -1
  6. package/dist/data/mock.d.ts +15 -0
  7. package/dist/data/mock.d.ts.map +1 -1
  8. package/dist/data/supabase.d.ts.map +1 -1
  9. package/dist/data/types.d.ts +2 -1
  10. package/dist/data/types.d.ts.map +1 -1
  11. package/dist/index.cjs +1531 -201
  12. package/dist/index.cjs.map +1 -1
  13. package/dist/index.d.ts +13 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1393 -80
  16. package/dist/index.js.map +1 -1
  17. package/dist/locales/en.d.ts.map +1 -1
  18. package/dist/locales/pt-BR.d.ts.map +1 -1
  19. package/dist/quick-add.d.ts +13 -0
  20. package/dist/quick-add.d.ts.map +1 -0
  21. package/dist/store.d.ts +3 -1
  22. package/dist/store.d.ts.map +1 -1
  23. package/dist/types.d.ts +42 -0
  24. package/dist/types.d.ts.map +1 -1
  25. package/dist/views/CardsView.d.ts.map +1 -1
  26. package/dist/views/InvoiceListView.d.ts.map +1 -1
  27. package/dist/views/QuickTransactionForm.d.ts +16 -0
  28. package/dist/views/QuickTransactionForm.d.ts.map +1 -0
  29. package/dist/views/SummaryView.d.ts +5 -4
  30. package/dist/views/SummaryView.d.ts.map +1 -1
  31. package/dist/views/dashboardWidgets.d.ts.map +1 -1
  32. package/package.json +4 -7
  33. package/src/FinancialContext.tsx +2 -0
  34. package/src/FinancialPage.tsx +86 -7
  35. package/src/data/capability.test.ts +45 -0
  36. package/src/data/index.ts +1 -0
  37. package/src/data/mock.ts +50 -10
  38. package/src/data/supabase.ts +46 -3
  39. package/src/data/types.ts +2 -1
  40. package/src/index.ts +24 -4
  41. package/src/locales/en.ts +75 -0
  42. package/src/locales/pt-BR.ts +75 -0
  43. package/src/quick-add.ts +58 -0
  44. package/src/store.ts +88 -0
  45. package/src/types.ts +45 -0
  46. package/src/views/CardsView.tsx +186 -50
  47. package/src/views/InvoiceListView.tsx +60 -3
  48. package/src/views/QuickTransactionForm.tsx +537 -0
  49. package/src/views/SummaryView.tsx +197 -6
  50. package/src/views/dashboardWidgets.tsx +251 -5
@@ -2,7 +2,7 @@ import type { FinancialDataProvider } from './types'
2
2
  import type {
3
3
  Invoice, InvoiceItem, FinancialMovement, BankAccount,
4
4
  CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
5
- CardTransaction,
5
+ CostCenter, CardTransaction,
6
6
  CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
7
7
  OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
8
8
  InvoiceQuery, MovementQuery, StatementQuery,
@@ -116,6 +116,24 @@ function round2(n: number): number {
116
116
  return Math.round(n * 100) / 100
117
117
  }
118
118
 
119
+ async function findOrderIdsByItemMetadata(
120
+ core: any,
121
+ filters: { accountId?: string; costCenterId?: string },
122
+ ): Promise<string[] | undefined> {
123
+ if (!filters.accountId && !filters.costCenterId) return undefined
124
+
125
+ let qb = core.from('order_items').select('order_id')
126
+ if (filters.accountId) qb = qb.filter('metadata->>accountId', 'eq', filters.accountId)
127
+ if (filters.costCenterId) qb = qb.filter('metadata->>costCenterId', 'eq', filters.costCenterId)
128
+
129
+ const { data } = await qb
130
+ const ids: string[] = []
131
+ for (const row of (data ?? []) as Array<{ order_id?: unknown }>) {
132
+ if (typeof row.order_id === 'string' && row.order_id.length > 0) ids.push(row.order_id)
133
+ }
134
+ return Array.from(new Set(ids))
135
+ }
136
+
119
137
  /** Card/MDR fee percent for a configured payment method (0 when none / not a fee method). */
120
138
  async function getMethodFeePct(pub: any, paymentMethodId?: string): Promise<number> {
121
139
  if (!paymentMethodId) return 0
@@ -151,6 +169,14 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
151
169
  const statuses = Array.isArray(query.status) ? query.status : [query.status]
152
170
  qb = qb.in('status', statuses)
153
171
  }
172
+ const itemScopedOrderIds = await findOrderIdsByItemMetadata(core, {
173
+ accountId: query.accountId,
174
+ costCenterId: query.costCenterId,
175
+ })
176
+ if (itemScopedOrderIds) {
177
+ if (itemScopedOrderIds.length === 0) return { data: [], total: 0 }
178
+ qb = qb.in('id', itemScopedOrderIds)
179
+ }
154
180
  if (query.contactId) qb = qb.eq('party_id', query.contactId)
155
181
  if (query.search) qb = qb.ilike('notes', `%${query.search}%`)
156
182
  if (query.dateRange) qb = qb.gte('created_at', query.dateRange.from).lte('created_at', query.dateRange.to)
@@ -224,6 +250,8 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
224
250
  unitPrice: i.unitPrice ?? 0, totalAmount: i.total ?? 0,
225
251
  discount: i.discount ?? 0, surcharge: 0,
226
252
  referenceId: i.productId ?? i.serviceId,
253
+ accountId: i.metadata?.accountId,
254
+ costCenterId: i.metadata?.costCenterId,
227
255
  createdAt: i.createdAt,
228
256
  } as InvoiceItem
229
257
  })
@@ -263,7 +291,12 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
263
291
  discount: item.discount ?? 0,
264
292
  total: item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0),
265
293
  sort_order: i,
266
- metadata: { itemKind: item.itemKind, referenceId: item.referenceId },
294
+ metadata: {
295
+ itemKind: item.itemKind,
296
+ referenceId: item.referenceId,
297
+ accountId: item.accountId,
298
+ costCenterId: item.costCenterId,
299
+ },
267
300
  }))
268
301
  )
269
302
  }
@@ -547,6 +580,12 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
547
580
  return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as ChartOfAccountsNode)
548
581
  },
549
582
 
583
+ async getCostCenters(): Promise<CostCenter[]> {
584
+ const { core, pub } = getClients()
585
+ const { data } = await pub.from('cost_centers').select('*').eq('is_active', true).order('code')
586
+ return (data ?? []).map((r: any) => snakeToCamel(r) as unknown as CostCenter)
587
+ },
588
+
550
589
  // --- Card Transactions ---
551
590
  async getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]> {
552
591
  const { core, pub } = getClients()
@@ -709,7 +748,11 @@ export function createSupabaseFinancialProvider(): FinancialDataProvider {
709
748
  const overdueCredit = unpaidCredit.filter((m: any) => m.status === 'overdue' || m.due_date < today)
710
749
  const overdueDebit = unpaidDebit.filter((m: any) => m.status === 'overdue' || m.due_date < today)
711
750
 
712
- const monthStart = today.slice(0, 7) + '-01'
751
+ // Monthly cash flow = a trailing 30-day rolling window (not calendar
752
+ // month-to-date, which reads empty in the first days of a month).
753
+ const monthAgo = new Date()
754
+ monthAgo.setDate(monthAgo.getDate() - 30)
755
+ const monthStart = monthAgo.toISOString().slice(0, 10)
713
756
  // Monthly cash flow = payment cash events (one row per payment), never bills.
714
757
  const monthPaid = allMovs.filter((m: any) => m.movement_kind === 'payment' && m.payment_date >= monthStart)
715
758
 
package/src/data/types.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type {
2
2
  Invoice, InvoiceItem, FinancialMovement, BankAccount,
3
3
  CashSession, PaymentMethod, PaymentMethodType, ChartOfAccountsNode,
4
- CardTransaction,
4
+ CostCenter, CardTransaction,
5
5
  CreateInvoiceInput, PayMovementInput, CreateTransferInput, TransferResult,
6
6
  OpenCashSessionInput, CloseCashSessionInput, CreateBankAccountInput,
7
7
  InvoiceQuery, MovementQuery, StatementQuery,
@@ -46,6 +46,7 @@ export interface FinancialDataProvider {
46
46
 
47
47
  // --- Chart of Accounts ---
48
48
  getChartOfAccounts(): Promise<ChartOfAccountsNode[]>
49
+ getCostCenters(): Promise<CostCenter[]>
49
50
 
50
51
  // --- Card Transactions ---
51
52
  getCardTransactions(dateRange?: DateRange): Promise<CardTransaction[]>
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
2
+ import type { PluginManifest, PluginRegistryDef, PluginScope, VerticalId } from '@fayz-ai/core'
3
3
  import type { EntityLookupMap } from '@fayz-ai/saas'
4
4
  import { FinancialPage } from './FinancialPage'
5
5
  import { createFinancialDashboardWidgets } from './views/dashboardWidgets'
@@ -75,6 +75,13 @@ export interface FinancialPluginOptions {
75
75
  locale?: string
76
76
  symbol?: string
77
77
  }
78
+ /**
79
+ * Show the plugin's own quick-add affordances (the "New transaction" + "Send
80
+ * receipt" header buttons and, on mobile, the FAB). Opt-in (default: false) so
81
+ * hosts that already surface a "New" menu / global center-action don't render
82
+ * redundant controls. The shared quick-add sheet still opens via openQuickAdd().
83
+ */
84
+ quickAdd?: boolean
78
85
  navPosition?: number
79
86
  navSection?: 'main' | 'secondary'
80
87
  scope?: PluginScope
@@ -109,6 +116,9 @@ export interface FinancialPluginOptions {
109
116
  /** Contact/person lookup for "Pay to" / "Receive from" fields. Queries persons archetype. */
110
117
  contactLookup?: import('@fayz-ai/saas').EntityLookup
111
118
 
119
+ /** App-owned registries shown under /settings/financial Properties. */
120
+ settingsRegistries?: PluginRegistryDef[]
121
+
112
122
  /** Callback when user clicks a booking link on an invoice. Receives the order ID. */
113
123
  onBookingClick?: (orderId: string) => void
114
124
  }
@@ -162,6 +172,8 @@ function resolveConfig(options?: FinancialPluginOptions): ResolvedFinancialConfi
162
172
  // Opt-in: only shown when an app explicitly enables reconciliation (bank connector present).
163
173
  reconciliation: options?.modules?.reconciliation === true,
164
174
  },
175
+ // Opt-in: only render the plugin's own quick-add buttons/FAB when the host asks.
176
+ quickAdd: options?.quickAdd === true,
165
177
  labels: { ...DEFAULT_LABELS, ...options?.labels },
166
178
  currency: { ...DEFAULT_CURRENCY, ...options?.currency },
167
179
  itemTypes: options?.itemTypes ?? DEFAULT_ITEM_TYPES,
@@ -188,6 +200,7 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
188
200
  const provider = options?.dataProvider ?? createSafeFinancialProvider()
189
201
  const store = createFinancialStore(provider)
190
202
  const dashboardWidgets = createFinancialDashboardWidgets({ config, provider, store })
203
+ const registries = [...financialRegistries, ...(options?.settingsRegistries ?? [])]
191
204
 
192
205
  // Per-person statement tab (account_central-style): bound to this plugin's provider.
193
206
  const PersonStatementBound: React.FC<any> = (props) =>
@@ -195,7 +208,7 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
195
208
  PersonStatementBound.displayName = 'FinancialPersonStatement'
196
209
 
197
210
  const PageComponent: React.FC<any> = () =>
198
- React.createElement(FinancialPage, { config, provider, store, registries: financialRegistries })
211
+ React.createElement(FinancialPage, { config, provider, store, registries })
199
212
 
200
213
  return {
201
214
  id: 'financial',
@@ -301,7 +314,7 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
301
314
  ],
302
315
  },
303
316
  ],
304
- registries: financialRegistries,
317
+ registries,
305
318
  settings: [
306
319
  {
307
320
  id: 'financial',
@@ -313,7 +326,7 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
313
326
  title: 'Financial Settings',
314
327
  subtitle: 'Payment methods, accounts, and configurations',
315
328
  generalSettings: React.createElement(FinancialGeneralSettings),
316
- registries: financialRegistries,
329
+ registries,
317
330
  hostPluginId: 'financial',
318
331
  routeBase: '/settings/financial',
319
332
  })
@@ -331,3 +344,10 @@ export function createFinancialPlugin(options?: FinancialPluginOptions): PluginM
331
344
  // Re-export types and factories for consumers
332
345
  export type { FinancialDataProvider } from './data/types'
333
346
  export type { ResolvedFinancialConfig } from './FinancialContext'
347
+ // Seedable in-memory provider — apps pass a seed to render the module populated
348
+ // (demo/dogfood data) instead of the empty onboarding state.
349
+ export { createMockFinancialProvider } from './data/mock'
350
+ export type { MockFinancialSeed, MockFinancialProviderOptions } from './data/mock'
351
+ // FAY-1242: global quick-add trigger — apps wire this to a shell action (e.g.
352
+ // the bottom-nav center "+") to open the transaction sheet from anywhere.
353
+ export { openQuickAdd } from './quick-add'
package/src/locales/en.ts CHANGED
@@ -1,7 +1,17 @@
1
1
  // financial.* — merged plugin-own + saas-core central i18n
2
2
  export const en: Record<string, string> = {
3
+ "financial.cards.addExpense": "Add expense",
4
+ "financial.cards.allBillsValue": "All bills value",
5
+ "financial.cards.availableLimit": "Available limit",
6
+ "financial.cards.currentBill": "Current bill",
7
+ "financial.cards.dueOn": "Due on the {{day}}",
3
8
  "financial.cards.installment": "Installment {{current}}/{{total}}",
9
+ "financial.cards.noCards": "No cards yet",
10
+ "financial.cards.noCardsHint": "Add a credit card in settings to track your bills here",
4
11
  "financial.cards.noTransactions": "No card transactions",
12
+ "financial.cards.tabClosed": "Closed",
13
+ "financial.cards.tabOpen": "Open",
14
+ "financial.cards.usedOfLimit": "{{percent}}% of limit used",
5
15
  "financial.cards.pending": "Pending",
6
16
  "financial.cards.received": "Received",
7
17
  "financial.cards.subtitle": "Credit and debit card transactions",
@@ -128,6 +138,10 @@ export const en: Record<string, string> = {
128
138
  "financial.nav.rules": "Rules",
129
139
  "financial.nav.statements": "Statements",
130
140
  "financial.nav.summary": "Summary",
141
+ "financial.invoice.filterAccount": "Chart account",
142
+ "financial.invoice.filterAllAccounts": "All accounts",
143
+ "financial.invoice.filterCostCenter": "Cost center",
144
+ "financial.invoice.filterAllCostCenters": "All cost centers",
131
145
  "financial.reconciliation.title": "Reconciliation",
132
146
  "financial.reconciliation.subtitle": "Match imported bank lines to your movements",
133
147
  "financial.reconciliation.pending": "{{count}} to reconcile",
@@ -242,6 +256,36 @@ export const en: Record<string, string> = {
242
256
  "financial.summary.totalBalance": "Total Balance",
243
257
  "financial.summary.transactionsAppear": "Transactions will appear here once recorded",
244
258
  "financial.summary.viewAll": "View all",
259
+ "financial.summary.today": "Today",
260
+ "financial.summary.yesterday": "Yesterday",
261
+ "financial.summary.transactionsTitle": "Transactions",
262
+ "financial.home.balance": "Balance",
263
+ "financial.home.inflow": "Income",
264
+ "financial.home.outflow": "Expenses",
265
+ "financial.home.showBalance": "Show balance",
266
+ "financial.home.hideBalance": "Hide balance",
267
+ "financial.home.upcomingBills": "Upcoming bills",
268
+ "financial.home.noBills": "No upcoming bills.",
269
+ "financial.home.dueToday": "Due today",
270
+ "financial.home.dueTomorrow": "Due tomorrow",
271
+ "financial.home.dueInDays": "Due in {{count}} days",
272
+ "financial.home.overdueDays": "Overdue by {{count}} days",
273
+ "financial.home.cards": "Cards",
274
+ "financial.home.currentBill": "Current bill",
275
+ "financial.home.availableLimit": "Available limit",
276
+ "financial.home.addCard": "New card",
277
+ "financial.home.bill": "Bill",
278
+ "financial.home.spendByCategory": "Spending by category",
279
+ "financial.home.noSpend": "No spending this month.",
280
+ "financial.home.category.groceries": "Groceries",
281
+ "financial.home.category.transport": "Transport",
282
+ "financial.home.category.subscriptions": "Subscriptions",
283
+ "financial.home.category.dining": "Dining",
284
+ "financial.home.category.home": "Home",
285
+ "financial.home.category.health": "Health",
286
+ "financial.home.category.leisure": "Leisure",
287
+ "financial.home.category.shopping": "Shopping",
288
+ "financial.home.category.other": "Other",
245
289
  "financial.quickActions.newPayable": "New Payable",
246
290
  "financial.quickActions.newPayableDesc": "Record an expense or bill to pay",
247
291
  "financial.quickActions.newReceivable": "New Receivable",
@@ -404,4 +448,35 @@ export const en: Record<string, string> = {
404
448
  "financial.transfer.failed": "Failed to record transfer",
405
449
  "financial.transfer.sameAccount": "Source and destination must differ",
406
450
  "financial.transfer.invalidAmount": "Amount must be greater than zero",
451
+ "financial.quickTx.title": "New transaction",
452
+ "financial.quickTx.close": "Close",
453
+ "financial.quickTx.newTransaction": "New transaction",
454
+ "financial.quickTx.expense": "Expense",
455
+ "financial.quickTx.income": "Income",
456
+ "financial.quickTx.transfer": "Transfer",
457
+ "financial.quickTx.paid": "Paid",
458
+ "financial.quickTx.received": "Received",
459
+ "financial.quickTx.yes": "Yes",
460
+ "financial.quickTx.no": "No",
461
+ "financial.quickTx.category": "Category",
462
+ "financial.quickTx.selectCategory": "No category",
463
+ "financial.quickTx.account": "Account",
464
+ "financial.quickTx.selectAccount": "Select account",
465
+ "financial.quickTx.fromAccount": "From account",
466
+ "financial.quickTx.toAccount": "To account",
467
+ "financial.quickTx.card": "Card",
468
+ "financial.quickTx.recurring": "Recurring",
469
+ "financial.quickTx.attachment": "Attachment",
470
+ "financial.quickTx.attachmentSoon": "Attachments coming soon",
471
+ "financial.quickTx.receipt": "Receipt",
472
+ "financial.quickTx.receiptCapture": "Snap or attach a receipt",
473
+ "financial.quickTx.receiptAttached": "Receipt attached",
474
+ "financial.quickTx.receiptReplace": "Replace",
475
+ "financial.quickTx.receiptRemove": "Remove receipt",
476
+ "financial.quickTx.receiptInvalid": "Please choose an image file",
477
+ "financial.quickTx.sendReceipt": "Send receipt",
478
+ "financial.quickTx.descriptionPlaceholder": "e.g. Groceries, Salary…",
479
+ "financial.quickTx.enterAmount": "Enter an amount",
480
+ "financial.quickTx.save": "Save",
481
+ "financial.quickTx.saving": "Saving…",
407
482
  }
@@ -1,7 +1,17 @@
1
1
  // financial.* — merged plugin-own + saas-core central i18n
2
2
  export const ptBR: Record<string, string> = {
3
+ "financial.cards.addExpense": "Adicionar despesa",
4
+ "financial.cards.allBillsValue": "Total das faturas",
5
+ "financial.cards.availableLimit": "Limite disponível",
6
+ "financial.cards.currentBill": "Fatura atual",
7
+ "financial.cards.dueOn": "Vence dia {{day}}",
3
8
  "financial.cards.installment": "Parcela {{current}}/{{total}}",
9
+ "financial.cards.noCards": "Nenhum cartão cadastrado",
10
+ "financial.cards.noCardsHint": "Adicione um cartão de crédito nas configurações para acompanhar suas faturas aqui",
4
11
  "financial.cards.noTransactions": "Nenhuma transação com cartão",
12
+ "financial.cards.tabClosed": "Fechados",
13
+ "financial.cards.tabOpen": "Abertos",
14
+ "financial.cards.usedOfLimit": "{{percent}}% do limite usado",
5
15
  "financial.cards.pending": "Pendente",
6
16
  "financial.cards.received": "Recebido",
7
17
  "financial.cards.subtitle": "Transações de cartão de crédito e débito",
@@ -128,6 +138,10 @@ export const ptBR: Record<string, string> = {
128
138
  "financial.nav.rules": "Regras",
129
139
  "financial.nav.statements": "Extratos",
130
140
  "financial.nav.summary": "Resumo",
141
+ "financial.invoice.filterAccount": "Plano de contas",
142
+ "financial.invoice.filterAllAccounts": "Todos os planos",
143
+ "financial.invoice.filterCostCenter": "Centro de custo",
144
+ "financial.invoice.filterAllCostCenters": "Todos os centros",
131
145
  "financial.reconciliation.title": "Conciliação",
132
146
  "financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lançamentos",
133
147
  "financial.reconciliation.pending": "{{count}} para conciliar",
@@ -242,6 +256,36 @@ export const ptBR: Record<string, string> = {
242
256
  "financial.summary.totalBalance": "Saldo Total",
243
257
  "financial.summary.transactionsAppear": "As transações aparecerão aqui após serem registradas",
244
258
  "financial.summary.viewAll": "Ver todos",
259
+ "financial.summary.today": "Hoje",
260
+ "financial.summary.yesterday": "Ontem",
261
+ "financial.summary.transactionsTitle": "Transações",
262
+ "financial.home.balance": "Saldo",
263
+ "financial.home.inflow": "Entradas",
264
+ "financial.home.outflow": "Saídas",
265
+ "financial.home.showBalance": "Mostrar saldo",
266
+ "financial.home.hideBalance": "Ocultar saldo",
267
+ "financial.home.upcomingBills": "Próximas contas",
268
+ "financial.home.noBills": "Nenhuma conta a vencer.",
269
+ "financial.home.dueToday": "Vence hoje",
270
+ "financial.home.dueTomorrow": "Vence amanhã",
271
+ "financial.home.dueInDays": "Vence em {{count}} dias",
272
+ "financial.home.overdueDays": "Venceu há {{count}} dias",
273
+ "financial.home.cards": "Cartões",
274
+ "financial.home.currentBill": "Fatura atual",
275
+ "financial.home.availableLimit": "Limite disponível",
276
+ "financial.home.addCard": "Novo cartão",
277
+ "financial.home.bill": "Conta",
278
+ "financial.home.spendByCategory": "Gastos por categoria",
279
+ "financial.home.noSpend": "Sem gastos neste mês.",
280
+ "financial.home.category.groceries": "Mercado",
281
+ "financial.home.category.transport": "Transporte",
282
+ "financial.home.category.subscriptions": "Assinaturas",
283
+ "financial.home.category.dining": "Alimentação",
284
+ "financial.home.category.home": "Casa",
285
+ "financial.home.category.health": "Saúde",
286
+ "financial.home.category.leisure": "Lazer",
287
+ "financial.home.category.shopping": "Compras",
288
+ "financial.home.category.other": "Outros",
245
289
  "financial.quickActions.newPayable": "Nova Conta a Pagar",
246
290
  "financial.quickActions.newPayableDesc": "Registrar uma despesa ou conta a pagar",
247
291
  "financial.quickActions.newReceivable": "Nova Conta a Receber",
@@ -404,4 +448,35 @@ export const ptBR: Record<string, string> = {
404
448
  "financial.transfer.failed": "Falha ao registrar transferência",
405
449
  "financial.transfer.sameAccount": "Origem e destino devem ser diferentes",
406
450
  "financial.transfer.invalidAmount": "O valor deve ser maior que zero",
451
+ "financial.quickTx.title": "Nova transação",
452
+ "financial.quickTx.close": "Fechar",
453
+ "financial.quickTx.newTransaction": "Nova transação",
454
+ "financial.quickTx.expense": "Despesa",
455
+ "financial.quickTx.income": "Receita",
456
+ "financial.quickTx.transfer": "Transferência",
457
+ "financial.quickTx.paid": "Pago",
458
+ "financial.quickTx.received": "Recebido",
459
+ "financial.quickTx.yes": "Sim",
460
+ "financial.quickTx.no": "Não",
461
+ "financial.quickTx.category": "Categoria",
462
+ "financial.quickTx.selectCategory": "Sem categoria",
463
+ "financial.quickTx.account": "Conta",
464
+ "financial.quickTx.selectAccount": "Selecione a conta",
465
+ "financial.quickTx.fromAccount": "Conta de origem",
466
+ "financial.quickTx.toAccount": "Conta de destino",
467
+ "financial.quickTx.card": "Cartão",
468
+ "financial.quickTx.recurring": "Recorrente",
469
+ "financial.quickTx.attachment": "Anexo",
470
+ "financial.quickTx.attachmentSoon": "Anexos em breve",
471
+ "financial.quickTx.receipt": "Recibo",
472
+ "financial.quickTx.receiptCapture": "Fotografar ou anexar recibo",
473
+ "financial.quickTx.receiptAttached": "Recibo anexado",
474
+ "financial.quickTx.receiptReplace": "Substituir",
475
+ "financial.quickTx.receiptRemove": "Remover recibo",
476
+ "financial.quickTx.receiptInvalid": "Escolha um arquivo de imagem",
477
+ "financial.quickTx.sendReceipt": "Enviar recibo",
478
+ "financial.quickTx.descriptionPlaceholder": "ex.: Mercado, Salário…",
479
+ "financial.quickTx.enterAmount": "Informe um valor",
480
+ "financial.quickTx.save": "Salvar",
481
+ "financial.quickTx.saving": "Salvando…",
407
482
  }
@@ -0,0 +1,58 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Global quick-add trigger (FAY-1242).
3
+ //
4
+ // A tiny module-level store so a "log a transaction" request can be fired from
5
+ // ANYWHERE in the app (e.g. the app shell's elevated center "+" bottom-nav
6
+ // button) without a shared React context. The FinancialPage subscribes and
7
+ // opens its QuickTransactionForm when a request arrives; the request is a
8
+ // one-shot (consumed on read) so re-mounting /financial doesn't re-open it.
9
+ //
10
+ // Flow: center "+" → openQuickAdd() → navigate to /financial → FinancialPage
11
+ // mounts, sees the pending request, opens the sheet. If already on /financial
12
+ // the subscription fires and the sheet opens instantly.
13
+ // ---------------------------------------------------------------------------
14
+
15
+ import * as React from 'react'
16
+ import type { QuickTransactionType } from './types'
17
+
18
+ export interface QuickAddRequest {
19
+ type: QuickTransactionType
20
+ /** Foreground the receipt/attachment step + auto-open the picker. */
21
+ receipt: boolean
22
+ }
23
+
24
+ let pending: QuickAddRequest | null = null
25
+ const listeners = new Set<() => void>()
26
+
27
+ function emit() {
28
+ for (const l of listeners) l()
29
+ }
30
+
31
+ /** Fire a quick-add request from anywhere. Default = an expense. */
32
+ export function openQuickAdd(type: QuickTransactionType = 'expense', receipt = false): void {
33
+ pending = { type, receipt }
34
+ emit()
35
+ }
36
+
37
+ /** Read + clear the pending request (one-shot). */
38
+ export function consumeQuickAdd(): QuickAddRequest | null {
39
+ const req = pending
40
+ pending = null
41
+ return req
42
+ }
43
+
44
+ function subscribe(cb: () => void): () => void {
45
+ listeners.add(cb)
46
+ return () => {
47
+ listeners.delete(cb)
48
+ }
49
+ }
50
+
51
+ function getSnapshot(): QuickAddRequest | null {
52
+ return pending
53
+ }
54
+
55
+ /** React binding — returns the pending request (null when none). */
56
+ export function usePendingQuickAdd(): QuickAddRequest | null {
57
+ return React.useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
58
+ }
package/src/store.ts CHANGED
@@ -9,6 +9,7 @@ import type {
9
9
  InvoiceQuery, MovementQuery, StatementQuery,
10
10
  CreateInvoiceInput, PayMovementInput, CreateTransferInput,
11
11
  OpenCashSessionInput, CloseCashSessionInput,
12
+ QuickTransactionInput,
12
13
  DateRange,
13
14
  } from './types'
14
15
 
@@ -55,6 +56,8 @@ export interface FinancialUIState {
55
56
  fetchStatement(query: StatementQuery): Promise<void>
56
57
  fetchCardTransactions(dateRange?: DateRange): Promise<void>
57
58
  createInvoice(input: CreateInvoiceInput): Promise<Invoice>
59
+ /** FAY-1225 "log money in a few taps": one call → the right provider plumbing + full surface refresh. */
60
+ createQuickTransaction(input: QuickTransactionInput): Promise<void>
58
61
  payMovement(input: PayMovementInput): Promise<void>
59
62
  createTransfer(input: CreateTransferInput): Promise<void>
60
63
  cancelInvoice(invoiceId: string): Promise<void>
@@ -169,6 +172,91 @@ export function createFinancialStore(provider: FinancialDataProvider): StoreApi<
169
172
  }
170
173
  },
171
174
 
175
+ async createQuickTransaction(input) {
176
+ try {
177
+ if (input.type === 'transfer') {
178
+ if (!input.bankAccountId || !input.toAccountId) {
179
+ throw new Error('Select source and destination accounts')
180
+ }
181
+ await provider.createTransfer({
182
+ fromAccountId: input.bankAccountId,
183
+ toAccountId: input.toAccountId,
184
+ amount: input.amount,
185
+ date: input.date,
186
+ notes: input.description?.trim() || undefined,
187
+ })
188
+ } else {
189
+ const direction: 'debit' | 'credit' = input.type === 'income' ? 'credit' : 'debit'
190
+ const desc = input.description?.trim() || (input.type === 'income' ? 'Receita' : 'Despesa')
191
+ // 1. The obligation (invoice + bill movement).
192
+ // Persist the recurring flag + the snapped-receipt data URL (FAY-1226)
193
+ // on the invoice metadata so the transaction feed can indicate/preview it.
194
+ const invoiceMeta: Record<string, unknown> = {}
195
+ if (input.recurring) invoiceMeta.recurring = true
196
+ if (input.receiptUrl) invoiceMeta.receiptUrl = input.receiptUrl
197
+ const invoice = await provider.createInvoice({
198
+ direction,
199
+ invoiceDate: input.date,
200
+ contactName: desc,
201
+ metadata: Object.keys(invoiceMeta).length > 0 ? invoiceMeta : undefined,
202
+ items: [{
203
+ itemKind: 'other',
204
+ description: desc,
205
+ quantity: 1,
206
+ unitPrice: input.amount,
207
+ accountId: input.categoryId,
208
+ }],
209
+ installments: [{
210
+ direction,
211
+ movementKind: 'bill',
212
+ amount: input.amount,
213
+ dueDate: input.date,
214
+ installmentNumber: 1,
215
+ bankAccountId: input.bankAccountId,
216
+ notes: desc,
217
+ }],
218
+ })
219
+ // 2. Settle it now when marked paid → a realized cash event that shows in
220
+ // the statement/extract and feeds the monthly cash-flow KPIs.
221
+ if (input.paid) {
222
+ const movements = await provider.getMovements({ direction, status: 'pending' })
223
+ const bill = movements.data.find((m) => m.invoiceId === invoice.id && m.movementKind === 'bill')
224
+ if (bill) {
225
+ await provider.payMovement({
226
+ movementId: bill.id,
227
+ amount: input.amount,
228
+ paymentDate: input.date,
229
+ bankAccountId: input.bankAccountId,
230
+ notes: desc,
231
+ })
232
+ }
233
+ }
234
+ }
235
+ // Refresh every surface this store feeds: Home/Resumo KPIs (summary),
236
+ // the mobile transaction list (statement), balances, and the payables/
237
+ // receivables tables (invoices).
238
+ const invoiceQuery = get().invoiceQuery
239
+ const statementQuery = get().statementQuery
240
+ const [summary, bankAccounts, invoicesRes, statement] = await Promise.all([
241
+ provider.getSummary(),
242
+ provider.getBankAccounts(),
243
+ provider.getInvoices(invoiceQuery),
244
+ statementQuery ? provider.getStatement(statementQuery) : Promise.resolve(get().statement),
245
+ ])
246
+ set({
247
+ summary,
248
+ bankAccounts,
249
+ invoices: invoicesRes.data,
250
+ invoicesTotal: invoicesRes.total,
251
+ statement,
252
+ })
253
+ toast.success('Transaction saved')
254
+ } catch (err: any) {
255
+ toast.error('Failed to save transaction', { description: err?.message })
256
+ throw err
257
+ }
258
+ },
259
+
172
260
  async payMovement(input) {
173
261
  try {
174
262
  await provider.payMovement(input)
package/src/types.ts CHANGED
@@ -199,6 +199,16 @@ export interface ChartOfAccountsNode {
199
199
  createdAt: string
200
200
  }
201
201
 
202
+ export interface CostCenter {
203
+ id: string
204
+ code: string
205
+ name: string
206
+ isActive: boolean
207
+ tenantId: string
208
+ createdAt: string
209
+ updatedAt: string
210
+ }
211
+
202
212
  export interface CardTransaction {
203
213
  id: string
204
214
  movementId: string
@@ -316,6 +326,8 @@ export interface InvoiceQuery {
316
326
  direction?: TransactionDirection
317
327
  status?: InvoiceStatus | InvoiceStatus[]
318
328
  contactId?: string
329
+ accountId?: string
330
+ costCenterId?: string
319
331
  dateRange?: DateRange
320
332
  search?: string
321
333
  page?: number
@@ -347,6 +359,39 @@ export interface CreateTransferInput {
347
359
  notes?: string
348
360
  }
349
361
 
362
+ /**
363
+ * "Log money in a few taps" (FAY-1225). A single-transaction quick-add that the
364
+ * store expands into the right provider calls: expense/income → createInvoice
365
+ * (+ payMovement when marked paid); transfer → createTransfer. Keeps the
366
+ * Mobills-style form dumb — it only collects fields, the store does the plumbing.
367
+ */
368
+ export type QuickTransactionType = 'expense' | 'income' | 'transfer'
369
+
370
+ export interface QuickTransactionInput {
371
+ type: QuickTransactionType
372
+ /** Positive amount in the account currency */
373
+ amount: number
374
+ /** YYYY-MM-DD */
375
+ date: string
376
+ /** When true, the movement is settled immediately (creates a paid cash event). */
377
+ paid: boolean
378
+ description?: string
379
+ /** Chart-of-accounts / cost-center id (category chip) */
380
+ categoryId?: string
381
+ /** expense/income: bank account or card. transfer: source account. */
382
+ bankAccountId?: string
383
+ /** transfer: destination account */
384
+ toAccountId?: string
385
+ /** Marks the transaction as recurring (stored in metadata; UI affordance). */
386
+ recurring?: boolean
387
+ /**
388
+ * FAY-1226 "snap a receipt": a captured/attached receipt image as a data URL
389
+ * (mock mode has no Supabase bucket). Persisted on the created invoice's
390
+ * metadata so the transaction feed can show a 📎 indicator + preview.
391
+ */
392
+ receiptUrl?: string
393
+ }
394
+
350
395
  export interface TransferResult {
351
396
  /** Shared correlation id stored in both legs' metadata */
352
397
  transferId: string