@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,38 @@
1
+ -- Financial plugin — seed: payment method types + card brands.
2
+ -- These are the read-only "system" registries the plugin's UI assumes exist
3
+ -- (PaymentModal, card selection). They live as registry seedData in TS but were
4
+ -- never written to the DB, so a fresh tenant had none. This backfills them for
5
+ -- every tenant that has none. Idempotent. Plugin-owned so every app gets them.
6
+
7
+ DO $$
8
+ DECLARE t_id uuid;
9
+ BEGIN
10
+ FOR t_id IN (
11
+ SELECT id FROM saas_core.tenants
12
+ WHERE id NOT IN (SELECT DISTINCT tenant_id FROM public.payment_method_types)
13
+ )
14
+ LOOP
15
+ INSERT INTO public.payment_method_types (tenant_id, name, transaction_type) VALUES
16
+ (t_id, 'Cash', 'cash'),
17
+ (t_id, 'PIX', 'pix'),
18
+ (t_id, 'Credit Card', 'credit_card'),
19
+ (t_id, 'Debit Card', 'debit_card'),
20
+ (t_id, 'Bank Transfer', 'bank_transfer'),
21
+ (t_id, 'Check', 'check');
22
+ END LOOP;
23
+ END $$;
24
+
25
+ DO $$
26
+ DECLARE t_id uuid;
27
+ BEGIN
28
+ FOR t_id IN (
29
+ SELECT id FROM saas_core.tenants
30
+ WHERE id NOT IN (SELECT DISTINCT tenant_id FROM public.card_brands)
31
+ )
32
+ LOOP
33
+ INSERT INTO public.card_brands (tenant_id, name) VALUES
34
+ (t_id, 'Visa'), (t_id, 'Mastercard'), (t_id, 'American Express'),
35
+ (t_id, 'Elo'), (t_id, 'Hipercard'), (t_id, 'Diners Club'),
36
+ (t_id, 'Discover'), (t_id, 'JCB'), (t_id, 'Aura'), (t_id, 'Hiper');
37
+ END LOOP;
38
+ END $$;
@@ -0,0 +1,305 @@
1
+ import type { PluginRegistryDef } from '@fayz-ai/core'
2
+ import type { EntityDef } from '@fayz-ai/core'
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Payment Method Types (seeded — Cash, PIX, Credit Card, etc.)
6
+ // ---------------------------------------------------------------------------
7
+
8
+ const paymentMethodTypeEntity: EntityDef = {
9
+ name: 'Payment Type',
10
+ namePlural: 'Payment Types',
11
+ icon: 'CreditCard',
12
+ displayField: 'name',
13
+ defaultSort: 'name',
14
+ fields: [
15
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
16
+ { key: 'transactionType', label: 'Transaction Type', type: 'select', options: ['cash', 'credit_card', 'debit_card', 'pix', 'bank_transfer', 'check', 'voucher'], showInTable: true, required: true },
17
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
18
+ ],
19
+ data: { table: 'payment_method_types', tenantScoped: true },
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Payment Methods (user-created — e.g. "Visa 2x", "PIX Store")
24
+ // ---------------------------------------------------------------------------
25
+
26
+ const paymentMethodEntity: EntityDef = {
27
+ name: 'Payment Method',
28
+ namePlural: 'Payment Methods',
29
+ icon: 'CreditCard',
30
+ displayField: 'name',
31
+ defaultSort: 'name',
32
+ fields: [
33
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
34
+ { key: 'paymentMethodTypeId', label: 'Type', type: 'select', options: ['cash', 'credit_card', 'debit_card', 'pix', 'bank_transfer', 'check'], showInTable: true, required: true },
35
+ { key: 'discountValue', label: 'Discount %', type: 'number', showInTable: true },
36
+ { key: 'interestValue', label: 'Interest %', type: 'number', showInTable: true },
37
+ { key: 'minInstallments', label: 'Min Installments', type: 'number', defaultValue: 1 },
38
+ { key: 'maxInstallments', label: 'Max Installments', type: 'number', defaultValue: 1, showInTable: true },
39
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
40
+ ],
41
+ data: { table: 'payment_methods', tenantScoped: true },
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Chart of Accounts (tree structure)
46
+ // ---------------------------------------------------------------------------
47
+
48
+ const chartOfAccountsEntity: EntityDef = {
49
+ name: 'Ledger Account',
50
+ namePlural: 'Chart of Accounts',
51
+ icon: 'BookOpen',
52
+ displayField: 'name',
53
+ defaultSort: 'code',
54
+ fields: [
55
+ { key: 'code', label: 'Code', type: 'text', required: true, showInTable: true },
56
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
57
+ { key: 'nodeType', label: 'Type', type: 'select', options: ['group', 'leaf'], showInTable: true, required: true, defaultValue: 'leaf' },
58
+ { key: 'parentId', label: 'Parent', type: 'text', showInTable: false },
59
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
60
+ ],
61
+ data: { table: 'chart_of_accounts', tenantScoped: true },
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Cost Centers
66
+ // ---------------------------------------------------------------------------
67
+
68
+ const costCenterEntity: EntityDef = {
69
+ name: 'Cost Center',
70
+ namePlural: 'Cost Centers',
71
+ icon: 'Target',
72
+ displayField: 'name',
73
+ defaultSort: 'code',
74
+ fields: [
75
+ { key: 'code', label: 'Code', type: 'text', required: true, showInTable: true },
76
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
77
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
78
+ ],
79
+ data: { table: 'cost_centers', tenantScoped: true },
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Bank Accounts (physical cash, bank, credit card, digital wallet)
84
+ // ---------------------------------------------------------------------------
85
+
86
+ const bankAccountEntity: EntityDef = {
87
+ name: 'Bank Account',
88
+ namePlural: 'Bank Accounts',
89
+ icon: 'Landmark',
90
+ displayField: 'name',
91
+ defaultSort: 'name',
92
+ fields: [
93
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
94
+ { key: 'accountType', label: 'Type', type: 'select', options: [
95
+ { label: 'Bank Account', value: 'bank_account' },
96
+ { label: 'Cash Register', value: 'cash_register' },
97
+ { label: 'Credit Card', value: 'credit_card' },
98
+ { label: 'Digital Wallet', value: 'digital_wallet' },
99
+ ], showInTable: true, required: true },
100
+ { key: 'bankName', label: 'Bank', type: 'text', showInTable: true },
101
+ { key: 'accountNumber', label: 'Account Number', type: 'text' },
102
+ { key: 'agencyNumber', label: 'Agency', type: 'text' },
103
+ { key: 'initialBalance', label: 'Initial Balance', type: 'currency', defaultValue: 0 },
104
+ { key: 'creditLimit', label: 'Credit Limit', type: 'currency' },
105
+ { key: 'closingDay', label: 'Closing Day', type: 'number' },
106
+ { key: 'dueDay', label: 'Due Day', type: 'number' },
107
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
108
+ ],
109
+ data: { table: 'bank_accounts', tenantScoped: true },
110
+ }
111
+
112
+ // ---------------------------------------------------------------------------
113
+ // Card Brands (seeded)
114
+ // ---------------------------------------------------------------------------
115
+
116
+ const cardBrandEntity: EntityDef = {
117
+ name: 'Card Brand',
118
+ namePlural: 'Card Brands',
119
+ icon: 'CreditCard',
120
+ displayField: 'name',
121
+ defaultSort: 'name',
122
+ fields: [
123
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
124
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
125
+ ],
126
+ data: { table: 'card_brands', tenantScoped: true },
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Commission Rules
131
+ // ---------------------------------------------------------------------------
132
+
133
+ const commissionRuleEntity: EntityDef = {
134
+ name: 'Commission Rule',
135
+ namePlural: 'Commission Rules',
136
+ icon: 'TrendingUp',
137
+ displayField: 'name',
138
+ defaultSort: 'priority',
139
+ defaultSortDir: 'desc',
140
+ fields: [
141
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
142
+ { key: 'priority', label: 'Priority', type: 'number', required: true, showInTable: true, defaultValue: 1 },
143
+ { key: 'valueType', label: 'Type', type: 'select', options: ['percentage', 'fixed'], required: true, showInTable: true, defaultValue: 'percentage' },
144
+ { key: 'value', label: 'Value', type: 'number', required: true, showInTable: true },
145
+ { key: 'description', label: 'Description', type: 'textarea' },
146
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
147
+ ],
148
+ data: { table: 'commission_rules', tenantScoped: true },
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Price Tables
153
+ // ---------------------------------------------------------------------------
154
+
155
+ const priceTableEntity: EntityDef = {
156
+ name: 'Price Table',
157
+ namePlural: 'Price Tables',
158
+ icon: 'Tag',
159
+ displayField: 'description',
160
+ defaultSort: 'createdAt',
161
+ defaultSortDir: 'desc',
162
+ fields: [
163
+ { key: 'description', label: 'Description', type: 'text', required: true, showInTable: true },
164
+ { key: 'type', label: 'Type', type: 'select', options: ['sale', 'cost'], required: true, showInTable: true, defaultValue: 'sale' },
165
+ { key: 'validFrom', label: 'Valid From', type: 'date', showInTable: true },
166
+ { key: 'validUntil', label: 'Valid Until', type: 'date', showInTable: true },
167
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
168
+ ],
169
+ data: { table: 'price_tables', tenantScoped: true },
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // Price Variations
174
+ // ---------------------------------------------------------------------------
175
+
176
+ const priceVariationEntity: EntityDef = {
177
+ name: 'Price Variation',
178
+ namePlural: 'Price Variations',
179
+ icon: 'Percent',
180
+ displayField: 'name',
181
+ defaultSort: 'createdAt',
182
+ defaultSortDir: 'desc',
183
+ fields: [
184
+ { key: 'name', label: 'Name', type: 'text', required: true, showInTable: true },
185
+ { key: 'variationType', label: 'Type', type: 'select', options: ['discount', 'increase'], required: true, showInTable: true },
186
+ { key: 'valueType', label: 'Value Type', type: 'select', options: ['percentage', 'fixed'], required: true, showInTable: true },
187
+ { key: 'value', label: 'Value', type: 'number', required: true, showInTable: true },
188
+ { key: 'firstAppointmentOnly', label: 'First Appointment Only', type: 'boolean', defaultValue: false },
189
+ { key: 'isActive', label: 'Active', type: 'boolean', showInTable: true, defaultValue: true },
190
+ ],
191
+ data: { table: 'price_variations', tenantScoped: true },
192
+ }
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Export all registries
196
+ // ---------------------------------------------------------------------------
197
+
198
+ export const financialRegistries: PluginRegistryDef[] = [
199
+ // --- Editable registries (user manages) ---
200
+ {
201
+ id: 'payment-methods',
202
+ entity: paymentMethodEntity,
203
+ icon: 'Wallet',
204
+ description: 'Configured payment methods with fees and installment rules',
205
+ display: 'table',
206
+ },
207
+ {
208
+ id: 'chart-of-accounts',
209
+ entity: chartOfAccountsEntity,
210
+ icon: 'BookOpen',
211
+ description: 'Hierarchical chart of accounts for categorizing transactions',
212
+ display: 'tree',
213
+ seedData: [
214
+ { id: 'coa-1', code: '1', name: 'Revenue', nodeType: 'group', isActive: true },
215
+ { id: 'coa-1.1', code: '1.1', name: 'Sales Revenue', nodeType: 'group', parentId: 'coa-1', isActive: true },
216
+ { id: 'coa-1.1.01', code: '1.1.01', name: 'Services', nodeType: 'leaf', parentId: 'coa-1.1', isActive: true },
217
+ { id: 'coa-1.1.02', code: '1.1.02', name: 'Products', nodeType: 'leaf', parentId: 'coa-1.1', isActive: true },
218
+ { id: 'coa-2', code: '2', name: 'Other Revenue', nodeType: 'group', isActive: true },
219
+ { id: 'coa-2.1', code: '2.1', name: 'Financial Income', nodeType: 'leaf', parentId: 'coa-2', isActive: true },
220
+ { id: 'coa-3', code: '3', name: 'Operational Costs', nodeType: 'group', isActive: true },
221
+ { id: 'coa-3.1', code: '3.1', name: 'Materials & Supplies', nodeType: 'group', parentId: 'coa-3', isActive: true },
222
+ { id: 'coa-3.1.01', code: '3.1.01', name: 'Raw Materials', nodeType: 'leaf', parentId: 'coa-3.1', isActive: true },
223
+ { id: 'coa-3.1.02', code: '3.1.02', name: 'Consumables', nodeType: 'leaf', parentId: 'coa-3.1', isActive: true },
224
+ { id: 'coa-4', code: '4', name: 'Expenses', nodeType: 'group', isActive: true },
225
+ { id: 'coa-4.1', code: '4.1', name: 'Rent', nodeType: 'leaf', parentId: 'coa-4', isActive: true },
226
+ { id: 'coa-4.2', code: '4.2', name: 'Utilities', nodeType: 'leaf', parentId: 'coa-4', isActive: true },
227
+ { id: 'coa-4.3', code: '4.3', name: 'Payroll', nodeType: 'leaf', parentId: 'coa-4', isActive: true },
228
+ ],
229
+ },
230
+ {
231
+ id: 'cost-centers',
232
+ entity: costCenterEntity,
233
+ icon: 'Target',
234
+ description: 'Cost centers for expense tracking and departmental budgets',
235
+ display: 'table',
236
+ seedData: [
237
+ { id: 'cc-001', code: '001', name: 'General', isActive: true },
238
+ ],
239
+ },
240
+ {
241
+ id: 'bank-accounts',
242
+ entity: bankAccountEntity,
243
+ icon: 'Landmark',
244
+ description: 'Bank accounts, cash registers, credit cards, and digital wallets',
245
+ display: 'table',
246
+ },
247
+ {
248
+ id: 'commission-rules',
249
+ entity: commissionRuleEntity,
250
+ icon: 'TrendingUp',
251
+ description: 'Commission rules for service and product revenue sharing',
252
+ display: 'table',
253
+ },
254
+ {
255
+ id: 'price-tables',
256
+ entity: priceTableEntity,
257
+ icon: 'Tag',
258
+ description: 'Price tables for sale and cost pricing',
259
+ display: 'table',
260
+ },
261
+ {
262
+ id: 'price-variations',
263
+ entity: priceVariationEntity,
264
+ icon: 'Percent',
265
+ description: 'Discount and surcharge rules for pricing',
266
+ display: 'table',
267
+ },
268
+ // --- Read-only registries (system / seeded data) ---
269
+ {
270
+ id: 'payment-method-types',
271
+ entity: paymentMethodTypeEntity,
272
+ icon: 'CreditCard',
273
+ description: 'System payment types (cash, card, PIX, etc.)',
274
+ display: 'table',
275
+ readOnly: true,
276
+ seedData: [
277
+ { id: 'pmt-cash', name: 'Cash', transactionType: 'cash', isActive: true },
278
+ { id: 'pmt-pix', name: 'PIX', transactionType: 'pix', isActive: true },
279
+ { id: 'pmt-credit', name: 'Credit Card', transactionType: 'credit_card', isActive: true },
280
+ { id: 'pmt-debit', name: 'Debit Card', transactionType: 'debit_card', isActive: true },
281
+ { id: 'pmt-transfer', name: 'Bank Transfer', transactionType: 'bank_transfer', isActive: true },
282
+ { id: 'pmt-check', name: 'Check', transactionType: 'check', isActive: true },
283
+ ],
284
+ },
285
+ {
286
+ id: 'card-brands',
287
+ entity: cardBrandEntity,
288
+ icon: 'CreditCard',
289
+ description: 'Credit and debit card brands',
290
+ display: 'table',
291
+ readOnly: true,
292
+ seedData: [
293
+ { id: 'cb-visa', name: 'Visa', isActive: true },
294
+ { id: 'cb-mastercard', name: 'Mastercard', isActive: true },
295
+ { id: 'cb-amex', name: 'American Express', isActive: true },
296
+ { id: 'cb-elo', name: 'Elo', isActive: true },
297
+ { id: 'cb-hipercard', name: 'Hipercard', isActive: true },
298
+ { id: 'cb-diners', name: 'Diners Club', isActive: true },
299
+ { id: 'cb-discover', name: 'Discover', isActive: true },
300
+ { id: 'cb-jcb', name: 'JCB', isActive: true },
301
+ { id: 'cb-aura', name: 'Aura', isActive: true },
302
+ { id: 'cb-hiper', name: 'Hiper', isActive: true },
303
+ ],
304
+ },
305
+ ]
package/src/store.ts ADDED
@@ -0,0 +1,221 @@
1
+ import { createStore, type StoreApi } from 'zustand/vanilla'
2
+ import { dedup } from '@fayz-ai/saas'
3
+ import { toast } from '@fayz-ai/ui'
4
+ import type { FinancialDataProvider } from './data/types'
5
+ import type {
6
+ Invoice, FinancialMovement, BankAccount, CashSession,
7
+ PaymentMethod, PaymentMethodType, CardTransaction,
8
+ FinancialSummary, StatementEntry,
9
+ InvoiceQuery, MovementQuery, StatementQuery,
10
+ CreateInvoiceInput, PayMovementInput,
11
+ OpenCashSessionInput, CloseCashSessionInput,
12
+ DateRange,
13
+ } from './types'
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Store state
17
+ // ---------------------------------------------------------------------------
18
+
19
+ export interface FinancialUIState {
20
+ // Data cache
21
+ invoices: Invoice[]
22
+ invoicesTotal: number
23
+ invoicesLoading: boolean
24
+ invoiceQuery: InvoiceQuery
25
+
26
+ movements: FinancialMovement[]
27
+ movementsTotal: number
28
+ movementsLoading: boolean
29
+
30
+ bankAccounts: BankAccount[]
31
+ bankAccountsLoading: boolean
32
+
33
+ cashSessions: CashSession[]
34
+ cashSessionsLoading: boolean
35
+
36
+ paymentMethods: PaymentMethod[]
37
+ paymentMethodTypes: PaymentMethodType[]
38
+
39
+ summary: FinancialSummary | null
40
+ summaryLoading: boolean
41
+
42
+ statementEntries: StatementEntry[]
43
+ statementLoading: boolean
44
+
45
+ cardTransactions: CardTransaction[]
46
+ cardsLoading: boolean
47
+
48
+ // Actions
49
+ fetchSummary(dateRange?: DateRange): Promise<void>
50
+ fetchInvoices(query: InvoiceQuery): Promise<void>
51
+ fetchBankAccounts(): Promise<void>
52
+ fetchCashSessions(bankAccountId?: string): Promise<void>
53
+ fetchPaymentMethods(): Promise<void>
54
+ fetchStatement(query: StatementQuery): Promise<void>
55
+ fetchCardTransactions(dateRange?: DateRange): Promise<void>
56
+ createInvoice(input: CreateInvoiceInput): Promise<Invoice>
57
+ payMovement(input: PayMovementInput): Promise<void>
58
+ cancelInvoice(invoiceId: string): Promise<void>
59
+ openCashSession(input: OpenCashSessionInput): Promise<CashSession>
60
+ closeCashSession(input: CloseCashSessionInput): Promise<CashSession>
61
+ }
62
+
63
+ // ---------------------------------------------------------------------------
64
+ // Store factory
65
+ // ---------------------------------------------------------------------------
66
+
67
+ export function createFinancialStore(provider: FinancialDataProvider): StoreApi<FinancialUIState> {
68
+ return createStore<FinancialUIState>((set, get) => ({
69
+ invoices: [],
70
+ invoicesTotal: 0,
71
+ invoicesLoading: false,
72
+ invoiceQuery: {},
73
+
74
+ movements: [],
75
+ movementsTotal: 0,
76
+ movementsLoading: false,
77
+
78
+ bankAccounts: [],
79
+ bankAccountsLoading: false,
80
+
81
+ cashSessions: [],
82
+ cashSessionsLoading: false,
83
+
84
+ paymentMethods: [],
85
+ paymentMethodTypes: [],
86
+
87
+ summary: null,
88
+ summaryLoading: false,
89
+
90
+ statementEntries: [],
91
+ statementLoading: false,
92
+
93
+ cardTransactions: [],
94
+ cardsLoading: false,
95
+
96
+ async fetchSummary(dateRange) {
97
+ return dedup('fin:summary', async () => {
98
+ set({ summaryLoading: true })
99
+ const summary = await provider.getSummary(dateRange)
100
+ set({ summary, summaryLoading: false })
101
+ })
102
+ },
103
+
104
+ async fetchInvoices(query) {
105
+ return dedup('fin:invoices:' + JSON.stringify(query), async () => {
106
+ set({ invoicesLoading: true, invoiceQuery: query })
107
+ const result = await provider.getInvoices(query)
108
+ set({ invoices: result.data, invoicesTotal: result.total, invoicesLoading: false })
109
+ })
110
+ },
111
+
112
+ async fetchBankAccounts() {
113
+ return dedup('fin:bankAccounts', async () => {
114
+ set({ bankAccountsLoading: true })
115
+ const bankAccounts = await provider.getBankAccounts()
116
+ set({ bankAccounts, bankAccountsLoading: false })
117
+ })
118
+ },
119
+
120
+ async fetchCashSessions(bankAccountId) {
121
+ return dedup('fin:cashSessions:' + (bankAccountId ?? ''), async () => {
122
+ set({ cashSessionsLoading: true })
123
+ const cashSessions = await provider.getCashSessions(bankAccountId)
124
+ set({ cashSessions, cashSessionsLoading: false })
125
+ })
126
+ },
127
+
128
+ async fetchPaymentMethods() {
129
+ return dedup('fin:paymentMethods', async () => {
130
+ const [paymentMethods, paymentMethodTypes] = await Promise.all([
131
+ provider.getPaymentMethods(),
132
+ provider.getPaymentMethodTypes(),
133
+ ])
134
+ set({ paymentMethods, paymentMethodTypes })
135
+ })
136
+ },
137
+
138
+ async fetchStatement(query) {
139
+ return dedup('fin:statement:' + JSON.stringify(query), async () => {
140
+ set({ statementLoading: true })
141
+ const statementEntries = await provider.getStatement(query)
142
+ set({ statementEntries, statementLoading: false })
143
+ })
144
+ },
145
+
146
+ async fetchCardTransactions(dateRange) {
147
+ return dedup('fin:cards', async () => {
148
+ set({ cardsLoading: true })
149
+ const cardTransactions = await provider.getCardTransactions(dateRange)
150
+ set({ cardTransactions, cardsLoading: false })
151
+ })
152
+ },
153
+
154
+ async createInvoice(input) {
155
+ try {
156
+ const invoice = await provider.createInvoice(input)
157
+ const query = get().invoiceQuery
158
+ const result = await provider.getInvoices(query)
159
+ const summary = await provider.getSummary()
160
+ set({ invoices: result.data, invoicesTotal: result.total, summary })
161
+ toast.success('Invoice created')
162
+ return invoice
163
+ } catch (err: any) {
164
+ toast.error('Failed to create invoice', { description: err?.message })
165
+ throw err
166
+ }
167
+ },
168
+
169
+ async payMovement(input) {
170
+ try {
171
+ await provider.payMovement(input)
172
+ const query = get().invoiceQuery
173
+ const [result, summary] = await Promise.all([provider.getInvoices(query), provider.getSummary()])
174
+ set({ invoices: result.data, invoicesTotal: result.total, summary })
175
+ toast.success('Payment recorded')
176
+ } catch (err: any) {
177
+ toast.error('Failed to record payment', { description: err?.message })
178
+ throw err
179
+ }
180
+ },
181
+
182
+ async cancelInvoice(invoiceId) {
183
+ try {
184
+ await provider.cancelInvoice(invoiceId)
185
+ const query = get().invoiceQuery
186
+ const [result, summary] = await Promise.all([provider.getInvoices(query), provider.getSummary()])
187
+ set({ invoices: result.data, invoicesTotal: result.total, summary })
188
+ toast.success('Invoice cancelled')
189
+ } catch (err: any) {
190
+ toast.error('Failed to cancel invoice', { description: err?.message })
191
+ throw err
192
+ }
193
+ },
194
+
195
+ async openCashSession(input) {
196
+ try {
197
+ const session = await provider.openCashSession(input)
198
+ const cashSessions = await provider.getCashSessions()
199
+ set({ cashSessions })
200
+ toast.success('Cash session opened')
201
+ return session
202
+ } catch (err: any) {
203
+ toast.error('Failed to open session', { description: err?.message })
204
+ throw err
205
+ }
206
+ },
207
+
208
+ async closeCashSession(input) {
209
+ try {
210
+ const session = await provider.closeCashSession(input)
211
+ const cashSessions = await provider.getCashSessions()
212
+ set({ cashSessions })
213
+ toast.success('Cash session closed')
214
+ return session
215
+ } catch (err: any) {
216
+ toast.error('Failed to close session', { description: err?.message })
217
+ throw err
218
+ }
219
+ },
220
+ }))
221
+ }