@fayz-ai/plugin-financial 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FinancialContext.d.ts +2 -0
- package/dist/FinancialContext.d.ts.map +1 -1
- package/dist/FinancialPage.d.ts.map +1 -1
- package/dist/data/index.d.ts +1 -0
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +15 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/types.d.ts +2 -1
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1531 -201
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1393 -80
- package/dist/index.js.map +1 -1
- package/dist/locales/en.d.ts.map +1 -1
- package/dist/locales/pt-BR.d.ts.map +1 -1
- package/dist/quick-add.d.ts +13 -0
- package/dist/quick-add.d.ts.map +1 -0
- package/dist/store.d.ts +3 -1
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/CardsView.d.ts.map +1 -1
- package/dist/views/InvoiceListView.d.ts.map +1 -1
- package/dist/views/QuickTransactionForm.d.ts +16 -0
- package/dist/views/QuickTransactionForm.d.ts.map +1 -0
- package/dist/views/SummaryView.d.ts +5 -4
- package/dist/views/SummaryView.d.ts.map +1 -1
- package/dist/views/dashboardWidgets.d.ts.map +1 -1
- package/package.json +4 -7
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +86 -7
- package/src/data/capability.test.ts +45 -0
- package/src/data/index.ts +1 -0
- package/src/data/mock.ts +50 -10
- package/src/data/supabase.ts +46 -3
- package/src/data/types.ts +2 -1
- package/src/index.ts +24 -4
- package/src/locales/en.ts +75 -0
- package/src/locales/pt-BR.ts +75 -0
- package/src/quick-add.ts +58 -0
- package/src/store.ts +88 -0
- package/src/types.ts +45 -0
- package/src/views/CardsView.tsx +186 -50
- package/src/views/InvoiceListView.tsx +60 -3
- package/src/views/QuickTransactionForm.tsx +537 -0
- package/src/views/SummaryView.tsx +197 -6
- package/src/views/dashboardWidgets.tsx +251 -5
package/dist/index.cjs
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React17 = require('react');
|
|
4
4
|
var core = require('@fayz-ai/core');
|
|
5
5
|
var ui = require('@fayz-ai/ui');
|
|
6
6
|
var saas = require('@fayz-ai/saas');
|
|
7
|
-
var jsxRuntime = require('react/jsx-runtime');
|
|
8
7
|
var lucideReact = require('lucide-react');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
9
|
var reactDom = require('react-dom');
|
|
10
10
|
var vanilla = require('zustand/vanilla');
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
13
29
|
|
|
14
|
-
var
|
|
30
|
+
var React17__namespace = /*#__PURE__*/_interopNamespace(React17);
|
|
15
31
|
|
|
16
32
|
// src/index.ts
|
|
17
33
|
var ctx = saas.createPluginContext("FinancialPage");
|
|
@@ -20,7 +36,628 @@ var useFinancialConfig = ctx.useConfig;
|
|
|
20
36
|
var useFinancialProvider = ctx.useProvider;
|
|
21
37
|
var useFinancialStore = ctx.useStore;
|
|
22
38
|
function SummaryView() {
|
|
23
|
-
return /* @__PURE__ */ jsxRuntime.
|
|
39
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
40
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "md:hidden", children: /* @__PURE__ */ jsxRuntime.jsx(MobileSummary, {}) }),
|
|
41
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
42
|
+
ui.DashboardCanvas,
|
|
43
|
+
{
|
|
44
|
+
surface: "plugin-home",
|
|
45
|
+
domain: "financial",
|
|
46
|
+
showHeader: false,
|
|
47
|
+
className: "hidden space-y-6 md:block"
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
] });
|
|
51
|
+
}
|
|
52
|
+
function categoryIcon(label) {
|
|
53
|
+
const s = label.toLowerCase();
|
|
54
|
+
if (/(mercado|super|padaria|extra|p[aã]o|hortifr)/.test(s)) return lucideReact.ShoppingCart;
|
|
55
|
+
if (/(uber|99|posto|shell|gasol|combust|carro)/.test(s)) return lucideReact.Car;
|
|
56
|
+
if (/(aluguel|casa|condom|im[oó]vel)/.test(s)) return lucideReact.Home;
|
|
57
|
+
if (/(ifood|restaurante|outback|lanche|food|bar\b)/.test(s)) return lucideReact.Utensils;
|
|
58
|
+
if (/(luz|energia|enel|internet|vivo|conta de|[aá]gua)/.test(s)) return lucideReact.Zap;
|
|
59
|
+
if (/(cinema|netflix|cinemark|filme|disney|prime)/.test(s)) return lucideReact.Film;
|
|
60
|
+
if (/(farm|drogasil|sa[uú]de|plano|hospital|m[eé]dic)/.test(s)) return lucideReact.HeartPulse;
|
|
61
|
+
if (/(academia|smartfit|gym|treino)/.test(s)) return lucideReact.Dumbbell;
|
|
62
|
+
if (/(spotify|music|deezer)/.test(s)) return lucideReact.Music;
|
|
63
|
+
if (/(amazon|shopping|loja|magaz|americanas|mercado livre)/.test(s)) return lucideReact.ShoppingBag;
|
|
64
|
+
if (/(fatura|cart[aã]o|nubank|banco|boleto)/.test(s)) return lucideReact.Landmark;
|
|
65
|
+
if (/(sal[aá]rio|freel|pix|reembolso|receb|renda|d[ií]vidend)/.test(s)) return lucideReact.Wallet;
|
|
66
|
+
return lucideReact.CircleDollarSign;
|
|
67
|
+
}
|
|
68
|
+
function useDateLabel() {
|
|
69
|
+
const t2 = core.useTranslation();
|
|
70
|
+
const { currency } = useFinancialConfig();
|
|
71
|
+
return React17.useMemo(() => {
|
|
72
|
+
const todayStr = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
73
|
+
const y = /* @__PURE__ */ new Date();
|
|
74
|
+
y.setDate(y.getDate() - 1);
|
|
75
|
+
const yesterdayStr = y.toISOString().slice(0, 10);
|
|
76
|
+
return (dateStr) => {
|
|
77
|
+
if (dateStr === todayStr) return t2("financial.summary.today");
|
|
78
|
+
if (dateStr === yesterdayStr) return t2("financial.summary.yesterday");
|
|
79
|
+
try {
|
|
80
|
+
return (/* @__PURE__ */ new Date(`${dateStr}T00:00:00`)).toLocaleDateString(currency.locale, {
|
|
81
|
+
weekday: "short",
|
|
82
|
+
day: "2-digit",
|
|
83
|
+
month: "short"
|
|
84
|
+
});
|
|
85
|
+
} catch {
|
|
86
|
+
return dateStr;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}, [t2, currency.locale]);
|
|
90
|
+
}
|
|
91
|
+
function MobileSummary() {
|
|
92
|
+
const t2 = core.useTranslation();
|
|
93
|
+
const { currency } = useFinancialConfig();
|
|
94
|
+
const summary = useFinancialStore((s) => s.summary);
|
|
95
|
+
const statement = useFinancialStore((s) => s.statement);
|
|
96
|
+
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
97
|
+
const fetchSummary = useFinancialStore((s) => s.fetchSummary);
|
|
98
|
+
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
99
|
+
const fetchStatement = useFinancialStore((s) => s.fetchStatement);
|
|
100
|
+
const dateLabel = useDateLabel();
|
|
101
|
+
React17.useEffect(() => {
|
|
102
|
+
void fetchSummary();
|
|
103
|
+
void fetchBankAccounts();
|
|
104
|
+
const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
105
|
+
const from = /* @__PURE__ */ new Date();
|
|
106
|
+
from.setDate(from.getDate() - 90);
|
|
107
|
+
void fetchStatement({ dateRange: { from: from.toISOString().slice(0, 10), to } });
|
|
108
|
+
}, []);
|
|
109
|
+
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
110
|
+
const inflow = summary?.monthlyInflow ?? 0;
|
|
111
|
+
const outflow = summary?.monthlyOutflow ?? 0;
|
|
112
|
+
const accountName = React17.useMemo(() => {
|
|
113
|
+
const map = new Map(bankAccounts.map((a) => [a.id, a.name]));
|
|
114
|
+
return (id) => id ? map.get(id) ?? "" : "";
|
|
115
|
+
}, [bankAccounts]);
|
|
116
|
+
const groups = React17.useMemo(() => {
|
|
117
|
+
const entries = (statement?.entries ?? []).filter((e) => e.entryKind === "movement");
|
|
118
|
+
const byDate = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const e of entries) {
|
|
120
|
+
const d = e.movement.paymentDate ?? e.movement.dueDate;
|
|
121
|
+
const bucket = byDate.get(d);
|
|
122
|
+
if (bucket) bucket.push(e);
|
|
123
|
+
else byDate.set(d, [e]);
|
|
124
|
+
}
|
|
125
|
+
return [...byDate.entries()].sort((a, b) => b[0].localeCompare(a[0]));
|
|
126
|
+
}, [statement]);
|
|
127
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
128
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
129
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "col-span-2 rounded-xl border bg-card p-4", children: [
|
|
130
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.summary.totalBalance") }),
|
|
131
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-2xl font-semibold", children: fmt(summary?.totalBalance ?? 0) })
|
|
132
|
+
] }),
|
|
133
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border bg-card p-3", children: [
|
|
134
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.summary.income") }),
|
|
135
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-base font-semibold text-success", children: fmt(inflow) })
|
|
136
|
+
] }),
|
|
137
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border bg-card p-3", children: [
|
|
138
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.summary.expenses") }),
|
|
139
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-base font-semibold text-destructive", children: fmt(outflow) })
|
|
140
|
+
] })
|
|
141
|
+
] }),
|
|
142
|
+
groups.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded-xl border border-dashed py-12 text-center text-sm text-muted-foreground", children: t2("financial.summary.noTransactions") }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-4", children: groups.map(([date, rows]) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-1.5", children: [
|
|
143
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-1 text-xs font-medium uppercase tracking-wide text-muted-foreground", children: dateLabel(date) }),
|
|
144
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y overflow-hidden rounded-xl border bg-card", children: rows.map((e) => {
|
|
145
|
+
const isCredit = e.movement.direction === "credit";
|
|
146
|
+
const title = e.invoice?.contactName || e.movement.notes || t2("financial.summary.transactionsTitle");
|
|
147
|
+
const Icon = categoryIcon(title);
|
|
148
|
+
const category = isCredit ? t2("financial.summary.income") : t2("financial.summary.expenses");
|
|
149
|
+
const acct = accountName(e.movement.bankAccountId);
|
|
150
|
+
const subtitle = acct ? `${category} \xB7 ${acct}` : category;
|
|
151
|
+
const receiptUrl = e.invoice?.metadata?.receiptUrl;
|
|
152
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 px-3 py-2.5", children: [
|
|
153
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
154
|
+
"span",
|
|
155
|
+
{
|
|
156
|
+
className: `flex h-9 w-9 shrink-0 items-center justify-center rounded-full ${isCredit ? "bg-success-soft text-success-soft-foreground" : "bg-destructive-soft text-destructive-soft-foreground"}`,
|
|
157
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4" })
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
161
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "flex items-center gap-1.5 truncate text-sm font-medium", children: [
|
|
162
|
+
title,
|
|
163
|
+
receiptUrl && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-3 w-3 shrink-0 text-muted-foreground", "aria-label": t2("financial.quickTx.receiptAttached") })
|
|
164
|
+
] }),
|
|
165
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-xs text-muted-foreground", children: subtitle })
|
|
166
|
+
] }),
|
|
167
|
+
receiptUrl && /* @__PURE__ */ jsxRuntime.jsx(
|
|
168
|
+
"img",
|
|
169
|
+
{
|
|
170
|
+
src: receiptUrl,
|
|
171
|
+
alt: t2("financial.quickTx.receiptAttached"),
|
|
172
|
+
className: "h-9 w-9 shrink-0 rounded-md border object-cover"
|
|
173
|
+
}
|
|
174
|
+
),
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: `shrink-0 text-sm font-semibold ${isCredit ? "text-success" : "text-destructive"}`, children: [
|
|
176
|
+
isCredit ? "+" : "-",
|
|
177
|
+
fmt(e.net)
|
|
178
|
+
] })
|
|
179
|
+
] }, e.movement.id);
|
|
180
|
+
}) })
|
|
181
|
+
] }, date)) })
|
|
182
|
+
] });
|
|
183
|
+
}
|
|
184
|
+
function typeTheme(type) {
|
|
185
|
+
switch (type) {
|
|
186
|
+
case "income":
|
|
187
|
+
return { header: "bg-success text-success-foreground", ring: "ring-success", icon: lucideReact.ArrowDownCircle };
|
|
188
|
+
case "transfer":
|
|
189
|
+
return { header: "bg-muted text-foreground", ring: "ring-ring", icon: lucideReact.ArrowLeftRight };
|
|
190
|
+
case "expense":
|
|
191
|
+
default:
|
|
192
|
+
return { header: "bg-destructive text-destructive-foreground", ring: "ring-destructive", icon: lucideReact.ArrowUpCircle };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function ChipSelect({ icon: Icon, label, value, options, onChange, emptyLabel }) {
|
|
196
|
+
const [open, setOpen] = React17.useState(false);
|
|
197
|
+
const ref = React17.useRef(null);
|
|
198
|
+
React17.useEffect(() => {
|
|
199
|
+
if (!open) return;
|
|
200
|
+
function onDoc(e) {
|
|
201
|
+
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
202
|
+
}
|
|
203
|
+
document.addEventListener("mousedown", onDoc);
|
|
204
|
+
return () => document.removeEventListener("mousedown", onDoc);
|
|
205
|
+
}, [open]);
|
|
206
|
+
const selected = options.find((o) => o.id === value);
|
|
207
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { ref, className: "relative", children: [
|
|
208
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground", children: label }),
|
|
209
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
210
|
+
"button",
|
|
211
|
+
{
|
|
212
|
+
type: "button",
|
|
213
|
+
onClick: () => setOpen((v) => !v),
|
|
214
|
+
className: "mt-1 flex w-full items-center gap-2 rounded-input border border-input bg-card px-3 py-2.5 text-left text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] transition-colors hover:bg-muted/30",
|
|
215
|
+
children: [
|
|
216
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-4 w-4 shrink-0 text-muted-foreground" }),
|
|
217
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `flex-1 truncate ${selected ? "" : "text-muted-foreground"}`, children: selected?.name ?? emptyLabel }),
|
|
218
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: `h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform ${open ? "rotate-180" : ""}` })
|
|
219
|
+
]
|
|
220
|
+
}
|
|
221
|
+
),
|
|
222
|
+
open && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "fayz-glass-surface absolute bottom-full left-0 right-0 z-50 mb-1 max-h-56 overflow-auto rounded-lg border bg-popover py-1 shadow-lg", children: options.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-3 py-4 text-center text-xs text-muted-foreground", children: emptyLabel }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
223
|
+
value != null && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
224
|
+
"button",
|
|
225
|
+
{
|
|
226
|
+
onClick: () => {
|
|
227
|
+
onChange(void 0);
|
|
228
|
+
setOpen(false);
|
|
229
|
+
},
|
|
230
|
+
className: "flex w-full items-center gap-2 px-3 py-2.5 text-left text-xs text-muted-foreground hover:bg-muted/50",
|
|
231
|
+
children: [
|
|
232
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3.5 w-3.5" }),
|
|
233
|
+
" ",
|
|
234
|
+
emptyLabel
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
),
|
|
238
|
+
options.map((o) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
239
|
+
"button",
|
|
240
|
+
{
|
|
241
|
+
onClick: () => {
|
|
242
|
+
onChange(o.id);
|
|
243
|
+
setOpen(false);
|
|
244
|
+
},
|
|
245
|
+
className: `flex w-full items-center gap-2 px-3 py-2.5 text-left text-sm hover:bg-muted/50 ${o.id === value ? "bg-muted/30" : ""}`,
|
|
246
|
+
children: [
|
|
247
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex-1 truncate", children: o.name }),
|
|
248
|
+
o.hint && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground", children: o.hint }),
|
|
249
|
+
o.id === value && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5 text-primary" })
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
o.id
|
|
253
|
+
))
|
|
254
|
+
] }) })
|
|
255
|
+
] });
|
|
256
|
+
}
|
|
257
|
+
function TogglePill({ icon: Icon, label, checked, onChange, accent }) {
|
|
258
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
259
|
+
"button",
|
|
260
|
+
{
|
|
261
|
+
type: "button",
|
|
262
|
+
onClick: () => onChange(!checked),
|
|
263
|
+
className: `flex items-center gap-2 rounded-full border px-3 py-2 text-xs font-medium transition-colors ${checked ? `border-transparent ${accent ?? "bg-primary text-primary-foreground"}` : "border-input bg-card text-muted-foreground hover:bg-muted/40"}`,
|
|
264
|
+
children: [
|
|
265
|
+
/* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "h-3.5 w-3.5" }),
|
|
266
|
+
label
|
|
267
|
+
]
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
function QuickTransactionForm({ open, onOpenChange, defaultType = "expense", defaultAccountId, focusReceipt = false }) {
|
|
272
|
+
const t2 = core.useTranslation();
|
|
273
|
+
const { currency } = useFinancialConfig();
|
|
274
|
+
const provider = useFinancialProvider();
|
|
275
|
+
const createQuickTransaction = useFinancialStore((s) => s.createQuickTransaction);
|
|
276
|
+
const [type, setType] = React17.useState(defaultType);
|
|
277
|
+
const [amount, setAmount] = React17.useState(0);
|
|
278
|
+
const [amountStr, setAmountStr] = React17.useState("");
|
|
279
|
+
const [paid, setPaid] = React17.useState(true);
|
|
280
|
+
const [date, setDate] = React17.useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
281
|
+
const [description, setDescription] = React17.useState("");
|
|
282
|
+
const [categoryId, setCategoryId] = React17.useState();
|
|
283
|
+
const [accountId, setAccountId] = React17.useState();
|
|
284
|
+
const [toAccountId, setToAccountId] = React17.useState();
|
|
285
|
+
const [recurring, setRecurring] = React17.useState(false);
|
|
286
|
+
const [saving, setSaving] = React17.useState(false);
|
|
287
|
+
const [receiptUrl, setReceiptUrl] = React17.useState();
|
|
288
|
+
const fileInputRef = React17.useRef(null);
|
|
289
|
+
const [accounts, setAccounts] = React17.useState([]);
|
|
290
|
+
const [categories, setCategories] = React17.useState([]);
|
|
291
|
+
React17.useEffect(() => {
|
|
292
|
+
if (!open) return;
|
|
293
|
+
setType(defaultType);
|
|
294
|
+
setAmount(0);
|
|
295
|
+
setAmountStr("");
|
|
296
|
+
setPaid(true);
|
|
297
|
+
setDate((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
298
|
+
setDescription("");
|
|
299
|
+
setCategoryId(void 0);
|
|
300
|
+
setAccountId(defaultAccountId);
|
|
301
|
+
setToAccountId(void 0);
|
|
302
|
+
setRecurring(false);
|
|
303
|
+
setReceiptUrl(void 0);
|
|
304
|
+
}, [open, defaultType, defaultAccountId]);
|
|
305
|
+
React17.useEffect(() => {
|
|
306
|
+
if (open && focusReceipt) {
|
|
307
|
+
const id = window.setTimeout(() => fileInputRef.current?.click(), 250);
|
|
308
|
+
return () => window.clearTimeout(id);
|
|
309
|
+
}
|
|
310
|
+
}, [open, focusReceipt]);
|
|
311
|
+
function onPickReceipt(e) {
|
|
312
|
+
const file = e.target.files?.[0];
|
|
313
|
+
e.target.value = "";
|
|
314
|
+
if (!file) return;
|
|
315
|
+
if (!file.type.startsWith("image/")) {
|
|
316
|
+
ui.toast.error(t2("financial.quickTx.receiptInvalid"));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const reader = new FileReader();
|
|
320
|
+
reader.onload = () => {
|
|
321
|
+
setReceiptUrl(typeof reader.result === "string" ? reader.result : void 0);
|
|
322
|
+
};
|
|
323
|
+
reader.onerror = () => ui.toast.error(t2("financial.quickTx.receiptInvalid"));
|
|
324
|
+
reader.readAsDataURL(file);
|
|
325
|
+
}
|
|
326
|
+
React17.useEffect(() => {
|
|
327
|
+
if (!open) return;
|
|
328
|
+
let cancelled = false;
|
|
329
|
+
void (async () => {
|
|
330
|
+
const [accs, coa] = await Promise.all([
|
|
331
|
+
provider.getBankAccounts(),
|
|
332
|
+
provider.getChartOfAccounts().catch(() => [])
|
|
333
|
+
]);
|
|
334
|
+
if (cancelled) return;
|
|
335
|
+
setAccounts(accs);
|
|
336
|
+
setCategories(coa);
|
|
337
|
+
setAccountId((prev) => prev ?? accs[0]?.id);
|
|
338
|
+
})();
|
|
339
|
+
return () => {
|
|
340
|
+
cancelled = true;
|
|
341
|
+
};
|
|
342
|
+
}, [open, provider]);
|
|
343
|
+
const theme = typeTheme(type);
|
|
344
|
+
const HeaderIcon = theme.icon;
|
|
345
|
+
const accountOptions = React17.useMemo(
|
|
346
|
+
() => accounts.map((a) => ({ id: a.id, name: a.name, hint: a.accountType === "credit_card" ? t2("financial.quickTx.card") : void 0 })),
|
|
347
|
+
[accounts, t2]
|
|
348
|
+
);
|
|
349
|
+
const categoryOptions = React17.useMemo(
|
|
350
|
+
() => categories.filter((c) => c.isActive).map((c) => ({ id: c.id, name: c.name, hint: c.code })),
|
|
351
|
+
[categories]
|
|
352
|
+
);
|
|
353
|
+
function onAmountChange(raw) {
|
|
354
|
+
const cleaned = raw.replace(/[^\d.,]/g, "").replace(",", ".");
|
|
355
|
+
setAmountStr(raw.replace(/[^\d.,]/g, ""));
|
|
356
|
+
const n = Number.parseFloat(cleaned);
|
|
357
|
+
setAmount(Number.isFinite(n) ? n : 0);
|
|
358
|
+
}
|
|
359
|
+
const canSave = amount > 0 && !saving && (type === "transfer" ? !!accountId && !!toAccountId && accountId !== toAccountId : true);
|
|
360
|
+
async function handleSave() {
|
|
361
|
+
if (!canSave) {
|
|
362
|
+
if (type === "transfer" && accountId && accountId === toAccountId) {
|
|
363
|
+
ui.toast.error(t2("financial.transfer.sameAccount"));
|
|
364
|
+
}
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
setSaving(true);
|
|
368
|
+
try {
|
|
369
|
+
await createQuickTransaction({
|
|
370
|
+
type,
|
|
371
|
+
amount,
|
|
372
|
+
date,
|
|
373
|
+
paid: type === "transfer" ? true : paid,
|
|
374
|
+
description: description || void 0,
|
|
375
|
+
categoryId: type === "transfer" ? void 0 : categoryId,
|
|
376
|
+
bankAccountId: accountId,
|
|
377
|
+
toAccountId: type === "transfer" ? toAccountId : void 0,
|
|
378
|
+
recurring: type === "transfer" ? false : recurring,
|
|
379
|
+
receiptUrl: type === "transfer" ? void 0 : receiptUrl
|
|
380
|
+
});
|
|
381
|
+
onOpenChange(false);
|
|
382
|
+
} catch {
|
|
383
|
+
} finally {
|
|
384
|
+
setSaving(false);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const TYPES = [
|
|
388
|
+
{ id: "expense", label: t2("financial.quickTx.expense") },
|
|
389
|
+
{ id: "income", label: t2("financial.quickTx.income") },
|
|
390
|
+
{ id: "transfer", label: t2("financial.quickTx.transfer") }
|
|
391
|
+
];
|
|
392
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.Modal, { open, onOpenChange, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.ModalContent, { size: "md", noPadding: true, hideClose: true, className: "overflow-hidden", children: [
|
|
393
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${theme.header} shrink-0 px-5 pb-5 pt-5`, children: [
|
|
394
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
395
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
396
|
+
/* @__PURE__ */ jsxRuntime.jsx(HeaderIcon, { className: "h-5 w-5" }),
|
|
397
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold", children: t2("financial.quickTx.title") })
|
|
398
|
+
] }),
|
|
399
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
400
|
+
"button",
|
|
401
|
+
{
|
|
402
|
+
onClick: () => onOpenChange(false),
|
|
403
|
+
className: "flex h-7 w-7 items-center justify-center rounded-full opacity-80 transition-colors hover:bg-black/10 hover:opacity-100",
|
|
404
|
+
"aria-label": t2("financial.quickTx.close"),
|
|
405
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" })
|
|
406
|
+
}
|
|
407
|
+
)
|
|
408
|
+
] }),
|
|
409
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-4 flex gap-1 rounded-full bg-black/15 p-1", children: TYPES.map((ty) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
410
|
+
"button",
|
|
411
|
+
{
|
|
412
|
+
onClick: () => setType(ty.id),
|
|
413
|
+
className: `flex-1 rounded-full px-2 py-1.5 text-xs font-medium transition-colors ${type === ty.id ? "bg-white/90 text-foreground shadow-sm" : "opacity-90 hover:bg-white/10"}`,
|
|
414
|
+
children: ty.label
|
|
415
|
+
},
|
|
416
|
+
ty.id
|
|
417
|
+
)) }),
|
|
418
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-5 flex items-baseline justify-center gap-2", children: [
|
|
419
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xl font-medium opacity-90", children: currency.symbol }),
|
|
420
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
421
|
+
"input",
|
|
422
|
+
{
|
|
423
|
+
type: "text",
|
|
424
|
+
inputMode: "decimal",
|
|
425
|
+
autoFocus: true,
|
|
426
|
+
value: amountStr,
|
|
427
|
+
onChange: (e) => onAmountChange(e.target.value),
|
|
428
|
+
onKeyDown: (e) => {
|
|
429
|
+
if (e.key === "Enter") void handleSave();
|
|
430
|
+
},
|
|
431
|
+
placeholder: "0,00",
|
|
432
|
+
className: "w-full max-w-[220px] bg-transparent text-center text-4xl font-bold tracking-tight outline-none placeholder:opacity-40"
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
] })
|
|
436
|
+
] }),
|
|
437
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 space-y-4 overflow-y-auto px-5 py-5", children: [
|
|
438
|
+
type !== "transfer" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between rounded-input border border-input bg-card px-3 py-2.5 shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)]", children: [
|
|
439
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-medium", children: type === "income" ? t2("financial.quickTx.received") : t2("financial.quickTx.paid") }),
|
|
440
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-1", children: [
|
|
441
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
442
|
+
"button",
|
|
443
|
+
{
|
|
444
|
+
onClick: () => setPaid(true),
|
|
445
|
+
className: `rounded-full px-3 py-1 text-xs font-medium transition-colors ${paid ? "bg-success text-success-foreground" : "bg-muted text-muted-foreground"}`,
|
|
446
|
+
children: t2("financial.quickTx.yes")
|
|
447
|
+
}
|
|
448
|
+
),
|
|
449
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
450
|
+
"button",
|
|
451
|
+
{
|
|
452
|
+
onClick: () => setPaid(false),
|
|
453
|
+
className: `rounded-full px-3 py-1 text-xs font-medium transition-colors ${!paid ? "bg-warning text-warning-foreground" : "bg-muted text-muted-foreground"}`,
|
|
454
|
+
children: t2("financial.quickTx.no")
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
] })
|
|
458
|
+
] }),
|
|
459
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
460
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [
|
|
461
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarDays, { className: "h-3.5 w-3.5" }),
|
|
462
|
+
" ",
|
|
463
|
+
t2("financial.invoiceForm.date")
|
|
464
|
+
] }),
|
|
465
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DatePicker, { value: date, onChange: setDate })
|
|
466
|
+
] }),
|
|
467
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
468
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.invoiceForm.description") }),
|
|
469
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
470
|
+
"input",
|
|
471
|
+
{
|
|
472
|
+
type: "text",
|
|
473
|
+
value: description,
|
|
474
|
+
onChange: (e) => setDescription(e.target.value),
|
|
475
|
+
onKeyDown: (e) => {
|
|
476
|
+
if (e.key === "Enter") void handleSave();
|
|
477
|
+
},
|
|
478
|
+
placeholder: t2("financial.quickTx.descriptionPlaceholder"),
|
|
479
|
+
className: "mt-1 w-full rounded-input border border-input bg-card px-3 py-2.5 text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] outline-none focus:ring-2 focus:ring-ring"
|
|
480
|
+
}
|
|
481
|
+
)
|
|
482
|
+
] }),
|
|
483
|
+
type !== "transfer" && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
484
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "mb-1 flex items-center gap-1.5 text-xs font-medium text-muted-foreground", children: [
|
|
485
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-3.5 w-3.5" }),
|
|
486
|
+
" ",
|
|
487
|
+
t2("financial.quickTx.receipt")
|
|
488
|
+
] }),
|
|
489
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
490
|
+
"input",
|
|
491
|
+
{
|
|
492
|
+
ref: fileInputRef,
|
|
493
|
+
type: "file",
|
|
494
|
+
accept: "image/*",
|
|
495
|
+
capture: "environment",
|
|
496
|
+
className: "hidden",
|
|
497
|
+
onChange: onPickReceipt
|
|
498
|
+
}
|
|
499
|
+
),
|
|
500
|
+
receiptUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 rounded-input border border-input bg-card p-2 shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)]", children: [
|
|
501
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
502
|
+
"img",
|
|
503
|
+
{
|
|
504
|
+
src: receiptUrl,
|
|
505
|
+
alt: t2("financial.quickTx.receipt"),
|
|
506
|
+
className: "h-16 w-16 shrink-0 rounded-md border object-cover"
|
|
507
|
+
}
|
|
508
|
+
),
|
|
509
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
510
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium", children: t2("financial.quickTx.receiptAttached") }),
|
|
511
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
512
|
+
"button",
|
|
513
|
+
{
|
|
514
|
+
type: "button",
|
|
515
|
+
onClick: () => fileInputRef.current?.click(),
|
|
516
|
+
className: "text-xs text-primary hover:underline",
|
|
517
|
+
children: t2("financial.quickTx.receiptReplace")
|
|
518
|
+
}
|
|
519
|
+
)
|
|
520
|
+
] }),
|
|
521
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
522
|
+
"button",
|
|
523
|
+
{
|
|
524
|
+
type: "button",
|
|
525
|
+
onClick: () => setReceiptUrl(void 0),
|
|
526
|
+
"aria-label": t2("financial.quickTx.receiptRemove"),
|
|
527
|
+
className: "flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted",
|
|
528
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" })
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(
|
|
532
|
+
"button",
|
|
533
|
+
{
|
|
534
|
+
type: "button",
|
|
535
|
+
onClick: () => fileInputRef.current?.click(),
|
|
536
|
+
className: `flex w-full items-center justify-center gap-2 rounded-input border border-dashed px-3 py-3 text-sm font-medium transition-colors hover:bg-muted/30 ${focusReceipt ? "border-primary text-primary ring-1 ring-primary/30" : "border-input text-muted-foreground"}`,
|
|
537
|
+
children: [
|
|
538
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Camera, { className: "h-4 w-4" }),
|
|
539
|
+
" ",
|
|
540
|
+
t2("financial.quickTx.receiptCapture")
|
|
541
|
+
]
|
|
542
|
+
}
|
|
543
|
+
)
|
|
544
|
+
] }),
|
|
545
|
+
type === "transfer" ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3", children: [
|
|
546
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
547
|
+
ChipSelect,
|
|
548
|
+
{
|
|
549
|
+
icon: lucideReact.Wallet,
|
|
550
|
+
label: t2("financial.quickTx.fromAccount"),
|
|
551
|
+
value: accountId,
|
|
552
|
+
options: accountOptions,
|
|
553
|
+
onChange: setAccountId,
|
|
554
|
+
emptyLabel: t2("financial.quickTx.selectAccount")
|
|
555
|
+
}
|
|
556
|
+
),
|
|
557
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
558
|
+
ChipSelect,
|
|
559
|
+
{
|
|
560
|
+
icon: lucideReact.Wallet,
|
|
561
|
+
label: t2("financial.quickTx.toAccount"),
|
|
562
|
+
value: toAccountId,
|
|
563
|
+
options: accountOptions.filter((o) => o.id !== accountId),
|
|
564
|
+
onChange: setToAccountId,
|
|
565
|
+
emptyLabel: t2("financial.quickTx.selectAccount")
|
|
566
|
+
}
|
|
567
|
+
)
|
|
568
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-1 gap-3", children: [
|
|
569
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
570
|
+
ChipSelect,
|
|
571
|
+
{
|
|
572
|
+
icon: lucideReact.Tag,
|
|
573
|
+
label: t2("financial.quickTx.category"),
|
|
574
|
+
value: categoryId,
|
|
575
|
+
options: categoryOptions,
|
|
576
|
+
onChange: setCategoryId,
|
|
577
|
+
emptyLabel: t2("financial.quickTx.selectCategory")
|
|
578
|
+
}
|
|
579
|
+
),
|
|
580
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
581
|
+
ChipSelect,
|
|
582
|
+
{
|
|
583
|
+
icon: lucideReact.Wallet,
|
|
584
|
+
label: t2("financial.quickTx.account"),
|
|
585
|
+
value: accountId,
|
|
586
|
+
options: accountOptions,
|
|
587
|
+
onChange: setAccountId,
|
|
588
|
+
emptyLabel: t2("financial.quickTx.selectAccount")
|
|
589
|
+
}
|
|
590
|
+
)
|
|
591
|
+
] }),
|
|
592
|
+
type !== "transfer" && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
593
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
594
|
+
TogglePill,
|
|
595
|
+
{
|
|
596
|
+
icon: lucideReact.Repeat,
|
|
597
|
+
label: t2("financial.quickTx.recurring"),
|
|
598
|
+
checked: recurring,
|
|
599
|
+
onChange: setRecurring
|
|
600
|
+
}
|
|
601
|
+
),
|
|
602
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
603
|
+
"button",
|
|
604
|
+
{
|
|
605
|
+
type: "button",
|
|
606
|
+
onClick: () => fileInputRef.current?.click(),
|
|
607
|
+
title: t2("financial.quickTx.attachment"),
|
|
608
|
+
className: `flex items-center gap-2 rounded-full border px-3 py-2 text-xs font-medium transition-colors ${receiptUrl ? "border-transparent bg-primary text-primary-foreground" : "border-input bg-card text-muted-foreground hover:bg-muted/40"}`,
|
|
609
|
+
children: [
|
|
610
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-3.5 w-3.5" }),
|
|
611
|
+
" ",
|
|
612
|
+
t2("financial.quickTx.attachment"),
|
|
613
|
+
receiptUrl && /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5" })
|
|
614
|
+
]
|
|
615
|
+
}
|
|
616
|
+
)
|
|
617
|
+
] })
|
|
618
|
+
] }),
|
|
619
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-3 border-t px-5 py-3", children: [
|
|
620
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: amount > 0 ? saas.formatCurrency(amount, currency) : t2("financial.quickTx.enterAmount") }),
|
|
621
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
622
|
+
ui.Button,
|
|
623
|
+
{
|
|
624
|
+
className: "ml-auto",
|
|
625
|
+
onClick: () => void handleSave(),
|
|
626
|
+
disabled: !canSave,
|
|
627
|
+
children: [
|
|
628
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-4 w-4" }),
|
|
629
|
+
saving ? t2("financial.quickTx.saving") : t2("financial.quickTx.save")
|
|
630
|
+
]
|
|
631
|
+
}
|
|
632
|
+
)
|
|
633
|
+
] })
|
|
634
|
+
] }) });
|
|
635
|
+
}
|
|
636
|
+
var pending = null;
|
|
637
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
638
|
+
function emit() {
|
|
639
|
+
for (const l of listeners) l();
|
|
640
|
+
}
|
|
641
|
+
function openQuickAdd(type = "expense", receipt = false) {
|
|
642
|
+
pending = { type, receipt };
|
|
643
|
+
emit();
|
|
644
|
+
}
|
|
645
|
+
function consumeQuickAdd() {
|
|
646
|
+
const req = pending;
|
|
647
|
+
pending = null;
|
|
648
|
+
return req;
|
|
649
|
+
}
|
|
650
|
+
function subscribe(cb) {
|
|
651
|
+
listeners.add(cb);
|
|
652
|
+
return () => {
|
|
653
|
+
listeners.delete(cb);
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
function getSnapshot() {
|
|
657
|
+
return pending;
|
|
658
|
+
}
|
|
659
|
+
function usePendingQuickAdd() {
|
|
660
|
+
return React17__namespace.useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
24
661
|
}
|
|
25
662
|
var STATUS_OPTIONS = [
|
|
26
663
|
{ value: "open", labelKey: "financial.invoice.statusOpen", color: "bg-info-soft text-info-soft-foreground", icon: lucideReact.CircleDashed },
|
|
@@ -41,7 +678,7 @@ function StatusBadge({ status }) {
|
|
|
41
678
|
}
|
|
42
679
|
function useInvoiceColumns(currency) {
|
|
43
680
|
const t2 = core.useTranslation();
|
|
44
|
-
return
|
|
681
|
+
return React17.useMemo(() => [
|
|
45
682
|
{
|
|
46
683
|
accessorKey: "fiscalNumber",
|
|
47
684
|
header: "#",
|
|
@@ -93,27 +730,49 @@ function useInvoiceColumns(currency) {
|
|
|
93
730
|
function InvoiceListView({ direction, onNew, onEdit }) {
|
|
94
731
|
const t2 = core.useTranslation();
|
|
95
732
|
const { currency } = useFinancialConfig();
|
|
733
|
+
const provider = useFinancialProvider();
|
|
96
734
|
const invoices = useFinancialStore((s) => s.invoices);
|
|
97
735
|
useFinancialStore((s) => s.invoicesTotal);
|
|
98
736
|
const invoicesLoading = useFinancialStore((s) => s.invoicesLoading);
|
|
99
737
|
const fetchInvoices = useFinancialStore((s) => s.fetchInvoices);
|
|
100
|
-
const [search, setSearch] =
|
|
101
|
-
const [statusFilter, setStatusFilter] =
|
|
102
|
-
|
|
738
|
+
const [search, setSearch] = React17.useState("");
|
|
739
|
+
const [statusFilter, setStatusFilter] = React17.useState([]);
|
|
740
|
+
const [accountId, setAccountId] = React17.useState("");
|
|
741
|
+
const [costCenterId, setCostCenterId] = React17.useState("");
|
|
742
|
+
const [accounts, setAccounts] = React17.useState([]);
|
|
743
|
+
const [costCenters, setCostCenters] = React17.useState([]);
|
|
744
|
+
React17.useEffect(() => {
|
|
745
|
+
let active = true;
|
|
746
|
+
Promise.all([provider.getChartOfAccounts(), provider.getCostCenters()]).then(([accountRows, costCenterRows]) => {
|
|
747
|
+
if (!active) return;
|
|
748
|
+
setAccounts(accountRows.filter((account) => account.nodeType === "leaf"));
|
|
749
|
+
setCostCenters(costCenterRows);
|
|
750
|
+
}).catch(() => {
|
|
751
|
+
if (!active) return;
|
|
752
|
+
setAccounts([]);
|
|
753
|
+
setCostCenters([]);
|
|
754
|
+
});
|
|
755
|
+
return () => {
|
|
756
|
+
active = false;
|
|
757
|
+
};
|
|
758
|
+
}, [provider]);
|
|
759
|
+
React17.useEffect(() => {
|
|
103
760
|
fetchInvoices({
|
|
104
761
|
direction,
|
|
105
762
|
status: statusFilter.length > 0 ? statusFilter : void 0,
|
|
106
|
-
search: search || void 0
|
|
763
|
+
search: search || void 0,
|
|
764
|
+
accountId: accountId || void 0,
|
|
765
|
+
costCenterId: costCenterId || void 0
|
|
107
766
|
});
|
|
108
|
-
}, [direction, statusFilter, search]);
|
|
767
|
+
}, [direction, statusFilter, search, accountId, costCenterId]);
|
|
109
768
|
const filtered = invoices.filter((inv) => inv.direction === direction);
|
|
110
769
|
const listTitle = direction === "debit" ? t2("financial.invoice.accountsPayable") : t2("financial.invoice.accountsReceivable");
|
|
111
770
|
const columns = useInvoiceColumns(currency);
|
|
112
|
-
const tags =
|
|
771
|
+
const tags = React17.useMemo(
|
|
113
772
|
() => STATUS_OPTIONS.filter((o) => o.value !== "cancelled").map((o) => ({ value: o.value, label: t2(o.labelKey) })),
|
|
114
773
|
[t2]
|
|
115
774
|
);
|
|
116
|
-
const summary =
|
|
775
|
+
const summary = React17.useMemo(() => {
|
|
117
776
|
let totalAmount = 0;
|
|
118
777
|
let paidAmount = 0;
|
|
119
778
|
let openAmount = 0;
|
|
@@ -135,6 +794,46 @@ function InvoiceListView({ direction, onNew, onEdit }) {
|
|
|
135
794
|
subtitle: t2("financial.invoice.invoices", { count: filtered.length })
|
|
136
795
|
}
|
|
137
796
|
),
|
|
797
|
+
(accounts.length > 0 || costCenters.length > 0) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-end gap-3 rounded-lg border bg-muted/20 px-4 py-3", children: [
|
|
798
|
+
accounts.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground", children: [
|
|
799
|
+
t2("financial.invoice.filterAccount"),
|
|
800
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
801
|
+
"select",
|
|
802
|
+
{
|
|
803
|
+
className: "h-9 rounded-md border bg-background px-3 text-sm text-foreground",
|
|
804
|
+
value: accountId,
|
|
805
|
+
onChange: (event) => setAccountId(event.target.value),
|
|
806
|
+
children: [
|
|
807
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t2("financial.invoice.filterAllAccounts") }),
|
|
808
|
+
accounts.map((account) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: account.id, children: [
|
|
809
|
+
account.code,
|
|
810
|
+
" - ",
|
|
811
|
+
account.name
|
|
812
|
+
] }, account.id))
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
] }),
|
|
817
|
+
costCenters.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "flex min-w-[220px] flex-col gap-1 text-xs font-medium text-muted-foreground", children: [
|
|
818
|
+
t2("financial.invoice.filterCostCenter"),
|
|
819
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
820
|
+
"select",
|
|
821
|
+
{
|
|
822
|
+
className: "h-9 rounded-md border bg-background px-3 text-sm text-foreground",
|
|
823
|
+
value: costCenterId,
|
|
824
|
+
onChange: (event) => setCostCenterId(event.target.value),
|
|
825
|
+
children: [
|
|
826
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t2("financial.invoice.filterAllCostCenters") }),
|
|
827
|
+
costCenters.map((costCenter) => /* @__PURE__ */ jsxRuntime.jsxs("option", { value: costCenter.id, children: [
|
|
828
|
+
costCenter.code,
|
|
829
|
+
" - ",
|
|
830
|
+
costCenter.name
|
|
831
|
+
] }, costCenter.id))
|
|
832
|
+
]
|
|
833
|
+
}
|
|
834
|
+
)
|
|
835
|
+
] })
|
|
836
|
+
] }),
|
|
138
837
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
139
838
|
ui.ListView,
|
|
140
839
|
{
|
|
@@ -177,10 +876,10 @@ function InvoiceListView({ direction, onNew, onEdit }) {
|
|
|
177
876
|
}
|
|
178
877
|
function LocationSelector({ locations, value, onChange }) {
|
|
179
878
|
const t2 = core.useTranslation();
|
|
180
|
-
const [open, setOpen] =
|
|
181
|
-
const [search, setSearch] =
|
|
182
|
-
const ref =
|
|
183
|
-
|
|
879
|
+
const [open, setOpen] = React17.useState(false);
|
|
880
|
+
const [search, setSearch] = React17.useState("");
|
|
881
|
+
const ref = React17.useRef(null);
|
|
882
|
+
React17.useEffect(() => {
|
|
184
883
|
function handler(e) {
|
|
185
884
|
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
186
885
|
}
|
|
@@ -260,7 +959,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
260
959
|
const total = item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0);
|
|
261
960
|
const kindLabel = itemTypes.find((tp) => tp.value === item.itemKind)?.label ?? item.itemKind;
|
|
262
961
|
const hasAdjustments = (item.discount ?? 0) > 0 || (item.surcharge ?? 0) > 0;
|
|
263
|
-
const [showAdjustments, setShowAdjustments] =
|
|
962
|
+
const [showAdjustments, setShowAdjustments] = React17.useState(hasAdjustments);
|
|
264
963
|
function handleFieldKeyDown(e) {
|
|
265
964
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
266
965
|
e.preventDefault();
|
|
@@ -496,25 +1195,25 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
496
1195
|
const createInvoice = useFinancialStore((s) => s.createInvoice);
|
|
497
1196
|
const hasLocations = locations.length > 0;
|
|
498
1197
|
const isEdit = !!editId;
|
|
499
|
-
const [loaded, setLoaded] =
|
|
500
|
-
const [loading, setLoading] =
|
|
501
|
-
const [contactName, setContactName] =
|
|
502
|
-
const [contactId, setContactId] =
|
|
503
|
-
const [unitId, setUnitId] =
|
|
504
|
-
const [invoiceDate, setInvoiceDate] =
|
|
505
|
-
const [fiscalNumber, setFiscalNumber] =
|
|
506
|
-
const [observations, setObservations] =
|
|
507
|
-
const [items, setItems] =
|
|
508
|
-
const [expandedItemId, setExpandedItemId] =
|
|
509
|
-
const [addingItem, setAddingItem] =
|
|
510
|
-
const [installments, setInstallments] =
|
|
511
|
-
const [saving, setSaving] =
|
|
512
|
-
const [menuOpen, setMenuOpen] =
|
|
513
|
-
const [confirmCancel, setConfirmCancel] =
|
|
514
|
-
const [cancelling, setCancelling] =
|
|
515
|
-
const menuRef =
|
|
1198
|
+
const [loaded, setLoaded] = React17.useState(!editId);
|
|
1199
|
+
const [loading, setLoading] = React17.useState(!!editId);
|
|
1200
|
+
const [contactName, setContactName] = React17.useState("");
|
|
1201
|
+
const [contactId, setContactId] = React17.useState();
|
|
1202
|
+
const [unitId, setUnitId] = React17.useState(locations[0]?.id ?? "");
|
|
1203
|
+
const [invoiceDate, setInvoiceDate] = React17.useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
1204
|
+
const [fiscalNumber, setFiscalNumber] = React17.useState("");
|
|
1205
|
+
const [observations, setObservations] = React17.useState("");
|
|
1206
|
+
const [items, setItems] = React17.useState([]);
|
|
1207
|
+
const [expandedItemId, setExpandedItemId] = React17.useState(null);
|
|
1208
|
+
const [addingItem, setAddingItem] = React17.useState(false);
|
|
1209
|
+
const [installments, setInstallments] = React17.useState([]);
|
|
1210
|
+
const [saving, setSaving] = React17.useState(false);
|
|
1211
|
+
const [menuOpen, setMenuOpen] = React17.useState(false);
|
|
1212
|
+
const [confirmCancel, setConfirmCancel] = React17.useState(false);
|
|
1213
|
+
const [cancelling, setCancelling] = React17.useState(false);
|
|
1214
|
+
const menuRef = React17.useRef(null);
|
|
516
1215
|
const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
|
|
517
|
-
|
|
1216
|
+
React17.useEffect(() => {
|
|
518
1217
|
if (!menuOpen) return;
|
|
519
1218
|
function handleClick(e) {
|
|
520
1219
|
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
@@ -533,7 +1232,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
533
1232
|
setCancelling(false);
|
|
534
1233
|
}
|
|
535
1234
|
}
|
|
536
|
-
|
|
1235
|
+
React17.useEffect(() => {
|
|
537
1236
|
if (!editId || loaded) return;
|
|
538
1237
|
setLoading(true);
|
|
539
1238
|
(async () => {
|
|
@@ -570,8 +1269,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
570
1269
|
const totalAmount = items.reduce((sum, it) => {
|
|
571
1270
|
return sum + it.quantity * it.unitPrice - (it.discount ?? 0) + (it.surcharge ?? 0);
|
|
572
1271
|
}, 0);
|
|
573
|
-
const prevItemCount =
|
|
574
|
-
|
|
1272
|
+
const prevItemCount = React17.useRef(items.length);
|
|
1273
|
+
React17.useEffect(() => {
|
|
575
1274
|
if (items.length > 0 && prevItemCount.current === 0 && installments.length === 0 && !isEdit) {
|
|
576
1275
|
const total = items.reduce((sum, it) => sum + it.quantity * it.unitPrice - (it.discount ?? 0) + (it.surcharge ?? 0), 0);
|
|
577
1276
|
if (total > 0) {
|
|
@@ -580,7 +1279,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
580
1279
|
}
|
|
581
1280
|
prevItemCount.current = items.length;
|
|
582
1281
|
}, [items.length]);
|
|
583
|
-
|
|
1282
|
+
React17.useEffect(() => {
|
|
584
1283
|
if (installments.length > 0 && totalAmount > 0 && !isEdit) {
|
|
585
1284
|
const count = installments.length;
|
|
586
1285
|
const perInstallment = Math.floor(totalAmount * 100 / count) / 100;
|
|
@@ -692,8 +1391,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
692
1391
|
items: items.map(({ _id, ...rest }) => rest),
|
|
693
1392
|
installments: installments.map(({ _id, ...rest }) => rest)
|
|
694
1393
|
};
|
|
695
|
-
const snapshot =
|
|
696
|
-
|
|
1394
|
+
const snapshot = React17.useRef(null);
|
|
1395
|
+
React17.useEffect(() => {
|
|
697
1396
|
if (loaded && snapshot.current === null) snapshot.current = JSON.stringify(fields);
|
|
698
1397
|
}, [loaded]);
|
|
699
1398
|
const dirty = snapshot.current !== null && JSON.stringify(fields) !== snapshot.current;
|
|
@@ -1003,40 +1702,40 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1003
1702
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
1004
1703
|
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
1005
1704
|
const remaining = movement.amount - movement.paidAmount;
|
|
1006
|
-
const [amount, setAmount] =
|
|
1007
|
-
const [paymentDate, setPaymentDate] =
|
|
1008
|
-
const [selectedTypeId, setSelectedTypeId] =
|
|
1009
|
-
const [paymentMethodId, setPaymentMethodId] =
|
|
1010
|
-
const [bankAccountId, setBankAccountId] =
|
|
1011
|
-
const [cardBrand, setCardBrand] =
|
|
1012
|
-
const [cardInstallments, setCardInstallments] =
|
|
1013
|
-
const [notes, setNotes] =
|
|
1014
|
-
const [saving, setSaving] =
|
|
1015
|
-
const [visible, setVisible] =
|
|
1016
|
-
const [expandMethodPicker, setExpandMethodPicker] =
|
|
1017
|
-
|
|
1705
|
+
const [amount, setAmount] = React17.useState(remaining);
|
|
1706
|
+
const [paymentDate, setPaymentDate] = React17.useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
1707
|
+
const [selectedTypeId, setSelectedTypeId] = React17.useState("");
|
|
1708
|
+
const [paymentMethodId, setPaymentMethodId] = React17.useState("");
|
|
1709
|
+
const [bankAccountId, setBankAccountId] = React17.useState("");
|
|
1710
|
+
const [cardBrand, setCardBrand] = React17.useState("");
|
|
1711
|
+
const [cardInstallments, setCardInstallments] = React17.useState(1);
|
|
1712
|
+
const [notes, setNotes] = React17.useState("");
|
|
1713
|
+
const [saving, setSaving] = React17.useState(false);
|
|
1714
|
+
const [visible, setVisible] = React17.useState(false);
|
|
1715
|
+
const [expandMethodPicker, setExpandMethodPicker] = React17.useState(true);
|
|
1716
|
+
React17.useEffect(() => {
|
|
1018
1717
|
fetchBankAccounts();
|
|
1019
1718
|
fetchPaymentMethods();
|
|
1020
1719
|
requestAnimationFrame(() => setVisible(true));
|
|
1021
1720
|
}, []);
|
|
1022
|
-
const selectedType =
|
|
1721
|
+
const selectedType = React17.useMemo(
|
|
1023
1722
|
() => paymentMethodTypes.find((tp) => tp.id === selectedTypeId),
|
|
1024
1723
|
[paymentMethodTypes, selectedTypeId]
|
|
1025
1724
|
);
|
|
1026
1725
|
const txType = selectedType?.transactionType ?? "";
|
|
1027
|
-
const filteredMethods =
|
|
1726
|
+
const filteredMethods = React17.useMemo(
|
|
1028
1727
|
() => paymentMethods.filter((m) => m.paymentMethodTypeId === selectedTypeId),
|
|
1029
1728
|
[paymentMethods, selectedTypeId]
|
|
1030
1729
|
);
|
|
1031
1730
|
const needsCard = txType === "credit_card" || txType === "debit_card";
|
|
1032
1731
|
const needsCardInstallments = txType === "credit_card";
|
|
1033
|
-
const eligibleAccounts =
|
|
1732
|
+
const eligibleAccounts = React17.useMemo(() => {
|
|
1034
1733
|
const allowed = selectedType?.allowedAccountTypes && selectedType.allowedAccountTypes.length ? selectedType.allowedAccountTypes : ACCOUNT_TYPES_FOR_TX[txType];
|
|
1035
1734
|
if (!allowed || !allowed.length) return bankAccounts;
|
|
1036
1735
|
const filtered = bankAccounts.filter((a) => allowed.includes(a.accountType));
|
|
1037
1736
|
return filtered.length ? filtered : bankAccounts;
|
|
1038
1737
|
}, [bankAccounts, selectedType, txType]);
|
|
1039
|
-
|
|
1738
|
+
React17.useEffect(() => {
|
|
1040
1739
|
if (selectedTypeId && !bankAccountId && eligibleAccounts.length) {
|
|
1041
1740
|
setBankAccountId(eligibleAccounts[0].id);
|
|
1042
1741
|
}
|
|
@@ -1280,19 +1979,19 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
1280
1979
|
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
1281
1980
|
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
1282
1981
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
1283
|
-
const [detailsLoaded, setDetailsLoaded] =
|
|
1284
|
-
const [detailsLoading, setDetailsLoading] =
|
|
1285
|
-
const [invoice, setInvoice] =
|
|
1286
|
-
const [items, setItems] =
|
|
1287
|
-
const [movements, setMovements] =
|
|
1288
|
-
const [payments, setPayments] =
|
|
1289
|
-
const [loading, setLoading] =
|
|
1290
|
-
const [payingMovement, setPayingMovement] =
|
|
1291
|
-
const [expandedMovId, setExpandedMovId] =
|
|
1292
|
-
const [menuOpen, setMenuOpen] =
|
|
1293
|
-
const [confirmCancel, setConfirmCancel] =
|
|
1294
|
-
const [cancelling, setCancelling] =
|
|
1295
|
-
const menuRef =
|
|
1982
|
+
const [detailsLoaded, setDetailsLoaded] = React17.useState(false);
|
|
1983
|
+
const [detailsLoading, setDetailsLoading] = React17.useState(false);
|
|
1984
|
+
const [invoice, setInvoice] = React17.useState(null);
|
|
1985
|
+
const [items, setItems] = React17.useState([]);
|
|
1986
|
+
const [movements, setMovements] = React17.useState([]);
|
|
1987
|
+
const [payments, setPayments] = React17.useState([]);
|
|
1988
|
+
const [loading, setLoading] = React17.useState(true);
|
|
1989
|
+
const [payingMovement, setPayingMovement] = React17.useState(null);
|
|
1990
|
+
const [expandedMovId, setExpandedMovId] = React17.useState(null);
|
|
1991
|
+
const [menuOpen, setMenuOpen] = React17.useState(false);
|
|
1992
|
+
const [confirmCancel, setConfirmCancel] = React17.useState(false);
|
|
1993
|
+
const [cancelling, setCancelling] = React17.useState(false);
|
|
1994
|
+
const menuRef = React17.useRef(null);
|
|
1296
1995
|
const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
|
|
1297
1996
|
async function loadData() {
|
|
1298
1997
|
setLoading(true);
|
|
@@ -1311,10 +2010,10 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel })
|
|
|
1311
2010
|
);
|
|
1312
2011
|
setLoading(false);
|
|
1313
2012
|
}
|
|
1314
|
-
|
|
2013
|
+
React17.useEffect(() => {
|
|
1315
2014
|
loadData();
|
|
1316
2015
|
}, [invoiceId]);
|
|
1317
|
-
|
|
2016
|
+
React17.useEffect(() => {
|
|
1318
2017
|
if (!menuOpen) return;
|
|
1319
2018
|
function handleClick(e) {
|
|
1320
2019
|
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
@@ -1915,16 +2614,16 @@ function CashOpeningPanel({
|
|
|
1915
2614
|
currency,
|
|
1916
2615
|
onOpen
|
|
1917
2616
|
}) {
|
|
1918
|
-
const [selectedAccountId, setSelectedAccountId] =
|
|
1919
|
-
const [mode, setMode] =
|
|
1920
|
-
const [quickBalance, setQuickBalance] =
|
|
1921
|
-
const [counts, setCounts] =
|
|
1922
|
-
const [notes, setNotes] =
|
|
1923
|
-
const [showNotes, setShowNotes] =
|
|
1924
|
-
const [confirming, setConfirming] =
|
|
1925
|
-
const [submitting, setSubmitting] =
|
|
2617
|
+
const [selectedAccountId, setSelectedAccountId] = React17.useState(cashAccounts[0]?.id ?? "");
|
|
2618
|
+
const [mode, setMode] = React17.useState("quick");
|
|
2619
|
+
const [quickBalance, setQuickBalance] = React17.useState(0);
|
|
2620
|
+
const [counts, setCounts] = React17.useState({});
|
|
2621
|
+
const [notes, setNotes] = React17.useState("");
|
|
2622
|
+
const [showNotes, setShowNotes] = React17.useState(false);
|
|
2623
|
+
const [confirming, setConfirming] = React17.useState(false);
|
|
2624
|
+
const [submitting, setSubmitting] = React17.useState(false);
|
|
1926
2625
|
const t2 = core.useTranslation();
|
|
1927
|
-
const countedTotal =
|
|
2626
|
+
const countedTotal = React17.useMemo(() => {
|
|
1928
2627
|
return BRL_DENOMINATIONS.reduce((sum, d) => sum + (counts[d.value] ?? 0) * d.value, 0);
|
|
1929
2628
|
}, [counts]);
|
|
1930
2629
|
const effectiveBalance = mode === "count" ? countedTotal : quickBalance;
|
|
@@ -2100,13 +2799,13 @@ function CashClosingPanel({
|
|
|
2100
2799
|
currency,
|
|
2101
2800
|
onClose
|
|
2102
2801
|
}) {
|
|
2103
|
-
const [mode, setMode] =
|
|
2104
|
-
const [quickBalance, setQuickBalance] =
|
|
2105
|
-
const [counts, setCounts] =
|
|
2106
|
-
const [notes, setNotes] =
|
|
2107
|
-
const [submitting, setSubmitting] =
|
|
2802
|
+
const [mode, setMode] = React17.useState("quick");
|
|
2803
|
+
const [quickBalance, setQuickBalance] = React17.useState(0);
|
|
2804
|
+
const [counts, setCounts] = React17.useState({});
|
|
2805
|
+
const [notes, setNotes] = React17.useState("");
|
|
2806
|
+
const [submitting, setSubmitting] = React17.useState(false);
|
|
2108
2807
|
const t2 = core.useTranslation();
|
|
2109
|
-
const countedTotal =
|
|
2808
|
+
const countedTotal = React17.useMemo(() => {
|
|
2110
2809
|
return BRL_DENOMINATIONS.reduce((sum, d) => sum + (counts[d.value] ?? 0) * d.value, 0);
|
|
2111
2810
|
}, [counts]);
|
|
2112
2811
|
const effectiveBalance = mode === "count" ? countedTotal : quickBalance;
|
|
@@ -2211,8 +2910,8 @@ function CashRegistersView() {
|
|
|
2211
2910
|
const fetchCashSessions = useFinancialStore((s) => s.fetchCashSessions);
|
|
2212
2911
|
const openCashSession = useFinancialStore((s) => s.openCashSession);
|
|
2213
2912
|
const closeCashSession = useFinancialStore((s) => s.closeCashSession);
|
|
2214
|
-
const [expandedClosing, setExpandedClosing] =
|
|
2215
|
-
|
|
2913
|
+
const [expandedClosing, setExpandedClosing] = React17.useState(null);
|
|
2914
|
+
React17.useEffect(() => {
|
|
2216
2915
|
fetchBankAccounts();
|
|
2217
2916
|
fetchCashSessions();
|
|
2218
2917
|
}, []);
|
|
@@ -2316,7 +3015,7 @@ function OpenAnotherCollapsible({
|
|
|
2316
3015
|
onOpen
|
|
2317
3016
|
}) {
|
|
2318
3017
|
const t2 = core.useTranslation();
|
|
2319
|
-
const [expanded, setExpanded] =
|
|
3018
|
+
const [expanded, setExpanded] = React17.useState(false);
|
|
2320
3019
|
const availableAccounts = cashAccounts.filter((a) => !openSessions.some((s) => s.bankAccountId === a.id));
|
|
2321
3020
|
if (availableAccounts.length === 0) return null;
|
|
2322
3021
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm", children: [
|
|
@@ -2343,14 +3042,14 @@ function TransferModal({ onClose, onDone, defaultFromId }) {
|
|
|
2343
3042
|
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
2344
3043
|
const createTransfer = useFinancialStore((s) => s.createTransfer);
|
|
2345
3044
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
2346
|
-
const [fromAccountId, setFromAccountId] =
|
|
2347
|
-
const [toAccountId, setToAccountId] =
|
|
2348
|
-
const [amount, setAmount] =
|
|
2349
|
-
const [date, setDate] =
|
|
2350
|
-
const [notes, setNotes] =
|
|
2351
|
-
const [saving, setSaving] =
|
|
2352
|
-
const [visible, setVisible] =
|
|
2353
|
-
|
|
3045
|
+
const [fromAccountId, setFromAccountId] = React17.useState(defaultFromId ?? "");
|
|
3046
|
+
const [toAccountId, setToAccountId] = React17.useState("");
|
|
3047
|
+
const [amount, setAmount] = React17.useState(0);
|
|
3048
|
+
const [date, setDate] = React17.useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
3049
|
+
const [notes, setNotes] = React17.useState("");
|
|
3050
|
+
const [saving, setSaving] = React17.useState(false);
|
|
3051
|
+
const [visible, setVisible] = React17.useState(false);
|
|
3052
|
+
React17.useEffect(() => {
|
|
2354
3053
|
fetchBankAccounts();
|
|
2355
3054
|
requestAnimationFrame(() => setVisible(true));
|
|
2356
3055
|
}, []);
|
|
@@ -2493,19 +3192,19 @@ function StatementsView({ onNavigate }) {
|
|
|
2493
3192
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
2494
3193
|
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
2495
3194
|
const fetchStatement = useFinancialStore((s) => s.fetchStatement);
|
|
2496
|
-
const [accountId, setAccountId] =
|
|
2497
|
-
const [dateFrom, setDateFrom] =
|
|
3195
|
+
const [accountId, setAccountId] = React17.useState("");
|
|
3196
|
+
const [dateFrom, setDateFrom] = React17.useState(() => {
|
|
2498
3197
|
const d = /* @__PURE__ */ new Date();
|
|
2499
3198
|
d.setMonth(d.getMonth() - 3);
|
|
2500
3199
|
return d.toISOString().slice(0, 10);
|
|
2501
3200
|
});
|
|
2502
|
-
const [dateTo, setDateTo] =
|
|
2503
|
-
const [showTransfer, setShowTransfer] =
|
|
2504
|
-
|
|
3201
|
+
const [dateTo, setDateTo] = React17.useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
3202
|
+
const [showTransfer, setShowTransfer] = React17.useState(false);
|
|
3203
|
+
React17.useEffect(() => {
|
|
2505
3204
|
fetchBankAccounts();
|
|
2506
3205
|
fetchPaymentMethods();
|
|
2507
3206
|
}, []);
|
|
2508
|
-
const methodName =
|
|
3207
|
+
const methodName = React17.useMemo(() => {
|
|
2509
3208
|
const byId = new Map(paymentMethods.map((m) => [m.id, m.name]));
|
|
2510
3209
|
const typeById = new Map(paymentMethodTypes.map((tp) => [tp.id, tp.name]));
|
|
2511
3210
|
return (mov) => {
|
|
@@ -2520,7 +3219,7 @@ function StatementsView({ onNavigate }) {
|
|
|
2520
3219
|
const base = e.movement.direction === "credit" ? "receivables" : "payables";
|
|
2521
3220
|
onNavigate(`${base}-detail:${e.movement.invoiceId}`);
|
|
2522
3221
|
}
|
|
2523
|
-
|
|
3222
|
+
React17.useEffect(() => {
|
|
2524
3223
|
if (accountId) {
|
|
2525
3224
|
fetchStatement({
|
|
2526
3225
|
bankAccountId: accountId === ALL ? void 0 : accountId,
|
|
@@ -2528,11 +3227,11 @@ function StatementsView({ onNavigate }) {
|
|
|
2528
3227
|
});
|
|
2529
3228
|
}
|
|
2530
3229
|
}, [accountId, dateFrom, dateTo]);
|
|
2531
|
-
|
|
3230
|
+
React17.useEffect(() => {
|
|
2532
3231
|
if (!accountId) setAccountId(ALL);
|
|
2533
3232
|
}, []);
|
|
2534
3233
|
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
2535
|
-
const columns =
|
|
3234
|
+
const columns = React17.useMemo(() => [
|
|
2536
3235
|
{
|
|
2537
3236
|
id: "date",
|
|
2538
3237
|
header: t2("financial.statements.columnDate"),
|
|
@@ -2707,59 +3406,141 @@ function CommissionsView() {
|
|
|
2707
3406
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-center rounded-lg border-2 border-dashed border-muted p-12", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.commissions.comingSoon") }) })
|
|
2708
3407
|
] });
|
|
2709
3408
|
}
|
|
3409
|
+
function cardMath(acc) {
|
|
3410
|
+
const limit = acc.creditLimit ?? 0;
|
|
3411
|
+
const used = Math.max(0, -acc.currentBalance);
|
|
3412
|
+
const available = Math.max(0, limit - used);
|
|
3413
|
+
const pct = limit > 0 ? Math.min(100, used / limit * 100) : 0;
|
|
3414
|
+
return { limit, used, available, pct };
|
|
3415
|
+
}
|
|
2710
3416
|
function CardsView() {
|
|
2711
3417
|
const t2 = core.useTranslation();
|
|
2712
|
-
const { currency
|
|
2713
|
-
const
|
|
2714
|
-
const
|
|
2715
|
-
const
|
|
2716
|
-
|
|
2717
|
-
|
|
3418
|
+
const { currency } = useFinancialConfig();
|
|
3419
|
+
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
3420
|
+
const bankAccountsLoading = useFinancialStore((s) => s.bankAccountsLoading);
|
|
3421
|
+
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
3422
|
+
const [tab, setTab] = React17.useState("open");
|
|
3423
|
+
const [addOpen, setAddOpen] = React17.useState(false);
|
|
3424
|
+
const [addCardId, setAddCardId] = React17.useState();
|
|
3425
|
+
React17.useEffect(() => {
|
|
3426
|
+
fetchBankAccounts();
|
|
2718
3427
|
}, []);
|
|
2719
|
-
const
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
3428
|
+
const cards = React17.useMemo(
|
|
3429
|
+
() => bankAccounts.filter((a) => a.accountType === "credit_card"),
|
|
3430
|
+
[bankAccounts]
|
|
3431
|
+
);
|
|
3432
|
+
const openCards = React17.useMemo(() => cards.filter((c) => c.isActive), [cards]);
|
|
3433
|
+
const closedCards = React17.useMemo(() => cards.filter((c) => !c.isActive), [cards]);
|
|
3434
|
+
const visibleCards = tab === "open" ? openCards : closedCards;
|
|
3435
|
+
const summary = React17.useMemo(() => {
|
|
3436
|
+
let totalAvailable = 0;
|
|
3437
|
+
let totalBills = 0;
|
|
3438
|
+
for (const c of openCards) {
|
|
3439
|
+
const { available, used } = cardMath(c);
|
|
3440
|
+
totalAvailable += available;
|
|
3441
|
+
totalBills += used;
|
|
3442
|
+
}
|
|
3443
|
+
return { totalAvailable, totalBills };
|
|
3444
|
+
}, [openCards]);
|
|
3445
|
+
function openAddExpense(cardId) {
|
|
3446
|
+
setAddCardId(cardId);
|
|
3447
|
+
setAddOpen(true);
|
|
2724
3448
|
}
|
|
2725
3449
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2726
3450
|
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: t2("financial.cards.title"), subtitle: t2("financial.cards.subtitle") }),
|
|
2727
|
-
|
|
2728
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-
|
|
2729
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm
|
|
2730
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-0.5", children: t2("financial.cards.
|
|
2731
|
-
] }) :
|
|
2732
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "
|
|
2733
|
-
/* @__PURE__ */ jsxRuntime.
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
"
|
|
3451
|
+
bankAccountsLoading && cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-center py-12 text-sm text-muted-foreground", children: "Loading..." }) : cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center rounded-2xl border-2 border-dashed border-muted", children: [
|
|
3452
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-12 w-12 items-center justify-center rounded-xl bg-gradient-to-br from-amber-300 to-yellow-600 mb-3 text-white shadow-sm", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CreditCard, { className: "h-6 w-6" }) }),
|
|
3453
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: t2("financial.cards.noCards") }),
|
|
3454
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-0.5 max-w-xs", children: t2("financial.cards.noCardsHint") })
|
|
3455
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
3456
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-px overflow-hidden rounded-2xl border bg-border shadow-sm", children: [
|
|
3457
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-card px-4 py-3", children: [
|
|
3458
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.cards.availableLimit") }),
|
|
3459
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-lg font-bold tabular-nums text-success", children: saas.formatCurrency(summary.totalAvailable, currency) })
|
|
3460
|
+
] }),
|
|
3461
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "bg-card px-4 py-3", children: [
|
|
3462
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.cards.allBillsValue") }),
|
|
3463
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-0.5 text-lg font-bold tabular-nums", children: saas.formatCurrency(summary.totalBills, currency) })
|
|
2739
3464
|
] })
|
|
2740
3465
|
] }),
|
|
2741
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
3466
|
+
closedCards.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1 rounded-full bg-muted p-1 w-full max-w-xs", children: ["open", "closed"].map((k) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
3467
|
+
"button",
|
|
3468
|
+
{
|
|
3469
|
+
onClick: () => setTab(k),
|
|
3470
|
+
className: `flex-1 rounded-full px-3 py-1.5 text-xs font-medium transition-colors ${tab === k ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground"}`,
|
|
3471
|
+
children: k === "open" ? t2("financial.cards.tabOpen") : t2("financial.cards.tabClosed")
|
|
3472
|
+
},
|
|
3473
|
+
k
|
|
3474
|
+
)) }),
|
|
3475
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3", children: visibleCards.map((card) => {
|
|
3476
|
+
const { limit, used, available, pct } = cardMath(card);
|
|
3477
|
+
const over = pct >= 90;
|
|
3478
|
+
const brand = `${card.bankName ?? ""} ${card.name ?? ""}`.toLowerCase();
|
|
3479
|
+
const headerGrad = brand.includes("nubank") || brand.includes("nu ") ? "from-purple-500 via-purple-600 to-indigo-700" : brand.includes("ita") ? "from-orange-400 via-orange-500 to-amber-600" : brand.includes("inter") ? "from-orange-500 via-orange-600 to-orange-700" : "from-indigo-500 via-indigo-600 to-violet-700";
|
|
3480
|
+
const barGrad = brand.includes("nubank") || brand.includes("nu ") ? "from-purple-500 to-indigo-600" : brand.includes("ita") ? "from-orange-400 to-amber-500" : brand.includes("inter") ? "from-orange-500 to-orange-600" : "from-indigo-500 to-violet-600";
|
|
3481
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "overflow-hidden rounded-2xl border bg-card shadow-sm", children: [
|
|
3482
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `bg-gradient-to-br ${headerGrad} px-4 py-4 text-white`, children: [
|
|
3483
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between", children: [
|
|
3484
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3485
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CreditCard, { className: "h-5 w-5" }),
|
|
3486
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: card.name })
|
|
3487
|
+
] }),
|
|
3488
|
+
card.bankName && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[11px] font-medium uppercase tracking-wide opacity-80", children: card.bankName })
|
|
3489
|
+
] }),
|
|
3490
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-4", children: [
|
|
3491
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] uppercase tracking-wide opacity-80", children: t2("financial.cards.currentBill") }),
|
|
3492
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-2xl font-bold tabular-nums leading-tight", children: saas.formatCurrency(used, currency) })
|
|
3493
|
+
] })
|
|
2748
3494
|
] }),
|
|
2749
|
-
/* @__PURE__ */ jsxRuntime.jsxs("
|
|
2750
|
-
"
|
|
2751
|
-
|
|
3495
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3 px-4 py-3", children: [
|
|
3496
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3497
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-[11px] text-muted-foreground", children: [
|
|
3498
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t2("financial.cards.usedOfLimit", { percent: String(Math.round(pct)) }) }),
|
|
3499
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "tabular-nums", children: saas.formatCurrency(limit, currency) })
|
|
3500
|
+
] }),
|
|
3501
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-1 h-2 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3502
|
+
"div",
|
|
3503
|
+
{
|
|
3504
|
+
className: `h-full rounded-full ${over ? "bg-destructive" : `bg-gradient-to-r ${barGrad}`}`,
|
|
3505
|
+
style: { width: `${pct}%` }
|
|
3506
|
+
}
|
|
3507
|
+
) })
|
|
3508
|
+
] }),
|
|
3509
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
3510
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
3511
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] text-muted-foreground", children: t2("financial.cards.availableLimit") }),
|
|
3512
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-semibold tabular-nums", children: saas.formatCurrency(available, currency) })
|
|
3513
|
+
] }),
|
|
3514
|
+
card.dueDay != null && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 text-right text-xs text-muted-foreground", children: [
|
|
3515
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CalendarClock, { className: "h-3.5 w-3.5" }),
|
|
3516
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: t2("financial.cards.dueOn", { day: String(card.dueDay) }) })
|
|
3517
|
+
] })
|
|
3518
|
+
] }),
|
|
3519
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3520
|
+
"button",
|
|
3521
|
+
{
|
|
3522
|
+
type: "button",
|
|
3523
|
+
onClick: () => openAddExpense(card.id),
|
|
3524
|
+
className: "flex w-full items-center justify-center gap-1.5 rounded-input border border-input bg-card px-3 py-2 text-xs font-medium text-foreground transition-colors hover:bg-muted/40",
|
|
3525
|
+
children: [
|
|
3526
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3.5 w-3.5" }),
|
|
3527
|
+
t2("financial.cards.addExpense")
|
|
3528
|
+
]
|
|
3529
|
+
}
|
|
3530
|
+
)
|
|
2752
3531
|
] })
|
|
2753
|
-
] })
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
3532
|
+
] }, card.id);
|
|
3533
|
+
}) })
|
|
3534
|
+
] }),
|
|
3535
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
3536
|
+
QuickTransactionForm,
|
|
3537
|
+
{
|
|
3538
|
+
open: addOpen,
|
|
3539
|
+
onOpenChange: setAddOpen,
|
|
3540
|
+
defaultType: "expense",
|
|
3541
|
+
defaultAccountId: addCardId
|
|
3542
|
+
}
|
|
3543
|
+
)
|
|
2763
3544
|
] });
|
|
2764
3545
|
}
|
|
2765
3546
|
function ReconciliationView() {
|
|
@@ -2767,14 +3548,14 @@ function ReconciliationView() {
|
|
|
2767
3548
|
const { currency } = useFinancialConfig();
|
|
2768
3549
|
const provider = useFinancialProvider();
|
|
2769
3550
|
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
2770
|
-
const [lines, setLines] =
|
|
2771
|
-
const [loading, setLoading] =
|
|
2772
|
-
const [selectedId, setSelectedId] =
|
|
2773
|
-
const [candidates, setCandidates] =
|
|
2774
|
-
const [candidatesLoading, setCandidatesLoading] =
|
|
2775
|
-
const [busy, setBusy] =
|
|
3551
|
+
const [lines, setLines] = React17.useState([]);
|
|
3552
|
+
const [loading, setLoading] = React17.useState(true);
|
|
3553
|
+
const [selectedId, setSelectedId] = React17.useState(null);
|
|
3554
|
+
const [candidates, setCandidates] = React17.useState([]);
|
|
3555
|
+
const [candidatesLoading, setCandidatesLoading] = React17.useState(false);
|
|
3556
|
+
const [busy, setBusy] = React17.useState(false);
|
|
2776
3557
|
const supported = typeof provider.getUnreconciled === "function";
|
|
2777
|
-
const load =
|
|
3558
|
+
const load = React17.useCallback(async () => {
|
|
2778
3559
|
if (!provider.getUnreconciled) return;
|
|
2779
3560
|
setLoading(true);
|
|
2780
3561
|
try {
|
|
@@ -2785,10 +3566,10 @@ function ReconciliationView() {
|
|
|
2785
3566
|
setLoading(false);
|
|
2786
3567
|
}
|
|
2787
3568
|
}, [provider]);
|
|
2788
|
-
|
|
3569
|
+
React17.useEffect(() => {
|
|
2789
3570
|
void load();
|
|
2790
3571
|
}, [load]);
|
|
2791
|
-
|
|
3572
|
+
React17.useEffect(() => {
|
|
2792
3573
|
if (!selectedId || !provider.suggestReconciliation) {
|
|
2793
3574
|
setCandidates([]);
|
|
2794
3575
|
return;
|
|
@@ -2940,7 +3721,7 @@ var STEPS = [
|
|
|
2940
3721
|
];
|
|
2941
3722
|
function FinancialOnboarding({ onComplete }) {
|
|
2942
3723
|
const t2 = core.useTranslation();
|
|
2943
|
-
const [step, setStep] =
|
|
3724
|
+
const [step, setStep] = React17.useState(0);
|
|
2944
3725
|
const current = STEPS[step];
|
|
2945
3726
|
const isLast = step === STEPS.length - 1;
|
|
2946
3727
|
const Icon = current.icon;
|
|
@@ -3085,18 +3866,47 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
3085
3866
|
cards: 0,
|
|
3086
3867
|
settings: 1
|
|
3087
3868
|
});
|
|
3088
|
-
const [onboardingComplete, setOnboardingComplete] =
|
|
3869
|
+
const [onboardingComplete, setOnboardingComplete] = React17.useState(() => {
|
|
3089
3870
|
try {
|
|
3090
3871
|
return localStorage.getItem("saas-core:financial-onboarded") === "true";
|
|
3091
3872
|
} catch {
|
|
3092
3873
|
return false;
|
|
3093
3874
|
}
|
|
3094
3875
|
});
|
|
3876
|
+
React17.useEffect(() => {
|
|
3877
|
+
if (onboardingComplete) return;
|
|
3878
|
+
let cancelled = false;
|
|
3879
|
+
void provider.getMovements({ pageSize: 1 }).then((res) => {
|
|
3880
|
+
if (!cancelled && res.total > 0) setOnboardingComplete(true);
|
|
3881
|
+
}).catch(() => {
|
|
3882
|
+
});
|
|
3883
|
+
return () => {
|
|
3884
|
+
cancelled = true;
|
|
3885
|
+
};
|
|
3886
|
+
}, [onboardingComplete, provider]);
|
|
3095
3887
|
const t2 = core.useTranslation();
|
|
3096
3888
|
const intent = parseIntent(view);
|
|
3097
3889
|
const isSettings = view === "settings";
|
|
3098
3890
|
const nav = buildNav(config, view, navigate, t2);
|
|
3099
|
-
const
|
|
3891
|
+
const [quickAddOpen, setQuickAddOpen] = React17.useState(false);
|
|
3892
|
+
const [quickAddType, setQuickAddType] = React17.useState("expense");
|
|
3893
|
+
const [quickAddReceipt, setQuickAddReceipt] = React17.useState(false);
|
|
3894
|
+
const openQuickAdd2 = React17.useCallback((type = "expense") => {
|
|
3895
|
+
setQuickAddType(type);
|
|
3896
|
+
setQuickAddReceipt(false);
|
|
3897
|
+
setQuickAddOpen(true);
|
|
3898
|
+
}, []);
|
|
3899
|
+
const pendingQuickAdd = usePendingQuickAdd();
|
|
3900
|
+
React17.useEffect(() => {
|
|
3901
|
+
if (!pendingQuickAdd) return;
|
|
3902
|
+
const req = consumeQuickAdd();
|
|
3903
|
+
if (!req) return;
|
|
3904
|
+
setQuickAddType(req.type);
|
|
3905
|
+
setQuickAddReceipt(req.receipt);
|
|
3906
|
+
setQuickAddOpen(true);
|
|
3907
|
+
}, [pendingQuickAdd]);
|
|
3908
|
+
const quickActions = React17.useMemo(() => {
|
|
3909
|
+
if (config.quickAdd) return [];
|
|
3100
3910
|
const actions = [];
|
|
3101
3911
|
if (config.modules.payables) {
|
|
3102
3912
|
actions.push({
|
|
@@ -3126,7 +3936,7 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
3126
3936
|
});
|
|
3127
3937
|
}
|
|
3128
3938
|
return actions;
|
|
3129
|
-
}, [config.modules]);
|
|
3939
|
+
}, [config.modules, config.quickAdd]);
|
|
3130
3940
|
function handleOnboardingComplete() {
|
|
3131
3941
|
setOnboardingComplete(true);
|
|
3132
3942
|
try {
|
|
@@ -3170,7 +3980,7 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
3170
3980
|
return /* @__PURE__ */ jsxRuntime.jsx(SummaryView, {});
|
|
3171
3981
|
}
|
|
3172
3982
|
}
|
|
3173
|
-
return /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.
|
|
3983
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3174
3984
|
ui.ModulePage,
|
|
3175
3985
|
{
|
|
3176
3986
|
title: config.labels.pageTitle,
|
|
@@ -3179,21 +3989,50 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
3179
3989
|
showHeader: intent.view === "summary" || view === "summary",
|
|
3180
3990
|
viewKey: view,
|
|
3181
3991
|
direction,
|
|
3182
|
-
headerAction: /* @__PURE__ */ jsxRuntime.
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3992
|
+
headerAction: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
3993
|
+
config.quickAdd && // One-primary-action rule (FAY-1247): quick-add is the single primary
|
|
3994
|
+
// add affordance in B2C mode. The receipt entry point lives inside the
|
|
3995
|
+
// sheet's "Fotografar ou anexar recibo" attachment step, so no separate
|
|
3996
|
+
// "Enviar recibo" button is needed here.
|
|
3997
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
3998
|
+
ui.Button,
|
|
3999
|
+
{
|
|
4000
|
+
size: "sm",
|
|
4001
|
+
className: "hidden md:inline-flex",
|
|
4002
|
+
onClick: () => openQuickAdd2("expense"),
|
|
4003
|
+
children: [
|
|
4004
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }),
|
|
4005
|
+
t2("financial.quickTx.newTransaction")
|
|
4006
|
+
]
|
|
4007
|
+
}
|
|
4008
|
+
),
|
|
4009
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4010
|
+
saas.ModuleActionBar,
|
|
4011
|
+
{
|
|
4012
|
+
quickActions,
|
|
4013
|
+
settingsPath: registries && registries.length > 0 ? "/settings/financial" : void 0,
|
|
4014
|
+
settingsLabel: t2("financial.settingsPage.title")
|
|
4015
|
+
}
|
|
4016
|
+
)
|
|
4017
|
+
] }),
|
|
4018
|
+
children: [
|
|
4019
|
+
renderView(),
|
|
4020
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4021
|
+
QuickTransactionForm,
|
|
4022
|
+
{
|
|
4023
|
+
open: quickAddOpen,
|
|
4024
|
+
onOpenChange: setQuickAddOpen,
|
|
4025
|
+
defaultType: quickAddType,
|
|
4026
|
+
focusReceipt: quickAddReceipt
|
|
4027
|
+
}
|
|
4028
|
+
)
|
|
4029
|
+
]
|
|
3191
4030
|
}
|
|
3192
4031
|
) });
|
|
3193
4032
|
}
|
|
3194
4033
|
function useEnsureSummary() {
|
|
3195
4034
|
const fetchSummary = useFinancialStore((s) => s.fetchSummary);
|
|
3196
|
-
|
|
4035
|
+
React17.useEffect(() => {
|
|
3197
4036
|
void fetchSummary();
|
|
3198
4037
|
}, []);
|
|
3199
4038
|
}
|
|
@@ -3317,7 +4156,7 @@ function RecentTransactions() {
|
|
|
3317
4156
|
const { currency } = useFinancialConfig();
|
|
3318
4157
|
const invoices = useFinancialStore((s) => s.invoices);
|
|
3319
4158
|
const fetchInvoices = useFinancialStore((s) => s.fetchInvoices);
|
|
3320
|
-
|
|
4159
|
+
React17.useEffect(() => {
|
|
3321
4160
|
void fetchInvoices({ pageSize: 5 });
|
|
3322
4161
|
}, []);
|
|
3323
4162
|
const columns = [
|
|
@@ -3334,6 +4173,202 @@ function RecentTransactions() {
|
|
|
3334
4173
|
];
|
|
3335
4174
|
return /* @__PURE__ */ jsxRuntime.jsx(ui.TableWidget, { title: t2("financial.summary.recentTransactions"), icon: "CircleDollarSign", columns, data: invoices.slice(0, 5), emptyMessage: t2("financial.summary.noTransactions") });
|
|
3336
4175
|
}
|
|
4176
|
+
function useDueLabel() {
|
|
4177
|
+
const t2 = core.useTranslation();
|
|
4178
|
+
return (dueDate) => {
|
|
4179
|
+
const today2 = /* @__PURE__ */ new Date();
|
|
4180
|
+
today2.setHours(0, 0, 0, 0);
|
|
4181
|
+
const due = /* @__PURE__ */ new Date(`${dueDate}T00:00:00`);
|
|
4182
|
+
const days = Math.round((due.getTime() - today2.getTime()) / 864e5);
|
|
4183
|
+
if (days < 0) return t2("financial.home.overdueDays", { count: String(Math.abs(days)) });
|
|
4184
|
+
if (days === 0) return t2("financial.home.dueToday");
|
|
4185
|
+
if (days === 1) return t2("financial.home.dueTomorrow");
|
|
4186
|
+
return t2("financial.home.dueInDays", { count: String(days) });
|
|
4187
|
+
};
|
|
4188
|
+
}
|
|
4189
|
+
function BalanceHero() {
|
|
4190
|
+
const t2 = core.useTranslation();
|
|
4191
|
+
const { currency } = useFinancialConfig();
|
|
4192
|
+
const summary = useFinancialStore((s) => s.summary);
|
|
4193
|
+
const navigate = ui.useDashboardNavigate();
|
|
4194
|
+
useEnsureSummary();
|
|
4195
|
+
const [hidden, setHidden] = React17__namespace.default.useState(false);
|
|
4196
|
+
const money = (v) => hidden ? "\u2022\u2022\u2022\u2022\u2022\u2022" : saas.formatCurrency(v ?? 0, currency);
|
|
4197
|
+
const inflow = summary?.monthlyInflow ?? 0;
|
|
4198
|
+
const outflow = summary?.monthlyOutflow ?? 0;
|
|
4199
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.GoldCard, { branded: true, className: "px-5 py-4 text-center md:px-8 md:py-5", children: [
|
|
4200
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs font-medium uppercase tracking-wide opacity-75", children: t2("financial.home.balance") }),
|
|
4201
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-0.5 flex items-center justify-center gap-2.5", children: [
|
|
4202
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-3xl font-bold tracking-tight tabular-nums md:text-4xl", children: money(summary?.totalBalance) }),
|
|
4203
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
4204
|
+
"button",
|
|
4205
|
+
{
|
|
4206
|
+
type: "button",
|
|
4207
|
+
onClick: () => setHidden((h) => !h),
|
|
4208
|
+
"aria-label": hidden ? t2("financial.home.showBalance") : t2("financial.home.hideBalance"),
|
|
4209
|
+
className: "rounded-full p-1.5 text-primary-foreground/80 transition-colors hover:bg-white/10",
|
|
4210
|
+
children: hidden ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.EyeOff, { className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Eye, { className: "h-5 w-5" })
|
|
4211
|
+
}
|
|
4212
|
+
)
|
|
4213
|
+
] }),
|
|
4214
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mx-auto mt-3 grid max-w-lg grid-cols-2 gap-3", children: [
|
|
4215
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4216
|
+
"button",
|
|
4217
|
+
{
|
|
4218
|
+
type: "button",
|
|
4219
|
+
onClick: () => navigate("/financial/receivables-list"),
|
|
4220
|
+
"aria-label": t2("financial.home.inflow"),
|
|
4221
|
+
className: "flex cursor-pointer items-center gap-2 rounded-2xl bg-white/10 px-3 py-2 text-left transition-colors hover:bg-white/20",
|
|
4222
|
+
children: [
|
|
4223
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-emerald-500/90", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUpRight, { className: "h-4 w-4 text-white" }) }),
|
|
4224
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
|
|
4225
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[11px] uppercase tracking-wide opacity-70", children: t2("financial.home.inflow") }),
|
|
4226
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-sm font-semibold tabular-nums", children: money(inflow) })
|
|
4227
|
+
] })
|
|
4228
|
+
]
|
|
4229
|
+
}
|
|
4230
|
+
),
|
|
4231
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
4232
|
+
"button",
|
|
4233
|
+
{
|
|
4234
|
+
type: "button",
|
|
4235
|
+
onClick: () => navigate("/financial/payables-list"),
|
|
4236
|
+
"aria-label": t2("financial.home.outflow"),
|
|
4237
|
+
className: "flex cursor-pointer items-center gap-2 rounded-2xl bg-white/10 px-3 py-2 text-left transition-colors hover:bg-white/20",
|
|
4238
|
+
children: [
|
|
4239
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-rose-500/90", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDownRight, { className: "h-4 w-4 text-white" }) }),
|
|
4240
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
|
|
4241
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-[11px] uppercase tracking-wide opacity-70", children: t2("financial.home.outflow") }),
|
|
4242
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block truncate text-sm font-semibold tabular-nums", children: money(outflow) })
|
|
4243
|
+
] })
|
|
4244
|
+
]
|
|
4245
|
+
}
|
|
4246
|
+
)
|
|
4247
|
+
] })
|
|
4248
|
+
] });
|
|
4249
|
+
}
|
|
4250
|
+
var SPEND_CATEGORIES = [
|
|
4251
|
+
{ key: "subscriptions", keywords: ["netflix", "spotify", "prime video", "disney", "hbo", " max", "youtube", "apple", "assinatura", "subscription"] },
|
|
4252
|
+
{ key: "groceries", keywords: ["supermercado", "mercado", "padaria", "hortifruti", "feira", "a\xE7ougue", "grocery", "market"] },
|
|
4253
|
+
{ key: "transport", keywords: ["uber", "99", "taxi", "posto", "shell", "ipiranga", "combust\xEDvel", "gasolina", "metr\xF4", "\xF4nibus", "transport", "fuel"] },
|
|
4254
|
+
{ key: "dining", keywords: ["ifood", "rappi", "restaurante", "outback", "lanchonete", "pizzaria", "caf\xE9", "cafeteria", "mcdonald", "burger", "food"] },
|
|
4255
|
+
{ key: "home", keywords: ["aluguel", "condom\xEDnio", "luz", "enel", "\xE1gua", "sabesp", "internet", "vivo", "claro", "g\xE1s", "iptu", "rent", "utilit"] },
|
|
4256
|
+
{ key: "health", keywords: ["farm\xE1cia", "farmacia", "drogasil", "drogaria", "academia", "smartfit", "cl\xEDnica", "hospital", "m\xE9dico", "sa\xFAde", "gym", "fitness", "health", "pharmac"] },
|
|
4257
|
+
{ key: "leisure", keywords: ["cinema", "cinemark", "ingresso", "show", "teatro", "viagem", "hotel", "steam", "leisure"] },
|
|
4258
|
+
{ key: "shopping", keywords: ["amazon", "magalu", "shopee", "aliexpress", "loja", "shopping", "zara", "renner", "store"] }
|
|
4259
|
+
];
|
|
4260
|
+
function categorizeSpend(notes) {
|
|
4261
|
+
const s = (notes ?? "").toLowerCase();
|
|
4262
|
+
for (const c of SPEND_CATEGORIES) if (c.keywords.some((k) => s.includes(k))) return c.key;
|
|
4263
|
+
return "other";
|
|
4264
|
+
}
|
|
4265
|
+
function SpendByCategoryChart() {
|
|
4266
|
+
const t2 = core.useTranslation();
|
|
4267
|
+
const provider = useFinancialProvider();
|
|
4268
|
+
const [rows, setRows] = React17__namespace.default.useState([]);
|
|
4269
|
+
React17.useEffect(() => {
|
|
4270
|
+
let alive = true;
|
|
4271
|
+
void (async () => {
|
|
4272
|
+
const res = await provider.getMovements({ status: "paid", direction: "debit" });
|
|
4273
|
+
if (!alive) return;
|
|
4274
|
+
const totals = /* @__PURE__ */ new Map();
|
|
4275
|
+
for (const m of res.data) {
|
|
4276
|
+
if (m.movementKind === "transfer") continue;
|
|
4277
|
+
const key = categorizeSpend(m.notes);
|
|
4278
|
+
totals.set(key, (totals.get(key) ?? 0) + (m.paidAmount || m.amount));
|
|
4279
|
+
}
|
|
4280
|
+
setRows(
|
|
4281
|
+
[...totals.entries()].sort((a, b) => b[1] - a[1]).map(([key, value]) => ({ name: t2(`financial.home.category.${key}`), value: Math.round(value * 100) / 100 }))
|
|
4282
|
+
);
|
|
4283
|
+
})();
|
|
4284
|
+
return () => {
|
|
4285
|
+
alive = false;
|
|
4286
|
+
};
|
|
4287
|
+
}, [provider]);
|
|
4288
|
+
if (rows.length === 0) {
|
|
4289
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.Card, { children: [
|
|
4290
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CardHeader, { children: /* @__PURE__ */ jsxRuntime.jsx(ui.CardTitle, { children: t2("financial.home.spendByCategory") }) }),
|
|
4291
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.CardContent, { className: "flex h-[200px] items-center justify-center text-sm text-muted-foreground", children: t2("financial.home.noSpend") })
|
|
4292
|
+
] });
|
|
4293
|
+
}
|
|
4294
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
4295
|
+
ui.ChartWidget,
|
|
4296
|
+
{
|
|
4297
|
+
type: "pie",
|
|
4298
|
+
title: t2("financial.home.spendByCategory"),
|
|
4299
|
+
icon: "PieChart",
|
|
4300
|
+
categoryKey: "name",
|
|
4301
|
+
data: rows,
|
|
4302
|
+
series: [{ dataKey: "value", label: t2("financial.home.spendByCategory") }]
|
|
4303
|
+
}
|
|
4304
|
+
);
|
|
4305
|
+
}
|
|
4306
|
+
function UpcomingBills() {
|
|
4307
|
+
const t2 = core.useTranslation();
|
|
4308
|
+
const { currency } = useFinancialConfig();
|
|
4309
|
+
const provider = useFinancialProvider();
|
|
4310
|
+
const dueLabel = useDueLabel();
|
|
4311
|
+
const [bills, setBills] = React17__namespace.default.useState([]);
|
|
4312
|
+
React17.useEffect(() => {
|
|
4313
|
+
let alive = true;
|
|
4314
|
+
void (async () => {
|
|
4315
|
+
const pending2 = await provider.getMovements({ status: "pending" });
|
|
4316
|
+
if (!alive) return;
|
|
4317
|
+
setBills(
|
|
4318
|
+
pending2.data.filter((m) => m.direction === "debit" && m.movementKind === "bill").sort((a, b) => a.dueDate.localeCompare(b.dueDate))
|
|
4319
|
+
);
|
|
4320
|
+
})();
|
|
4321
|
+
return () => {
|
|
4322
|
+
alive = false;
|
|
4323
|
+
};
|
|
4324
|
+
}, [provider]);
|
|
4325
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.GoldCard, { className: "p-0 overflow-hidden", children: [
|
|
4326
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center justify-between px-4 pt-4", children: /* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-sm font-semibold text-foreground", children: t2("financial.home.upcomingBills") }) }),
|
|
4327
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3", children: bills.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-6 text-center text-xs text-muted-foreground", children: t2("financial.home.noBills") }) : bills.map((bill, i) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-4 py-3.5" + (i > 0 ? " border-t border-border" : ""), children: [
|
|
4328
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0", children: [
|
|
4329
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "truncate text-sm font-medium text-foreground", children: bill.notes ?? t2("financial.home.bill") }),
|
|
4330
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: dueLabel(bill.dueDate) })
|
|
4331
|
+
] }),
|
|
4332
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "shrink-0 text-sm font-semibold tabular-nums text-foreground", children: saas.formatCurrency(bill.amount - bill.paidAmount, currency) })
|
|
4333
|
+
] }, bill.id)) })
|
|
4334
|
+
] });
|
|
4335
|
+
}
|
|
4336
|
+
function CreditCards() {
|
|
4337
|
+
const t2 = core.useTranslation();
|
|
4338
|
+
const { currency } = useFinancialConfig();
|
|
4339
|
+
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
4340
|
+
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
4341
|
+
React17.useEffect(() => {
|
|
4342
|
+
void fetchBankAccounts();
|
|
4343
|
+
}, []);
|
|
4344
|
+
const cards = bankAccounts.filter((a) => a.accountType === "credit_card");
|
|
4345
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(ui.GoldCard, { className: "bg-transparent border-0 p-0 shadow-none", children: [
|
|
4346
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "mb-3 text-sm font-semibold text-foreground", children: t2("financial.home.cards") }),
|
|
4347
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
4348
|
+
cards.map((card) => {
|
|
4349
|
+
const fatura = Math.abs(Math.min(0, card.currentBalance));
|
|
4350
|
+
const limite = Math.max(0, (card.creditLimit ?? 0) - fatura);
|
|
4351
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-2xl bg-gradient-to-br from-violet-600 to-indigo-700 p-4 text-white shadow-md", children: [
|
|
4352
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
|
|
4353
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: card.bankName ?? card.name }),
|
|
4354
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CreditCard, { className: "h-5 w-5 opacity-80" })
|
|
4355
|
+
] }),
|
|
4356
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "mt-6 text-[11px] uppercase tracking-wide opacity-70", children: t2("financial.home.currentBill") }),
|
|
4357
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xl font-bold tabular-nums", children: saas.formatCurrency(fatura, currency) }),
|
|
4358
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "mt-2 text-[11px] opacity-70", children: [
|
|
4359
|
+
t2("financial.home.availableLimit"),
|
|
4360
|
+
" ",
|
|
4361
|
+
saas.formatCurrency(limite, currency)
|
|
4362
|
+
] })
|
|
4363
|
+
] }, card.id);
|
|
4364
|
+
}),
|
|
4365
|
+
cards.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-center rounded-2xl border border-dashed border-border py-6 text-sm font-medium text-muted-foreground", children: [
|
|
4366
|
+
"+ ",
|
|
4367
|
+
t2("financial.home.addCard")
|
|
4368
|
+
] }) : null
|
|
4369
|
+
] })
|
|
4370
|
+
] });
|
|
4371
|
+
}
|
|
3337
4372
|
function createFinancialDashboardWidgets(ctx2) {
|
|
3338
4373
|
const withCtx = (Inner) => {
|
|
3339
4374
|
const Wrapped = () => /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config: ctx2.config, provider: ctx2.provider, store: ctx2.store, children: /* @__PURE__ */ jsxRuntime.jsx(Inner, {}) });
|
|
@@ -3350,7 +4385,19 @@ function createFinancialDashboardWidgets(ctx2) {
|
|
|
3350
4385
|
ui.defineChartWidget({ id: "financial.chart.cash-flow", title: "financial.summary.cashFlow", domain: "financial", span: 2, defaultOrder: 10, surfaces: ["plugin-home"], component: withCtx(CashFlowChart) }),
|
|
3351
4386
|
ui.defineCustomWidget({ id: "financial.panel.breakdown", title: "financial.summary.cashFlowBreakdown", domain: "financial", span: 1, defaultOrder: 11, surfaces: ["plugin-home"], component: withCtx(CashFlowBreakdown) }),
|
|
3352
4387
|
ui.defineCustomWidget({ id: "financial.panel.overdue", title: "financial.summary.overdueTitle", domain: "financial", span: 1, defaultOrder: 12, surfaces: ["plugin-home"], component: withCtx(OverdueAlerts) }),
|
|
3353
|
-
ui.defineTableWidget({ id: "financial.table.recent", title: "financial.summary.recentTransactions", domain: "financial", span: 4, defaultOrder: 20, surfaces: ["plugin-home"], component: withCtx(RecentTransactions) })
|
|
4388
|
+
ui.defineTableWidget({ id: "financial.table.recent", title: "financial.summary.recentTransactions", domain: "financial", span: 4, defaultOrder: 20, surfaces: ["plugin-home"], component: withCtx(RecentTransactions) }),
|
|
4389
|
+
// FINANCE-HOME surface (B2C phone-first dashboard) — a compact analytical
|
|
4390
|
+
// layout: hero band → charts row (spend-by-category + cash-flow) → bills +
|
|
4391
|
+
// cards. This is a dedicated consumer surface (norman-ai's money screen), NOT
|
|
4392
|
+
// the universal B2B 'home': a full-bleed balance hero and credit-card tiles
|
|
4393
|
+
// are personal-finance chrome that would leak into every app that merely
|
|
4394
|
+
// enables plugin-financial (e.g. the beauty-saas Painel). Apps that want them
|
|
4395
|
+
// opt in explicitly by rendering <DashboardCanvas surface="finance-home">.
|
|
4396
|
+
ui.defineCustomWidget({ id: "financial.hero.balance", title: "financial.home.balance", domain: "financial", span: 4, defaultOrder: 0, surfaces: ["finance-home"], component: withCtx(BalanceHero) }),
|
|
4397
|
+
ui.defineChartWidget({ id: "financial.chart.spend-by-category", title: "financial.home.spendByCategory", domain: "financial", span: 2, defaultOrder: 1, surfaces: ["finance-home"], component: withCtx(SpendByCategoryChart) }),
|
|
4398
|
+
ui.defineChartWidget({ id: "financial.chart.cash-flow-home", title: "financial.summary.cashFlow", domain: "financial", span: 2, defaultOrder: 2, surfaces: ["finance-home"], component: withCtx(CashFlowChart) }),
|
|
4399
|
+
ui.defineCustomWidget({ id: "financial.list.upcoming-bills", title: "financial.home.upcomingBills", domain: "financial", span: 2, defaultOrder: 3, surfaces: ["finance-home"], component: withCtx(UpcomingBills) }),
|
|
4400
|
+
ui.defineCustomWidget({ id: "financial.cards", title: "financial.home.cards", domain: "financial", span: 2, defaultOrder: 4, surfaces: ["finance-home"], component: withCtx(CreditCards) })
|
|
3354
4401
|
];
|
|
3355
4402
|
}
|
|
3356
4403
|
|
|
@@ -3383,9 +4430,10 @@ function paginate(items, page, pageSize) {
|
|
|
3383
4430
|
const start = (p - 1) * ps;
|
|
3384
4431
|
return { data: items.slice(start, start + ps), total: items.length };
|
|
3385
4432
|
}
|
|
3386
|
-
function createStore(
|
|
4433
|
+
function createStore(options) {
|
|
3387
4434
|
const tenantId = "mock-tenant";
|
|
3388
|
-
const
|
|
4435
|
+
const seed = options?.seed;
|
|
4436
|
+
const defaultPMTypes = (options?.paymentMethodTypes ?? [
|
|
3389
4437
|
{ name: "Cash", transactionType: "cash" },
|
|
3390
4438
|
{ name: "Credit Card", transactionType: "credit_card" },
|
|
3391
4439
|
{ name: "Debit Card", transactionType: "debit_card" },
|
|
@@ -3423,19 +4471,23 @@ function createStore(seedPaymentMethodTypes) {
|
|
|
3423
4471
|
updatedAt: now()
|
|
3424
4472
|
};
|
|
3425
4473
|
return {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
4474
|
+
// Seeded data (when provided) makes the module render as if the user already
|
|
4475
|
+
// has accounts + transactions — a populated Summary/Statements instead of the
|
|
4476
|
+
// empty onboarding state. Seed arrays are copied so the caller's data is not mutated.
|
|
4477
|
+
invoices: seed?.invoices ? [...seed.invoices] : [],
|
|
4478
|
+
invoiceItems: seed?.invoiceItems ? [...seed.invoiceItems] : [],
|
|
4479
|
+
movements: seed?.movements ? [...seed.movements] : [],
|
|
4480
|
+
bankAccounts: seed?.bankAccounts ? [...seed.bankAccounts] : [cashAccount, bankAccount],
|
|
3430
4481
|
cashSessions: [],
|
|
3431
|
-
paymentMethods: [],
|
|
4482
|
+
paymentMethods: seed?.paymentMethods ? [...seed.paymentMethods] : [],
|
|
3432
4483
|
paymentMethodTypes: defaultPMTypes,
|
|
3433
4484
|
chartOfAccounts: [],
|
|
4485
|
+
costCenters: [],
|
|
3434
4486
|
cardTransactions: []
|
|
3435
4487
|
};
|
|
3436
4488
|
}
|
|
3437
4489
|
function createMockFinancialProvider(options) {
|
|
3438
|
-
const store = createStore();
|
|
4490
|
+
const store = createStore(options);
|
|
3439
4491
|
const tenantId = "mock-tenant";
|
|
3440
4492
|
function recalcInvoiceStatus(invoice) {
|
|
3441
4493
|
if (invoice.paidAmount >= invoice.totalAmount) {
|
|
@@ -3456,6 +4508,14 @@ function createMockFinancialProvider(options) {
|
|
|
3456
4508
|
if (query.direction) results = results.filter((i) => i.direction === query.direction);
|
|
3457
4509
|
if (query.status) results = results.filter((i) => matchesStatus(i.status, query.status));
|
|
3458
4510
|
if (query.contactId) results = results.filter((i) => i.contactId === query.contactId);
|
|
4511
|
+
if (query.accountId) {
|
|
4512
|
+
const invoiceIds = new Set(store.invoiceItems.filter((item) => item.accountId === query.accountId).map((item) => item.invoiceId));
|
|
4513
|
+
results = results.filter((invoice) => invoiceIds.has(invoice.id));
|
|
4514
|
+
}
|
|
4515
|
+
if (query.costCenterId) {
|
|
4516
|
+
const invoiceIds = new Set(store.invoiceItems.filter((item) => item.costCenterId === query.costCenterId).map((item) => item.invoiceId));
|
|
4517
|
+
results = results.filter((invoice) => invoiceIds.has(invoice.id));
|
|
4518
|
+
}
|
|
3459
4519
|
if (query.dateRange) results = results.filter((i) => matchesDateRange(i.invoiceDate, query.dateRange));
|
|
3460
4520
|
if (query.search) {
|
|
3461
4521
|
const s = query.search.toLowerCase();
|
|
@@ -3778,6 +4838,9 @@ function createMockFinancialProvider(options) {
|
|
|
3778
4838
|
async getChartOfAccounts() {
|
|
3779
4839
|
return store.chartOfAccounts.filter((n) => n.isActive);
|
|
3780
4840
|
},
|
|
4841
|
+
async getCostCenters() {
|
|
4842
|
+
return store.costCenters.filter((c) => c.isActive);
|
|
4843
|
+
},
|
|
3781
4844
|
// --- Card Transactions ---
|
|
3782
4845
|
async getCardTransactions(dateRange) {
|
|
3783
4846
|
let results = [...store.cardTransactions];
|
|
@@ -3865,7 +4928,9 @@ function createMockFinancialProvider(options) {
|
|
|
3865
4928
|
(m) => m.direction === "debit" && m.movementKind === "bill" && ["pending", "partial"].includes(m.status)
|
|
3866
4929
|
);
|
|
3867
4930
|
const todayStr = today();
|
|
3868
|
-
const
|
|
4931
|
+
const monthAgo = /* @__PURE__ */ new Date();
|
|
4932
|
+
monthAgo.setDate(monthAgo.getDate() - 30);
|
|
4933
|
+
const monthStart = monthAgo.toISOString().slice(0, 10);
|
|
3869
4934
|
const monthEnd = todayStr;
|
|
3870
4935
|
const monthMovements = store.movements.filter(
|
|
3871
4936
|
(m) => m.movementKind === "payment" && matchesDateRange(m.paymentDate, dateRange ?? { from: monthStart, to: monthEnd })
|
|
@@ -4049,6 +5114,18 @@ async function fetchInvoiceBalances(pub, ids) {
|
|
|
4049
5114
|
function round22(n) {
|
|
4050
5115
|
return Math.round(n * 100) / 100;
|
|
4051
5116
|
}
|
|
5117
|
+
async function findOrderIdsByItemMetadata(core, filters) {
|
|
5118
|
+
if (!filters.accountId && !filters.costCenterId) return void 0;
|
|
5119
|
+
let qb = core.from("order_items").select("order_id");
|
|
5120
|
+
if (filters.accountId) qb = qb.filter("metadata->>accountId", "eq", filters.accountId);
|
|
5121
|
+
if (filters.costCenterId) qb = qb.filter("metadata->>costCenterId", "eq", filters.costCenterId);
|
|
5122
|
+
const { data } = await qb;
|
|
5123
|
+
const ids = [];
|
|
5124
|
+
for (const row of data ?? []) {
|
|
5125
|
+
if (typeof row.order_id === "string" && row.order_id.length > 0) ids.push(row.order_id);
|
|
5126
|
+
}
|
|
5127
|
+
return Array.from(new Set(ids));
|
|
5128
|
+
}
|
|
4052
5129
|
async function getMethodFeePct(pub, paymentMethodId) {
|
|
4053
5130
|
if (!paymentMethodId) return 0;
|
|
4054
5131
|
const { data } = await pub.from("payment_methods").select("discount_value").eq("id", paymentMethodId).single();
|
|
@@ -4076,6 +5153,14 @@ function createSupabaseFinancialProvider() {
|
|
|
4076
5153
|
const statuses = Array.isArray(query.status) ? query.status : [query.status];
|
|
4077
5154
|
qb = qb.in("status", statuses);
|
|
4078
5155
|
}
|
|
5156
|
+
const itemScopedOrderIds = await findOrderIdsByItemMetadata(core, {
|
|
5157
|
+
accountId: query.accountId,
|
|
5158
|
+
costCenterId: query.costCenterId
|
|
5159
|
+
});
|
|
5160
|
+
if (itemScopedOrderIds) {
|
|
5161
|
+
if (itemScopedOrderIds.length === 0) return { data: [], total: 0 };
|
|
5162
|
+
qb = qb.in("id", itemScopedOrderIds);
|
|
5163
|
+
}
|
|
4079
5164
|
if (query.contactId) qb = qb.eq("party_id", query.contactId);
|
|
4080
5165
|
if (query.search) qb = qb.ilike("notes", `%${query.search}%`);
|
|
4081
5166
|
if (query.dateRange) qb = qb.gte("created_at", query.dateRange.from).lte("created_at", query.dateRange.to);
|
|
@@ -4151,6 +5236,8 @@ function createSupabaseFinancialProvider() {
|
|
|
4151
5236
|
discount: i.discount ?? 0,
|
|
4152
5237
|
surcharge: 0,
|
|
4153
5238
|
referenceId: i.productId ?? i.serviceId,
|
|
5239
|
+
accountId: i.metadata?.accountId,
|
|
5240
|
+
costCenterId: i.metadata?.costCenterId,
|
|
4154
5241
|
createdAt: i.createdAt
|
|
4155
5242
|
};
|
|
4156
5243
|
});
|
|
@@ -4190,7 +5277,12 @@ function createSupabaseFinancialProvider() {
|
|
|
4190
5277
|
discount: item.discount ?? 0,
|
|
4191
5278
|
total: item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0),
|
|
4192
5279
|
sort_order: i,
|
|
4193
|
-
metadata: {
|
|
5280
|
+
metadata: {
|
|
5281
|
+
itemKind: item.itemKind,
|
|
5282
|
+
referenceId: item.referenceId,
|
|
5283
|
+
accountId: item.accountId,
|
|
5284
|
+
costCenterId: item.costCenterId
|
|
5285
|
+
}
|
|
4194
5286
|
}))
|
|
4195
5287
|
);
|
|
4196
5288
|
}
|
|
@@ -4450,6 +5542,11 @@ function createSupabaseFinancialProvider() {
|
|
|
4450
5542
|
const { data } = await pub.from("chart_of_accounts").select("*").eq("is_active", true).order("code");
|
|
4451
5543
|
return (data ?? []).map((r) => snakeToCamel(r));
|
|
4452
5544
|
},
|
|
5545
|
+
async getCostCenters() {
|
|
5546
|
+
const { pub } = getClients();
|
|
5547
|
+
const { data } = await pub.from("cost_centers").select("*").eq("is_active", true).order("code");
|
|
5548
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
5549
|
+
},
|
|
4453
5550
|
// --- Card Transactions ---
|
|
4454
5551
|
async getCardTransactions(dateRange) {
|
|
4455
5552
|
getClients();
|
|
@@ -4566,7 +5663,9 @@ function createSupabaseFinancialProvider() {
|
|
|
4566
5663
|
const unpaidDebit = unpaid.filter((m) => m.direction === "debit");
|
|
4567
5664
|
const overdueCredit = unpaidCredit.filter((m) => m.status === "overdue" || m.due_date < today2);
|
|
4568
5665
|
const overdueDebit = unpaidDebit.filter((m) => m.status === "overdue" || m.due_date < today2);
|
|
4569
|
-
const
|
|
5666
|
+
const monthAgo = /* @__PURE__ */ new Date();
|
|
5667
|
+
monthAgo.setDate(monthAgo.getDate() - 30);
|
|
5668
|
+
const monthStart = monthAgo.toISOString().slice(0, 10);
|
|
4570
5669
|
const monthPaid = allMovs.filter((m) => m.movement_kind === "payment" && m.payment_date >= monthStart);
|
|
4571
5670
|
return {
|
|
4572
5671
|
totalBalance,
|
|
@@ -4757,6 +5856,82 @@ function createFinancialStore(provider) {
|
|
|
4757
5856
|
throw err;
|
|
4758
5857
|
}
|
|
4759
5858
|
},
|
|
5859
|
+
async createQuickTransaction(input) {
|
|
5860
|
+
try {
|
|
5861
|
+
if (input.type === "transfer") {
|
|
5862
|
+
if (!input.bankAccountId || !input.toAccountId) {
|
|
5863
|
+
throw new Error("Select source and destination accounts");
|
|
5864
|
+
}
|
|
5865
|
+
await provider.createTransfer({
|
|
5866
|
+
fromAccountId: input.bankAccountId,
|
|
5867
|
+
toAccountId: input.toAccountId,
|
|
5868
|
+
amount: input.amount,
|
|
5869
|
+
date: input.date,
|
|
5870
|
+
notes: input.description?.trim() || void 0
|
|
5871
|
+
});
|
|
5872
|
+
} else {
|
|
5873
|
+
const direction = input.type === "income" ? "credit" : "debit";
|
|
5874
|
+
const desc = input.description?.trim() || (input.type === "income" ? "Receita" : "Despesa");
|
|
5875
|
+
const invoiceMeta = {};
|
|
5876
|
+
if (input.recurring) invoiceMeta.recurring = true;
|
|
5877
|
+
if (input.receiptUrl) invoiceMeta.receiptUrl = input.receiptUrl;
|
|
5878
|
+
const invoice = await provider.createInvoice({
|
|
5879
|
+
direction,
|
|
5880
|
+
invoiceDate: input.date,
|
|
5881
|
+
contactName: desc,
|
|
5882
|
+
metadata: Object.keys(invoiceMeta).length > 0 ? invoiceMeta : void 0,
|
|
5883
|
+
items: [{
|
|
5884
|
+
itemKind: "other",
|
|
5885
|
+
description: desc,
|
|
5886
|
+
quantity: 1,
|
|
5887
|
+
unitPrice: input.amount,
|
|
5888
|
+
accountId: input.categoryId
|
|
5889
|
+
}],
|
|
5890
|
+
installments: [{
|
|
5891
|
+
direction,
|
|
5892
|
+
movementKind: "bill",
|
|
5893
|
+
amount: input.amount,
|
|
5894
|
+
dueDate: input.date,
|
|
5895
|
+
installmentNumber: 1,
|
|
5896
|
+
bankAccountId: input.bankAccountId,
|
|
5897
|
+
notes: desc
|
|
5898
|
+
}]
|
|
5899
|
+
});
|
|
5900
|
+
if (input.paid) {
|
|
5901
|
+
const movements = await provider.getMovements({ direction, status: "pending" });
|
|
5902
|
+
const bill = movements.data.find((m) => m.invoiceId === invoice.id && m.movementKind === "bill");
|
|
5903
|
+
if (bill) {
|
|
5904
|
+
await provider.payMovement({
|
|
5905
|
+
movementId: bill.id,
|
|
5906
|
+
amount: input.amount,
|
|
5907
|
+
paymentDate: input.date,
|
|
5908
|
+
bankAccountId: input.bankAccountId,
|
|
5909
|
+
notes: desc
|
|
5910
|
+
});
|
|
5911
|
+
}
|
|
5912
|
+
}
|
|
5913
|
+
}
|
|
5914
|
+
const invoiceQuery = get().invoiceQuery;
|
|
5915
|
+
const statementQuery = get().statementQuery;
|
|
5916
|
+
const [summary, bankAccounts, invoicesRes, statement] = await Promise.all([
|
|
5917
|
+
provider.getSummary(),
|
|
5918
|
+
provider.getBankAccounts(),
|
|
5919
|
+
provider.getInvoices(invoiceQuery),
|
|
5920
|
+
statementQuery ? provider.getStatement(statementQuery) : Promise.resolve(get().statement)
|
|
5921
|
+
]);
|
|
5922
|
+
set({
|
|
5923
|
+
summary,
|
|
5924
|
+
bankAccounts,
|
|
5925
|
+
invoices: invoicesRes.data,
|
|
5926
|
+
invoicesTotal: invoicesRes.total,
|
|
5927
|
+
statement
|
|
5928
|
+
});
|
|
5929
|
+
ui.toast.success("Transaction saved");
|
|
5930
|
+
} catch (err) {
|
|
5931
|
+
ui.toast.error("Failed to save transaction", { description: err?.message });
|
|
5932
|
+
throw err;
|
|
5933
|
+
}
|
|
5934
|
+
},
|
|
4760
5935
|
async payMovement(input) {
|
|
4761
5936
|
try {
|
|
4762
5937
|
await provider.payMovement(input);
|
|
@@ -5083,8 +6258,18 @@ var financialRegistries = [
|
|
|
5083
6258
|
|
|
5084
6259
|
// src/locales/en.ts
|
|
5085
6260
|
var en = {
|
|
6261
|
+
"financial.cards.addExpense": "Add expense",
|
|
6262
|
+
"financial.cards.allBillsValue": "All bills value",
|
|
6263
|
+
"financial.cards.availableLimit": "Available limit",
|
|
6264
|
+
"financial.cards.currentBill": "Current bill",
|
|
6265
|
+
"financial.cards.dueOn": "Due on the {{day}}",
|
|
5086
6266
|
"financial.cards.installment": "Installment {{current}}/{{total}}",
|
|
6267
|
+
"financial.cards.noCards": "No cards yet",
|
|
6268
|
+
"financial.cards.noCardsHint": "Add a credit card in settings to track your bills here",
|
|
5087
6269
|
"financial.cards.noTransactions": "No card transactions",
|
|
6270
|
+
"financial.cards.tabClosed": "Closed",
|
|
6271
|
+
"financial.cards.tabOpen": "Open",
|
|
6272
|
+
"financial.cards.usedOfLimit": "{{percent}}% of limit used",
|
|
5088
6273
|
"financial.cards.pending": "Pending",
|
|
5089
6274
|
"financial.cards.received": "Received",
|
|
5090
6275
|
"financial.cards.subtitle": "Credit and debit card transactions",
|
|
@@ -5211,6 +6396,10 @@ var en = {
|
|
|
5211
6396
|
"financial.nav.rules": "Rules",
|
|
5212
6397
|
"financial.nav.statements": "Statements",
|
|
5213
6398
|
"financial.nav.summary": "Summary",
|
|
6399
|
+
"financial.invoice.filterAccount": "Chart account",
|
|
6400
|
+
"financial.invoice.filterAllAccounts": "All accounts",
|
|
6401
|
+
"financial.invoice.filterCostCenter": "Cost center",
|
|
6402
|
+
"financial.invoice.filterAllCostCenters": "All cost centers",
|
|
5214
6403
|
"financial.reconciliation.title": "Reconciliation",
|
|
5215
6404
|
"financial.reconciliation.subtitle": "Match imported bank lines to your movements",
|
|
5216
6405
|
"financial.reconciliation.pending": "{{count}} to reconcile",
|
|
@@ -5325,6 +6514,36 @@ var en = {
|
|
|
5325
6514
|
"financial.summary.totalBalance": "Total Balance",
|
|
5326
6515
|
"financial.summary.transactionsAppear": "Transactions will appear here once recorded",
|
|
5327
6516
|
"financial.summary.viewAll": "View all",
|
|
6517
|
+
"financial.summary.today": "Today",
|
|
6518
|
+
"financial.summary.yesterday": "Yesterday",
|
|
6519
|
+
"financial.summary.transactionsTitle": "Transactions",
|
|
6520
|
+
"financial.home.balance": "Balance",
|
|
6521
|
+
"financial.home.inflow": "Income",
|
|
6522
|
+
"financial.home.outflow": "Expenses",
|
|
6523
|
+
"financial.home.showBalance": "Show balance",
|
|
6524
|
+
"financial.home.hideBalance": "Hide balance",
|
|
6525
|
+
"financial.home.upcomingBills": "Upcoming bills",
|
|
6526
|
+
"financial.home.noBills": "No upcoming bills.",
|
|
6527
|
+
"financial.home.dueToday": "Due today",
|
|
6528
|
+
"financial.home.dueTomorrow": "Due tomorrow",
|
|
6529
|
+
"financial.home.dueInDays": "Due in {{count}} days",
|
|
6530
|
+
"financial.home.overdueDays": "Overdue by {{count}} days",
|
|
6531
|
+
"financial.home.cards": "Cards",
|
|
6532
|
+
"financial.home.currentBill": "Current bill",
|
|
6533
|
+
"financial.home.availableLimit": "Available limit",
|
|
6534
|
+
"financial.home.addCard": "New card",
|
|
6535
|
+
"financial.home.bill": "Bill",
|
|
6536
|
+
"financial.home.spendByCategory": "Spending by category",
|
|
6537
|
+
"financial.home.noSpend": "No spending this month.",
|
|
6538
|
+
"financial.home.category.groceries": "Groceries",
|
|
6539
|
+
"financial.home.category.transport": "Transport",
|
|
6540
|
+
"financial.home.category.subscriptions": "Subscriptions",
|
|
6541
|
+
"financial.home.category.dining": "Dining",
|
|
6542
|
+
"financial.home.category.home": "Home",
|
|
6543
|
+
"financial.home.category.health": "Health",
|
|
6544
|
+
"financial.home.category.leisure": "Leisure",
|
|
6545
|
+
"financial.home.category.shopping": "Shopping",
|
|
6546
|
+
"financial.home.category.other": "Other",
|
|
5328
6547
|
"financial.quickActions.newPayable": "New Payable",
|
|
5329
6548
|
"financial.quickActions.newPayableDesc": "Record an expense or bill to pay",
|
|
5330
6549
|
"financial.quickActions.newReceivable": "New Receivable",
|
|
@@ -5486,13 +6705,54 @@ var en = {
|
|
|
5486
6705
|
"financial.transfer.created": "Transfer recorded",
|
|
5487
6706
|
"financial.transfer.failed": "Failed to record transfer",
|
|
5488
6707
|
"financial.transfer.sameAccount": "Source and destination must differ",
|
|
5489
|
-
"financial.transfer.invalidAmount": "Amount must be greater than zero"
|
|
6708
|
+
"financial.transfer.invalidAmount": "Amount must be greater than zero",
|
|
6709
|
+
"financial.quickTx.title": "New transaction",
|
|
6710
|
+
"financial.quickTx.close": "Close",
|
|
6711
|
+
"financial.quickTx.newTransaction": "New transaction",
|
|
6712
|
+
"financial.quickTx.expense": "Expense",
|
|
6713
|
+
"financial.quickTx.income": "Income",
|
|
6714
|
+
"financial.quickTx.transfer": "Transfer",
|
|
6715
|
+
"financial.quickTx.paid": "Paid",
|
|
6716
|
+
"financial.quickTx.received": "Received",
|
|
6717
|
+
"financial.quickTx.yes": "Yes",
|
|
6718
|
+
"financial.quickTx.no": "No",
|
|
6719
|
+
"financial.quickTx.category": "Category",
|
|
6720
|
+
"financial.quickTx.selectCategory": "No category",
|
|
6721
|
+
"financial.quickTx.account": "Account",
|
|
6722
|
+
"financial.quickTx.selectAccount": "Select account",
|
|
6723
|
+
"financial.quickTx.fromAccount": "From account",
|
|
6724
|
+
"financial.quickTx.toAccount": "To account",
|
|
6725
|
+
"financial.quickTx.card": "Card",
|
|
6726
|
+
"financial.quickTx.recurring": "Recurring",
|
|
6727
|
+
"financial.quickTx.attachment": "Attachment",
|
|
6728
|
+
"financial.quickTx.attachmentSoon": "Attachments coming soon",
|
|
6729
|
+
"financial.quickTx.receipt": "Receipt",
|
|
6730
|
+
"financial.quickTx.receiptCapture": "Snap or attach a receipt",
|
|
6731
|
+
"financial.quickTx.receiptAttached": "Receipt attached",
|
|
6732
|
+
"financial.quickTx.receiptReplace": "Replace",
|
|
6733
|
+
"financial.quickTx.receiptRemove": "Remove receipt",
|
|
6734
|
+
"financial.quickTx.receiptInvalid": "Please choose an image file",
|
|
6735
|
+
"financial.quickTx.sendReceipt": "Send receipt",
|
|
6736
|
+
"financial.quickTx.descriptionPlaceholder": "e.g. Groceries, Salary\u2026",
|
|
6737
|
+
"financial.quickTx.enterAmount": "Enter an amount",
|
|
6738
|
+
"financial.quickTx.save": "Save",
|
|
6739
|
+
"financial.quickTx.saving": "Saving\u2026"
|
|
5490
6740
|
};
|
|
5491
6741
|
|
|
5492
6742
|
// src/locales/pt-BR.ts
|
|
5493
6743
|
var ptBR = {
|
|
6744
|
+
"financial.cards.addExpense": "Adicionar despesa",
|
|
6745
|
+
"financial.cards.allBillsValue": "Total das faturas",
|
|
6746
|
+
"financial.cards.availableLimit": "Limite dispon\xEDvel",
|
|
6747
|
+
"financial.cards.currentBill": "Fatura atual",
|
|
6748
|
+
"financial.cards.dueOn": "Vence dia {{day}}",
|
|
5494
6749
|
"financial.cards.installment": "Parcela {{current}}/{{total}}",
|
|
6750
|
+
"financial.cards.noCards": "Nenhum cart\xE3o cadastrado",
|
|
6751
|
+
"financial.cards.noCardsHint": "Adicione um cart\xE3o de cr\xE9dito nas configura\xE7\xF5es para acompanhar suas faturas aqui",
|
|
5495
6752
|
"financial.cards.noTransactions": "Nenhuma transa\xE7\xE3o com cart\xE3o",
|
|
6753
|
+
"financial.cards.tabClosed": "Fechados",
|
|
6754
|
+
"financial.cards.tabOpen": "Abertos",
|
|
6755
|
+
"financial.cards.usedOfLimit": "{{percent}}% do limite usado",
|
|
5496
6756
|
"financial.cards.pending": "Pendente",
|
|
5497
6757
|
"financial.cards.received": "Recebido",
|
|
5498
6758
|
"financial.cards.subtitle": "Transa\xE7\xF5es de cart\xE3o de cr\xE9dito e d\xE9bito",
|
|
@@ -5619,6 +6879,10 @@ var ptBR = {
|
|
|
5619
6879
|
"financial.nav.rules": "Regras",
|
|
5620
6880
|
"financial.nav.statements": "Extratos",
|
|
5621
6881
|
"financial.nav.summary": "Resumo",
|
|
6882
|
+
"financial.invoice.filterAccount": "Plano de contas",
|
|
6883
|
+
"financial.invoice.filterAllAccounts": "Todos os planos",
|
|
6884
|
+
"financial.invoice.filterCostCenter": "Centro de custo",
|
|
6885
|
+
"financial.invoice.filterAllCostCenters": "Todos os centros",
|
|
5622
6886
|
"financial.reconciliation.title": "Concilia\xE7\xE3o",
|
|
5623
6887
|
"financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lan\xE7amentos",
|
|
5624
6888
|
"financial.reconciliation.pending": "{{count}} para conciliar",
|
|
@@ -5733,6 +6997,36 @@ var ptBR = {
|
|
|
5733
6997
|
"financial.summary.totalBalance": "Saldo Total",
|
|
5734
6998
|
"financial.summary.transactionsAppear": "As transa\xE7\xF5es aparecer\xE3o aqui ap\xF3s serem registradas",
|
|
5735
6999
|
"financial.summary.viewAll": "Ver todos",
|
|
7000
|
+
"financial.summary.today": "Hoje",
|
|
7001
|
+
"financial.summary.yesterday": "Ontem",
|
|
7002
|
+
"financial.summary.transactionsTitle": "Transa\xE7\xF5es",
|
|
7003
|
+
"financial.home.balance": "Saldo",
|
|
7004
|
+
"financial.home.inflow": "Entradas",
|
|
7005
|
+
"financial.home.outflow": "Sa\xEDdas",
|
|
7006
|
+
"financial.home.showBalance": "Mostrar saldo",
|
|
7007
|
+
"financial.home.hideBalance": "Ocultar saldo",
|
|
7008
|
+
"financial.home.upcomingBills": "Pr\xF3ximas contas",
|
|
7009
|
+
"financial.home.noBills": "Nenhuma conta a vencer.",
|
|
7010
|
+
"financial.home.dueToday": "Vence hoje",
|
|
7011
|
+
"financial.home.dueTomorrow": "Vence amanh\xE3",
|
|
7012
|
+
"financial.home.dueInDays": "Vence em {{count}} dias",
|
|
7013
|
+
"financial.home.overdueDays": "Venceu h\xE1 {{count}} dias",
|
|
7014
|
+
"financial.home.cards": "Cart\xF5es",
|
|
7015
|
+
"financial.home.currentBill": "Fatura atual",
|
|
7016
|
+
"financial.home.availableLimit": "Limite dispon\xEDvel",
|
|
7017
|
+
"financial.home.addCard": "Novo cart\xE3o",
|
|
7018
|
+
"financial.home.bill": "Conta",
|
|
7019
|
+
"financial.home.spendByCategory": "Gastos por categoria",
|
|
7020
|
+
"financial.home.noSpend": "Sem gastos neste m\xEAs.",
|
|
7021
|
+
"financial.home.category.groceries": "Mercado",
|
|
7022
|
+
"financial.home.category.transport": "Transporte",
|
|
7023
|
+
"financial.home.category.subscriptions": "Assinaturas",
|
|
7024
|
+
"financial.home.category.dining": "Alimenta\xE7\xE3o",
|
|
7025
|
+
"financial.home.category.home": "Casa",
|
|
7026
|
+
"financial.home.category.health": "Sa\xFAde",
|
|
7027
|
+
"financial.home.category.leisure": "Lazer",
|
|
7028
|
+
"financial.home.category.shopping": "Compras",
|
|
7029
|
+
"financial.home.category.other": "Outros",
|
|
5736
7030
|
"financial.quickActions.newPayable": "Nova Conta a Pagar",
|
|
5737
7031
|
"financial.quickActions.newPayableDesc": "Registrar uma despesa ou conta a pagar",
|
|
5738
7032
|
"financial.quickActions.newReceivable": "Nova Conta a Receber",
|
|
@@ -5894,7 +7188,38 @@ var ptBR = {
|
|
|
5894
7188
|
"financial.transfer.created": "Transfer\xEAncia registrada",
|
|
5895
7189
|
"financial.transfer.failed": "Falha ao registrar transfer\xEAncia",
|
|
5896
7190
|
"financial.transfer.sameAccount": "Origem e destino devem ser diferentes",
|
|
5897
|
-
"financial.transfer.invalidAmount": "O valor deve ser maior que zero"
|
|
7191
|
+
"financial.transfer.invalidAmount": "O valor deve ser maior que zero",
|
|
7192
|
+
"financial.quickTx.title": "Nova transa\xE7\xE3o",
|
|
7193
|
+
"financial.quickTx.close": "Fechar",
|
|
7194
|
+
"financial.quickTx.newTransaction": "Nova transa\xE7\xE3o",
|
|
7195
|
+
"financial.quickTx.expense": "Despesa",
|
|
7196
|
+
"financial.quickTx.income": "Receita",
|
|
7197
|
+
"financial.quickTx.transfer": "Transfer\xEAncia",
|
|
7198
|
+
"financial.quickTx.paid": "Pago",
|
|
7199
|
+
"financial.quickTx.received": "Recebido",
|
|
7200
|
+
"financial.quickTx.yes": "Sim",
|
|
7201
|
+
"financial.quickTx.no": "N\xE3o",
|
|
7202
|
+
"financial.quickTx.category": "Categoria",
|
|
7203
|
+
"financial.quickTx.selectCategory": "Sem categoria",
|
|
7204
|
+
"financial.quickTx.account": "Conta",
|
|
7205
|
+
"financial.quickTx.selectAccount": "Selecione a conta",
|
|
7206
|
+
"financial.quickTx.fromAccount": "Conta de origem",
|
|
7207
|
+
"financial.quickTx.toAccount": "Conta de destino",
|
|
7208
|
+
"financial.quickTx.card": "Cart\xE3o",
|
|
7209
|
+
"financial.quickTx.recurring": "Recorrente",
|
|
7210
|
+
"financial.quickTx.attachment": "Anexo",
|
|
7211
|
+
"financial.quickTx.attachmentSoon": "Anexos em breve",
|
|
7212
|
+
"financial.quickTx.receipt": "Recibo",
|
|
7213
|
+
"financial.quickTx.receiptCapture": "Fotografar ou anexar recibo",
|
|
7214
|
+
"financial.quickTx.receiptAttached": "Recibo anexado",
|
|
7215
|
+
"financial.quickTx.receiptReplace": "Substituir",
|
|
7216
|
+
"financial.quickTx.receiptRemove": "Remover recibo",
|
|
7217
|
+
"financial.quickTx.receiptInvalid": "Escolha um arquivo de imagem",
|
|
7218
|
+
"financial.quickTx.sendReceipt": "Enviar recibo",
|
|
7219
|
+
"financial.quickTx.descriptionPlaceholder": "ex.: Mercado, Sal\xE1rio\u2026",
|
|
7220
|
+
"financial.quickTx.enterAmount": "Informe um valor",
|
|
7221
|
+
"financial.quickTx.save": "Salvar",
|
|
7222
|
+
"financial.quickTx.saving": "Salvando\u2026"
|
|
5898
7223
|
};
|
|
5899
7224
|
|
|
5900
7225
|
// src/locales/index.ts
|
|
@@ -5912,9 +7237,9 @@ var STATUS_STYLE = {
|
|
|
5912
7237
|
function PersonStatementWidget({ person, provider, config }) {
|
|
5913
7238
|
const t2 = core.useTranslation();
|
|
5914
7239
|
const currency = config.currency;
|
|
5915
|
-
const [invoices, setInvoices] =
|
|
5916
|
-
const [loading, setLoading] =
|
|
5917
|
-
|
|
7240
|
+
const [invoices, setInvoices] = React17.useState([]);
|
|
7241
|
+
const [loading, setLoading] = React17.useState(true);
|
|
7242
|
+
React17.useEffect(() => {
|
|
5918
7243
|
let alive = true;
|
|
5919
7244
|
if (!person?.id) {
|
|
5920
7245
|
setLoading(false);
|
|
@@ -5934,7 +7259,7 @@ function PersonStatementWidget({ person, provider, config }) {
|
|
|
5934
7259
|
};
|
|
5935
7260
|
}, [person?.id, provider]);
|
|
5936
7261
|
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
5937
|
-
const totals =
|
|
7262
|
+
const totals = React17.useMemo(() => {
|
|
5938
7263
|
const active = invoices.filter((i) => i.status !== "cancelled");
|
|
5939
7264
|
const billed = active.reduce((s, i) => s + (i.totalAmount ?? 0), 0);
|
|
5940
7265
|
const paid = active.reduce((s, i) => s + (i.paidAmount ?? 0), 0);
|
|
@@ -5944,7 +7269,7 @@ function PersonStatementWidget({ person, provider, config }) {
|
|
|
5944
7269
|
const base = inv.direction === "credit" ? "receivables" : "payables";
|
|
5945
7270
|
window.location.hash = `/financial/${base}/detail/${inv.id}`;
|
|
5946
7271
|
};
|
|
5947
|
-
const columns =
|
|
7272
|
+
const columns = React17.useMemo(() => [
|
|
5948
7273
|
{
|
|
5949
7274
|
id: "date",
|
|
5950
7275
|
header: t2("financial.invoice.columnDate"),
|
|
@@ -6044,6 +7369,8 @@ function resolveConfig(options) {
|
|
|
6044
7369
|
// Opt-in: only shown when an app explicitly enables reconciliation (bank connector present).
|
|
6045
7370
|
reconciliation: options?.modules?.reconciliation === true
|
|
6046
7371
|
},
|
|
7372
|
+
// Opt-in: only render the plugin's own quick-add buttons/FAB when the host asks.
|
|
7373
|
+
quickAdd: options?.quickAdd === true,
|
|
6047
7374
|
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
6048
7375
|
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
6049
7376
|
itemTypes: options?.itemTypes ?? DEFAULT_ITEM_TYPES,
|
|
@@ -6064,9 +7391,10 @@ function createFinancialPlugin(options) {
|
|
|
6064
7391
|
const provider = options?.dataProvider ?? createSafeFinancialProvider();
|
|
6065
7392
|
const store = createFinancialStore(provider);
|
|
6066
7393
|
const dashboardWidgets = createFinancialDashboardWidgets({ config, provider, store });
|
|
6067
|
-
const
|
|
7394
|
+
const registries = [...financialRegistries, ...options?.settingsRegistries ?? []];
|
|
7395
|
+
const PersonStatementBound = (props) => React17__namespace.default.createElement(PersonStatementWidget, { person: props.item, provider, config });
|
|
6068
7396
|
PersonStatementBound.displayName = "FinancialPersonStatement";
|
|
6069
|
-
const PageComponent = () =>
|
|
7397
|
+
const PageComponent = () => React17__namespace.default.createElement(FinancialPage, { config, provider, store, registries });
|
|
6070
7398
|
return {
|
|
6071
7399
|
id: "financial",
|
|
6072
7400
|
name: config.labels.pageTitle,
|
|
@@ -6171,18 +7499,18 @@ function createFinancialPlugin(options) {
|
|
|
6171
7499
|
]
|
|
6172
7500
|
}
|
|
6173
7501
|
],
|
|
6174
|
-
registries
|
|
7502
|
+
registries,
|
|
6175
7503
|
settings: [
|
|
6176
7504
|
{
|
|
6177
7505
|
id: "financial",
|
|
6178
7506
|
label: "Financial",
|
|
6179
7507
|
icon: "DollarSign",
|
|
6180
7508
|
component: (() => {
|
|
6181
|
-
const FinancialSettingsTab = () =>
|
|
7509
|
+
const FinancialSettingsTab = () => React17__namespace.default.createElement(saas.PluginSettingsPanel, {
|
|
6182
7510
|
title: "Financial Settings",
|
|
6183
7511
|
subtitle: "Payment methods, accounts, and configurations",
|
|
6184
|
-
generalSettings:
|
|
6185
|
-
registries
|
|
7512
|
+
generalSettings: React17__namespace.default.createElement(FinancialGeneralSettings),
|
|
7513
|
+
registries,
|
|
6186
7514
|
hostPluginId: "financial",
|
|
6187
7515
|
routeBase: "/settings/financial"
|
|
6188
7516
|
});
|
|
@@ -6198,6 +7526,8 @@ function createFinancialPlugin(options) {
|
|
|
6198
7526
|
}
|
|
6199
7527
|
|
|
6200
7528
|
exports.createFinancialPlugin = createFinancialPlugin;
|
|
7529
|
+
exports.createMockFinancialProvider = createMockFinancialProvider;
|
|
6201
7530
|
exports.createSafeFinancialProvider = createSafeFinancialProvider;
|
|
7531
|
+
exports.openQuickAdd = openQuickAdd;
|
|
6202
7532
|
//# sourceMappingURL=index.cjs.map
|
|
6203
7533
|
//# sourceMappingURL=index.cjs.map
|