@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,597 @@
1
+ import React, { useEffect, useState, useMemo } from 'react'
2
+ import { Landmark, Play, Square, Clock, ChevronDown, ChevronUp, Calculator, X, Check, StickyNote } from 'lucide-react'
3
+ import { useFinancialConfig, useFinancialStore, formatCurrency } from '../FinancialContext'
4
+ import { useTranslation } from '@fayz-ai/core'
5
+ import { SubpageHeader } from '@fayz-ai/ui'
6
+ import { CurrencyInput } from '@fayz-ai/ui'
7
+ import { Button } from '@fayz-ai/ui'
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Denomination definitions (BRL default — overridden by currency config)
11
+ // ---------------------------------------------------------------------------
12
+
13
+ interface Denomination {
14
+ label: string
15
+ value: number
16
+ type: 'bill' | 'coin'
17
+ }
18
+
19
+ const BRL_DENOMINATIONS: Denomination[] = [
20
+ { label: 'R$ 200', value: 200, type: 'bill' },
21
+ { label: 'R$ 100', value: 100, type: 'bill' },
22
+ { label: 'R$ 50', value: 50, type: 'bill' },
23
+ { label: 'R$ 20', value: 20, type: 'bill' },
24
+ { label: 'R$ 10', value: 10, type: 'bill' },
25
+ { label: 'R$ 5', value: 5, type: 'bill' },
26
+ { label: 'R$ 2', value: 2, type: 'bill' },
27
+ { label: 'R$ 1', value: 1, type: 'coin' },
28
+ { label: 'R$ 0,50', value: 0.5, type: 'coin' },
29
+ { label: 'R$ 0,25', value: 0.25, type: 'coin' },
30
+ { label: 'R$ 0,10', value: 0.1, type: 'coin' },
31
+ { label: 'R$ 0,05', value: 0.05, type: 'coin' },
32
+ ]
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Denomination counter sub-component
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function DenominationCounter({
39
+ denominations,
40
+ counts,
41
+ onChange,
42
+ currency,
43
+ }: {
44
+ denominations: Denomination[]
45
+ counts: Record<number, number>
46
+ onChange: (counts: Record<number, number>) => void
47
+ currency: { code: string; locale: string; symbol: string }
48
+ }) {
49
+ const t = useTranslation()
50
+ const bills = denominations.filter((d) => d.type === 'bill')
51
+ const coins = denominations.filter((d) => d.type === 'coin')
52
+
53
+ function setCount(value: number, count: number) {
54
+ onChange({ ...counts, [value]: Math.max(0, count) })
55
+ }
56
+
57
+ function renderRow(d: Denomination) {
58
+ const qty = counts[d.value] ?? 0
59
+ const subtotal = qty * d.value
60
+ return (
61
+ <div key={d.value} className="flex items-center gap-2 py-1">
62
+ <span className="w-20 text-xs font-medium text-muted-foreground">{d.label}</span>
63
+ <span className="text-muted-foreground text-xs">×</span>
64
+ <input
65
+ type="number"
66
+ min={0}
67
+ value={qty || ''}
68
+ onChange={(e) => setCount(d.value, parseInt(e.target.value) || 0)}
69
+ placeholder="0"
70
+ className="w-16 rounded border bg-background px-2 py-1 text-xs text-center tabular-nums"
71
+ />
72
+ <span className="text-xs text-muted-foreground">=</span>
73
+ <span className="w-24 text-xs text-right tabular-nums font-medium">
74
+ {formatCurrency(subtotal, currency)}
75
+ </span>
76
+ </div>
77
+ )
78
+ }
79
+
80
+ return (
81
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
82
+ {bills.length > 0 && (
83
+ <div>
84
+ <p className="text-[10px] font-semibold text-muted-foreground uppercase mb-1">{t('financial.cash.bills')}</p>
85
+ {bills.map(renderRow)}
86
+ </div>
87
+ )}
88
+ {coins.length > 0 && (
89
+ <div>
90
+ <p className="text-[10px] font-semibold text-muted-foreground uppercase mb-1">{t('financial.cash.coins')}</p>
91
+ {coins.map(renderRow)}
92
+ </div>
93
+ )}
94
+ </div>
95
+ )
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Opening panel (step-based)
100
+ // ---------------------------------------------------------------------------
101
+
102
+ type OpenMode = 'quick' | 'count'
103
+
104
+ function CashOpeningPanel({
105
+ cashAccounts,
106
+ currency,
107
+ onOpen,
108
+ }: {
109
+ cashAccounts: { id: string; name: string }[]
110
+ currency: { code: string; locale: string; symbol: string }
111
+ onOpen: (accountId: string, balance: number, notes?: string) => Promise<void>
112
+ }) {
113
+ const [selectedAccountId, setSelectedAccountId] = useState<string>(cashAccounts[0]?.id ?? '')
114
+ const [mode, setMode] = useState<OpenMode>('quick')
115
+ const [quickBalance, setQuickBalance] = useState(0)
116
+ const [counts, setCounts] = useState<Record<number, number>>({})
117
+ const [notes, setNotes] = useState('')
118
+ const [showNotes, setShowNotes] = useState(false)
119
+ const [confirming, setConfirming] = useState(false)
120
+ const [submitting, setSubmitting] = useState(false)
121
+ const t = useTranslation()
122
+
123
+ const countedTotal = useMemo(() => {
124
+ return BRL_DENOMINATIONS.reduce((sum, d) => sum + (counts[d.value] ?? 0) * d.value, 0)
125
+ }, [counts])
126
+
127
+ const effectiveBalance = mode === 'count' ? countedTotal : quickBalance
128
+
129
+ function resetForm() {
130
+ setQuickBalance(0)
131
+ setCounts({})
132
+ setNotes('')
133
+ setShowNotes(false)
134
+ setConfirming(false)
135
+ }
136
+
137
+ async function handleConfirm() {
138
+ setSubmitting(true)
139
+ try {
140
+ await onOpen(selectedAccountId, effectiveBalance, notes || undefined)
141
+ resetForm()
142
+ } finally {
143
+ setSubmitting(false)
144
+ }
145
+ }
146
+
147
+ if (cashAccounts.length === 0) {
148
+ return (
149
+ <p className="text-sm text-muted-foreground p-4">
150
+ {t('financial.cash.noAccounts')}
151
+ </p>
152
+ )
153
+ }
154
+
155
+ // Confirmation step
156
+ if (confirming) {
157
+ return (
158
+ <div className="p-4 space-y-4">
159
+ <div className="rounded-lg border border-primary/20 bg-primary/5 p-4 space-y-3">
160
+ <p className="text-sm font-semibold">{t('financial.cash.confirmOpening')}</p>
161
+ <div className="grid grid-cols-2 gap-2 text-xs">
162
+ <span className="text-muted-foreground">{t('financial.cash.register')}</span>
163
+ <span className="font-medium">{cashAccounts.find((a) => a.id === selectedAccountId)?.name}</span>
164
+ <span className="text-muted-foreground">{t('financial.cash.openingBalance')}</span>
165
+ <span className="font-bold text-base">{formatCurrency(effectiveBalance, currency)}</span>
166
+ {mode === 'count' && (
167
+ <>
168
+ <span className="text-muted-foreground">{t('financial.cash.method')}</span>
169
+ <span className="font-medium">{t('financial.cash.denominationCount')}</span>
170
+ </>
171
+ )}
172
+ {notes && (
173
+ <>
174
+ <span className="text-muted-foreground">{t('financial.cash.notes')}</span>
175
+ <span className="font-medium">{notes}</span>
176
+ </>
177
+ )}
178
+ </div>
179
+ </div>
180
+ <div className="flex gap-2">
181
+ <button
182
+ onClick={() => setConfirming(false)}
183
+ className="inline-flex items-center gap-1.5 rounded-lg border px-3 py-1.5 text-xs font-medium hover:bg-muted bg-card shadow-button active:shadow-button-inset transition-colors"
184
+ >
185
+ <X className="h-3 w-3" /> {t('common.back')}
186
+ </button>
187
+ <button
188
+ onClick={handleConfirm}
189
+ disabled={submitting}
190
+ className="inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-4 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors disabled:opacity-50"
191
+ >
192
+ <Check className="h-3 w-3" /> {submitting ? t('financial.cash.opening') : t('financial.cash.confirmAndOpen')}
193
+ </button>
194
+ </div>
195
+ </div>
196
+ )
197
+ }
198
+
199
+ return (
200
+ <div className="p-4 space-y-4">
201
+ {/* Account selector */}
202
+ {cashAccounts.length > 1 && (
203
+ <div>
204
+ <label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.cash.register')}</label>
205
+ <select
206
+ value={selectedAccountId}
207
+ onChange={(e) => setSelectedAccountId(e.target.value)}
208
+ className="w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-xs"
209
+ >
210
+ {cashAccounts.map((a) => (
211
+ <option key={a.id} value={a.id}>{a.name}</option>
212
+ ))}
213
+ </select>
214
+ </div>
215
+ )}
216
+
217
+ {/* Mode toggle */}
218
+ <div className="flex gap-1 rounded-lg border p-0.5 w-fit">
219
+ <button
220
+ onClick={() => setMode('quick')}
221
+ className={`px-3 py-1 text-xs rounded-md font-medium transition-colors ${
222
+ mode === 'quick' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
223
+ }`}
224
+ >
225
+ {t('financial.cash.quickAmount')}
226
+ </button>
227
+ <button
228
+ onClick={() => setMode('count')}
229
+ className={`inline-flex items-center gap-1 px-3 py-1 text-xs rounded-md font-medium transition-colors ${
230
+ mode === 'count' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
231
+ }`}
232
+ >
233
+ <Calculator className="h-3 w-3" /> {t('financial.cash.countBillsCoins')}
234
+ </button>
235
+ </div>
236
+
237
+ {/* Quick mode */}
238
+ {mode === 'quick' && (
239
+ <CurrencyInput
240
+ value={quickBalance}
241
+ onChange={setQuickBalance}
242
+ symbol={currency.symbol}
243
+ locale={currency.locale}
244
+ currencyCode={currency.code}
245
+ label={t('financial.cash.openingBalance')}
246
+ />
247
+ )}
248
+
249
+ {/* Count mode */}
250
+ {mode === 'count' && (
251
+ <div className="space-y-3">
252
+ <DenominationCounter
253
+ denominations={BRL_DENOMINATIONS}
254
+ counts={counts}
255
+ onChange={setCounts}
256
+ currency={currency}
257
+ />
258
+ <div className="flex items-center justify-between pt-2 border-t">
259
+ <span className="text-xs font-medium text-muted-foreground uppercase">{t('financial.cash.countedTotal')}</span>
260
+ <span className="text-lg font-bold">{formatCurrency(countedTotal, currency)}</span>
261
+ </div>
262
+ </div>
263
+ )}
264
+
265
+ {/* Notes toggle */}
266
+ <div>
267
+ {!showNotes ? (
268
+ <button
269
+ onClick={() => setShowNotes(true)}
270
+ className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
271
+ >
272
+ <StickyNote className="h-3 w-3" /> {t('financial.cash.addNotes')}
273
+ </button>
274
+ ) : (
275
+ <div>
276
+ <label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.cash.notes')}</label>
277
+ <textarea
278
+ value={notes}
279
+ onChange={(e) => setNotes(e.target.value)}
280
+ rows={2}
281
+ placeholder={t('financial.cash.openingObservations')}
282
+ className="w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-xs resize-none"
283
+ />
284
+ </div>
285
+ )}
286
+ </div>
287
+
288
+ {/* Open button */}
289
+ <button
290
+ onClick={() => setConfirming(true)}
291
+ disabled={effectiveBalance <= 0}
292
+ className="inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-4 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors disabled:opacity-50"
293
+ >
294
+ <Play className="h-3 w-3" /> {t('financial.cash.openSession')}
295
+ </button>
296
+ </div>
297
+ )
298
+ }
299
+
300
+ // ---------------------------------------------------------------------------
301
+ // Closing panel (inline within open session card)
302
+ // ---------------------------------------------------------------------------
303
+
304
+ function CashClosingPanel({
305
+ session,
306
+ currency,
307
+ onClose,
308
+ }: {
309
+ session: { id: string; openingBalance: number; bankAccountName?: string }
310
+ currency: { code: string; locale: string; symbol: string }
311
+ onClose: (sessionId: string, balance: number, notes?: string) => Promise<void>
312
+ }) {
313
+ const [mode, setMode] = useState<OpenMode>('quick')
314
+ const [quickBalance, setQuickBalance] = useState(0)
315
+ const [counts, setCounts] = useState<Record<number, number>>({})
316
+ const [notes, setNotes] = useState('')
317
+ const [submitting, setSubmitting] = useState(false)
318
+ const t = useTranslation()
319
+
320
+ const countedTotal = useMemo(() => {
321
+ return BRL_DENOMINATIONS.reduce((sum, d) => sum + (counts[d.value] ?? 0) * d.value, 0)
322
+ }, [counts])
323
+
324
+ const effectiveBalance = mode === 'count' ? countedTotal : quickBalance
325
+ const difference = effectiveBalance - session.openingBalance
326
+
327
+ async function handleClose() {
328
+ setSubmitting(true)
329
+ try {
330
+ await onClose(session.id, effectiveBalance, notes || undefined)
331
+ } finally {
332
+ setSubmitting(false)
333
+ }
334
+ }
335
+
336
+ return (
337
+ <div className="space-y-3 pt-3 border-t">
338
+ {/* Mode toggle */}
339
+ <div className="flex gap-1 rounded-lg border p-0.5 w-fit">
340
+ <button
341
+ onClick={() => setMode('quick')}
342
+ className={`px-3 py-1 text-xs rounded-md font-medium transition-colors ${
343
+ mode === 'quick' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
344
+ }`}
345
+ >
346
+ {t('financial.cash.quickAmount')}
347
+ </button>
348
+ <button
349
+ onClick={() => setMode('count')}
350
+ className={`inline-flex items-center gap-1 px-3 py-1 text-xs rounded-md font-medium transition-colors ${
351
+ mode === 'count' ? 'bg-primary text-primary-foreground' : 'hover:bg-muted'
352
+ }`}
353
+ >
354
+ <Calculator className="h-3 w-3" /> {t('financial.cash.count')}
355
+ </button>
356
+ </div>
357
+
358
+ {mode === 'quick' ? (
359
+ <CurrencyInput
360
+ value={quickBalance}
361
+ onChange={setQuickBalance}
362
+ symbol={currency.symbol}
363
+ locale={currency.locale}
364
+ currencyCode={currency.code}
365
+ label={t('financial.cash.closingBalance')}
366
+ />
367
+ ) : (
368
+ <div className="space-y-3">
369
+ <DenominationCounter
370
+ denominations={BRL_DENOMINATIONS}
371
+ counts={counts}
372
+ onChange={setCounts}
373
+ currency={currency}
374
+ />
375
+ <div className="flex items-center justify-between pt-2 border-t">
376
+ <span className="text-xs font-medium text-muted-foreground uppercase">{t('financial.cash.countedTotal')}</span>
377
+ <span className="text-lg font-bold">{formatCurrency(countedTotal, currency)}</span>
378
+ </div>
379
+ </div>
380
+ )}
381
+
382
+ {/* Difference indicator */}
383
+ {effectiveBalance > 0 && (
384
+ <div className="flex items-center justify-between rounded-lg bg-muted/50 px-3 py-2">
385
+ <span className="text-xs text-muted-foreground">{t('financial.cash.differenceFromOpening')}</span>
386
+ <span className={`text-sm font-bold tabular-nums ${
387
+ difference > 0 ? 'text-success' : difference < 0 ? 'text-destructive' : 'text-muted-foreground'
388
+ }`}>
389
+ {difference >= 0 ? '+' : ''}{formatCurrency(difference, currency)}
390
+ </span>
391
+ </div>
392
+ )}
393
+
394
+ {/* Notes */}
395
+ <div>
396
+ <label className="text-[10px] font-medium text-muted-foreground uppercase">{t('financial.cash.closingNotes')}</label>
397
+ <textarea
398
+ value={notes}
399
+ onChange={(e) => setNotes(e.target.value)}
400
+ rows={2}
401
+ placeholder={t('financial.cash.closingObservations')}
402
+ className="w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-xs resize-none"
403
+ />
404
+ </div>
405
+
406
+ <Button
407
+ variant="destructive"
408
+ size="sm"
409
+ onClick={handleClose}
410
+ disabled={submitting || effectiveBalance <= 0}
411
+ >
412
+ <Square className="h-3.5 w-3.5" />
413
+ {submitting ? t('financial.cash.closing') : t('financial.cash.closeSession')}
414
+ </Button>
415
+ </div>
416
+ )
417
+ }
418
+
419
+ // ---------------------------------------------------------------------------
420
+ // Main view
421
+ // ---------------------------------------------------------------------------
422
+
423
+ export function CashRegistersView() {
424
+ const t = useTranslation()
425
+ const { currency, labels } = useFinancialConfig()
426
+ const bankAccounts = useFinancialStore((s) => s.bankAccounts)
427
+ const cashSessions = useFinancialStore((s) => s.cashSessions)
428
+ const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts)
429
+ const fetchCashSessions = useFinancialStore((s) => s.fetchCashSessions)
430
+ const openCashSession = useFinancialStore((s) => s.openCashSession)
431
+ const closeCashSession = useFinancialStore((s) => s.closeCashSession)
432
+
433
+ const [expandedClosing, setExpandedClosing] = useState<string | null>(null)
434
+
435
+ useEffect(() => {
436
+ fetchBankAccounts()
437
+ fetchCashSessions()
438
+ }, [])
439
+
440
+ const cashAccounts = bankAccounts.filter((a) => a.accountType === 'cash_register')
441
+ const openSessions = cashSessions.filter((s) => s.status === 'open')
442
+ const closedSessions = cashSessions.filter((s) => s.status === 'closed')
443
+
444
+ async function handleOpen(accountId: string, balance: number, notes?: string) {
445
+ await openCashSession({ bankAccountId: accountId, openingBalance: balance, notes })
446
+ }
447
+
448
+ async function handleClose(sessionId: string, balance: number, notes?: string) {
449
+ await closeCashSession({ sessionId, closingBalance: balance, notes })
450
+ setExpandedClosing(null)
451
+ }
452
+
453
+ return (
454
+ <div className="space-y-6">
455
+ <SubpageHeader title={labels.cashRegisters ?? t('financial.cash.title')} subtitle={t('financial.cash.subtitle')} />
456
+
457
+ {/* Open sessions */}
458
+ <div className="rounded-lg border bg-card shadow-sm">
459
+ <div className="flex items-center justify-between px-4 py-3 border-b">
460
+ <div className="flex items-center gap-2">
461
+ <Landmark className="h-4 w-4 text-info" />
462
+ <h3 className="text-sm font-semibold">{t('financial.cash.openSessions')}</h3>
463
+ {openSessions.length > 0 && (
464
+ <span className="rounded-full bg-info/10 text-info px-2 py-0.5 text-[10px] font-semibold">
465
+ {openSessions.length}
466
+ </span>
467
+ )}
468
+ </div>
469
+ </div>
470
+
471
+ {openSessions.length === 0 ? (
472
+ <CashOpeningPanel cashAccounts={cashAccounts} currency={currency} onOpen={handleOpen} />
473
+ ) : (
474
+ <div className="divide-y">
475
+ {openSessions.map((session) => (
476
+ <div key={session.id} className="p-4 space-y-3">
477
+ <div className="flex items-center justify-between">
478
+ <div>
479
+ <p className="text-sm font-medium">{session.bankAccountName ?? t('financial.cash.title')}</p>
480
+ <p className="text-xs text-muted-foreground">
481
+ {t('financial.cash.opened')} {new Date(session.openedAt).toLocaleString()}
482
+ {session.openedByName ? ` ${t('financial.cash.by')} ${session.openedByName}` : ''}
483
+ </p>
484
+ </div>
485
+ <div className="text-right">
486
+ <p className="text-[10px] text-muted-foreground uppercase">{t('financial.cash.openingBalance')}</p>
487
+ <p className="text-lg font-bold tabular-nums">{formatCurrency(session.openingBalance, currency)}</p>
488
+ </div>
489
+ </div>
490
+
491
+ {/* Expand/collapse closing controls */}
492
+ {expandedClosing === session.id ? (
493
+ <CashClosingPanel session={session} currency={currency} onClose={handleClose} />
494
+ ) : (
495
+ <button
496
+ onClick={() => setExpandedClosing(session.id)}
497
+ className="inline-flex items-center gap-1.5 text-xs text-destructive hover:text-destructive font-medium transition-colors"
498
+ >
499
+ <ChevronDown className="h-3 w-3" /> {t('financial.cash.closeThisSession')}
500
+ </button>
501
+ )}
502
+ </div>
503
+ ))}
504
+ </div>
505
+ )}
506
+ </div>
507
+
508
+ {/* Quick open — when sessions already exist but user may want another */}
509
+ {openSessions.length > 0 && cashAccounts.length > openSessions.length && (
510
+ <OpenAnotherCollapsible cashAccounts={cashAccounts} openSessions={openSessions} currency={currency} onOpen={handleOpen} />
511
+ )}
512
+
513
+ {/* Session history */}
514
+ <div className="rounded-lg border bg-card shadow-sm">
515
+ <div className="flex items-center gap-2 px-4 py-3 border-b">
516
+ <Clock className="h-4 w-4 text-muted-foreground" />
517
+ <h3 className="text-sm font-semibold">{t('financial.cash.sessionHistory')}</h3>
518
+ </div>
519
+ {closedSessions.length === 0 ? (
520
+ <div className="px-4 py-8 text-center text-sm text-muted-foreground">{t('financial.cash.noClosedSessions')}</div>
521
+ ) : (
522
+ <div className="divide-y">
523
+ {closedSessions.map((session) => (
524
+ <div key={session.id} className="px-4 py-3">
525
+ <div className="flex items-center justify-between">
526
+ <div>
527
+ <p className="text-sm font-medium">{session.bankAccountName ?? t('financial.cash.title')}</p>
528
+ <p className="text-xs text-muted-foreground">
529
+ {new Date(session.openedAt).toLocaleDateString()} — {session.closedAt ? new Date(session.closedAt).toLocaleDateString() : ''}
530
+ </p>
531
+ {session.openedByName && (
532
+ <p className="text-[10px] text-muted-foreground">{t('financial.cash.by')} {session.openedByName}</p>
533
+ )}
534
+ </div>
535
+ <div className="text-right">
536
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
537
+ <span>{t('financial.cash.openLabel')}: {formatCurrency(session.openingBalance, currency)}</span>
538
+ <span>{t('financial.cash.closeLabel')}: {formatCurrency(session.closingBalance ?? 0, currency)}</span>
539
+ </div>
540
+ {session.difference !== undefined && session.difference !== 0 && (
541
+ <p className={`text-xs font-semibold tabular-nums mt-0.5 ${session.difference > 0 ? 'text-success' : 'text-destructive'}`}>
542
+ {t('financial.cash.diff')}: {session.difference >= 0 ? '+' : ''}{formatCurrency(session.difference, currency)}
543
+ </p>
544
+ )}
545
+ {session.notes && (
546
+ <p className="text-[10px] text-muted-foreground mt-0.5 max-w-[200px] truncate" title={session.notes}>
547
+ {session.notes}
548
+ </p>
549
+ )}
550
+ </div>
551
+ </div>
552
+ </div>
553
+ ))}
554
+ </div>
555
+ )}
556
+ </div>
557
+ </div>
558
+ )
559
+ }
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // Collapsible "Open another register"
563
+ // ---------------------------------------------------------------------------
564
+
565
+ function OpenAnotherCollapsible({
566
+ cashAccounts,
567
+ openSessions,
568
+ currency,
569
+ onOpen,
570
+ }: {
571
+ cashAccounts: { id: string; name: string }[]
572
+ openSessions: { bankAccountId: string }[]
573
+ currency: { code: string; locale: string; symbol: string }
574
+ onOpen: (accountId: string, balance: number, notes?: string) => Promise<void>
575
+ }) {
576
+ const t = useTranslation()
577
+ const [expanded, setExpanded] = useState(false)
578
+ const availableAccounts = cashAccounts.filter((a) => !openSessions.some((s) => s.bankAccountId === a.id))
579
+
580
+ if (availableAccounts.length === 0) return null
581
+
582
+ return (
583
+ <div className="rounded-lg border bg-card shadow-sm">
584
+ <button
585
+ onClick={() => setExpanded(!expanded)}
586
+ className="w-full flex items-center justify-between px-4 py-3 text-xs font-medium hover:bg-muted/50 transition-colors"
587
+ >
588
+ <span className="flex items-center gap-2">
589
+ <Play className="h-3 w-3 text-primary" />
590
+ {t('financial.cash.openAnother')}
591
+ </span>
592
+ {expanded ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
593
+ </button>
594
+ {expanded && <CashOpeningPanel cashAccounts={availableAccounts} currency={currency} onOpen={onOpen} />}
595
+ </div>
596
+ )
597
+ }
@@ -0,0 +1,18 @@
1
+ import React from 'react'
2
+ import { useFinancialConfig } from '../FinancialContext'
3
+ import { SubpageHeader } from '@fayz-ai/ui'
4
+ import { useTranslation } from '@fayz-ai/core'
5
+
6
+ export function CommissionsView() {
7
+ const t = useTranslation()
8
+ const { labels } = useFinancialConfig()
9
+
10
+ return (
11
+ <div className="space-y-4">
12
+ <SubpageHeader title={labels.commissions} subtitle={t('financial.commissions.subtitle')} />
13
+ <div className="flex items-center justify-center rounded-lg border-2 border-dashed border-muted p-12">
14
+ <p className="text-sm text-muted-foreground">{t('financial.commissions.comingSoon')}</p>
15
+ </div>
16
+ </div>
17
+ )
18
+ }