@fayz-ai/plugin-financial 0.1.1

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 (98) hide show
  1. package/dist/FinancialContext.d.ts +44 -0
  2. package/dist/FinancialContext.d.ts.map +1 -0
  3. package/dist/FinancialPage.d.ts +18 -0
  4. package/dist/FinancialPage.d.ts.map +1 -0
  5. package/dist/components/FinancialGeneralSettings.d.ts +3 -0
  6. package/dist/components/FinancialGeneralSettings.d.ts.map +1 -0
  7. package/dist/components/FinancialOnboarding.d.ts +5 -0
  8. package/dist/components/FinancialOnboarding.d.ts.map +1 -0
  9. package/dist/components/FinancialSettings.d.ts +8 -0
  10. package/dist/components/FinancialSettings.d.ts.map +1 -0
  11. package/dist/components/PaymentModal.d.ts +8 -0
  12. package/dist/components/PaymentModal.d.ts.map +1 -0
  13. package/dist/data/index.d.ts +3 -0
  14. package/dist/data/index.d.ts.map +1 -0
  15. package/dist/data/mock.d.ts +9 -0
  16. package/dist/data/mock.d.ts.map +1 -0
  17. package/dist/data/supabase.d.ts +3 -0
  18. package/dist/data/supabase.d.ts.map +1 -0
  19. package/dist/data/types.d.ts +27 -0
  20. package/dist/data/types.d.ts.map +1 -0
  21. package/dist/index.cjs +4937 -0
  22. package/dist/index.cjs.map +1 -0
  23. package/dist/index.d.ts +91 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +4930 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/lib/tenant.d.ts +3 -0
  28. package/dist/lib/tenant.d.ts.map +1 -0
  29. package/dist/locales/en.d.ts +2 -0
  30. package/dist/locales/en.d.ts.map +1 -0
  31. package/dist/locales/index.d.ts +2 -0
  32. package/dist/locales/index.d.ts.map +1 -0
  33. package/dist/locales/pt-BR.d.ts +2 -0
  34. package/dist/locales/pt-BR.d.ts.map +1 -0
  35. package/dist/registries.d.ts +3 -0
  36. package/dist/registries.d.ts.map +1 -0
  37. package/dist/store.d.ts +38 -0
  38. package/dist/store.d.ts.map +1 -0
  39. package/dist/types.d.ts +302 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/views/CardsView.d.ts +3 -0
  42. package/dist/views/CardsView.d.ts.map +1 -0
  43. package/dist/views/CashRegistersView.d.ts +3 -0
  44. package/dist/views/CashRegistersView.d.ts.map +1 -0
  45. package/dist/views/CommissionsView.d.ts +3 -0
  46. package/dist/views/CommissionsView.d.ts.map +1 -0
  47. package/dist/views/InvoiceDetailView.d.ts +9 -0
  48. package/dist/views/InvoiceDetailView.d.ts.map +1 -0
  49. package/dist/views/InvoiceFormView.d.ts +13 -0
  50. package/dist/views/InvoiceFormView.d.ts.map +1 -0
  51. package/dist/views/InvoiceListView.d.ts +8 -0
  52. package/dist/views/InvoiceListView.d.ts.map +1 -0
  53. package/dist/views/PayablesView.d.ts +7 -0
  54. package/dist/views/PayablesView.d.ts.map +1 -0
  55. package/dist/views/ReceivablesView.d.ts +7 -0
  56. package/dist/views/ReceivablesView.d.ts.map +1 -0
  57. package/dist/views/StatementsView.d.ts +3 -0
  58. package/dist/views/StatementsView.d.ts.map +1 -0
  59. package/dist/views/SummaryView.d.ts +9 -0
  60. package/dist/views/SummaryView.d.ts.map +1 -0
  61. package/dist/views/dashboardWidgets.d.ts +11 -0
  62. package/dist/views/dashboardWidgets.d.ts.map +1 -0
  63. package/package.json +55 -0
  64. package/src/FinancialContext.tsx +47 -0
  65. package/src/FinancialPage.tsx +261 -0
  66. package/src/README.md +155 -0
  67. package/src/components/FinancialGeneralSettings.tsx +26 -0
  68. package/src/components/FinancialOnboarding.tsx +87 -0
  69. package/src/components/FinancialSettings.tsx +31 -0
  70. package/src/components/PaymentModal.tsx +322 -0
  71. package/src/data/index.ts +2 -0
  72. package/src/data/mock.ts +520 -0
  73. package/src/data/supabase.ts +509 -0
  74. package/src/data/types.ts +53 -0
  75. package/src/index.ts +307 -0
  76. package/src/lib/tenant.ts +3 -0
  77. package/src/locales/en.ts +331 -0
  78. package/src/locales/index.ts +7 -0
  79. package/src/locales/pt-BR.ts +331 -0
  80. package/src/migrations/001_financial_base.sql +109 -0
  81. package/src/migrations/002_chart_of_accounts.sql +28 -0
  82. package/src/migrations/003_card_brands.sql +12 -0
  83. package/src/migrations/004_order_to_cash.sql +164 -0
  84. package/src/migrations/005_seed_defaults.sql +38 -0
  85. package/src/registries.ts +305 -0
  86. package/src/store.ts +221 -0
  87. package/src/types.ts +359 -0
  88. package/src/views/CardsView.tsx +74 -0
  89. package/src/views/CashRegistersView.tsx +597 -0
  90. package/src/views/CommissionsView.tsx +18 -0
  91. package/src/views/InvoiceDetailView.tsx +595 -0
  92. package/src/views/InvoiceFormView.tsx +900 -0
  93. package/src/views/InvoiceListView.tsx +177 -0
  94. package/src/views/PayablesView.tsx +37 -0
  95. package/src/views/ReceivablesView.tsx +37 -0
  96. package/src/views/StatementsView.tsx +123 -0
  97. package/src/views/SummaryView.tsx +12 -0
  98. package/src/views/dashboardWidgets.tsx +191 -0
@@ -0,0 +1,322 @@
1
+ import React, { useState, useEffect, useMemo } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { X, Check, Banknote, QrCode, CreditCard, Building2, ArrowRightLeft, FileCheck, Pencil } from 'lucide-react'
4
+ import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
5
+ import { useTranslation } from '@fayz-ai/core'
6
+ import { CurrencyInput } from '@fayz-ai/ui'
7
+ import { DatePicker } from '@fayz-ai/ui'
8
+ import { Button } from '@fayz-ai/ui'
9
+ import type { FinancialMovement, PaymentMethodType } from '../types'
10
+
11
+ const TYPE_ICONS: Record<string, React.ElementType> = {
12
+ cash: Banknote,
13
+ pix: QrCode,
14
+ credit_card: CreditCard,
15
+ debit_card: CreditCard,
16
+ bank_transfer: ArrowRightLeft,
17
+ check: FileCheck,
18
+ }
19
+
20
+ const TYPE_COLORS: Record<string, string> = {
21
+ cash: 'border-success/30 bg-success/5 hover:bg-success/10 data-[active=true]:border-success data-[active=true]:bg-success/15',
22
+ pix: 'border-teal-500/30 bg-teal-500/5 hover:bg-teal-500/10 data-[active=true]:border-teal-500 data-[active=true]:bg-teal-500/15',
23
+ credit_card: 'border-info/30 bg-info/5 hover:bg-info/10 data-[active=true]:border-info data-[active=true]:bg-info/15',
24
+ debit_card: 'border-magic/30 bg-magic/5 hover:bg-magic/10 data-[active=true]:border-magic data-[active=true]:bg-magic/15',
25
+ bank_transfer: 'border-warning/30 bg-warning/5 hover:bg-warning/10 data-[active=true]:border-warning data-[active=true]:bg-warning/15',
26
+ check: 'border-border bg-muted/40 hover:bg-muted/60 data-[active=true]:border-foreground data-[active=true]:bg-muted',
27
+ }
28
+
29
+ const ICON_COLORS: Record<string, string> = {
30
+ cash: 'text-success',
31
+ pix: 'text-teal-500',
32
+ credit_card: 'text-info',
33
+ debit_card: 'text-magic',
34
+ bank_transfer: 'text-warning',
35
+ check: 'text-muted-foreground',
36
+ }
37
+
38
+ export function PaymentModal({ movement, onClose, onPaid }: {
39
+ movement: FinancialMovement
40
+ onClose: () => void
41
+ onPaid: () => void
42
+ }) {
43
+ const t = useTranslation()
44
+ const { currency } = useFinancialConfig()
45
+ const payMovement = useFinancialStore((s) => s.payMovement)
46
+ const bankAccounts = useFinancialStore((s) => s.bankAccounts)
47
+ const paymentMethods = useFinancialStore((s) => s.paymentMethods)
48
+ const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes)
49
+ const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
50
+ const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods)
51
+
52
+ const remaining = movement.amount - movement.paidAmount
53
+ const [amount, setAmount] = useState(remaining)
54
+ const [paymentDate, setPaymentDate] = useState(new Date().toISOString().slice(0, 10))
55
+ const [selectedTypeId, setSelectedTypeId] = useState('')
56
+ const [paymentMethodId, setPaymentMethodId] = useState('')
57
+ const [bankAccountId, setBankAccountId] = useState('')
58
+ const [cardBrand, setCardBrand] = useState('')
59
+ const [cardInstallments, setCardInstallments] = useState(1)
60
+ const [notes, setNotes] = useState('')
61
+ const [saving, setSaving] = useState(false)
62
+ const [visible, setVisible] = useState(false)
63
+ const [expandMethodPicker, setExpandMethodPicker] = useState(true)
64
+
65
+ useEffect(() => {
66
+ fetchBankAccounts()
67
+ fetchPaymentMethods()
68
+ requestAnimationFrame(() => setVisible(true))
69
+ }, [])
70
+
71
+ const selectedType = useMemo(() =>
72
+ paymentMethodTypes.find((tp) => tp.id === selectedTypeId),
73
+ [paymentMethodTypes, selectedTypeId]
74
+ )
75
+
76
+ const txType = selectedType?.transactionType ?? ''
77
+
78
+ const filteredMethods = useMemo(() =>
79
+ paymentMethods.filter((m) => m.paymentMethodTypeId === selectedTypeId),
80
+ [paymentMethods, selectedTypeId]
81
+ )
82
+
83
+ const needsBankAccount = txType === 'bank_transfer' || txType === 'pix' || txType === 'check'
84
+ const needsCard = txType === 'credit_card' || txType === 'debit_card'
85
+ const needsCardInstallments = txType === 'credit_card'
86
+
87
+ function handleSelectType(typeId: string) {
88
+ setSelectedTypeId(typeId)
89
+ setExpandMethodPicker(false)
90
+ setPaymentMethodId('')
91
+ setBankAccountId('')
92
+ setCardBrand('')
93
+ setCardInstallments(1)
94
+ }
95
+
96
+ function handleClose() {
97
+ setVisible(false)
98
+ setTimeout(onClose, 200)
99
+ }
100
+
101
+ async function handlePay() {
102
+ if (amount <= 0 || !selectedTypeId) return
103
+ setSaving(true)
104
+ try {
105
+ await payMovement({
106
+ movementId: movement.id,
107
+ amount,
108
+ paymentDate,
109
+ paymentMethodId: paymentMethodId || undefined,
110
+ paymentMethodTypeId: selectedTypeId || undefined,
111
+ bankAccountId: bankAccountId || undefined,
112
+ cardBrand: needsCard ? cardBrand || undefined : undefined,
113
+ cardInstallments: needsCardInstallments ? cardInstallments : undefined,
114
+ })
115
+ setVisible(false)
116
+ setTimeout(onPaid, 200)
117
+ } catch {
118
+ // toast handled by store
119
+ } finally {
120
+ setSaving(false)
121
+ }
122
+ }
123
+
124
+ const SelectedIcon = selectedType ? (TYPE_ICONS[txType] ?? Banknote) : null
125
+
126
+ return createPortal(
127
+ <div
128
+ className="fixed inset-0 z-50 flex items-center justify-center transition-colors duration-200"
129
+ style={{ backgroundColor: visible ? 'rgba(0,0,0,0.5)' : 'rgba(0,0,0,0)' }}
130
+ onClick={handleClose}
131
+ >
132
+ <div
133
+ className="w-full max-w-md rounded-modal border bg-card shadow-lg mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto"
134
+ style={{
135
+ opacity: visible ? 1 : 0,
136
+ transform: visible ? 'scale(1) translateY(0)' : 'scale(0.95) translateY(8px)',
137
+ }}
138
+ onClick={(e) => e.stopPropagation()}
139
+ >
140
+ {/* Header */}
141
+ <div className="flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10">
142
+ <div>
143
+ <h3 className="text-base font-semibold">{t('financial.payment.title')}</h3>
144
+ <p className="text-xs text-muted-foreground">
145
+ {t('financial.payment.installment', { number: String(movement.installmentNumber ?? 1) })} &middot; {t('financial.payment.remaining', { amount: formatCurrency(remaining, currency) })}
146
+ </p>
147
+ </div>
148
+ <button onClick={handleClose} className="p-1 text-muted-foreground hover:text-foreground transition-colors">
149
+ <X className="h-4 w-4" />
150
+ </button>
151
+ </div>
152
+
153
+ <div className="px-5 py-4 space-y-4">
154
+ {/* Payment method type selector */}
155
+ <div>
156
+ <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.method')}</label>
157
+
158
+ {/* Collapsed: show selected as inline chip */}
159
+ <div
160
+ className="grid transition-all duration-200 ease-out"
161
+ style={{ gridTemplateRows: selectedTypeId && !expandMethodPicker ? '1fr' : '0fr' }}
162
+ >
163
+ <div className="overflow-hidden">
164
+ {selectedType && (
165
+ <button
166
+ onClick={() => setExpandMethodPicker(true)}
167
+ className={`flex items-center gap-2 mt-1.5 w-full rounded-lg border px-3 py-2.5 text-left transition-colors hover:bg-muted/30 ${TYPE_COLORS[txType] ?? ''}`}
168
+ data-active="true"
169
+ >
170
+ {SelectedIcon && <SelectedIcon className={`h-4 w-4 ${ICON_COLORS[txType] ?? ''}`} />}
171
+ <span className="text-sm font-medium flex-1">{selectedType?.name}</span>
172
+ <Pencil className="h-3 w-3 text-muted-foreground" />
173
+ </button>
174
+ )}
175
+ </div>
176
+ </div>
177
+
178
+ {/* Expanded: full grid */}
179
+ <div
180
+ className="grid transition-all duration-200 ease-out"
181
+ style={{ gridTemplateRows: expandMethodPicker ? '1fr' : '0fr' }}
182
+ >
183
+ <div className="overflow-hidden">
184
+ {paymentMethodTypes.length === 0 ? (
185
+ <div className="rounded-lg border border-dashed border-input bg-muted/30 px-3 py-3 text-center mt-1.5">
186
+ <p className="text-xs text-muted-foreground">{t('financial.payment.noMethods')}</p>
187
+ <a href="#/financial/settings" className="mt-1.5 inline-block text-xs font-medium text-primary hover:underline">
188
+ {t('financial.payment.configureMethods')} →
189
+ </a>
190
+ </div>
191
+ ) : (
192
+ <div className="grid grid-cols-3 gap-2 mt-1.5">
193
+ {paymentMethodTypes.map((type) => {
194
+ const tt = type.transactionType ?? ''
195
+ const Icon = TYPE_ICONS[tt] ?? Banknote
196
+ const isActive = selectedTypeId === type.id
197
+ return (
198
+ <button
199
+ key={type.id}
200
+ data-active={isActive}
201
+ onClick={() => handleSelectType(type.id)}
202
+ className={`flex flex-col items-center gap-1.5 rounded-lg border px-2 py-3 text-center transition-all ${TYPE_COLORS[tt] ?? TYPE_COLORS.cash}`}
203
+ >
204
+ <Icon className={`h-5 w-5 ${isActive ? ICON_COLORS[tt] ?? '' : 'text-muted-foreground'}`} />
205
+ <span className={`text-[11px] font-medium leading-tight ${isActive ? 'text-foreground' : 'text-muted-foreground'}`}>{type.name}</span>
206
+ </button>
207
+ )
208
+ })}
209
+ </div>
210
+ )}
211
+ </div>
212
+ </div>
213
+ </div>
214
+
215
+ {/* Form fields — only after method selected */}
216
+ {selectedTypeId && (
217
+ <div key={selectedTypeId} className="space-y-4">
218
+ {/* Amount + Date side by side */}
219
+ <div className="grid grid-cols-2 gap-3 stagger-field" style={{ animationDelay: '0ms' }}>
220
+ <CurrencyInput
221
+ label={t('financial.payment.amount')}
222
+ value={amount}
223
+ onChange={setAmount}
224
+ symbol={currency.symbol}
225
+ locale={currency.locale}
226
+ currencyCode={currency.code}
227
+ />
228
+ <div>
229
+ <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.date')}</label>
230
+ <DatePicker value={paymentDate} onChange={setPaymentDate} className="mt-1" />
231
+ </div>
232
+ </div>
233
+
234
+ {/* Specific payment method */}
235
+ {filteredMethods.length > 0 && (
236
+ <div className="stagger-field" style={{ animationDelay: '50ms' }}>
237
+ <label className="text-xs font-medium text-muted-foreground">
238
+ {needsCard ? t('financial.payment.card') : t('financial.payment.account')}
239
+ </label>
240
+ <select value={paymentMethodId} onChange={(e) => setPaymentMethodId(e.target.value)} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
241
+ <option value="">{t('financial.payment.select')}</option>
242
+ {filteredMethods.map((m) => <option key={m.id} value={m.id}>{m.name}</option>)}
243
+ </select>
244
+ </div>
245
+ )}
246
+
247
+ {/* Card fields side by side */}
248
+ {needsCard && (
249
+ <div className={`grid gap-3 stagger-field ${needsCardInstallments ? 'grid-cols-2' : 'grid-cols-1'}`} style={{ animationDelay: '100ms' }}>
250
+ <div>
251
+ <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.cardBrand')}</label>
252
+ <select value={cardBrand} onChange={(e) => setCardBrand(e.target.value)} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
253
+ <option value="">{t('financial.payment.selectBrand')}</option>
254
+ {['Visa', 'Mastercard', 'American Express', 'Elo', 'Hipercard', 'Diners Club'].map((b) => (
255
+ <option key={b} value={b}>{b}</option>
256
+ ))}
257
+ </select>
258
+ </div>
259
+ {needsCardInstallments && (
260
+ <div>
261
+ <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.cardInstallments')}</label>
262
+ <select value={cardInstallments} onChange={(e) => setCardInstallments(Number(e.target.value))} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
263
+ {Array.from({ length: 12 }, (_, i) => i + 1).map((n) => (
264
+ <option key={n} value={n}>{n}x {n > 1 ? formatCurrency(amount / n, currency) : ''}</option>
265
+ ))}
266
+ </select>
267
+ </div>
268
+ )}
269
+ </div>
270
+ )}
271
+
272
+ {/* Bank account */}
273
+ {needsBankAccount && bankAccounts.length > 0 && (
274
+ <div className="stagger-field" style={{ animationDelay: '100ms' }}>
275
+ <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.bankAccount')}</label>
276
+ <select value={bankAccountId} onChange={(e) => setBankAccountId(e.target.value)} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm">
277
+ <option value="">{t('financial.payment.selectAccount')}</option>
278
+ {bankAccounts.map((a) => <option key={a.id} value={a.id}>{a.name}</option>)}
279
+ </select>
280
+ </div>
281
+ )}
282
+
283
+ {/* Notes */}
284
+ <div className="stagger-field" style={{ animationDelay: '150ms' }}>
285
+ <label className="text-xs font-medium text-muted-foreground">{t('financial.payment.notes')}</label>
286
+ <input type="text" value={notes} onChange={(e) => setNotes(e.target.value)} placeholder={t('financial.payment.optionalNotes')} className="w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring" />
287
+ </div>
288
+ </div>
289
+ )}
290
+ </div>
291
+
292
+ {/* Keyframe for staggered field entrance */}
293
+ <style>{`
294
+ .stagger-field {
295
+ animation: field-slide-in 250ms ease-out both;
296
+ }
297
+ @keyframes field-slide-in {
298
+ from { opacity: 0; transform: translateY(8px); }
299
+ to { opacity: 1; transform: translateY(0); }
300
+ }
301
+ `}</style>
302
+
303
+ {/* Actions */}
304
+ <div className="flex justify-end gap-2 px-5 py-4 border-t sticky bottom-0 bg-card">
305
+ <Button variant="outline" size="sm" onClick={handleClose}>
306
+ {t('financial.payment.cancel')}
307
+ </Button>
308
+ <Button
309
+ variant="default"
310
+ size="sm"
311
+ onClick={handlePay}
312
+ disabled={amount <= 0 || !selectedTypeId || saving}
313
+ >
314
+ <Check className="h-3.5 w-3.5" />
315
+ {saving ? t('financial.payment.processing') : t('financial.payment.payAmount', { amount: formatCurrency(amount, currency) })}
316
+ </Button>
317
+ </div>
318
+ </div>
319
+ </div>,
320
+ document.body
321
+ )
322
+ }
@@ -0,0 +1,2 @@
1
+ export type { FinancialDataProvider } from './types'
2
+ export { createMockFinancialProvider } from './mock'