@fayz-ai/plugin-financial 0.1.7 → 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.
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +15 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +2 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1531 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1393 -80
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/quick-add.d.ts +13 -0
- package/dist/quick-add.d.ts.map +1 -0
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CardsView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/QuickTransactionForm.d.ts +16 -0
- package/dist/views/QuickTransactionForm.d.ts.map +1 -0
- package/dist/views/SummaryView.d.ts +5 -4
- package/dist/views/SummaryView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +4 -7
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +86 -7
- package/src/data/capability.test.ts +45 -0
- package/src/data/index.ts +1 -0
- package/src/data/mock.ts +50 -10
- package/src/data/supabase.ts +46 -3
- package/src/data/types.ts +2 -1
- package/src/index.ts +24 -4
- package/src/locales/en.ts +75 -0
- package/src/locales/pt-BR.ts +75 -0
- package/src/quick-add.ts +58 -0
- package/src/store.ts +88 -0
- package/src/types.ts +45 -0
- package/src/views/CardsView.tsx +186 -50
- package/src/views/InvoiceListView.tsx +60 -3
- package/src/views/QuickTransactionForm.tsx +537 -0
- package/src/views/SummaryView.tsx +197 -6
- package/src/views/dashboardWidgets.tsx +251 -5
package/src/data/supabase.ts
CHANGED
|
@@ -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: {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/locales/pt-BR.ts
CHANGED
|
@@ -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
|
}
|
package/src/quick-add.ts
ADDED
|
@@ -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
|