@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.
- package/dist/FinancialContext.d.ts +44 -0
- package/dist/FinancialContext.d.ts.map +1 -0
- package/dist/FinancialPage.d.ts +18 -0
- package/dist/FinancialPage.d.ts.map +1 -0
- package/dist/components/FinancialGeneralSettings.d.ts +3 -0
- package/dist/components/FinancialGeneralSettings.d.ts.map +1 -0
- package/dist/components/FinancialOnboarding.d.ts +5 -0
- package/dist/components/FinancialOnboarding.d.ts.map +1 -0
- package/dist/components/FinancialSettings.d.ts +8 -0
- package/dist/components/FinancialSettings.d.ts.map +1 -0
- package/dist/components/PaymentModal.d.ts +8 -0
- package/dist/components/PaymentModal.d.ts.map +1 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +9 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +27 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +4937 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4930 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/tenant.d.ts +3 -0
- package/dist/lib/tenant.d.ts.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +38 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +302 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/CardsView.d.ts +3 -0
- package/dist/views/CardsView.d.ts.map +1 -0
- package/dist/views/CashRegistersView.d.ts +3 -0
- package/dist/views/CashRegistersView.d.ts.map +1 -0
- package/dist/views/CommissionsView.d.ts +3 -0
- package/dist/views/CommissionsView.d.ts.map +1 -0
- package/dist/views/InvoiceDetailView.d.ts +9 -0
- package/dist/views/InvoiceDetailView.d.ts.map +1 -0
- package/dist/views/InvoiceFormView.d.ts +13 -0
- package/dist/views/InvoiceFormView.d.ts.map +1 -0
- package/dist/views/InvoiceListView.d.ts +8 -0
- package/dist/views/InvoiceListView.d.ts.map +1 -0
- package/dist/views/PayablesView.d.ts +7 -0
- package/dist/views/PayablesView.d.ts.map +1 -0
- package/dist/views/ReceivablesView.d.ts +7 -0
- package/dist/views/ReceivablesView.d.ts.map +1 -0
- package/dist/views/StatementsView.d.ts +3 -0
- package/dist/views/StatementsView.d.ts.map +1 -0
- package/dist/views/SummaryView.d.ts +9 -0
- package/dist/views/SummaryView.d.ts.map +1 -0
- package/dist/views/dashboardWidgets.d.ts +11 -0
- package/dist/views/dashboardWidgets.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/FinancialContext.tsx +47 -0
- package/src/FinancialPage.tsx +261 -0
- package/src/README.md +155 -0
- package/src/components/FinancialGeneralSettings.tsx +26 -0
- package/src/components/FinancialOnboarding.tsx +87 -0
- package/src/components/FinancialSettings.tsx +31 -0
- package/src/components/PaymentModal.tsx +322 -0
- package/src/data/index.ts +2 -0
- package/src/data/mock.ts +520 -0
- package/src/data/supabase.ts +509 -0
- package/src/data/types.ts +53 -0
- package/src/index.ts +307 -0
- package/src/lib/tenant.ts +3 -0
- package/src/locales/en.ts +331 -0
- package/src/locales/index.ts +7 -0
- package/src/locales/pt-BR.ts +331 -0
- package/src/migrations/001_financial_base.sql +109 -0
- package/src/migrations/002_chart_of_accounts.sql +28 -0
- package/src/migrations/003_card_brands.sql +12 -0
- package/src/migrations/004_order_to_cash.sql +164 -0
- package/src/migrations/005_seed_defaults.sql +38 -0
- package/src/registries.ts +305 -0
- package/src/store.ts +221 -0
- package/src/types.ts +359 -0
- package/src/views/CardsView.tsx +74 -0
- package/src/views/CashRegistersView.tsx +597 -0
- package/src/views/CommissionsView.tsx +18 -0
- package/src/views/InvoiceDetailView.tsx +595 -0
- package/src/views/InvoiceFormView.tsx +900 -0
- package/src/views/InvoiceListView.tsx +177 -0
- package/src/views/PayablesView.tsx +37 -0
- package/src/views/ReceivablesView.tsx +37 -0
- package/src/views/StatementsView.tsx +123 -0
- package/src/views/SummaryView.tsx +12 -0
- 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
|
+
}
|