@fayz-ai/plugin-financial 0.1.1 → 0.1.4
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/LICENSE +21 -0
- package/README.md +50 -0
- 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/components/PaymentModal.d.ts.map +1 -1
- package/dist/components/PersonStatementWidget.d.ts +18 -0
- package/dist/components/PersonStatementWidget.d.ts.map +1 -0
- package/dist/components/TransferModal.d.ts +7 -0
- package/dist/components/TransferModal.d.ts.map +1 -0
- package/dist/contract.test.d.ts +2 -0
- package/dist/contract.test.d.ts.map +1 -0
- package/dist/data/capability.test.d.ts +2 -0
- package/dist/data/capability.test.d.ts.map +1 -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 +13 -2
- package/dist/data/types.d.ts.map +1 -1
- package/dist/index.cjs +1529 -263
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1437 -171
- 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/registries.d.ts.map +1 -1
- package/dist/store.d.ts +4 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/types.d.ts +111 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/views/InvoiceDetailView.d.ts +3 -1
- package/dist/views/InvoiceDetailView.d.ts.map +1 -1
- package/dist/views/InvoiceFormView.d.ts.map +1 -1
- package/dist/views/PayablesView.d.ts +5 -1
- package/dist/views/PayablesView.d.ts.map +1 -1
- package/dist/views/ReceivablesView.d.ts +5 -1
- package/dist/views/ReceivablesView.d.ts.map +1 -1
- package/dist/views/ReconciliationView.d.ts +3 -0
- package/dist/views/ReconciliationView.d.ts.map +1 -0
- package/dist/views/StatementsView.d.ts +3 -1
- package/dist/views/StatementsView.d.ts.map +1 -1
- package/dist/views/navReferrer.d.ts +5 -0
- package/dist/views/navReferrer.d.ts.map +1 -0
- package/package.json +8 -5
- package/src/FinancialContext.tsx +2 -0
- package/src/FinancialPage.tsx +32 -17
- package/src/components/PaymentModal.tsx +58 -11
- package/src/components/PersonStatementWidget.tsx +133 -0
- package/src/components/TransferModal.tsx +150 -0
- package/src/contract.test.ts +58 -0
- package/src/data/capability.test.ts +90 -0
- package/src/data/mock.ts +265 -52
- package/src/data/supabase.ts +359 -30
- package/src/data/types.ts +22 -3
- package/src/index.ts +28 -2
- package/src/locales/en.ts +76 -0
- package/src/locales/pt-BR.ts +76 -0
- package/src/migrations/006_extract_fee_amount.sql +20 -0
- package/src/migrations/006_rls_policies.sql +37 -0
- package/src/migrations/007_reconciliation.sql +32 -0
- package/src/migrations/008_split_payment_movements.sql +29 -0
- package/src/registries.ts +3 -1
- package/src/store.ts +27 -7
- package/src/types.ts +126 -1
- package/src/views/InvoiceDetailView.tsx +70 -51
- package/src/views/InvoiceFormView.tsx +55 -20
- package/src/views/PayablesView.tsx +13 -2
- package/src/views/ReceivablesView.tsx +13 -2
- package/src/views/ReconciliationView.tsx +211 -0
- package/src/views/StatementsView.tsx +241 -29
- package/src/views/navReferrer.ts +24 -0
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React14 = 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');
|
|
@@ -11,7 +11,7 @@ var vanilla = require('zustand/vanilla');
|
|
|
11
11
|
|
|
12
12
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
13
|
|
|
14
|
-
var
|
|
14
|
+
var React14__default = /*#__PURE__*/_interopDefault(React14);
|
|
15
15
|
|
|
16
16
|
// src/index.ts
|
|
17
17
|
var ctx = saas.createPluginContext("FinancialPage");
|
|
@@ -41,7 +41,7 @@ function StatusBadge({ status }) {
|
|
|
41
41
|
}
|
|
42
42
|
function useInvoiceColumns(currency) {
|
|
43
43
|
const t2 = core.useTranslation();
|
|
44
|
-
return
|
|
44
|
+
return React14.useMemo(() => [
|
|
45
45
|
{
|
|
46
46
|
accessorKey: "fiscalNumber",
|
|
47
47
|
header: "#",
|
|
@@ -97,9 +97,9 @@ function InvoiceListView({ direction, onNew, onEdit }) {
|
|
|
97
97
|
useFinancialStore((s) => s.invoicesTotal);
|
|
98
98
|
const invoicesLoading = useFinancialStore((s) => s.invoicesLoading);
|
|
99
99
|
const fetchInvoices = useFinancialStore((s) => s.fetchInvoices);
|
|
100
|
-
const [search, setSearch] =
|
|
101
|
-
const [statusFilter, setStatusFilter] =
|
|
102
|
-
|
|
100
|
+
const [search, setSearch] = React14.useState("");
|
|
101
|
+
const [statusFilter, setStatusFilter] = React14.useState([]);
|
|
102
|
+
React14.useEffect(() => {
|
|
103
103
|
fetchInvoices({
|
|
104
104
|
direction,
|
|
105
105
|
status: statusFilter.length > 0 ? statusFilter : void 0,
|
|
@@ -109,11 +109,11 @@ function InvoiceListView({ direction, onNew, onEdit }) {
|
|
|
109
109
|
const filtered = invoices.filter((inv) => inv.direction === direction);
|
|
110
110
|
const listTitle = direction === "debit" ? t2("financial.invoice.accountsPayable") : t2("financial.invoice.accountsReceivable");
|
|
111
111
|
const columns = useInvoiceColumns(currency);
|
|
112
|
-
const tags =
|
|
112
|
+
const tags = React14.useMemo(
|
|
113
113
|
() => STATUS_OPTIONS.filter((o) => o.value !== "cancelled").map((o) => ({ value: o.value, label: t2(o.labelKey) })),
|
|
114
114
|
[t2]
|
|
115
115
|
);
|
|
116
|
-
const summary =
|
|
116
|
+
const summary = React14.useMemo(() => {
|
|
117
117
|
let totalAmount = 0;
|
|
118
118
|
let paidAmount = 0;
|
|
119
119
|
let openAmount = 0;
|
|
@@ -177,10 +177,10 @@ function InvoiceListView({ direction, onNew, onEdit }) {
|
|
|
177
177
|
}
|
|
178
178
|
function LocationSelector({ locations, value, onChange }) {
|
|
179
179
|
const t2 = core.useTranslation();
|
|
180
|
-
const [open, setOpen] =
|
|
181
|
-
const [search, setSearch] =
|
|
182
|
-
const ref =
|
|
183
|
-
|
|
180
|
+
const [open, setOpen] = React14.useState(false);
|
|
181
|
+
const [search, setSearch] = React14.useState("");
|
|
182
|
+
const ref = React14.useRef(null);
|
|
183
|
+
React14.useEffect(() => {
|
|
184
184
|
function handler(e) {
|
|
185
185
|
if (ref.current && !ref.current.contains(e.target)) setOpen(false);
|
|
186
186
|
}
|
|
@@ -255,12 +255,18 @@ var formId = 1;
|
|
|
255
255
|
function fid() {
|
|
256
256
|
return `f${formId++}`;
|
|
257
257
|
}
|
|
258
|
-
function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdate, onRemove, entityLookups }) {
|
|
258
|
+
function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdate, onRemove, entityLookups, showTypeSelector }) {
|
|
259
259
|
const t2 = core.useTranslation();
|
|
260
260
|
const total = item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0);
|
|
261
261
|
const kindLabel = itemTypes.find((tp) => tp.value === item.itemKind)?.label ?? item.itemKind;
|
|
262
262
|
const hasAdjustments = (item.discount ?? 0) > 0 || (item.surcharge ?? 0) > 0;
|
|
263
|
-
const [showAdjustments, setShowAdjustments] =
|
|
263
|
+
const [showAdjustments, setShowAdjustments] = React14.useState(hasAdjustments);
|
|
264
|
+
function handleFieldKeyDown(e) {
|
|
265
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
266
|
+
e.preventDefault();
|
|
267
|
+
onToggle();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
264
270
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "border-b last:border-0", children: [
|
|
265
271
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
266
272
|
"div",
|
|
@@ -297,7 +303,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
297
303
|
}
|
|
298
304
|
),
|
|
299
305
|
expanded && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-4 pb-4 pt-1 ml-8 space-y-3 bg-muted/10", children: [
|
|
300
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1.5", children: itemTypes.map((tp) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
306
|
+
showTypeSelector && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-1.5", children: itemTypes.map((tp) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
301
307
|
"button",
|
|
302
308
|
{
|
|
303
309
|
onClick: () => onUpdate({ itemKind: tp.value }),
|
|
@@ -346,6 +352,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
346
352
|
type: "text",
|
|
347
353
|
value: item.description,
|
|
348
354
|
onChange: (e) => onUpdate({ description: e.target.value }),
|
|
355
|
+
onKeyDown: handleFieldKeyDown,
|
|
349
356
|
placeholder: t2("financial.invoiceForm.itemDescription"),
|
|
350
357
|
autoFocus: true,
|
|
351
358
|
className: "w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2.5 py-1.5 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
@@ -361,11 +368,12 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
361
368
|
min: 1,
|
|
362
369
|
value: item.quantity,
|
|
363
370
|
onChange: (e) => onUpdate({ quantity: Number(e.target.value) || 1 }),
|
|
371
|
+
onKeyDown: handleFieldKeyDown,
|
|
364
372
|
className: "w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-2 py-1.5 text-sm text-center focus:outline-none focus:ring-2 focus:ring-ring"
|
|
365
373
|
}
|
|
366
374
|
)
|
|
367
375
|
] }),
|
|
368
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-4", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
376
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "col-span-4", onKeyDown: handleFieldKeyDown, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
369
377
|
ui.CurrencyInput,
|
|
370
378
|
{
|
|
371
379
|
label: t2("financial.invoiceForm.unitPrice"),
|
|
@@ -401,6 +409,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
401
409
|
step: 0.01,
|
|
402
410
|
value: item.discount ?? 0,
|
|
403
411
|
onChange: (e) => onUpdate({ discount: Number(e.target.value) || 0 }),
|
|
412
|
+
onKeyDown: handleFieldKeyDown,
|
|
404
413
|
className: "flex-1 bg-transparent px-2 py-1 text-xs outline-none"
|
|
405
414
|
}
|
|
406
415
|
)
|
|
@@ -418,6 +427,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
418
427
|
step: 0.01,
|
|
419
428
|
value: item.surcharge ?? 0,
|
|
420
429
|
onChange: (e) => onUpdate({ surcharge: Number(e.target.value) || 0 }),
|
|
430
|
+
onKeyDown: handleFieldKeyDown,
|
|
421
431
|
className: "flex-1 bg-transparent px-2 py-1 text-xs outline-none"
|
|
422
432
|
}
|
|
423
433
|
)
|
|
@@ -481,28 +491,30 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
481
491
|
const config = useFinancialConfig();
|
|
482
492
|
const { currency, itemTypes, locations } = config;
|
|
483
493
|
const provider = useFinancialProvider();
|
|
494
|
+
const hasItemLookups = itemTypes.some((tp) => config.entityLookups[tp.value]);
|
|
495
|
+
const defaultItemKind = itemTypes.find((tp) => tp.value === "other")?.value ?? itemTypes.find((tp) => !config.entityLookups[tp.value])?.value ?? itemTypes[0]?.value ?? "other";
|
|
484
496
|
const createInvoice = useFinancialStore((s) => s.createInvoice);
|
|
485
497
|
const hasLocations = locations.length > 0;
|
|
486
498
|
const isEdit = !!editId;
|
|
487
|
-
const [loaded, setLoaded] =
|
|
488
|
-
const [loading, setLoading] =
|
|
489
|
-
const [contactName, setContactName] =
|
|
490
|
-
const [contactId, setContactId] =
|
|
491
|
-
const [unitId, setUnitId] =
|
|
492
|
-
const [invoiceDate, setInvoiceDate] =
|
|
493
|
-
const [fiscalNumber, setFiscalNumber] =
|
|
494
|
-
const [observations, setObservations] =
|
|
495
|
-
const [items, setItems] =
|
|
496
|
-
const [expandedItemId, setExpandedItemId] =
|
|
497
|
-
const [addingItem, setAddingItem] =
|
|
498
|
-
const [installments, setInstallments] =
|
|
499
|
-
const [saving, setSaving] =
|
|
500
|
-
const [menuOpen, setMenuOpen] =
|
|
501
|
-
const [confirmCancel, setConfirmCancel] =
|
|
502
|
-
const [cancelling, setCancelling] =
|
|
503
|
-
const menuRef =
|
|
499
|
+
const [loaded, setLoaded] = React14.useState(!editId);
|
|
500
|
+
const [loading, setLoading] = React14.useState(!!editId);
|
|
501
|
+
const [contactName, setContactName] = React14.useState("");
|
|
502
|
+
const [contactId, setContactId] = React14.useState();
|
|
503
|
+
const [unitId, setUnitId] = React14.useState(locations[0]?.id ?? "");
|
|
504
|
+
const [invoiceDate, setInvoiceDate] = React14.useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
505
|
+
const [fiscalNumber, setFiscalNumber] = React14.useState("");
|
|
506
|
+
const [observations, setObservations] = React14.useState("");
|
|
507
|
+
const [items, setItems] = React14.useState([]);
|
|
508
|
+
const [expandedItemId, setExpandedItemId] = React14.useState(null);
|
|
509
|
+
const [addingItem, setAddingItem] = React14.useState(false);
|
|
510
|
+
const [installments, setInstallments] = React14.useState([]);
|
|
511
|
+
const [saving, setSaving] = React14.useState(false);
|
|
512
|
+
const [menuOpen, setMenuOpen] = React14.useState(false);
|
|
513
|
+
const [confirmCancel, setConfirmCancel] = React14.useState(false);
|
|
514
|
+
const [cancelling, setCancelling] = React14.useState(false);
|
|
515
|
+
const menuRef = React14.useRef(null);
|
|
504
516
|
const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
|
|
505
|
-
|
|
517
|
+
React14.useEffect(() => {
|
|
506
518
|
if (!menuOpen) return;
|
|
507
519
|
function handleClick(e) {
|
|
508
520
|
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
@@ -521,7 +533,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
521
533
|
setCancelling(false);
|
|
522
534
|
}
|
|
523
535
|
}
|
|
524
|
-
|
|
536
|
+
React14.useEffect(() => {
|
|
525
537
|
if (!editId || loaded) return;
|
|
526
538
|
setLoading(true);
|
|
527
539
|
(async () => {
|
|
@@ -558,8 +570,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
558
570
|
const totalAmount = items.reduce((sum, it) => {
|
|
559
571
|
return sum + it.quantity * it.unitPrice - (it.discount ?? 0) + (it.surcharge ?? 0);
|
|
560
572
|
}, 0);
|
|
561
|
-
const prevItemCount =
|
|
562
|
-
|
|
573
|
+
const prevItemCount = React14.useRef(items.length);
|
|
574
|
+
React14.useEffect(() => {
|
|
563
575
|
if (items.length > 0 && prevItemCount.current === 0 && installments.length === 0 && !isEdit) {
|
|
564
576
|
const total = items.reduce((sum, it) => sum + it.quantity * it.unitPrice - (it.discount ?? 0) + (it.surcharge ?? 0), 0);
|
|
565
577
|
if (total > 0) {
|
|
@@ -568,7 +580,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
568
580
|
}
|
|
569
581
|
prevItemCount.current = items.length;
|
|
570
582
|
}, [items.length]);
|
|
571
|
-
|
|
583
|
+
React14.useEffect(() => {
|
|
572
584
|
if (installments.length > 0 && totalAmount > 0 && !isEdit) {
|
|
573
585
|
const count = installments.length;
|
|
574
586
|
const perInstallment = Math.floor(totalAmount * 100 / count) / 100;
|
|
@@ -591,6 +603,10 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
591
603
|
setExpandedItemId(newItem._id);
|
|
592
604
|
setAddingItem(false);
|
|
593
605
|
}
|
|
606
|
+
function startAddItem() {
|
|
607
|
+
if (hasItemLookups) setAddingItem(true);
|
|
608
|
+
else addItemOfKind(defaultItemKind);
|
|
609
|
+
}
|
|
594
610
|
function updateItem(id, data) {
|
|
595
611
|
setItems(items.map((it) => it._id === id ? { ...it, ...data } : it));
|
|
596
612
|
}
|
|
@@ -676,8 +692,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
676
692
|
items: items.map(({ _id, ...rest }) => rest),
|
|
677
693
|
installments: installments.map(({ _id, ...rest }) => rest)
|
|
678
694
|
};
|
|
679
|
-
const snapshot =
|
|
680
|
-
|
|
695
|
+
const snapshot = React14.useRef(null);
|
|
696
|
+
React14.useEffect(() => {
|
|
681
697
|
if (loaded && snapshot.current === null) snapshot.current = JSON.stringify(fields);
|
|
682
698
|
}, [loaded]);
|
|
683
699
|
const dirty = snapshot.current !== null && JSON.stringify(fields) !== snapshot.current;
|
|
@@ -810,7 +826,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
810
826
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
811
827
|
"button",
|
|
812
828
|
{
|
|
813
|
-
onClick:
|
|
829
|
+
onClick: startAddItem,
|
|
814
830
|
className: "inline-flex items-center gap-1.5 rounded-lg bg-primary border border-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 shadow-button-primary active:shadow-button-inset transition-colors",
|
|
815
831
|
children: [
|
|
816
832
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-3 w-3" }),
|
|
@@ -832,7 +848,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
832
848
|
items.length === 0 && !addingItem ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-10 text-center", children: [
|
|
833
849
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.LayoutList, { className: "h-8 w-8 text-muted-foreground/30 mx-auto mb-2" }),
|
|
834
850
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.invoiceForm.noItems") }),
|
|
835
|
-
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick:
|
|
851
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: startAddItem, className: "text-xs text-primary hover:underline mt-1", children: t2("financial.invoiceForm.addFirstItem") })
|
|
836
852
|
] }) : items.map((item, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
837
853
|
ItemRow,
|
|
838
854
|
{
|
|
@@ -847,7 +863,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
847
863
|
removeItem(item._id);
|
|
848
864
|
if (expandedItemId === item._id) setExpandedItemId(null);
|
|
849
865
|
},
|
|
850
|
-
entityLookups: config.entityLookups
|
|
866
|
+
entityLookups: config.entityLookups,
|
|
867
|
+
showTypeSelector: hasItemLookups
|
|
851
868
|
},
|
|
852
869
|
item._id
|
|
853
870
|
))
|
|
@@ -943,6 +960,15 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
943
960
|
` })
|
|
944
961
|
] });
|
|
945
962
|
}
|
|
963
|
+
var ACCOUNT_TYPES_FOR_TX = {
|
|
964
|
+
cash: ["cash_register"],
|
|
965
|
+
credit_card: ["credit_card", "bank_account"],
|
|
966
|
+
debit_card: ["bank_account", "cash_register"],
|
|
967
|
+
pix: ["bank_account", "digital_wallet"],
|
|
968
|
+
bank_transfer: ["bank_account"],
|
|
969
|
+
check: ["bank_account"],
|
|
970
|
+
voucher: ["bank_account", "digital_wallet"]
|
|
971
|
+
};
|
|
946
972
|
var TYPE_ICONS = {
|
|
947
973
|
cash: lucideReact.Banknote,
|
|
948
974
|
pix: lucideReact.QrCode,
|
|
@@ -977,34 +1003,44 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
977
1003
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
978
1004
|
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
979
1005
|
const remaining = movement.amount - movement.paidAmount;
|
|
980
|
-
const [amount, setAmount] =
|
|
981
|
-
const [paymentDate, setPaymentDate] =
|
|
982
|
-
const [selectedTypeId, setSelectedTypeId] =
|
|
983
|
-
const [paymentMethodId, setPaymentMethodId] =
|
|
984
|
-
const [bankAccountId, setBankAccountId] =
|
|
985
|
-
const [cardBrand, setCardBrand] =
|
|
986
|
-
const [cardInstallments, setCardInstallments] =
|
|
987
|
-
const [notes, setNotes] =
|
|
988
|
-
const [saving, setSaving] =
|
|
989
|
-
const [visible, setVisible] =
|
|
990
|
-
const [expandMethodPicker, setExpandMethodPicker] =
|
|
991
|
-
|
|
1006
|
+
const [amount, setAmount] = React14.useState(remaining);
|
|
1007
|
+
const [paymentDate, setPaymentDate] = React14.useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
1008
|
+
const [selectedTypeId, setSelectedTypeId] = React14.useState("");
|
|
1009
|
+
const [paymentMethodId, setPaymentMethodId] = React14.useState("");
|
|
1010
|
+
const [bankAccountId, setBankAccountId] = React14.useState("");
|
|
1011
|
+
const [cardBrand, setCardBrand] = React14.useState("");
|
|
1012
|
+
const [cardInstallments, setCardInstallments] = React14.useState(1);
|
|
1013
|
+
const [notes, setNotes] = React14.useState("");
|
|
1014
|
+
const [saving, setSaving] = React14.useState(false);
|
|
1015
|
+
const [visible, setVisible] = React14.useState(false);
|
|
1016
|
+
const [expandMethodPicker, setExpandMethodPicker] = React14.useState(true);
|
|
1017
|
+
React14.useEffect(() => {
|
|
992
1018
|
fetchBankAccounts();
|
|
993
1019
|
fetchPaymentMethods();
|
|
994
1020
|
requestAnimationFrame(() => setVisible(true));
|
|
995
1021
|
}, []);
|
|
996
|
-
const selectedType =
|
|
1022
|
+
const selectedType = React14.useMemo(
|
|
997
1023
|
() => paymentMethodTypes.find((tp) => tp.id === selectedTypeId),
|
|
998
1024
|
[paymentMethodTypes, selectedTypeId]
|
|
999
1025
|
);
|
|
1000
1026
|
const txType = selectedType?.transactionType ?? "";
|
|
1001
|
-
const filteredMethods =
|
|
1027
|
+
const filteredMethods = React14.useMemo(
|
|
1002
1028
|
() => paymentMethods.filter((m) => m.paymentMethodTypeId === selectedTypeId),
|
|
1003
1029
|
[paymentMethods, selectedTypeId]
|
|
1004
1030
|
);
|
|
1005
|
-
const needsBankAccount = txType === "bank_transfer" || txType === "pix" || txType === "check";
|
|
1006
1031
|
const needsCard = txType === "credit_card" || txType === "debit_card";
|
|
1007
1032
|
const needsCardInstallments = txType === "credit_card";
|
|
1033
|
+
const eligibleAccounts = React14.useMemo(() => {
|
|
1034
|
+
const allowed = selectedType?.allowedAccountTypes && selectedType.allowedAccountTypes.length ? selectedType.allowedAccountTypes : ACCOUNT_TYPES_FOR_TX[txType];
|
|
1035
|
+
if (!allowed || !allowed.length) return bankAccounts;
|
|
1036
|
+
const filtered = bankAccounts.filter((a) => allowed.includes(a.accountType));
|
|
1037
|
+
return filtered.length ? filtered : bankAccounts;
|
|
1038
|
+
}, [bankAccounts, selectedType, txType]);
|
|
1039
|
+
React14.useEffect(() => {
|
|
1040
|
+
if (selectedTypeId && !bankAccountId && eligibleAccounts.length) {
|
|
1041
|
+
setBankAccountId(eligibleAccounts[0].id);
|
|
1042
|
+
}
|
|
1043
|
+
}, [selectedTypeId, eligibleAccounts]);
|
|
1008
1044
|
function handleSelectType(typeId) {
|
|
1009
1045
|
setSelectedTypeId(typeId);
|
|
1010
1046
|
setExpandMethodPicker(false);
|
|
@@ -1017,8 +1053,14 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1017
1053
|
setVisible(false);
|
|
1018
1054
|
setTimeout(onClose, 200);
|
|
1019
1055
|
}
|
|
1056
|
+
function handleKeyDown(e) {
|
|
1057
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
1058
|
+
if (e.target.tagName === "TEXTAREA") return;
|
|
1059
|
+
e.preventDefault();
|
|
1060
|
+
if (amount > 0 && selectedTypeId && bankAccountId && !saving) void handlePay();
|
|
1061
|
+
}
|
|
1020
1062
|
async function handlePay() {
|
|
1021
|
-
if (amount <= 0 || !selectedTypeId) return;
|
|
1063
|
+
if (amount <= 0 || !selectedTypeId || !bankAccountId) return;
|
|
1022
1064
|
setSaving(true);
|
|
1023
1065
|
try {
|
|
1024
1066
|
await payMovement({
|
|
@@ -1029,7 +1071,8 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1029
1071
|
paymentMethodTypeId: selectedTypeId || void 0,
|
|
1030
1072
|
bankAccountId: bankAccountId || void 0,
|
|
1031
1073
|
cardBrand: needsCard ? cardBrand || void 0 : void 0,
|
|
1032
|
-
cardInstallments: needsCardInstallments ? cardInstallments : void 0
|
|
1074
|
+
cardInstallments: needsCardInstallments ? cardInstallments : void 0,
|
|
1075
|
+
notes: notes.trim() || void 0
|
|
1033
1076
|
});
|
|
1034
1077
|
setVisible(false);
|
|
1035
1078
|
setTimeout(onPaid, 200);
|
|
@@ -1055,6 +1098,7 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1055
1098
|
transform: visible ? "scale(1) translateY(0)" : "scale(0.95) translateY(8px)"
|
|
1056
1099
|
},
|
|
1057
1100
|
onClick: (e) => e.stopPropagation(),
|
|
1101
|
+
onKeyDown: handleKeyDown,
|
|
1058
1102
|
children: [
|
|
1059
1103
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10", children: [
|
|
1060
1104
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
@@ -1164,11 +1208,17 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1164
1208
|
] }, n)) })
|
|
1165
1209
|
] })
|
|
1166
1210
|
] }),
|
|
1167
|
-
|
|
1211
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stagger-field", style: { animationDelay: "100ms" }, children: [
|
|
1168
1212
|
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.bankAccount") }),
|
|
1169
|
-
/* @__PURE__ */ jsxRuntime.jsxs("
|
|
1213
|
+
eligibleAccounts.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-1 rounded-lg border border-dashed border-input bg-muted/30 px-3 py-2.5 text-center", children: [
|
|
1214
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.payment.noAccounts") }),
|
|
1215
|
+
/* @__PURE__ */ jsxRuntime.jsxs("a", { href: "#/settings/financial/bank-accounts", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
|
|
1216
|
+
t2("financial.payment.configureAccounts"),
|
|
1217
|
+
" \u2192"
|
|
1218
|
+
] })
|
|
1219
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("select", { value: bankAccountId, onChange: (e) => setBankAccountId(e.target.value), className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm", children: [
|
|
1170
1220
|
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t2("financial.payment.selectAccount") }),
|
|
1171
|
-
|
|
1221
|
+
eligibleAccounts.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a.id, children: a.name }, a.id))
|
|
1172
1222
|
] })
|
|
1173
1223
|
] }),
|
|
1174
1224
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "stagger-field", style: { animationDelay: "150ms" }, children: [
|
|
@@ -1194,7 +1244,7 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1194
1244
|
variant: "default",
|
|
1195
1245
|
size: "sm",
|
|
1196
1246
|
onClick: handlePay,
|
|
1197
|
-
disabled: amount <= 0 || !selectedTypeId || saving,
|
|
1247
|
+
disabled: amount <= 0 || !selectedTypeId || !bankAccountId || saving,
|
|
1198
1248
|
children: [
|
|
1199
1249
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5" }),
|
|
1200
1250
|
saving ? t2("financial.payment.processing") : t2("financial.payment.payAmount", { amount: saas.formatCurrency(amount, currency) })
|
|
@@ -1219,28 +1269,30 @@ var STATUS_COLORS = {
|
|
|
1219
1269
|
draft: { bg: "bg-muted text-muted-foreground", icon: lucideReact.CircleDashed, labelKey: "financial.invoice.statusDraft" },
|
|
1220
1270
|
cancelled: { bg: "bg-muted text-muted-foreground", icon: lucideReact.Ban, labelKey: "financial.invoice.statusCancelled" }
|
|
1221
1271
|
};
|
|
1222
|
-
function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
1272
|
+
function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel }) {
|
|
1223
1273
|
const t2 = core.useTranslation();
|
|
1224
1274
|
const config = useFinancialConfig();
|
|
1225
1275
|
const { currency } = config;
|
|
1276
|
+
const parentLabel = backLabel ?? (direction === "debit" ? t2("financial.nav.payables") : t2("financial.nav.receivables"));
|
|
1226
1277
|
const provider = useFinancialProvider();
|
|
1227
1278
|
const paymentMethods = useFinancialStore((s) => s.paymentMethods);
|
|
1228
1279
|
const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes);
|
|
1229
1280
|
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
1230
1281
|
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
1231
1282
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
1232
|
-
const [detailsLoaded, setDetailsLoaded] =
|
|
1233
|
-
const [detailsLoading, setDetailsLoading] =
|
|
1234
|
-
const [invoice, setInvoice] =
|
|
1235
|
-
const [items, setItems] =
|
|
1236
|
-
const [movements, setMovements] =
|
|
1237
|
-
const [
|
|
1238
|
-
const [
|
|
1239
|
-
const [
|
|
1240
|
-
const [
|
|
1241
|
-
const [
|
|
1242
|
-
const [
|
|
1243
|
-
const
|
|
1283
|
+
const [detailsLoaded, setDetailsLoaded] = React14.useState(false);
|
|
1284
|
+
const [detailsLoading, setDetailsLoading] = React14.useState(false);
|
|
1285
|
+
const [invoice, setInvoice] = React14.useState(null);
|
|
1286
|
+
const [items, setItems] = React14.useState([]);
|
|
1287
|
+
const [movements, setMovements] = React14.useState([]);
|
|
1288
|
+
const [payments, setPayments] = React14.useState([]);
|
|
1289
|
+
const [loading, setLoading] = React14.useState(true);
|
|
1290
|
+
const [payingMovement, setPayingMovement] = React14.useState(null);
|
|
1291
|
+
const [expandedMovId, setExpandedMovId] = React14.useState(null);
|
|
1292
|
+
const [menuOpen, setMenuOpen] = React14.useState(false);
|
|
1293
|
+
const [confirmCancel, setConfirmCancel] = React14.useState(false);
|
|
1294
|
+
const [cancelling, setCancelling] = React14.useState(false);
|
|
1295
|
+
const menuRef = React14.useRef(null);
|
|
1244
1296
|
const cancelInvoice = useFinancialStore((s) => s.cancelInvoice);
|
|
1245
1297
|
async function loadData() {
|
|
1246
1298
|
setLoading(true);
|
|
@@ -1254,12 +1306,15 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1254
1306
|
setMovements(
|
|
1255
1307
|
movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "bill").sort((a, b) => (a.installmentNumber ?? 0) - (b.installmentNumber ?? 0))
|
|
1256
1308
|
);
|
|
1309
|
+
setPayments(
|
|
1310
|
+
movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "payment").sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? ""))
|
|
1311
|
+
);
|
|
1257
1312
|
setLoading(false);
|
|
1258
1313
|
}
|
|
1259
|
-
|
|
1314
|
+
React14.useEffect(() => {
|
|
1260
1315
|
loadData();
|
|
1261
1316
|
}, [invoiceId]);
|
|
1262
|
-
|
|
1317
|
+
React14.useEffect(() => {
|
|
1263
1318
|
if (!menuOpen) return;
|
|
1264
1319
|
function handleClick(e) {
|
|
1265
1320
|
if (menuRef.current && !menuRef.current.contains(e.target)) setMenuOpen(false);
|
|
@@ -1293,7 +1348,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1293
1348
|
}
|
|
1294
1349
|
if (loading) {
|
|
1295
1350
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-5", children: [
|
|
1296
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: "", onBack, parentLabel
|
|
1351
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: "", onBack, parentLabel }),
|
|
1297
1352
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-xl border bg-card shadow-sm overflow-hidden", children: [
|
|
1298
1353
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-1 bg-muted animate-pulse" }),
|
|
1299
1354
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "p-5 pb-4", children: [
|
|
@@ -1347,7 +1402,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1347
1402
|
}
|
|
1348
1403
|
if (!invoice) {
|
|
1349
1404
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-6", children: [
|
|
1350
|
-
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: t2("financial.invoice.notFound"), onBack, parentLabel
|
|
1405
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.SubpageHeader, { title: t2("financial.invoice.notFound"), onBack, parentLabel }),
|
|
1351
1406
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.invoice.notFoundDesc") })
|
|
1352
1407
|
] });
|
|
1353
1408
|
}
|
|
@@ -1394,7 +1449,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1394
1449
|
{
|
|
1395
1450
|
title: invoice.fiscalNumber ? `#${invoice.fiscalNumber}` : invoice.contactName ?? invoice.invoiceDate,
|
|
1396
1451
|
onBack,
|
|
1397
|
-
parentLabel
|
|
1452
|
+
parentLabel,
|
|
1398
1453
|
actions: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1399
1454
|
invoice.status !== "cancelled" && invoice.status !== "paid" && /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: onEdit, children: [
|
|
1400
1455
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Pencil, { className: "h-3.5 w-3.5" }),
|
|
@@ -1536,9 +1591,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1536
1591
|
const isExpanded = expandedMovId === mov.id;
|
|
1537
1592
|
const movStatus = STATUS_COLORS[mov.status] ?? STATUS_COLORS.pending;
|
|
1538
1593
|
const MovIcon = movStatus.icon;
|
|
1539
|
-
const
|
|
1540
|
-
const methodType = paymentMethodTypes.find((t3) => t3.id === (method?.paymentMethodTypeId ?? mov.paymentMethodTypeId));
|
|
1541
|
-
const account = bankAccounts.find((a) => a.id === mov.bankAccountId);
|
|
1594
|
+
const instPayments = payments.filter((p) => (p.installmentNumber ?? null) === (mov.installmentNumber ?? null));
|
|
1542
1595
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1543
1596
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1544
1597
|
"div",
|
|
@@ -1612,43 +1665,53 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1612
1665
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-20 rounded bg-muted/40 animate-pulse" }),
|
|
1613
1666
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-16 rounded bg-muted/30 animate-pulse" }),
|
|
1614
1667
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-3 w-24 rounded bg-muted/40 animate-pulse" })
|
|
1615
|
-
] }) : /* @__PURE__ */ jsxRuntime.
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1668
|
+
] }) : instPayments.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "text-[11px] text-muted-foreground", children: t2("financial.invoice.noDetails") }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-1", children: instPayments.map((p) => {
|
|
1669
|
+
const pMethod = paymentMethods.find((m) => m.id === p.paymentMethodId);
|
|
1670
|
+
const pType = paymentMethodTypes.find((tp) => tp.id === (pMethod?.paymentMethodTypeId ?? p.paymentMethodTypeId));
|
|
1671
|
+
const pAccount = bankAccounts.find((a) => a.id === p.bankAccountId);
|
|
1672
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-wrap items-center gap-x-1 gap-y-0.5 text-[11px] text-muted-foreground", children: [
|
|
1673
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-success", children: saas.formatCurrency(p.paidAmount, currency) }),
|
|
1674
|
+
pType && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1675
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1676
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: pType.name }),
|
|
1677
|
+
pMethod && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1678
|
+
"(",
|
|
1679
|
+
pMethod.name,
|
|
1680
|
+
")"
|
|
1681
|
+
] })
|
|
1682
|
+
] }),
|
|
1683
|
+
p.cardBrand && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1684
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1685
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.CreditCard, { className: "h-3 w-3 shrink-0" }),
|
|
1686
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-foreground", children: p.cardBrand }),
|
|
1687
|
+
p.cardInstallments && /* @__PURE__ */ jsxRuntime.jsxs("span", { children: [
|
|
1688
|
+
p.cardInstallments,
|
|
1689
|
+
"x"
|
|
1690
|
+
] })
|
|
1691
|
+
] }),
|
|
1692
|
+
pAccount && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1693
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1694
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Building2, { className: "h-3 w-3 shrink-0" }),
|
|
1695
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: pAccount.name })
|
|
1696
|
+
] }),
|
|
1697
|
+
(p.feeAmount ?? 0) > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1698
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1699
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "text-warning", children: [
|
|
1700
|
+
t2("financial.statements.columnFee"),
|
|
1701
|
+
" ",
|
|
1702
|
+
saas.formatCurrency(p.feeAmount ?? 0, currency)
|
|
1703
|
+
] })
|
|
1704
|
+
] }),
|
|
1705
|
+
p.paymentDate && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1706
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1707
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: p.paymentDate })
|
|
1708
|
+
] }),
|
|
1709
|
+
p.notes && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1710
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1711
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic truncate max-w-[200px]", children: p.notes })
|
|
1631
1712
|
] })
|
|
1632
|
-
] })
|
|
1633
|
-
|
|
1634
|
-
(methodType || mov.cardBrand) && /* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1635
|
-
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Building2, { className: "h-3 w-3 shrink-0" }),
|
|
1636
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: account.name })
|
|
1637
|
-
] }),
|
|
1638
|
-
mov.paidAmount > 0 && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1639
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1640
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "font-medium text-success", children: saas.formatCurrency(mov.paidAmount, currency) })
|
|
1641
|
-
] }),
|
|
1642
|
-
mov.paymentDate && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1643
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1644
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: mov.paymentDate })
|
|
1645
|
-
] }),
|
|
1646
|
-
mov.notes && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
1647
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { children: "\xB7" }),
|
|
1648
|
-
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic truncate max-w-[200px]", children: mov.notes })
|
|
1649
|
-
] }),
|
|
1650
|
-
!methodType && !mov.cardBrand && !account && !mov.notes && /* @__PURE__ */ jsxRuntime.jsx("span", { children: t2("financial.invoice.noDetails") })
|
|
1651
|
-
] }) }) })
|
|
1713
|
+
] }, p.id);
|
|
1714
|
+
}) }) }) })
|
|
1652
1715
|
}
|
|
1653
1716
|
)
|
|
1654
1717
|
] }, mov.id);
|
|
@@ -1712,7 +1775,25 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1712
1775
|
` })
|
|
1713
1776
|
] });
|
|
1714
1777
|
}
|
|
1715
|
-
|
|
1778
|
+
|
|
1779
|
+
// src/views/navReferrer.ts
|
|
1780
|
+
var REFERRER_LABEL_KEYS = {
|
|
1781
|
+
summary: "financial.nav.summary",
|
|
1782
|
+
statements: "financial.nav.statements",
|
|
1783
|
+
"cash-registers": "financial.nav.cashRegisters",
|
|
1784
|
+
reconciliation: "financial.nav.reconciliation",
|
|
1785
|
+
commissions: "financial.nav.commissions",
|
|
1786
|
+
cards: "financial.nav.cards",
|
|
1787
|
+
"receivables-list": "financial.nav.receivables",
|
|
1788
|
+
"payables-list": "financial.nav.payables"
|
|
1789
|
+
};
|
|
1790
|
+
function referrerLabel(view, t2) {
|
|
1791
|
+
if (!view) return null;
|
|
1792
|
+
const key = REFERRER_LABEL_KEYS[view];
|
|
1793
|
+
return key ? t2(key) : null;
|
|
1794
|
+
}
|
|
1795
|
+
function PayablesView({ intent, onNavigate, previousView, back }) {
|
|
1796
|
+
const t2 = core.useTranslation();
|
|
1716
1797
|
if (intent.mode === "new") {
|
|
1717
1798
|
return /* @__PURE__ */ jsxRuntime.jsx(InvoiceFormView, { direction: "debit", onSaved: (id) => onNavigate(id ? `payables-detail:${id}` : "payables-list") });
|
|
1718
1799
|
}
|
|
@@ -1720,12 +1801,14 @@ function PayablesView({ intent, onNavigate }) {
|
|
|
1720
1801
|
return /* @__PURE__ */ jsxRuntime.jsx(InvoiceFormView, { direction: "debit", editId: intent.editId, onSaved: () => onNavigate(`payables-detail:${intent.editId}`) });
|
|
1721
1802
|
}
|
|
1722
1803
|
if (intent.mode === "detail" && intent.editId) {
|
|
1804
|
+
const prevLabel = referrerLabel(previousView, t2);
|
|
1723
1805
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1724
1806
|
InvoiceDetailView,
|
|
1725
1807
|
{
|
|
1726
1808
|
invoiceId: intent.editId,
|
|
1727
1809
|
direction: "debit",
|
|
1728
|
-
|
|
1810
|
+
backLabel: prevLabel ?? void 0,
|
|
1811
|
+
onBack: prevLabel && back ? () => back("payables-list") : () => onNavigate("payables-list"),
|
|
1729
1812
|
onEdit: () => onNavigate(`payables-edit:${intent.editId}`)
|
|
1730
1813
|
}
|
|
1731
1814
|
);
|
|
@@ -1739,7 +1822,8 @@ function PayablesView({ intent, onNavigate }) {
|
|
|
1739
1822
|
}
|
|
1740
1823
|
);
|
|
1741
1824
|
}
|
|
1742
|
-
function ReceivablesView({ intent, onNavigate }) {
|
|
1825
|
+
function ReceivablesView({ intent, onNavigate, previousView, back }) {
|
|
1826
|
+
const t2 = core.useTranslation();
|
|
1743
1827
|
if (intent.mode === "new") {
|
|
1744
1828
|
return /* @__PURE__ */ jsxRuntime.jsx(InvoiceFormView, { direction: "credit", onSaved: (id) => onNavigate(id ? `receivables-detail:${id}` : "receivables-list") });
|
|
1745
1829
|
}
|
|
@@ -1747,12 +1831,14 @@ function ReceivablesView({ intent, onNavigate }) {
|
|
|
1747
1831
|
return /* @__PURE__ */ jsxRuntime.jsx(InvoiceFormView, { direction: "credit", editId: intent.editId, onSaved: () => onNavigate(`receivables-detail:${intent.editId}`) });
|
|
1748
1832
|
}
|
|
1749
1833
|
if (intent.mode === "detail" && intent.editId) {
|
|
1834
|
+
const prevLabel = referrerLabel(previousView, t2);
|
|
1750
1835
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1751
1836
|
InvoiceDetailView,
|
|
1752
1837
|
{
|
|
1753
1838
|
invoiceId: intent.editId,
|
|
1754
1839
|
direction: "credit",
|
|
1755
|
-
|
|
1840
|
+
backLabel: prevLabel ?? void 0,
|
|
1841
|
+
onBack: prevLabel && back ? () => back("receivables-list") : () => onNavigate("receivables-list"),
|
|
1756
1842
|
onEdit: () => onNavigate(`receivables-edit:${intent.editId}`)
|
|
1757
1843
|
}
|
|
1758
1844
|
);
|
|
@@ -1829,16 +1915,16 @@ function CashOpeningPanel({
|
|
|
1829
1915
|
currency,
|
|
1830
1916
|
onOpen
|
|
1831
1917
|
}) {
|
|
1832
|
-
const [selectedAccountId, setSelectedAccountId] =
|
|
1833
|
-
const [mode, setMode] =
|
|
1834
|
-
const [quickBalance, setQuickBalance] =
|
|
1835
|
-
const [counts, setCounts] =
|
|
1836
|
-
const [notes, setNotes] =
|
|
1837
|
-
const [showNotes, setShowNotes] =
|
|
1838
|
-
const [confirming, setConfirming] =
|
|
1839
|
-
const [submitting, setSubmitting] =
|
|
1918
|
+
const [selectedAccountId, setSelectedAccountId] = React14.useState(cashAccounts[0]?.id ?? "");
|
|
1919
|
+
const [mode, setMode] = React14.useState("quick");
|
|
1920
|
+
const [quickBalance, setQuickBalance] = React14.useState(0);
|
|
1921
|
+
const [counts, setCounts] = React14.useState({});
|
|
1922
|
+
const [notes, setNotes] = React14.useState("");
|
|
1923
|
+
const [showNotes, setShowNotes] = React14.useState(false);
|
|
1924
|
+
const [confirming, setConfirming] = React14.useState(false);
|
|
1925
|
+
const [submitting, setSubmitting] = React14.useState(false);
|
|
1840
1926
|
const t2 = core.useTranslation();
|
|
1841
|
-
const countedTotal =
|
|
1927
|
+
const countedTotal = React14.useMemo(() => {
|
|
1842
1928
|
return BRL_DENOMINATIONS.reduce((sum, d) => sum + (counts[d.value] ?? 0) * d.value, 0);
|
|
1843
1929
|
}, [counts]);
|
|
1844
1930
|
const effectiveBalance = mode === "count" ? countedTotal : quickBalance;
|
|
@@ -2014,13 +2100,13 @@ function CashClosingPanel({
|
|
|
2014
2100
|
currency,
|
|
2015
2101
|
onClose
|
|
2016
2102
|
}) {
|
|
2017
|
-
const [mode, setMode] =
|
|
2018
|
-
const [quickBalance, setQuickBalance] =
|
|
2019
|
-
const [counts, setCounts] =
|
|
2020
|
-
const [notes, setNotes] =
|
|
2021
|
-
const [submitting, setSubmitting] =
|
|
2103
|
+
const [mode, setMode] = React14.useState("quick");
|
|
2104
|
+
const [quickBalance, setQuickBalance] = React14.useState(0);
|
|
2105
|
+
const [counts, setCounts] = React14.useState({});
|
|
2106
|
+
const [notes, setNotes] = React14.useState("");
|
|
2107
|
+
const [submitting, setSubmitting] = React14.useState(false);
|
|
2022
2108
|
const t2 = core.useTranslation();
|
|
2023
|
-
const countedTotal =
|
|
2109
|
+
const countedTotal = React14.useMemo(() => {
|
|
2024
2110
|
return BRL_DENOMINATIONS.reduce((sum, d) => sum + (counts[d.value] ?? 0) * d.value, 0);
|
|
2025
2111
|
}, [counts]);
|
|
2026
2112
|
const effectiveBalance = mode === "count" ? countedTotal : quickBalance;
|
|
@@ -2125,8 +2211,8 @@ function CashRegistersView() {
|
|
|
2125
2211
|
const fetchCashSessions = useFinancialStore((s) => s.fetchCashSessions);
|
|
2126
2212
|
const openCashSession = useFinancialStore((s) => s.openCashSession);
|
|
2127
2213
|
const closeCashSession = useFinancialStore((s) => s.closeCashSession);
|
|
2128
|
-
const [expandedClosing, setExpandedClosing] =
|
|
2129
|
-
|
|
2214
|
+
const [expandedClosing, setExpandedClosing] = React14.useState(null);
|
|
2215
|
+
React14.useEffect(() => {
|
|
2130
2216
|
fetchBankAccounts();
|
|
2131
2217
|
fetchCashSessions();
|
|
2132
2218
|
}, []);
|
|
@@ -2230,7 +2316,7 @@ function OpenAnotherCollapsible({
|
|
|
2230
2316
|
onOpen
|
|
2231
2317
|
}) {
|
|
2232
2318
|
const t2 = core.useTranslation();
|
|
2233
|
-
const [expanded, setExpanded] =
|
|
2319
|
+
const [expanded, setExpanded] = React14.useState(false);
|
|
2234
2320
|
const availableAccounts = cashAccounts.filter((a) => !openSessions.some((s) => s.bankAccountId === a.id));
|
|
2235
2321
|
if (availableAccounts.length === 0) return null;
|
|
2236
2322
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm", children: [
|
|
@@ -2251,33 +2337,202 @@ function OpenAnotherCollapsible({
|
|
|
2251
2337
|
expanded && /* @__PURE__ */ jsxRuntime.jsx(CashOpeningPanel, { cashAccounts: availableAccounts, currency, onOpen })
|
|
2252
2338
|
] });
|
|
2253
2339
|
}
|
|
2254
|
-
function
|
|
2340
|
+
function TransferModal({ onClose, onDone, defaultFromId }) {
|
|
2255
2341
|
const t2 = core.useTranslation();
|
|
2256
2342
|
const { currency } = useFinancialConfig();
|
|
2257
2343
|
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
2258
|
-
const
|
|
2344
|
+
const createTransfer = useFinancialStore((s) => s.createTransfer);
|
|
2345
|
+
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
2346
|
+
const [fromAccountId, setFromAccountId] = React14.useState(defaultFromId ?? "");
|
|
2347
|
+
const [toAccountId, setToAccountId] = React14.useState("");
|
|
2348
|
+
const [amount, setAmount] = React14.useState(0);
|
|
2349
|
+
const [date, setDate] = React14.useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2350
|
+
const [notes, setNotes] = React14.useState("");
|
|
2351
|
+
const [saving, setSaving] = React14.useState(false);
|
|
2352
|
+
const [visible, setVisible] = React14.useState(false);
|
|
2353
|
+
React14.useEffect(() => {
|
|
2354
|
+
fetchBankAccounts();
|
|
2355
|
+
requestAnimationFrame(() => setVisible(true));
|
|
2356
|
+
}, []);
|
|
2357
|
+
function handleClose() {
|
|
2358
|
+
setVisible(false);
|
|
2359
|
+
setTimeout(onClose, 200);
|
|
2360
|
+
}
|
|
2361
|
+
const sameAccount = !!fromAccountId && fromAccountId === toAccountId;
|
|
2362
|
+
const canSubmit = !!fromAccountId && !!toAccountId && !sameAccount && amount > 0 && !saving;
|
|
2363
|
+
function handleKeyDown(e) {
|
|
2364
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
2365
|
+
if (e.target.tagName === "TEXTAREA") return;
|
|
2366
|
+
e.preventDefault();
|
|
2367
|
+
if (canSubmit) void handleSubmit();
|
|
2368
|
+
}
|
|
2369
|
+
async function handleSubmit() {
|
|
2370
|
+
if (!canSubmit) return;
|
|
2371
|
+
setSaving(true);
|
|
2372
|
+
try {
|
|
2373
|
+
await createTransfer({ fromAccountId, toAccountId, amount, date, notes: notes || void 0 });
|
|
2374
|
+
setVisible(false);
|
|
2375
|
+
setTimeout(onDone, 200);
|
|
2376
|
+
} catch {
|
|
2377
|
+
} finally {
|
|
2378
|
+
setSaving(false);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
const selectClass = "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm";
|
|
2382
|
+
return reactDom.createPortal(
|
|
2383
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2384
|
+
"div",
|
|
2385
|
+
{
|
|
2386
|
+
className: "fixed inset-0 z-50 flex items-center justify-center transition-colors duration-200",
|
|
2387
|
+
style: { backgroundColor: visible ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0)" },
|
|
2388
|
+
onClick: handleClose,
|
|
2389
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2390
|
+
"div",
|
|
2391
|
+
{
|
|
2392
|
+
className: "w-full max-w-md rounded-modal border bg-card shadow-lg mx-4 transition-all duration-200 max-h-[90vh] overflow-y-auto",
|
|
2393
|
+
style: { opacity: visible ? 1 : 0, transform: visible ? "scale(1) translateY(0)" : "scale(0.95) translateY(8px)" },
|
|
2394
|
+
onClick: (e) => e.stopPropagation(),
|
|
2395
|
+
onKeyDown: handleKeyDown,
|
|
2396
|
+
children: [
|
|
2397
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b", children: [
|
|
2398
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2399
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowRightLeft, { className: "h-4 w-4 text-warning" }),
|
|
2400
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-base font-semibold", children: t2("financial.transfer.title") })
|
|
2401
|
+
] }),
|
|
2402
|
+
/* @__PURE__ */ jsxRuntime.jsx("button", { onClick: handleClose, className: "p-1 text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-4 w-4" }) })
|
|
2403
|
+
] }),
|
|
2404
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "px-5 py-4 space-y-4", children: [
|
|
2405
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
2406
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2407
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.from") }),
|
|
2408
|
+
/* @__PURE__ */ jsxRuntime.jsxs("select", { value: fromAccountId, onChange: (e) => setFromAccountId(e.target.value), className: selectClass, children: [
|
|
2409
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t2("financial.transfer.selectAccount") }),
|
|
2410
|
+
bankAccounts.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a.id, children: a.name }, a.id))
|
|
2411
|
+
] })
|
|
2412
|
+
] }),
|
|
2413
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2414
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.to") }),
|
|
2415
|
+
/* @__PURE__ */ jsxRuntime.jsxs("select", { value: toAccountId, onChange: (e) => setToAccountId(e.target.value), className: selectClass, children: [
|
|
2416
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: "", children: t2("financial.transfer.selectAccount") }),
|
|
2417
|
+
bankAccounts.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a.id, children: a.name }, a.id))
|
|
2418
|
+
] })
|
|
2419
|
+
] })
|
|
2420
|
+
] }),
|
|
2421
|
+
sameAccount && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-destructive", children: t2("financial.transfer.sameAccount") }),
|
|
2422
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
2423
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2424
|
+
ui.CurrencyInput,
|
|
2425
|
+
{
|
|
2426
|
+
label: t2("financial.transfer.amount"),
|
|
2427
|
+
value: amount,
|
|
2428
|
+
onChange: setAmount,
|
|
2429
|
+
symbol: currency.symbol,
|
|
2430
|
+
locale: currency.locale,
|
|
2431
|
+
currencyCode: currency.code
|
|
2432
|
+
}
|
|
2433
|
+
),
|
|
2434
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2435
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.date") }),
|
|
2436
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.DatePicker, { value: date, onChange: setDate, className: "mt-1" })
|
|
2437
|
+
] })
|
|
2438
|
+
] }),
|
|
2439
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2440
|
+
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.notes") }),
|
|
2441
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2442
|
+
"input",
|
|
2443
|
+
{
|
|
2444
|
+
type: "text",
|
|
2445
|
+
value: notes,
|
|
2446
|
+
onChange: (e) => setNotes(e.target.value),
|
|
2447
|
+
placeholder: t2("financial.transfer.optionalNotes"),
|
|
2448
|
+
className: "w-full mt-1 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"
|
|
2449
|
+
}
|
|
2450
|
+
)
|
|
2451
|
+
] })
|
|
2452
|
+
] }),
|
|
2453
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2 px-5 py-4 border-t", children: [
|
|
2454
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.Button, { variant: "outline", size: "sm", onClick: handleClose, children: t2("financial.transfer.cancel") }),
|
|
2455
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "default", size: "sm", onClick: handleSubmit, disabled: !canSubmit, children: [
|
|
2456
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowRightLeft, { className: "h-3.5 w-3.5" }),
|
|
2457
|
+
saving ? t2("financial.transfer.processing") : t2("financial.transfer.submit")
|
|
2458
|
+
] })
|
|
2459
|
+
] })
|
|
2460
|
+
]
|
|
2461
|
+
}
|
|
2462
|
+
)
|
|
2463
|
+
}
|
|
2464
|
+
),
|
|
2465
|
+
document.body
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
var ALL = "all";
|
|
2469
|
+
function ReconcileMarker({ kind, label }) {
|
|
2470
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.TooltipProvider, { delayDuration: 150, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Tooltip, { children: [
|
|
2471
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "inline-flex shrink-0 cursor-default", children: kind === "reconciled" ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.CheckCheck, { className: "h-3 w-3 text-success/70" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Landmark, { className: "h-3 w-3 text-muted-foreground/40" }) }) }),
|
|
2472
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.TooltipContent, { className: "text-xs", children: label })
|
|
2473
|
+
] }) });
|
|
2474
|
+
}
|
|
2475
|
+
function ReconciledWithRef({ refLabel, tooltip, onOpen }) {
|
|
2476
|
+
const inner = /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "inline-flex items-center gap-0.5 text-[10px] text-muted-foreground max-w-[160px]", children: [
|
|
2477
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowRightLeft, { className: "h-2.5 w-2.5 shrink-0" }),
|
|
2478
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: refLabel })
|
|
2479
|
+
] });
|
|
2480
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ui.TooltipProvider, { delayDuration: 150, children: /* @__PURE__ */ jsxRuntime.jsxs(ui.Tooltip, { children: [
|
|
2481
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.TooltipTrigger, { asChild: true, children: onOpen ? /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: onOpen, className: "hover:text-primary transition-colors", children: inner }) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "cursor-default", children: inner }) }),
|
|
2482
|
+
/* @__PURE__ */ jsxRuntime.jsx(ui.TooltipContent, { className: "text-xs", children: tooltip })
|
|
2483
|
+
] }) });
|
|
2484
|
+
}
|
|
2485
|
+
function StatementsView({ onNavigate }) {
|
|
2486
|
+
const t2 = core.useTranslation();
|
|
2487
|
+
const { currency } = useFinancialConfig();
|
|
2488
|
+
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
2489
|
+
const statement = useFinancialStore((s) => s.statement);
|
|
2259
2490
|
const statementLoading = useFinancialStore((s) => s.statementLoading);
|
|
2491
|
+
const paymentMethods = useFinancialStore((s) => s.paymentMethods);
|
|
2492
|
+
const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes);
|
|
2260
2493
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
2494
|
+
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
2261
2495
|
const fetchStatement = useFinancialStore((s) => s.fetchStatement);
|
|
2262
|
-
const [accountId, setAccountId] =
|
|
2263
|
-
const [dateFrom, setDateFrom] =
|
|
2496
|
+
const [accountId, setAccountId] = React14.useState("");
|
|
2497
|
+
const [dateFrom, setDateFrom] = React14.useState(() => {
|
|
2264
2498
|
const d = /* @__PURE__ */ new Date();
|
|
2265
|
-
d.setMonth(d.getMonth() -
|
|
2499
|
+
d.setMonth(d.getMonth() - 3);
|
|
2266
2500
|
return d.toISOString().slice(0, 10);
|
|
2267
2501
|
});
|
|
2268
|
-
const [dateTo, setDateTo] =
|
|
2269
|
-
|
|
2502
|
+
const [dateTo, setDateTo] = React14.useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2503
|
+
const [showTransfer, setShowTransfer] = React14.useState(false);
|
|
2504
|
+
React14.useEffect(() => {
|
|
2270
2505
|
fetchBankAccounts();
|
|
2506
|
+
fetchPaymentMethods();
|
|
2271
2507
|
}, []);
|
|
2272
|
-
|
|
2508
|
+
const methodName = React14.useMemo(() => {
|
|
2509
|
+
const byId = new Map(paymentMethods.map((m) => [m.id, m.name]));
|
|
2510
|
+
const typeById = new Map(paymentMethodTypes.map((tp) => [tp.id, tp.name]));
|
|
2511
|
+
return (mov) => {
|
|
2512
|
+
if (mov.paymentMethodId && byId.has(mov.paymentMethodId)) return byId.get(mov.paymentMethodId);
|
|
2513
|
+
if (mov.paymentMethodTypeId && typeById.has(mov.paymentMethodTypeId)) return typeById.get(mov.paymentMethodTypeId);
|
|
2514
|
+
if (mov.cardBrand) return mov.cardBrand;
|
|
2515
|
+
return "\u2014";
|
|
2516
|
+
};
|
|
2517
|
+
}, [paymentMethods, paymentMethodTypes]);
|
|
2518
|
+
function openInvoice(e) {
|
|
2519
|
+
if (!onNavigate || !e.movement.invoiceId) return;
|
|
2520
|
+
const base = e.movement.direction === "credit" ? "receivables" : "payables";
|
|
2521
|
+
onNavigate(`${base}-detail:${e.movement.invoiceId}`);
|
|
2522
|
+
}
|
|
2523
|
+
React14.useEffect(() => {
|
|
2273
2524
|
if (accountId) {
|
|
2274
|
-
fetchStatement({
|
|
2525
|
+
fetchStatement({
|
|
2526
|
+
bankAccountId: accountId === ALL ? void 0 : accountId,
|
|
2527
|
+
dateRange: { from: dateFrom, to: dateTo }
|
|
2528
|
+
});
|
|
2275
2529
|
}
|
|
2276
2530
|
}, [accountId, dateFrom, dateTo]);
|
|
2277
|
-
|
|
2278
|
-
if (!accountId
|
|
2279
|
-
}, [
|
|
2280
|
-
const
|
|
2531
|
+
React14.useEffect(() => {
|
|
2532
|
+
if (!accountId) setAccountId(ALL);
|
|
2533
|
+
}, []);
|
|
2534
|
+
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
2535
|
+
const columns = React14.useMemo(() => [
|
|
2281
2536
|
{
|
|
2282
2537
|
id: "date",
|
|
2283
2538
|
header: t2("financial.statements.columnDate"),
|
|
@@ -2286,29 +2541,112 @@ function StatementsView() {
|
|
|
2286
2541
|
{
|
|
2287
2542
|
id: "description",
|
|
2288
2543
|
header: t2("financial.statements.columnDescription"),
|
|
2289
|
-
cell: ({ row }) =>
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2544
|
+
cell: ({ row }) => {
|
|
2545
|
+
const e = row.original;
|
|
2546
|
+
const isTransfer = e.entryKind !== "movement";
|
|
2547
|
+
const memo = e.movement.notes;
|
|
2548
|
+
const title = e.invoice?.contactName || (isTransfer ? t2("financial.statements.transfer") : memo || "Transaction");
|
|
2549
|
+
const memoLine = memo && memo !== title ? memo : void 0;
|
|
2550
|
+
const doc = e.invoice?.fiscalNumber;
|
|
2551
|
+
const inst = e.movement.installmentNumber;
|
|
2552
|
+
const canOpen = !!onNavigate && !!e.movement.invoiceId;
|
|
2553
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2554
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2555
|
+
/* @__PURE__ */ jsxRuntime.jsx(saas.PersonLink, { personId: e.invoice?.contactId, name: title, size: "sm" }),
|
|
2556
|
+
doc && (canOpen ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
2557
|
+
"button",
|
|
2558
|
+
{
|
|
2559
|
+
onClick: () => openInvoice(e),
|
|
2560
|
+
className: "text-[10px] text-muted-foreground font-mono hover:text-primary hover:underline",
|
|
2561
|
+
title: t2("financial.statements.viewInvoice"),
|
|
2562
|
+
children: doc
|
|
2563
|
+
}
|
|
2564
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: doc })),
|
|
2565
|
+
isTransfer && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-[9px] uppercase tracking-wide px-1 py-px rounded bg-warning/15 text-warning font-semibold", children: t2("financial.statements.transfer") }),
|
|
2566
|
+
e.movement.reconciledAt ? /* @__PURE__ */ jsxRuntime.jsx(ReconcileMarker, { kind: "reconciled", label: t2("financial.statements.reconciled") }) : e.movement.externalSource ? /* @__PURE__ */ jsxRuntime.jsx(ReconcileMarker, { kind: "imported", label: t2("financial.statements.imported") }) : null,
|
|
2567
|
+
e.reconciledWith && (() => {
|
|
2568
|
+
const rw = e.reconciledWith;
|
|
2569
|
+
const refLabel = rw.fiscalNumber || rw.contactName || rw.description || rw.movementId.slice(0, 8);
|
|
2570
|
+
const base = e.movement.direction === "credit" ? "receivables" : "payables";
|
|
2571
|
+
const onOpen = onNavigate && rw.invoiceId ? () => onNavigate(`${base}-detail:${rw.invoiceId}`) : void 0;
|
|
2572
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2573
|
+
ReconciledWithRef,
|
|
2574
|
+
{
|
|
2575
|
+
refLabel,
|
|
2576
|
+
tooltip: t2("financial.statements.reconciledWith", { ref: refLabel }),
|
|
2577
|
+
onOpen
|
|
2578
|
+
}
|
|
2579
|
+
);
|
|
2580
|
+
})()
|
|
2581
|
+
] }),
|
|
2582
|
+
memoLine && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[11px] text-foreground/80", children: memoLine }),
|
|
2583
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2584
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "capitalize", children: isTransfer && e.counterAccountName ? e.entryKind === "transfer-in" ? `\u2190 ${e.counterAccountName}` : `\u2192 ${e.counterAccountName}` : e.movement.movementKind }),
|
|
2585
|
+
inst ? ` \xB7 ${t2("financial.statements.installmentShort", { n: inst })}` : ""
|
|
2586
|
+
] })
|
|
2587
|
+
] });
|
|
2588
|
+
}
|
|
2589
|
+
},
|
|
2590
|
+
{
|
|
2591
|
+
id: "method",
|
|
2592
|
+
header: t2("financial.statements.columnMethod"),
|
|
2593
|
+
cell: ({ row }) => {
|
|
2594
|
+
const mov = row.original.movement;
|
|
2595
|
+
const name = methodName(mov);
|
|
2596
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2597
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs", children: name }),
|
|
2598
|
+
mov.cardInstallments && mov.cardInstallments > 1 && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2599
|
+
mov.cardInstallments,
|
|
2600
|
+
"x"
|
|
2601
|
+
] })
|
|
2602
|
+
] });
|
|
2603
|
+
}
|
|
2293
2604
|
},
|
|
2294
2605
|
{
|
|
2295
2606
|
id: "debit",
|
|
2296
2607
|
header: t2("financial.statements.columnDebit"),
|
|
2297
|
-
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-destructive text-xs", children: row.original.movement.direction === "debit" ?
|
|
2608
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-destructive text-xs", children: row.original.movement.direction === "debit" ? fmt(row.original.net) : "" })
|
|
2298
2609
|
},
|
|
2299
2610
|
{
|
|
2300
2611
|
id: "credit",
|
|
2301
2612
|
header: t2("financial.statements.columnCredit"),
|
|
2302
|
-
cell: ({ row }) =>
|
|
2613
|
+
cell: ({ row }) => {
|
|
2614
|
+
const e = row.original;
|
|
2615
|
+
if (e.movement.direction !== "credit") return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-right" });
|
|
2616
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-right", children: [
|
|
2617
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-success text-xs", children: fmt(e.net) }),
|
|
2618
|
+
e.fee > 0 && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "block text-[10px] text-muted-foreground", children: [
|
|
2619
|
+
fmt(e.gross),
|
|
2620
|
+
" \u2212 ",
|
|
2621
|
+
fmt(e.fee)
|
|
2622
|
+
] })
|
|
2623
|
+
] });
|
|
2624
|
+
}
|
|
2625
|
+
},
|
|
2626
|
+
{
|
|
2627
|
+
id: "fee",
|
|
2628
|
+
header: t2("financial.statements.columnFee"),
|
|
2629
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-warning text-xs", children: row.original.fee > 0 ? fmt(row.original.fee) : "" })
|
|
2303
2630
|
},
|
|
2304
2631
|
{
|
|
2305
2632
|
id: "balance",
|
|
2306
2633
|
header: t2("financial.statements.columnBalance"),
|
|
2307
|
-
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block font-medium text-xs", children:
|
|
2634
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block font-medium text-xs", children: fmt(row.original.runningBalance) })
|
|
2308
2635
|
}
|
|
2309
|
-
], [currency, t2]);
|
|
2636
|
+
], [currency, t2, methodName, onNavigate]);
|
|
2637
|
+
const entries = statement?.entries ?? [];
|
|
2310
2638
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2311
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2639
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2640
|
+
ui.SubpageHeader,
|
|
2641
|
+
{
|
|
2642
|
+
title: t2("financial.statements.title"),
|
|
2643
|
+
subtitle: t2("financial.statements.subtitle"),
|
|
2644
|
+
actions: /* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => setShowTransfer(true), disabled: bankAccounts.length < 2, children: [
|
|
2645
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowRightLeft, { className: "h-3.5 w-3.5" }),
|
|
2646
|
+
t2("financial.statements.newTransfer")
|
|
2647
|
+
] })
|
|
2648
|
+
}
|
|
2649
|
+
),
|
|
2312
2650
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-end", children: [
|
|
2313
2651
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-1 max-w-xs", children: [
|
|
2314
2652
|
/* @__PURE__ */ jsxRuntime.jsx("label", { className: "text-[10px] font-medium text-muted-foreground uppercase", children: t2("financial.statements.account") }),
|
|
@@ -2317,9 +2655,9 @@ function StatementsView() {
|
|
|
2317
2655
|
{
|
|
2318
2656
|
value: accountId,
|
|
2319
2657
|
onChange: (e) => setAccountId(e.target.value),
|
|
2320
|
-
className: "w-full mt-0.5 rounded-input border border-input
|
|
2658
|
+
className: "w-full mt-0.5 rounded-input border border-input bg-card shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] px-3 py-2 text-sm",
|
|
2321
2659
|
children: [
|
|
2322
|
-
/* @__PURE__ */ jsxRuntime.jsx("option", { value:
|
|
2660
|
+
/* @__PURE__ */ jsxRuntime.jsx("option", { value: ALL, children: t2("financial.summary.allAccounts") }),
|
|
2323
2661
|
bankAccounts.map((a) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: a.id, children: a.name }, a.id))
|
|
2324
2662
|
]
|
|
2325
2663
|
}
|
|
@@ -2334,10 +2672,31 @@ function StatementsView() {
|
|
|
2334
2672
|
/* @__PURE__ */ jsxRuntime.jsx(ui.DatePicker, { value: dateTo, onChange: setDateTo, className: "mt-0.5" })
|
|
2335
2673
|
] })
|
|
2336
2674
|
] }),
|
|
2675
|
+
accountId && statement && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-2", children: [
|
|
2676
|
+
/* @__PURE__ */ jsxRuntime.jsx(SummaryStat, { label: t2("financial.statements.opening"), value: fmt(statement.openingBalance) }),
|
|
2677
|
+
/* @__PURE__ */ jsxRuntime.jsx(SummaryStat, { label: t2("financial.statements.totalCredits"), value: fmt(statement.totalCredits), positive: true }),
|
|
2678
|
+
/* @__PURE__ */ jsxRuntime.jsx(SummaryStat, { label: t2("financial.statements.totalDebits"), value: fmt(statement.totalDebits), negative: true }),
|
|
2679
|
+
/* @__PURE__ */ jsxRuntime.jsx(SummaryStat, { label: t2("financial.statements.closing"), value: fmt(statement.closingBalance), accent: true })
|
|
2680
|
+
] }),
|
|
2337
2681
|
!accountId ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
2338
2682
|
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Receipt, { className: "h-8 w-8 text-muted-foreground mb-2" }),
|
|
2339
2683
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.statements.selectToView") })
|
|
2340
|
-
] }) : statementLoading ? /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: [], loading: true, skeletonRows:
|
|
2684
|
+
] }) : statementLoading ? /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: [], loading: true, skeletonRows: 8, compact: true }) : entries.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: [], emptyMessage: t2("financial.statements.noTransactions"), compact: true }) : /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: entries, compact: true }),
|
|
2685
|
+
showTransfer && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2686
|
+
TransferModal,
|
|
2687
|
+
{
|
|
2688
|
+
defaultFromId: accountId && accountId !== ALL ? accountId : void 0,
|
|
2689
|
+
onClose: () => setShowTransfer(false),
|
|
2690
|
+
onDone: () => setShowTransfer(false)
|
|
2691
|
+
}
|
|
2692
|
+
)
|
|
2693
|
+
] });
|
|
2694
|
+
}
|
|
2695
|
+
function SummaryStat({ label, value, accent, positive, negative }) {
|
|
2696
|
+
const valueClass = positive ? "text-success" : negative ? "text-destructive" : accent ? "text-foreground" : "text-foreground";
|
|
2697
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rounded-lg border px-3 py-2 ${accent ? "bg-muted/40" : "bg-card"}`, children: [
|
|
2698
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] font-medium text-muted-foreground uppercase truncate", children: label }),
|
|
2699
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm font-semibold mt-0.5 ${valueClass}`, children: value })
|
|
2341
2700
|
] });
|
|
2342
2701
|
}
|
|
2343
2702
|
function CommissionsView() {
|
|
@@ -2354,7 +2713,7 @@ function CardsView() {
|
|
|
2354
2713
|
const cardTransactions = useFinancialStore((s) => s.cardTransactions);
|
|
2355
2714
|
const cardsLoading = useFinancialStore((s) => s.cardsLoading);
|
|
2356
2715
|
const fetchCardTransactions = useFinancialStore((s) => s.fetchCardTransactions);
|
|
2357
|
-
|
|
2716
|
+
React14.useEffect(() => {
|
|
2358
2717
|
fetchCardTransactions();
|
|
2359
2718
|
}, []);
|
|
2360
2719
|
const byBrand = {};
|
|
@@ -2403,6 +2762,151 @@ function CardsView() {
|
|
|
2403
2762
|
] }, brand))
|
|
2404
2763
|
] });
|
|
2405
2764
|
}
|
|
2765
|
+
function ReconciliationView() {
|
|
2766
|
+
const t2 = core.useTranslation();
|
|
2767
|
+
const { currency } = useFinancialConfig();
|
|
2768
|
+
const provider = useFinancialProvider();
|
|
2769
|
+
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
2770
|
+
const [lines, setLines] = React14.useState([]);
|
|
2771
|
+
const [loading, setLoading] = React14.useState(true);
|
|
2772
|
+
const [selectedId, setSelectedId] = React14.useState(null);
|
|
2773
|
+
const [candidates, setCandidates] = React14.useState([]);
|
|
2774
|
+
const [candidatesLoading, setCandidatesLoading] = React14.useState(false);
|
|
2775
|
+
const [busy, setBusy] = React14.useState(false);
|
|
2776
|
+
const supported = typeof provider.getUnreconciled === "function";
|
|
2777
|
+
const load = React14.useCallback(async () => {
|
|
2778
|
+
if (!provider.getUnreconciled) return;
|
|
2779
|
+
setLoading(true);
|
|
2780
|
+
try {
|
|
2781
|
+
const data = await provider.getUnreconciled();
|
|
2782
|
+
setLines(data);
|
|
2783
|
+
setSelectedId((prev) => prev && data.some((l) => l.id === prev) ? prev : null);
|
|
2784
|
+
} finally {
|
|
2785
|
+
setLoading(false);
|
|
2786
|
+
}
|
|
2787
|
+
}, [provider]);
|
|
2788
|
+
React14.useEffect(() => {
|
|
2789
|
+
void load();
|
|
2790
|
+
}, [load]);
|
|
2791
|
+
React14.useEffect(() => {
|
|
2792
|
+
if (!selectedId || !provider.suggestReconciliation) {
|
|
2793
|
+
setCandidates([]);
|
|
2794
|
+
return;
|
|
2795
|
+
}
|
|
2796
|
+
let cancelled = false;
|
|
2797
|
+
setCandidatesLoading(true);
|
|
2798
|
+
provider.suggestReconciliation(selectedId).then((c) => {
|
|
2799
|
+
if (!cancelled) setCandidates(c);
|
|
2800
|
+
}).finally(() => {
|
|
2801
|
+
if (!cancelled) setCandidatesLoading(false);
|
|
2802
|
+
});
|
|
2803
|
+
return () => {
|
|
2804
|
+
cancelled = true;
|
|
2805
|
+
};
|
|
2806
|
+
}, [selectedId, provider]);
|
|
2807
|
+
async function match(bankMovementId, matchedMovementId) {
|
|
2808
|
+
if (!provider.reconcileMovement) return;
|
|
2809
|
+
setBusy(true);
|
|
2810
|
+
try {
|
|
2811
|
+
await provider.reconcileMovement({ bankMovementId, matchedMovementId });
|
|
2812
|
+
ui.toast.success(t2("financial.reconciliation.match"));
|
|
2813
|
+
await load();
|
|
2814
|
+
} catch {
|
|
2815
|
+
ui.toast.error(t2("common.error"));
|
|
2816
|
+
} finally {
|
|
2817
|
+
setBusy(false);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
if (!supported) {
|
|
2821
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "py-16 text-center", children: [
|
|
2822
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Landmark, { className: "h-8 w-8 text-muted-foreground/30 mx-auto mb-2" }),
|
|
2823
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.reconciliation.empty") })
|
|
2824
|
+
] });
|
|
2825
|
+
}
|
|
2826
|
+
const selected = lines.find((l) => l.id === selectedId) ?? null;
|
|
2827
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
2828
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
2829
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
2830
|
+
/* @__PURE__ */ jsxRuntime.jsx("h2", { className: "text-lg font-semibold tracking-tight", children: t2("financial.reconciliation.title") }),
|
|
2831
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.reconciliation.subtitle") })
|
|
2832
|
+
] }),
|
|
2833
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2834
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: lines.length > 0 ? t2("financial.reconciliation.pending", { count: lines.length }) : t2("financial.reconciliation.allDone") }),
|
|
2835
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading || busy, children: [
|
|
2836
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }),
|
|
2837
|
+
t2("financial.reconciliation.refresh")
|
|
2838
|
+
] })
|
|
2839
|
+
] })
|
|
2840
|
+
] }),
|
|
2841
|
+
lines.length === 0 && !loading ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm py-16 text-center", children: [
|
|
2842
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-8 w-8 text-success/40 mx-auto mb-2" }),
|
|
2843
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm font-medium", children: t2("financial.reconciliation.empty") }),
|
|
2844
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground mt-1", children: t2("financial.reconciliation.emptyHint") })
|
|
2845
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
|
|
2846
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm overflow-hidden", children: [
|
|
2847
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-b", children: [
|
|
2848
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Landmark, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2849
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: t2("financial.reconciliation.bankLines") })
|
|
2850
|
+
] }),
|
|
2851
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y max-h-[60vh] overflow-auto", children: lines.map((line) => {
|
|
2852
|
+
const isCredit = line.direction === "credit";
|
|
2853
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2854
|
+
"button",
|
|
2855
|
+
{
|
|
2856
|
+
onClick: () => setSelectedId(line.id),
|
|
2857
|
+
className: `w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-muted/40 transition-colors ${selectedId === line.id ? "bg-muted/50" : ""}`,
|
|
2858
|
+
children: [
|
|
2859
|
+
isCredit ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowDownCircle, { className: "h-4 w-4 text-success shrink-0" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUpCircle, { className: "h-4 w-4 text-destructive shrink-0" }),
|
|
2860
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2861
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm truncate", children: line.notes || line.externalId }),
|
|
2862
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2863
|
+
line.paymentDate ?? line.dueDate,
|
|
2864
|
+
line.externalSource && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "ml-1 uppercase tracking-wide", children: [
|
|
2865
|
+
"\xB7 ",
|
|
2866
|
+
line.externalSource
|
|
2867
|
+
] })
|
|
2868
|
+
] })
|
|
2869
|
+
] }),
|
|
2870
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-sm font-semibold shrink-0 ${isCredit ? "text-success" : "text-destructive"}`, children: fmt(line.amount) })
|
|
2871
|
+
]
|
|
2872
|
+
},
|
|
2873
|
+
line.id
|
|
2874
|
+
);
|
|
2875
|
+
}) })
|
|
2876
|
+
] }),
|
|
2877
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "rounded-lg border bg-card shadow-sm overflow-hidden", children: [
|
|
2878
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-b", children: [
|
|
2879
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Sparkles, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2880
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-sm font-semibold", children: t2("financial.reconciliation.candidates") })
|
|
2881
|
+
] }),
|
|
2882
|
+
!selected ? /* @__PURE__ */ jsxRuntime.jsx("p", { className: "px-4 py-16 text-center text-sm text-muted-foreground", children: t2("financial.reconciliation.selectLine") }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "p-3 space-y-2", children: candidatesLoading ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "py-10 text-center text-xs text-muted-foreground", children: "\u2026" }) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2883
|
+
candidates.map((c) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-3 rounded-md border p-2.5", children: [
|
|
2884
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2885
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm truncate", children: c.invoice?.contactName || c.movement.notes || c.invoice?.fiscalNumber || t2("financial.reconciliation.match") }),
|
|
2886
|
+
/* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2887
|
+
c.movement.dueDate,
|
|
2888
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1.5 inline-flex items-center rounded px-1 py-px bg-primary/10 text-primary font-medium", children: t2("financial.reconciliation.matchScore", { score: Math.round(c.score * 100) }) })
|
|
2889
|
+
] })
|
|
2890
|
+
] }),
|
|
2891
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold shrink-0", children: fmt(c.movement.amount) }),
|
|
2892
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { size: "sm", disabled: busy, onClick: () => match(selected.id, c.movement.id), children: [
|
|
2893
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Link2, { className: "h-3.5 w-3.5" }),
|
|
2894
|
+
t2("financial.reconciliation.match")
|
|
2895
|
+
] })
|
|
2896
|
+
] }, c.movement.id)),
|
|
2897
|
+
candidates.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "py-6 text-center text-xs text-muted-foreground", children: t2("financial.reconciliation.noCandidates") }),
|
|
2898
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "pt-2 mt-1 border-t", children: [
|
|
2899
|
+
/* @__PURE__ */ jsxRuntime.jsxs(ui.Button, { variant: "outline", size: "sm", className: "w-full", disabled: busy, onClick: () => match(selected.id), children: [
|
|
2900
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Check, { className: "h-3.5 w-3.5" }),
|
|
2901
|
+
t2("financial.reconciliation.accept")
|
|
2902
|
+
] }),
|
|
2903
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] text-muted-foreground text-center mt-1", children: t2("financial.reconciliation.acceptHint") })
|
|
2904
|
+
] })
|
|
2905
|
+
] }) })
|
|
2906
|
+
] })
|
|
2907
|
+
] })
|
|
2908
|
+
] });
|
|
2909
|
+
}
|
|
2406
2910
|
function FinancialGeneralSettings() {
|
|
2407
2911
|
const t2 = core.useTranslation();
|
|
2408
2912
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
@@ -2436,7 +2940,7 @@ var STEPS = [
|
|
|
2436
2940
|
];
|
|
2437
2941
|
function FinancialOnboarding({ onComplete }) {
|
|
2438
2942
|
const t2 = core.useTranslation();
|
|
2439
|
-
const [step, setStep] =
|
|
2943
|
+
const [step, setStep] = React14.useState(0);
|
|
2440
2944
|
const current = STEPS[step];
|
|
2441
2945
|
const isLast = step === STEPS.length - 1;
|
|
2442
2946
|
const Icon = current.icon;
|
|
@@ -2527,13 +3031,20 @@ function buildNav(config, activeView, navigate, t2) {
|
|
|
2527
3031
|
onClick: () => navigate("cash-registers")
|
|
2528
3032
|
});
|
|
2529
3033
|
}
|
|
2530
|
-
|
|
3034
|
+
items.push({
|
|
3035
|
+
id: "statements",
|
|
3036
|
+
label: t2("financial.nav.statements"),
|
|
3037
|
+
icon: "Receipt",
|
|
3038
|
+
active: activeView === "statements",
|
|
3039
|
+
onClick: () => navigate("statements")
|
|
3040
|
+
});
|
|
3041
|
+
if (config.modules.reconciliation) {
|
|
2531
3042
|
items.push({
|
|
2532
|
-
id: "
|
|
2533
|
-
label: t2("financial.nav.
|
|
2534
|
-
icon: "
|
|
2535
|
-
active: activeView === "
|
|
2536
|
-
onClick: () => navigate("
|
|
3043
|
+
id: "reconciliation",
|
|
3044
|
+
label: t2("financial.nav.reconciliation"),
|
|
3045
|
+
icon: "ArrowLeftRight",
|
|
3046
|
+
active: activeView === "reconciliation",
|
|
3047
|
+
onClick: () => navigate("reconciliation")
|
|
2537
3048
|
});
|
|
2538
3049
|
}
|
|
2539
3050
|
if (config.modules.commissions) {
|
|
@@ -2561,7 +3072,7 @@ function buildNav(config, activeView, navigate, t2) {
|
|
|
2561
3072
|
return items;
|
|
2562
3073
|
}
|
|
2563
3074
|
function FinancialPage({ config, provider, store, registries }) {
|
|
2564
|
-
const { view, direction, navigate } = saas.useModuleNavigation("/financial", {
|
|
3075
|
+
const { view, direction, navigate, previousView, back } = saas.useModuleNavigation("/financial", {
|
|
2565
3076
|
summary: 0,
|
|
2566
3077
|
"payables-list": 0,
|
|
2567
3078
|
"payables-new": 1,
|
|
@@ -2569,11 +3080,12 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
2569
3080
|
"receivables-new": 1,
|
|
2570
3081
|
"cash-registers": 0,
|
|
2571
3082
|
statements: 0,
|
|
3083
|
+
reconciliation: 0,
|
|
2572
3084
|
commissions: 0,
|
|
2573
3085
|
cards: 0,
|
|
2574
3086
|
settings: 1
|
|
2575
3087
|
});
|
|
2576
|
-
const [onboardingComplete, setOnboardingComplete] =
|
|
3088
|
+
const [onboardingComplete, setOnboardingComplete] = React14.useState(() => {
|
|
2577
3089
|
try {
|
|
2578
3090
|
return localStorage.getItem("saas-core:financial-onboarded") === "true";
|
|
2579
3091
|
} catch {
|
|
@@ -2584,7 +3096,7 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
2584
3096
|
const intent = parseIntent(view);
|
|
2585
3097
|
const isSettings = view === "settings";
|
|
2586
3098
|
const nav = buildNav(config, view, navigate, t2);
|
|
2587
|
-
const quickActions =
|
|
3099
|
+
const quickActions = React14.useMemo(() => {
|
|
2588
3100
|
const actions = [];
|
|
2589
3101
|
if (config.modules.payables) {
|
|
2590
3102
|
actions.push({
|
|
@@ -2626,24 +3138,30 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
2626
3138
|
return /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(FinancialOnboarding, { onComplete: handleOnboardingComplete }) });
|
|
2627
3139
|
}
|
|
2628
3140
|
if (isSettings && registries && registries.length > 0) {
|
|
2629
|
-
return /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ui.PageTransition, { transitionKey: "settings", direction, children: /* @__PURE__ */ jsxRuntime.
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
3141
|
+
return /* @__PURE__ */ jsxRuntime.jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsxRuntime.jsx(ui.PageTransition, { transitionKey: "settings", direction, children: /* @__PURE__ */ jsxRuntime.jsx("div", { style: { padding: "24px" }, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3142
|
+
saas.PluginSettingsPanel,
|
|
3143
|
+
{
|
|
3144
|
+
title: t2("financial.settingsPage.title"),
|
|
3145
|
+
subtitle: t2("financial.settingsPage.subtitle"),
|
|
3146
|
+
generalSettings: /* @__PURE__ */ jsxRuntime.jsx(FinancialGeneralSettings, {}),
|
|
3147
|
+
registries,
|
|
3148
|
+
hostPluginId: "financial",
|
|
3149
|
+
routeBase: "/settings/financial"
|
|
3150
|
+
}
|
|
3151
|
+
) }) }) });
|
|
2636
3152
|
}
|
|
2637
3153
|
function renderView() {
|
|
2638
3154
|
switch (intent.view) {
|
|
2639
3155
|
case "payables":
|
|
2640
|
-
return /* @__PURE__ */ jsxRuntime.jsx(PayablesView, { intent, onNavigate: navigate });
|
|
3156
|
+
return /* @__PURE__ */ jsxRuntime.jsx(PayablesView, { intent, onNavigate: navigate, previousView, back });
|
|
2641
3157
|
case "receivables":
|
|
2642
|
-
return /* @__PURE__ */ jsxRuntime.jsx(ReceivablesView, { intent, onNavigate: navigate });
|
|
3158
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ReceivablesView, { intent, onNavigate: navigate, previousView, back });
|
|
2643
3159
|
case "cash-registers":
|
|
2644
3160
|
return /* @__PURE__ */ jsxRuntime.jsx(CashRegistersView, {});
|
|
2645
3161
|
case "statements":
|
|
2646
|
-
return /* @__PURE__ */ jsxRuntime.jsx(StatementsView, {});
|
|
3162
|
+
return /* @__PURE__ */ jsxRuntime.jsx(StatementsView, { onNavigate: navigate });
|
|
3163
|
+
case "reconciliation":
|
|
3164
|
+
return /* @__PURE__ */ jsxRuntime.jsx(ReconciliationView, {});
|
|
2647
3165
|
case "commissions":
|
|
2648
3166
|
return /* @__PURE__ */ jsxRuntime.jsx(CommissionsView, {});
|
|
2649
3167
|
case "cards":
|
|
@@ -2675,7 +3193,7 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
2675
3193
|
}
|
|
2676
3194
|
function useEnsureSummary() {
|
|
2677
3195
|
const fetchSummary = useFinancialStore((s) => s.fetchSummary);
|
|
2678
|
-
|
|
3196
|
+
React14.useEffect(() => {
|
|
2679
3197
|
void fetchSummary();
|
|
2680
3198
|
}, []);
|
|
2681
3199
|
}
|
|
@@ -2799,7 +3317,7 @@ function RecentTransactions() {
|
|
|
2799
3317
|
const { currency } = useFinancialConfig();
|
|
2800
3318
|
const invoices = useFinancialStore((s) => s.invoices);
|
|
2801
3319
|
const fetchInvoices = useFinancialStore((s) => s.fetchInvoices);
|
|
2802
|
-
|
|
3320
|
+
React14.useEffect(() => {
|
|
2803
3321
|
void fetchInvoices({ pageSize: 5 });
|
|
2804
3322
|
}, []);
|
|
2805
3323
|
const columns = [
|
|
@@ -2847,6 +3365,9 @@ function now() {
|
|
|
2847
3365
|
function today() {
|
|
2848
3366
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2849
3367
|
}
|
|
3368
|
+
function round2(n) {
|
|
3369
|
+
return Math.round(n * 100) / 100;
|
|
3370
|
+
}
|
|
2850
3371
|
function matchesDateRange(dateStr, range) {
|
|
2851
3372
|
if (!range || !dateStr) return true;
|
|
2852
3373
|
return dateStr >= range.from && dateStr <= range.to;
|
|
@@ -3050,37 +3571,60 @@ function createMockFinancialProvider(options) {
|
|
|
3050
3571
|
return paginate(results, query.page, query.pageSize);
|
|
3051
3572
|
},
|
|
3052
3573
|
async payMovement(input) {
|
|
3053
|
-
const
|
|
3054
|
-
if (!
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3574
|
+
const bill = store.movements.find((m) => m.id === input.movementId);
|
|
3575
|
+
if (!bill) throw new Error(`Movement ${input.movementId} not found`);
|
|
3576
|
+
const method = input.paymentMethodId ? store.paymentMethods.find((pm) => pm.id === input.paymentMethodId) : void 0;
|
|
3577
|
+
const feePct = bill.direction === "credit" ? method?.discountValue ?? 0 : 0;
|
|
3578
|
+
const thisFee = round2(input.amount * (feePct / 100));
|
|
3579
|
+
const payment = {
|
|
3580
|
+
id: uid(),
|
|
3581
|
+
invoiceId: bill.invoiceId,
|
|
3582
|
+
direction: bill.direction,
|
|
3583
|
+
movementKind: "payment",
|
|
3584
|
+
amount: input.amount,
|
|
3585
|
+
paidAmount: input.amount,
|
|
3586
|
+
feeAmount: thisFee,
|
|
3587
|
+
status: "paid",
|
|
3588
|
+
dueDate: bill.dueDate,
|
|
3589
|
+
paymentDate: input.paymentDate,
|
|
3590
|
+
installmentNumber: bill.installmentNumber,
|
|
3591
|
+
paymentMethodId: input.paymentMethodId,
|
|
3592
|
+
paymentMethodTypeId: input.paymentMethodTypeId,
|
|
3593
|
+
bankAccountId: input.bankAccountId,
|
|
3594
|
+
cashSessionId: input.cashSessionId,
|
|
3595
|
+
cardBrand: input.cardBrand,
|
|
3596
|
+
cardInstallments: input.cardInstallments,
|
|
3597
|
+
notes: input.notes,
|
|
3598
|
+
tenantId,
|
|
3599
|
+
createdAt: now(),
|
|
3600
|
+
updatedAt: now()
|
|
3601
|
+
};
|
|
3602
|
+
store.movements.push(payment);
|
|
3603
|
+
bill.paidAmount = round2(bill.paidAmount + input.amount);
|
|
3604
|
+
bill.status = bill.paidAmount >= bill.amount ? "paid" : "partial";
|
|
3605
|
+
bill.paymentDate = void 0;
|
|
3606
|
+
bill.bankAccountId = void 0;
|
|
3607
|
+
bill.updatedAt = now();
|
|
3608
|
+
if (input.bankAccountId) {
|
|
3609
|
+
const account = store.bankAccounts.find((a) => a.id === input.bankAccountId);
|
|
3066
3610
|
if (account) {
|
|
3067
|
-
if (
|
|
3068
|
-
account.currentBalance
|
|
3611
|
+
if (bill.direction === "credit") {
|
|
3612
|
+
account.currentBalance = round2(account.currentBalance + (input.amount - thisFee));
|
|
3069
3613
|
} else {
|
|
3070
|
-
account.currentBalance
|
|
3614
|
+
account.currentBalance = round2(account.currentBalance - input.amount);
|
|
3071
3615
|
}
|
|
3072
3616
|
account.updatedAt = now();
|
|
3073
3617
|
}
|
|
3074
3618
|
}
|
|
3075
|
-
if (
|
|
3076
|
-
const invoice = store.invoices.find((i) => i.id ===
|
|
3619
|
+
if (bill.invoiceId) {
|
|
3620
|
+
const invoice = store.invoices.find((i) => i.id === bill.invoiceId);
|
|
3077
3621
|
if (invoice) {
|
|
3078
|
-
const
|
|
3079
|
-
invoice.paidAmount =
|
|
3622
|
+
const bills = store.movements.filter((m) => m.invoiceId === invoice.id && m.movementKind === "bill");
|
|
3623
|
+
invoice.paidAmount = bills.reduce((sum, m) => sum + m.paidAmount, 0);
|
|
3080
3624
|
recalcInvoiceStatus(invoice);
|
|
3081
3625
|
}
|
|
3082
3626
|
}
|
|
3083
|
-
return
|
|
3627
|
+
return payment;
|
|
3084
3628
|
},
|
|
3085
3629
|
async cancelMovement(id) {
|
|
3086
3630
|
const movement = store.movements.find((m) => m.id === id);
|
|
@@ -3088,6 +3632,54 @@ function createMockFinancialProvider(options) {
|
|
|
3088
3632
|
movement.status = "cancelled";
|
|
3089
3633
|
movement.updatedAt = now();
|
|
3090
3634
|
},
|
|
3635
|
+
// --- Transfers (between accounts) ---
|
|
3636
|
+
async createTransfer(input) {
|
|
3637
|
+
if (!input.fromAccountId || !input.toAccountId || input.fromAccountId === input.toAccountId) {
|
|
3638
|
+
throw new Error("Source and destination accounts must differ");
|
|
3639
|
+
}
|
|
3640
|
+
if (!(input.amount > 0)) throw new Error("Amount must be greater than zero");
|
|
3641
|
+
const transferId = uid();
|
|
3642
|
+
const debitMovementId = uid();
|
|
3643
|
+
const creditMovementId = uid();
|
|
3644
|
+
const base = {
|
|
3645
|
+
movementKind: "transfer",
|
|
3646
|
+
amount: input.amount,
|
|
3647
|
+
paidAmount: input.amount,
|
|
3648
|
+
feeAmount: 0,
|
|
3649
|
+
status: "paid",
|
|
3650
|
+
dueDate: input.date,
|
|
3651
|
+
paymentDate: input.date,
|
|
3652
|
+
notes: input.notes,
|
|
3653
|
+
tenantId,
|
|
3654
|
+
createdAt: now(),
|
|
3655
|
+
updatedAt: now()
|
|
3656
|
+
};
|
|
3657
|
+
store.movements.push({
|
|
3658
|
+
...base,
|
|
3659
|
+
id: debitMovementId,
|
|
3660
|
+
direction: "debit",
|
|
3661
|
+
bankAccountId: input.fromAccountId,
|
|
3662
|
+
metadata: { transferId, transferRole: "out", counterAccountId: input.toAccountId }
|
|
3663
|
+
});
|
|
3664
|
+
store.movements.push({
|
|
3665
|
+
...base,
|
|
3666
|
+
id: creditMovementId,
|
|
3667
|
+
direction: "credit",
|
|
3668
|
+
bankAccountId: input.toAccountId,
|
|
3669
|
+
metadata: { transferId, transferRole: "in", counterAccountId: input.fromAccountId }
|
|
3670
|
+
});
|
|
3671
|
+
const from = store.bankAccounts.find((a) => a.id === input.fromAccountId);
|
|
3672
|
+
const to = store.bankAccounts.find((a) => a.id === input.toAccountId);
|
|
3673
|
+
if (from) {
|
|
3674
|
+
from.currentBalance = round2(from.currentBalance - input.amount);
|
|
3675
|
+
from.updatedAt = now();
|
|
3676
|
+
}
|
|
3677
|
+
if (to) {
|
|
3678
|
+
to.currentBalance = round2(to.currentBalance + input.amount);
|
|
3679
|
+
to.updatedAt = now();
|
|
3680
|
+
}
|
|
3681
|
+
return { transferId, debitMovementId, creditMovementId };
|
|
3682
|
+
},
|
|
3091
3683
|
// --- Bank Accounts ---
|
|
3092
3684
|
async getBankAccounts() {
|
|
3093
3685
|
return store.bankAccounts.filter((a) => a.isActive);
|
|
@@ -3192,25 +3784,76 @@ function createMockFinancialProvider(options) {
|
|
|
3192
3784
|
if (dateRange) results = results.filter((t2) => matchesDateRange(t2.expectedDate, dateRange));
|
|
3193
3785
|
return results.sort((a, b) => a.expectedDate.localeCompare(b.expectedDate));
|
|
3194
3786
|
},
|
|
3195
|
-
// --- Statements ---
|
|
3787
|
+
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
3196
3788
|
async getStatement(query) {
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
const
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3789
|
+
const todayStr = today();
|
|
3790
|
+
const isRealized = (m) => m.status !== "cancelled" && !!m.paymentDate;
|
|
3791
|
+
const consolidated = !query.bankAccountId;
|
|
3792
|
+
const scopeAccounts = consolidated ? store.bankAccounts.filter((a) => a.isActive) : store.bankAccounts.filter((a) => a.id === query.bankAccountId);
|
|
3793
|
+
const accountIds = new Set(scopeAccounts.map((a) => a.id));
|
|
3794
|
+
const accountNameById = new Map(store.bankAccounts.map((a) => [a.id, a.name]));
|
|
3795
|
+
const initialSum = scopeAccounts.reduce((s, a) => s + a.initialBalance, 0);
|
|
3796
|
+
if (!consolidated && accountIds.size === 0) {
|
|
3797
|
+
return { entries: [], openingBalance: 0, closingBalance: 0, totalCredits: 0, totalDebits: 0, totalFees: 0, net: 0, accountId: query.bankAccountId };
|
|
3798
|
+
}
|
|
3799
|
+
const netOf = (m) => m.direction === "credit" ? m.paidAmount - (m.feeAmount ?? 0) : -m.paidAmount;
|
|
3800
|
+
const inScope = store.movements.filter((m) => isRealized(m) && (consolidated || m.bankAccountId && accountIds.has(m.bankAccountId)));
|
|
3801
|
+
const openingBalance = round2(initialSum + inScope.filter((m) => (m.paymentDate ?? "") < query.dateRange.from).reduce((s, m) => s + netOf(m), 0));
|
|
3802
|
+
const rangeMovs = inScope.filter((m) => matchesDateRange(m.paymentDate, query.dateRange)).sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? "") || a.createdAt.localeCompare(b.createdAt));
|
|
3803
|
+
let balance = openingBalance;
|
|
3804
|
+
let totalCredits = 0, totalDebits = 0, totalFees = 0;
|
|
3805
|
+
const entries = rangeMovs.map((movement) => {
|
|
3806
|
+
const gross = movement.paidAmount;
|
|
3807
|
+
const isCredit = movement.direction === "credit";
|
|
3808
|
+
const fee = isCredit ? movement.feeAmount ?? 0 : 0;
|
|
3809
|
+
const net = isCredit ? round2(gross - fee) : gross;
|
|
3810
|
+
const isTransfer = movement.movementKind === "transfer";
|
|
3811
|
+
const meta = movement.metadata ?? {};
|
|
3812
|
+
const entryKind = isTransfer ? meta.transferRole === "in" ? "transfer-in" : "transfer-out" : "movement";
|
|
3813
|
+
const counterAccountId = isTransfer ? meta.counterAccountId : void 0;
|
|
3814
|
+
balance = round2(isCredit ? balance + net : balance - net);
|
|
3815
|
+
if (!(consolidated && isTransfer)) {
|
|
3816
|
+
if (isCredit) totalCredits = round2(totalCredits + net);
|
|
3817
|
+
else totalDebits = round2(totalDebits + net);
|
|
3818
|
+
totalFees = round2(totalFees + fee);
|
|
3209
3819
|
}
|
|
3210
|
-
|
|
3211
|
-
|
|
3820
|
+
return {
|
|
3821
|
+
movement,
|
|
3822
|
+
invoice: movement.invoiceId ? store.invoices.find((i) => i.id === movement.invoiceId) : void 0,
|
|
3823
|
+
runningBalance: balance,
|
|
3824
|
+
gross,
|
|
3825
|
+
fee,
|
|
3826
|
+
net,
|
|
3827
|
+
entryKind,
|
|
3828
|
+
counterAccountId,
|
|
3829
|
+
counterAccountName: counterAccountId ? accountNameById.get(counterAccountId) : void 0
|
|
3830
|
+
};
|
|
3831
|
+
});
|
|
3832
|
+
for (const e of entries) {
|
|
3833
|
+
const mid = e.movement.matchedMovementId;
|
|
3834
|
+
const mm = mid ? store.movements.find((m) => m.id === mid) : void 0;
|
|
3835
|
+
if (!mm) continue;
|
|
3836
|
+
const inv = mm.invoiceId ? store.invoices.find((i) => i.id === mm.invoiceId) : void 0;
|
|
3837
|
+
e.reconciledWith = {
|
|
3838
|
+
movementId: mm.id,
|
|
3839
|
+
description: mm.notes ?? void 0,
|
|
3840
|
+
invoiceId: mm.invoiceId,
|
|
3841
|
+
fiscalNumber: inv?.fiscalNumber,
|
|
3842
|
+
contactName: inv?.contactName
|
|
3843
|
+
};
|
|
3212
3844
|
}
|
|
3213
|
-
|
|
3845
|
+
const accountCurrentBalance = round2(initialSum + inScope.filter((m) => (m.paymentDate ?? "") <= todayStr).reduce((s, m) => s + netOf(m), 0));
|
|
3846
|
+
return {
|
|
3847
|
+
entries,
|
|
3848
|
+
openingBalance,
|
|
3849
|
+
closingBalance: balance,
|
|
3850
|
+
totalCredits,
|
|
3851
|
+
totalDebits,
|
|
3852
|
+
totalFees,
|
|
3853
|
+
net: round2(totalCredits - totalDebits),
|
|
3854
|
+
accountId: query.bankAccountId,
|
|
3855
|
+
accountCurrentBalance
|
|
3856
|
+
};
|
|
3214
3857
|
},
|
|
3215
3858
|
// --- Summary ---
|
|
3216
3859
|
async getSummary(dateRange) {
|
|
@@ -3225,7 +3868,7 @@ function createMockFinancialProvider(options) {
|
|
|
3225
3868
|
const monthStart = todayStr.slice(0, 7) + "-01";
|
|
3226
3869
|
const monthEnd = todayStr;
|
|
3227
3870
|
const monthMovements = store.movements.filter(
|
|
3228
|
-
(m) => m.
|
|
3871
|
+
(m) => m.movementKind === "payment" && matchesDateRange(m.paymentDate, dateRange ?? { from: monthStart, to: monthEnd })
|
|
3229
3872
|
);
|
|
3230
3873
|
const overdueReceivable = pendingReceivable.filter((m) => m.dueDate < todayStr);
|
|
3231
3874
|
const overduePayable = pendingPayable.filter((m) => m.dueDate < todayStr);
|
|
@@ -3240,6 +3883,90 @@ function createMockFinancialProvider(options) {
|
|
|
3240
3883
|
overduePayableCount: overduePayable.length,
|
|
3241
3884
|
overduePayableAmount: overduePayable.reduce((s, m) => s + (m.amount - m.paidAmount), 0)
|
|
3242
3885
|
};
|
|
3886
|
+
},
|
|
3887
|
+
// --- Reconciliation (conciliação) ---
|
|
3888
|
+
async importBankTransactions(input) {
|
|
3889
|
+
let imported = 0, duplicates = 0;
|
|
3890
|
+
for (const line of input.lines) {
|
|
3891
|
+
const dup = store.movements.some((m) => m.externalSource === line.externalSource && m.externalId === line.externalId);
|
|
3892
|
+
if (dup) {
|
|
3893
|
+
duplicates++;
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3896
|
+
store.movements.push({
|
|
3897
|
+
id: uid(),
|
|
3898
|
+
direction: line.direction,
|
|
3899
|
+
movementKind: "payment",
|
|
3900
|
+
amount: line.amount,
|
|
3901
|
+
paidAmount: line.amount,
|
|
3902
|
+
status: "paid",
|
|
3903
|
+
dueDate: line.date,
|
|
3904
|
+
paymentDate: line.date,
|
|
3905
|
+
bankAccountId: line.bankAccountId ?? input.bankAccountId,
|
|
3906
|
+
notes: line.description,
|
|
3907
|
+
externalId: line.externalId,
|
|
3908
|
+
externalSource: line.externalSource,
|
|
3909
|
+
tenantId,
|
|
3910
|
+
createdAt: now(),
|
|
3911
|
+
updatedAt: now()
|
|
3912
|
+
});
|
|
3913
|
+
imported++;
|
|
3914
|
+
}
|
|
3915
|
+
return { imported, duplicates };
|
|
3916
|
+
},
|
|
3917
|
+
async getUnreconciled(query) {
|
|
3918
|
+
let results = store.movements.filter((m) => m.externalSource && !m.reconciledAt);
|
|
3919
|
+
if (query?.bankAccountId) results = results.filter((m) => m.bankAccountId === query.bankAccountId);
|
|
3920
|
+
if (query?.dateRange) results = results.filter((m) => matchesDateRange(m.paymentDate, query.dateRange));
|
|
3921
|
+
return results.sort((a, b) => (b.paymentDate ?? "").localeCompare(a.paymentDate ?? ""));
|
|
3922
|
+
},
|
|
3923
|
+
async suggestReconciliation(bankMovementId) {
|
|
3924
|
+
const line = store.movements.find((m) => m.id === bankMovementId);
|
|
3925
|
+
if (!line) return [];
|
|
3926
|
+
const amount = line.amount;
|
|
3927
|
+
const tol = Math.max(0.01, round2(amount * 0.02));
|
|
3928
|
+
const lineDate = line.paymentDate ?? line.dueDate;
|
|
3929
|
+
return store.movements.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction && m.id !== bankMovementId && Math.abs(m.amount - amount) <= tol).map((m) => {
|
|
3930
|
+
const amtScore = amount > 0 ? Math.max(0, 1 - Math.abs(m.amount - amount) / amount) : 0;
|
|
3931
|
+
const refDate = m.dueDate ?? m.paymentDate;
|
|
3932
|
+
const days = lineDate && refDate ? Math.abs((new Date(lineDate).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
|
|
3933
|
+
const dateScore = Math.max(0, 1 - days / 30);
|
|
3934
|
+
return {
|
|
3935
|
+
movement: m,
|
|
3936
|
+
invoice: m.invoiceId ? store.invoices.find((i) => i.id === m.invoiceId) : void 0,
|
|
3937
|
+
score: round2(0.7 * amtScore + 0.3 * dateScore)
|
|
3938
|
+
};
|
|
3939
|
+
}).sort((a, b) => b.score - a.score).slice(0, 25);
|
|
3940
|
+
},
|
|
3941
|
+
async reconcileMovement(input) {
|
|
3942
|
+
const line = store.movements.find((m) => m.id === input.bankMovementId);
|
|
3943
|
+
if (line) {
|
|
3944
|
+
line.reconciledAt = now();
|
|
3945
|
+
line.matchedMovementId = input.matchedMovementId;
|
|
3946
|
+
line.updatedAt = now();
|
|
3947
|
+
}
|
|
3948
|
+
if (input.matchedMovementId) {
|
|
3949
|
+
const internal = store.movements.find((m) => m.id === input.matchedMovementId);
|
|
3950
|
+
if (internal) {
|
|
3951
|
+
internal.reconciledAt = now();
|
|
3952
|
+
internal.updatedAt = now();
|
|
3953
|
+
}
|
|
3954
|
+
}
|
|
3955
|
+
},
|
|
3956
|
+
async unreconcileMovement(bankMovementId) {
|
|
3957
|
+
const line = store.movements.find((m) => m.id === bankMovementId);
|
|
3958
|
+
if (!line) return;
|
|
3959
|
+
const matchedId = line.matchedMovementId;
|
|
3960
|
+
line.reconciledAt = void 0;
|
|
3961
|
+
line.matchedMovementId = void 0;
|
|
3962
|
+
line.updatedAt = now();
|
|
3963
|
+
if (matchedId) {
|
|
3964
|
+
const internal = store.movements.find((m) => m.id === matchedId);
|
|
3965
|
+
if (internal) {
|
|
3966
|
+
internal.reconciledAt = void 0;
|
|
3967
|
+
internal.updatedAt = now();
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3243
3970
|
}
|
|
3244
3971
|
};
|
|
3245
3972
|
return provider;
|
|
@@ -3319,6 +4046,21 @@ async function fetchInvoiceBalances(pub, ids) {
|
|
|
3319
4046
|
}
|
|
3320
4047
|
return map;
|
|
3321
4048
|
}
|
|
4049
|
+
function round22(n) {
|
|
4050
|
+
return Math.round(n * 100) / 100;
|
|
4051
|
+
}
|
|
4052
|
+
async function getMethodFeePct(pub, paymentMethodId) {
|
|
4053
|
+
if (!paymentMethodId) return 0;
|
|
4054
|
+
const { data } = await pub.from("payment_methods").select("discount_value").eq("id", paymentMethodId).single();
|
|
4055
|
+
return Number(data?.discount_value) || 0;
|
|
4056
|
+
}
|
|
4057
|
+
function genId() {
|
|
4058
|
+
try {
|
|
4059
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
|
|
4060
|
+
} catch {
|
|
4061
|
+
}
|
|
4062
|
+
return "tr-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
|
|
4063
|
+
}
|
|
3322
4064
|
function createSupabaseFinancialProvider() {
|
|
3323
4065
|
const provider = {
|
|
3324
4066
|
// --- Invoices (orders at stage=invoiced/paid/partial/overdue/cancelled with direction) ---
|
|
@@ -3519,16 +4261,38 @@ function createSupabaseFinancialProvider() {
|
|
|
3519
4261
|
const { core, pub } = getClients();
|
|
3520
4262
|
const { data: mov } = await pub.from("financial_movements").select("*").eq("id", input.movementId).single();
|
|
3521
4263
|
if (!mov) throw new Error("Movement not found");
|
|
3522
|
-
const
|
|
3523
|
-
const
|
|
3524
|
-
const
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
4264
|
+
const tenantId = mov.tenant_id ?? getTenantId();
|
|
4265
|
+
const feePct = mov.direction === "credit" ? await getMethodFeePct(pub, input.paymentMethodId) : 0;
|
|
4266
|
+
const thisFee = round22(input.amount * (feePct / 100));
|
|
4267
|
+
const paymentRow = {
|
|
4268
|
+
tenant_id: tenantId,
|
|
4269
|
+
invoice_id: mov.invoice_id,
|
|
4270
|
+
direction: mov.direction,
|
|
4271
|
+
movement_kind: "payment",
|
|
4272
|
+
amount: input.amount,
|
|
4273
|
+
paid_amount: input.amount,
|
|
4274
|
+
fee_amount: thisFee,
|
|
4275
|
+
status: "paid",
|
|
4276
|
+
due_date: mov.due_date,
|
|
4277
|
+
payment_date: input.paymentDate,
|
|
4278
|
+
installment_number: mov.installment_number,
|
|
4279
|
+
payment_method_id: input.paymentMethodId ?? null,
|
|
4280
|
+
payment_method_type_id: input.paymentMethodTypeId ?? null,
|
|
4281
|
+
bank_account_id: input.bankAccountId ?? null,
|
|
4282
|
+
cash_session_id: input.cashSessionId ?? null,
|
|
4283
|
+
card_brand: input.cardBrand ?? null,
|
|
4284
|
+
card_installments: input.cardInstallments ?? null,
|
|
4285
|
+
notes: input.notes ?? null
|
|
4286
|
+
};
|
|
4287
|
+
let { data, error } = await pub.from("financial_movements").insert(paymentRow).select().single();
|
|
4288
|
+
if (error && /fee_amount/i.test(error.message ?? "")) {
|
|
4289
|
+
const { fee_amount, ...rest } = paymentRow;
|
|
4290
|
+
({ data, error } = await pub.from("financial_movements").insert(rest).select().single());
|
|
4291
|
+
}
|
|
4292
|
+
if (error) throw error;
|
|
4293
|
+
const newPaid = round22((mov.paid_amount ?? 0) + input.amount);
|
|
4294
|
+
const billStatus = newPaid >= mov.amount ? "paid" : "partial";
|
|
4295
|
+
await pub.from("financial_movements").update({ paid_amount: newPaid, status: billStatus, payment_date: null, bank_account_id: null }).eq("id", mov.id);
|
|
3532
4296
|
if (mov.invoice_id) {
|
|
3533
4297
|
const { data: allMovs } = await pub.from("financial_movements").select("status, amount, paid_amount").eq("invoice_id", mov.invoice_id).eq("movement_kind", "bill");
|
|
3534
4298
|
const movs = allMovs ?? [];
|
|
@@ -3549,6 +4313,51 @@ function createSupabaseFinancialProvider() {
|
|
|
3549
4313
|
const { pub } = getClients();
|
|
3550
4314
|
await pub.from("financial_movements").update({ status: "cancelled" }).eq("id", id);
|
|
3551
4315
|
},
|
|
4316
|
+
// --- Transfers (between accounts) ---
|
|
4317
|
+
async createTransfer(input) {
|
|
4318
|
+
const { pub } = getClients();
|
|
4319
|
+
if (!input.fromAccountId || !input.toAccountId || input.fromAccountId === input.toAccountId) {
|
|
4320
|
+
throw new Error("Source and destination accounts must differ");
|
|
4321
|
+
}
|
|
4322
|
+
if (!(input.amount > 0)) throw new Error("Amount must be greater than zero");
|
|
4323
|
+
const tenantId = getTenantId();
|
|
4324
|
+
const transferId = genId();
|
|
4325
|
+
const base = {
|
|
4326
|
+
tenant_id: tenantId,
|
|
4327
|
+
movement_kind: "transfer",
|
|
4328
|
+
amount: input.amount,
|
|
4329
|
+
paid_amount: input.amount,
|
|
4330
|
+
fee_amount: 0,
|
|
4331
|
+
status: "paid",
|
|
4332
|
+
due_date: input.date,
|
|
4333
|
+
payment_date: input.date,
|
|
4334
|
+
notes: input.notes ?? null
|
|
4335
|
+
};
|
|
4336
|
+
const rowsToInsert = [
|
|
4337
|
+
{
|
|
4338
|
+
...base,
|
|
4339
|
+
direction: "debit",
|
|
4340
|
+
bank_account_id: input.fromAccountId,
|
|
4341
|
+
metadata: { transferId, transferRole: "out", counterAccountId: input.toAccountId }
|
|
4342
|
+
},
|
|
4343
|
+
{
|
|
4344
|
+
...base,
|
|
4345
|
+
direction: "credit",
|
|
4346
|
+
bank_account_id: input.toAccountId,
|
|
4347
|
+
metadata: { transferId, transferRole: "in", counterAccountId: input.fromAccountId }
|
|
4348
|
+
}
|
|
4349
|
+
];
|
|
4350
|
+
let { data, error } = await pub.from("financial_movements").insert(rowsToInsert).select("id, direction");
|
|
4351
|
+
if (error && /fee_amount/i.test(error.message ?? "")) {
|
|
4352
|
+
const stripped = rowsToInsert.map(({ fee_amount, ...rest }) => rest);
|
|
4353
|
+
({ data, error } = await pub.from("financial_movements").insert(stripped).select("id, direction"));
|
|
4354
|
+
}
|
|
4355
|
+
if (error) throw error;
|
|
4356
|
+
const rows = data ?? [];
|
|
4357
|
+
const debitMovementId = rows.find((r) => r.direction === "debit")?.id ?? "";
|
|
4358
|
+
const creditMovementId = rows.find((r) => r.direction === "credit")?.id ?? "";
|
|
4359
|
+
return { transferId, debitMovementId, creditMovementId };
|
|
4360
|
+
},
|
|
3552
4361
|
// --- Bank Accounts ---
|
|
3553
4362
|
async getBankAccounts() {
|
|
3554
4363
|
const { pub } = getClients();
|
|
@@ -3646,18 +4455,102 @@ function createSupabaseFinancialProvider() {
|
|
|
3646
4455
|
getClients();
|
|
3647
4456
|
return [];
|
|
3648
4457
|
},
|
|
3649
|
-
// --- Statements ---
|
|
4458
|
+
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
3650
4459
|
async getStatement(query) {
|
|
3651
|
-
const { pub } = getClients();
|
|
3652
|
-
const
|
|
3653
|
-
const { data:
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
4460
|
+
const { core, pub } = getClients();
|
|
4461
|
+
const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4462
|
+
const { data: acctRows } = await pub.from("bank_accounts").select("id, name, initial_balance, is_active");
|
|
4463
|
+
const allAccounts = acctRows ?? [];
|
|
4464
|
+
const consolidated = !query.bankAccountId;
|
|
4465
|
+
const scopeAccounts = consolidated ? allAccounts.filter((a) => a.is_active) : allAccounts.filter((a) => a.id === query.bankAccountId);
|
|
4466
|
+
const accountIds = scopeAccounts.map((a) => a.id);
|
|
4467
|
+
const accountNameById = new Map(allAccounts.map((a) => [a.id, a.name]));
|
|
4468
|
+
const initialSum = scopeAccounts.reduce((s, a) => s + (Number(a.initial_balance) || 0), 0);
|
|
4469
|
+
if (!consolidated && accountIds.length === 0) {
|
|
4470
|
+
return { entries: [], openingBalance: 0, closingBalance: 0, totalCredits: 0, totalDebits: 0, totalFees: 0, net: 0, accountId: query.bankAccountId };
|
|
4471
|
+
}
|
|
4472
|
+
const scopeFilter = (qb) => consolidated ? qb : qb.in("bank_account_id", accountIds);
|
|
4473
|
+
const netOf = (r) => r.direction === "credit" ? (Number(r.paid_amount) || 0) - (Number(r.fee_amount) || 0) : -(Number(r.paid_amount) || 0);
|
|
4474
|
+
const { data: priorRows } = await scopeFilter(pub.from("financial_movements").select("*").neq("status", "cancelled").lt("payment_date", query.dateRange.from));
|
|
4475
|
+
const priorNet = (priorRows ?? []).reduce((s, r) => s + netOf(r), 0);
|
|
4476
|
+
const openingBalance = round22(initialSum + priorNet);
|
|
4477
|
+
const { data: movs } = await scopeFilter(pub.from("financial_movements").select("*").neq("status", "cancelled").gte("payment_date", query.dateRange.from).lte("payment_date", query.dateRange.to).order("payment_date").order("created_at"));
|
|
4478
|
+
const invoiceIds = [...new Set((movs ?? []).map((r) => r.invoice_id).filter(Boolean))];
|
|
4479
|
+
const invoiceById = /* @__PURE__ */ new Map();
|
|
4480
|
+
if (invoiceIds.length) {
|
|
4481
|
+
const { data: orders } = await core.from("orders").select("id, party_id, reference_number, metadata").in("id", invoiceIds);
|
|
4482
|
+
for (const o of orders ?? []) {
|
|
4483
|
+
invoiceById.set(o.id, { id: o.id, contactId: o.party_id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number });
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
let balance = openingBalance;
|
|
4487
|
+
let totalCredits = 0, totalDebits = 0, totalFees = 0;
|
|
4488
|
+
const entries = (movs ?? []).map((r) => {
|
|
4489
|
+
const movement = snakeToCamel(r);
|
|
4490
|
+
const gross = Number(r.paid_amount) || 0;
|
|
4491
|
+
const isCredit = r.direction === "credit";
|
|
4492
|
+
const fee = isCredit ? Number(r.fee_amount) || 0 : 0;
|
|
4493
|
+
const net = isCredit ? round22(gross - fee) : gross;
|
|
4494
|
+
const meta = r.metadata ?? {};
|
|
4495
|
+
const isTransfer = r.movement_kind === "transfer";
|
|
4496
|
+
const entryKind = isTransfer ? meta.transferRole === "in" ? "transfer-in" : "transfer-out" : "movement";
|
|
4497
|
+
const counterAccountId = isTransfer ? meta.counterAccountId : void 0;
|
|
4498
|
+
if (isCredit) balance = round22(balance + net);
|
|
4499
|
+
else balance = round22(balance - net);
|
|
4500
|
+
if (!(consolidated && isTransfer)) {
|
|
4501
|
+
if (isCredit) totalCredits = round22(totalCredits + net);
|
|
4502
|
+
else totalDebits = round22(totalDebits + net);
|
|
4503
|
+
totalFees = round22(totalFees + fee);
|
|
4504
|
+
}
|
|
4505
|
+
return {
|
|
4506
|
+
movement,
|
|
4507
|
+
invoice: r.invoice_id ? invoiceById.get(r.invoice_id) : void 0,
|
|
4508
|
+
runningBalance: balance,
|
|
4509
|
+
gross,
|
|
4510
|
+
fee,
|
|
4511
|
+
net,
|
|
4512
|
+
entryKind,
|
|
4513
|
+
counterAccountId,
|
|
4514
|
+
counterAccountName: counterAccountId ? accountNameById.get(counterAccountId) : void 0
|
|
4515
|
+
};
|
|
3660
4516
|
});
|
|
4517
|
+
const matchedIds = [...new Set(entries.map((e) => e.movement.matchedMovementId).filter(Boolean))];
|
|
4518
|
+
if (matchedIds.length) {
|
|
4519
|
+
const { data: matched } = await pub.from("financial_movements").select("id, invoice_id, notes").in("id", matchedIds);
|
|
4520
|
+
const matchedById = new Map((matched ?? []).map((m) => [m.id, m]));
|
|
4521
|
+
const matchedInvoiceIds = [...new Set((matched ?? []).map((m) => m.invoice_id).filter(Boolean))];
|
|
4522
|
+
const invById = /* @__PURE__ */ new Map();
|
|
4523
|
+
if (matchedInvoiceIds.length) {
|
|
4524
|
+
const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", matchedInvoiceIds);
|
|
4525
|
+
for (const o of orders ?? []) invById.set(o.id, o);
|
|
4526
|
+
}
|
|
4527
|
+
for (const e of entries) {
|
|
4528
|
+
const mid = e.movement.matchedMovementId;
|
|
4529
|
+
const mm = mid ? matchedById.get(mid) : void 0;
|
|
4530
|
+
if (!mm) continue;
|
|
4531
|
+
const inv = mm.invoice_id ? invById.get(mm.invoice_id) : void 0;
|
|
4532
|
+
e.reconciledWith = {
|
|
4533
|
+
movementId: mm.id,
|
|
4534
|
+
description: mm.notes ?? void 0,
|
|
4535
|
+
invoiceId: mm.invoice_id ?? void 0,
|
|
4536
|
+
fiscalNumber: inv?.reference_number ?? void 0,
|
|
4537
|
+
contactName: inv?.metadata?.contactName ?? void 0
|
|
4538
|
+
};
|
|
4539
|
+
}
|
|
4540
|
+
}
|
|
4541
|
+
const { data: todayRows } = await scopeFilter(pub.from("financial_movements").select("*").neq("status", "cancelled").lte("payment_date", today2));
|
|
4542
|
+
const accountCurrentBalance = round22(initialSum + (todayRows ?? []).reduce((s, r) => s + netOf(r), 0));
|
|
4543
|
+
return {
|
|
4544
|
+
entries,
|
|
4545
|
+
openingBalance,
|
|
4546
|
+
closingBalance: balance,
|
|
4547
|
+
totalCredits,
|
|
4548
|
+
totalDebits,
|
|
4549
|
+
totalFees,
|
|
4550
|
+
net: round22(totalCredits - totalDebits),
|
|
4551
|
+
accountId: query.bankAccountId,
|
|
4552
|
+
accountCurrentBalance
|
|
4553
|
+
};
|
|
3661
4554
|
},
|
|
3662
4555
|
// --- Summary ---
|
|
3663
4556
|
async getSummary(dateRange) {
|
|
@@ -3674,7 +4567,7 @@ function createSupabaseFinancialProvider() {
|
|
|
3674
4567
|
const overdueCredit = unpaidCredit.filter((m) => m.status === "overdue" || m.due_date < today2);
|
|
3675
4568
|
const overdueDebit = unpaidDebit.filter((m) => m.status === "overdue" || m.due_date < today2);
|
|
3676
4569
|
const monthStart = today2.slice(0, 7) + "-01";
|
|
3677
|
-
const monthPaid = allMovs.filter((m) => m.
|
|
4570
|
+
const monthPaid = allMovs.filter((m) => m.movement_kind === "payment" && m.payment_date >= monthStart);
|
|
3678
4571
|
return {
|
|
3679
4572
|
totalBalance,
|
|
3680
4573
|
totalReceivable: unpaidCredit.reduce((s, m) => s + m.amount - m.paid_amount, 0),
|
|
@@ -3686,6 +4579,93 @@ function createSupabaseFinancialProvider() {
|
|
|
3686
4579
|
overduePayableCount: overdueDebit.length,
|
|
3687
4580
|
overduePayableAmount: overdueDebit.reduce((s, m) => s + m.amount - m.paid_amount, 0)
|
|
3688
4581
|
};
|
|
4582
|
+
},
|
|
4583
|
+
// --- Reconciliation (conciliação) ---
|
|
4584
|
+
// Imported bank-statement lines live in financial_movements tagged with
|
|
4585
|
+
// (external_source, external_id), status='paid'. Reconciling links a line
|
|
4586
|
+
// to the internal movement it settles (or accepts it standalone).
|
|
4587
|
+
async importBankTransactions(input) {
|
|
4588
|
+
const { pub } = getClients();
|
|
4589
|
+
const tenantId = getTenantId();
|
|
4590
|
+
let imported = 0, duplicates = 0;
|
|
4591
|
+
for (const line of input.lines) {
|
|
4592
|
+
const { error } = await pub.from("financial_movements").insert({
|
|
4593
|
+
tenant_id: tenantId,
|
|
4594
|
+
direction: line.direction,
|
|
4595
|
+
movement_kind: "payment",
|
|
4596
|
+
amount: line.amount,
|
|
4597
|
+
paid_amount: line.amount,
|
|
4598
|
+
status: "paid",
|
|
4599
|
+
due_date: line.date,
|
|
4600
|
+
payment_date: line.date,
|
|
4601
|
+
bank_account_id: line.bankAccountId ?? input.bankAccountId ?? null,
|
|
4602
|
+
notes: line.description,
|
|
4603
|
+
external_id: line.externalId,
|
|
4604
|
+
external_source: line.externalSource
|
|
4605
|
+
});
|
|
4606
|
+
if (error) {
|
|
4607
|
+
if (error.code === "23505") duplicates++;
|
|
4608
|
+
else throw error;
|
|
4609
|
+
} else imported++;
|
|
4610
|
+
}
|
|
4611
|
+
return { imported, duplicates };
|
|
4612
|
+
},
|
|
4613
|
+
async getUnreconciled(query) {
|
|
4614
|
+
const { pub } = getClients();
|
|
4615
|
+
let qb = pub.from("financial_movements").select("*").not("external_source", "is", null).is("reconciled_at", null);
|
|
4616
|
+
if (query?.bankAccountId) qb = qb.eq("bank_account_id", query.bankAccountId);
|
|
4617
|
+
if (query?.dateRange) qb = qb.gte("payment_date", query.dateRange.from).lte("payment_date", query.dateRange.to);
|
|
4618
|
+
const { data } = await qb.order("payment_date", { ascending: false });
|
|
4619
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
4620
|
+
},
|
|
4621
|
+
async suggestReconciliation(bankMovementId) {
|
|
4622
|
+
const { core, pub } = getClients();
|
|
4623
|
+
const { data: line } = await pub.from("financial_movements").select("*").eq("id", bankMovementId).single();
|
|
4624
|
+
if (!line) return [];
|
|
4625
|
+
const amount = Number(line.amount) || 0;
|
|
4626
|
+
const lineDate = line.payment_date ?? line.due_date;
|
|
4627
|
+
const tol = Math.max(0.01, round22(amount * 0.02));
|
|
4628
|
+
const { data } = await pub.from("financial_movements").select("*").is("external_source", null).is("reconciled_at", null).eq("direction", line.direction).gte("amount", round22(amount - tol)).lte("amount", round22(amount + tol)).neq("id", bankMovementId).limit(25);
|
|
4629
|
+
const candidates = (data ?? []).map((r) => {
|
|
4630
|
+
const amtDiff = Math.abs((Number(r.amount) || 0) - amount);
|
|
4631
|
+
const amtScore = amount > 0 ? Math.max(0, 1 - amtDiff / amount) : 0;
|
|
4632
|
+
const refDate = r.due_date ?? r.payment_date;
|
|
4633
|
+
const days = lineDate && refDate ? Math.abs((new Date(lineDate).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
|
|
4634
|
+
const dateScore = Math.max(0, 1 - days / 30);
|
|
4635
|
+
return {
|
|
4636
|
+
movement: snakeToCamel(r),
|
|
4637
|
+
score: round22(0.7 * amtScore + 0.3 * dateScore)
|
|
4638
|
+
};
|
|
4639
|
+
}).sort((a, b) => b.score - a.score);
|
|
4640
|
+
const invoiceIds = [...new Set(candidates.map((c) => c.movement.invoiceId).filter(Boolean))];
|
|
4641
|
+
if (invoiceIds.length) {
|
|
4642
|
+
const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", invoiceIds);
|
|
4643
|
+
const byId = new Map((orders ?? []).map((o) => [o.id, o]));
|
|
4644
|
+
for (const c of candidates) {
|
|
4645
|
+
const o = c.movement.invoiceId ? byId.get(c.movement.invoiceId) : void 0;
|
|
4646
|
+
if (o) c.invoice = { id: o.id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number };
|
|
4647
|
+
}
|
|
4648
|
+
}
|
|
4649
|
+
return candidates;
|
|
4650
|
+
},
|
|
4651
|
+
async reconcileMovement(input) {
|
|
4652
|
+
const { pub } = getClients();
|
|
4653
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4654
|
+
await pub.from("financial_movements").update({
|
|
4655
|
+
reconciled_at: now2,
|
|
4656
|
+
matched_movement_id: input.matchedMovementId ?? null
|
|
4657
|
+
}).eq("id", input.bankMovementId);
|
|
4658
|
+
if (input.matchedMovementId) {
|
|
4659
|
+
await pub.from("financial_movements").update({ reconciled_at: now2 }).eq("id", input.matchedMovementId);
|
|
4660
|
+
}
|
|
4661
|
+
},
|
|
4662
|
+
async unreconcileMovement(bankMovementId) {
|
|
4663
|
+
const { pub } = getClients();
|
|
4664
|
+
const { data: line } = await pub.from("financial_movements").select("matched_movement_id").eq("id", bankMovementId).single();
|
|
4665
|
+
await pub.from("financial_movements").update({ reconciled_at: null, matched_movement_id: null }).eq("id", bankMovementId);
|
|
4666
|
+
if (line?.matched_movement_id) {
|
|
4667
|
+
await pub.from("financial_movements").update({ reconciled_at: null }).eq("id", line.matched_movement_id);
|
|
4668
|
+
}
|
|
3689
4669
|
}
|
|
3690
4670
|
};
|
|
3691
4671
|
return provider;
|
|
@@ -3707,8 +4687,9 @@ function createFinancialStore(provider) {
|
|
|
3707
4687
|
paymentMethodTypes: [],
|
|
3708
4688
|
summary: null,
|
|
3709
4689
|
summaryLoading: false,
|
|
3710
|
-
|
|
4690
|
+
statement: null,
|
|
3711
4691
|
statementLoading: false,
|
|
4692
|
+
statementQuery: null,
|
|
3712
4693
|
cardTransactions: [],
|
|
3713
4694
|
cardsLoading: false,
|
|
3714
4695
|
async fetchSummary(dateRange) {
|
|
@@ -3750,9 +4731,9 @@ function createFinancialStore(provider) {
|
|
|
3750
4731
|
},
|
|
3751
4732
|
async fetchStatement(query) {
|
|
3752
4733
|
return saas.dedup("fin:statement:" + JSON.stringify(query), async () => {
|
|
3753
|
-
set({ statementLoading: true });
|
|
3754
|
-
const
|
|
3755
|
-
set({
|
|
4734
|
+
set({ statementLoading: true, statementQuery: query });
|
|
4735
|
+
const statement = await provider.getStatement(query);
|
|
4736
|
+
set({ statement, statementLoading: false });
|
|
3756
4737
|
});
|
|
3757
4738
|
},
|
|
3758
4739
|
async fetchCardTransactions(dateRange) {
|
|
@@ -3788,6 +4769,22 @@ function createFinancialStore(provider) {
|
|
|
3788
4769
|
throw err;
|
|
3789
4770
|
}
|
|
3790
4771
|
},
|
|
4772
|
+
async createTransfer(input) {
|
|
4773
|
+
try {
|
|
4774
|
+
await provider.createTransfer(input);
|
|
4775
|
+
const query = get().statementQuery;
|
|
4776
|
+
const [bankAccounts, summary, statement] = await Promise.all([
|
|
4777
|
+
provider.getBankAccounts(),
|
|
4778
|
+
provider.getSummary(),
|
|
4779
|
+
query ? provider.getStatement(query) : Promise.resolve(get().statement)
|
|
4780
|
+
]);
|
|
4781
|
+
set({ bankAccounts, summary, statement });
|
|
4782
|
+
ui.toast.success("Transfer recorded");
|
|
4783
|
+
} catch (err) {
|
|
4784
|
+
ui.toast.error("Failed to record transfer", { description: err?.message });
|
|
4785
|
+
throw err;
|
|
4786
|
+
}
|
|
4787
|
+
},
|
|
3791
4788
|
async cancelInvoice(invoiceId) {
|
|
3792
4789
|
try {
|
|
3793
4790
|
await provider.cancelInvoice(invoiceId);
|
|
@@ -3849,7 +4846,9 @@ var paymentMethodEntity = {
|
|
|
3849
4846
|
defaultSort: "name",
|
|
3850
4847
|
fields: [
|
|
3851
4848
|
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
3852
|
-
|
|
4849
|
+
// FK to the seeded payment_method_types catalog — load real ids at runtime
|
|
4850
|
+
// (a static enum select would write the string into the uuid column).
|
|
4851
|
+
{ key: "paymentMethodTypeId", label: "Type", type: "relation", relation: { table: "payment_method_types", labelField: "name" }, showInTable: true, required: true },
|
|
3853
4852
|
{ key: "discountValue", label: "Discount %", type: "number", showInTable: true },
|
|
3854
4853
|
{ key: "interestValue", label: "Interest %", type: "number", showInTable: true },
|
|
3855
4854
|
{ key: "minInstallments", label: "Min Installments", type: "number", defaultValue: 1 },
|
|
@@ -4212,6 +5211,21 @@ var en = {
|
|
|
4212
5211
|
"financial.nav.rules": "Rules",
|
|
4213
5212
|
"financial.nav.statements": "Statements",
|
|
4214
5213
|
"financial.nav.summary": "Summary",
|
|
5214
|
+
"financial.reconciliation.title": "Reconciliation",
|
|
5215
|
+
"financial.reconciliation.subtitle": "Match imported bank lines to your movements",
|
|
5216
|
+
"financial.reconciliation.pending": "{{count}} to reconcile",
|
|
5217
|
+
"financial.reconciliation.allDone": "All caught up",
|
|
5218
|
+
"financial.reconciliation.empty": "Nothing to reconcile",
|
|
5219
|
+
"financial.reconciliation.emptyHint": "Imported bank transactions will appear here for matching.",
|
|
5220
|
+
"financial.reconciliation.bankLines": "Bank lines",
|
|
5221
|
+
"financial.reconciliation.candidates": "Suggested matches",
|
|
5222
|
+
"financial.reconciliation.selectLine": "Select a bank line to see suggested matches",
|
|
5223
|
+
"financial.reconciliation.noCandidates": "No suggested matches for this line",
|
|
5224
|
+
"financial.reconciliation.match": "Match",
|
|
5225
|
+
"financial.reconciliation.accept": "Accept as new",
|
|
5226
|
+
"financial.reconciliation.acceptHint": "No internal counterpart \u2014 keep as a standalone entry",
|
|
5227
|
+
"financial.reconciliation.matchScore": "{{score}}% match",
|
|
5228
|
+
"financial.reconciliation.refresh": "Refresh",
|
|
4215
5229
|
"financial.onboarding.back": "Back",
|
|
4216
5230
|
"financial.onboarding.configureHint": "You can configure this in detail from the Settings gear icon inside the Financial module.",
|
|
4217
5231
|
"financial.onboarding.getStarted": "Get Started",
|
|
@@ -4276,6 +5290,8 @@ var en = {
|
|
|
4276
5290
|
"financial.settingsPage.title": "Financial Settings",
|
|
4277
5291
|
"financial.payment.noMethods": "No payment methods configured.",
|
|
4278
5292
|
"financial.payment.configureMethods": "Set up payment methods",
|
|
5293
|
+
"financial.payment.noAccounts": "No compatible account. Create one in Settings.",
|
|
5294
|
+
"financial.payment.configureAccounts": "Set up accounts",
|
|
4279
5295
|
"financial.statements.account": "Account",
|
|
4280
5296
|
"financial.statements.from": "From",
|
|
4281
5297
|
"financial.statements.noTransactions": "No transactions in this period",
|
|
@@ -4411,7 +5427,66 @@ var en = {
|
|
|
4411
5427
|
"financial.cashRegister.close": "Close Register",
|
|
4412
5428
|
"financial.movements.title": "Movements",
|
|
4413
5429
|
"financial.bankAccounts.title": "Bank Accounts",
|
|
4414
|
-
"financial.settings.title": "Financial Settings"
|
|
5430
|
+
"financial.settings.title": "Financial Settings",
|
|
5431
|
+
// Settings registry tabs
|
|
5432
|
+
"registry.payment-methods": "Payment Methods",
|
|
5433
|
+
"registry.payment-methods.description": "Configured payment methods with fees and installment rules",
|
|
5434
|
+
"registry.bank-accounts": "Accounts",
|
|
5435
|
+
"registry.bank-accounts.description": "Bank accounts, cash registers, credit cards, and digital wallets",
|
|
5436
|
+
"registry.chart-of-accounts": "Chart of Accounts",
|
|
5437
|
+
"registry.chart-of-accounts.description": "Hierarchical chart of accounts for categorizing transactions",
|
|
5438
|
+
"registry.cost-centers": "Cost Centers",
|
|
5439
|
+
"registry.cost-centers.description": "Cost centers for expense tracking and departmental budgets",
|
|
5440
|
+
"registry.commission-rules": "Commission Rules",
|
|
5441
|
+
"registry.commission-rules.description": "Commission rules for service and product revenue sharing",
|
|
5442
|
+
"registry.price-tables": "Price Tables",
|
|
5443
|
+
"registry.price-tables.description": "Price tables for sale and cost pricing",
|
|
5444
|
+
"registry.price-variations": "Price Variations",
|
|
5445
|
+
"registry.price-variations.description": "Discount and surcharge rules for pricing",
|
|
5446
|
+
"registry.payment-method-types": "Payment Types",
|
|
5447
|
+
"registry.payment-method-types.description": "System payment types (cash, card, PIX, etc.)",
|
|
5448
|
+
"registry.card-brands": "Card Brands",
|
|
5449
|
+
"registry.card-brands.description": "Credit and debit card brands",
|
|
5450
|
+
// Statements / extract
|
|
5451
|
+
"financial.statements.columnDate": "Date",
|
|
5452
|
+
"financial.statements.columnDescription": "Description",
|
|
5453
|
+
"financial.statements.columnDebit": "Debit",
|
|
5454
|
+
"financial.statements.columnCredit": "Credit",
|
|
5455
|
+
"financial.statements.columnFee": "Fee",
|
|
5456
|
+
"financial.statements.columnBalance": "Balance",
|
|
5457
|
+
"financial.statements.columnMethod": "Method",
|
|
5458
|
+
"financial.statements.columnDoc": "Document",
|
|
5459
|
+
"financial.statements.installmentShort": "Inst. {{n}}",
|
|
5460
|
+
"financial.statements.viewInvoice": "View invoice",
|
|
5461
|
+
"financial.person.empty": "No financial activity for this contact yet",
|
|
5462
|
+
"financial.statements.opening": "Opening balance",
|
|
5463
|
+
"financial.statements.closing": "Closing balance",
|
|
5464
|
+
"financial.statements.currentBalance": "Current balance",
|
|
5465
|
+
"financial.statements.totalCredits": "Credits",
|
|
5466
|
+
"financial.statements.totalDebits": "Debits",
|
|
5467
|
+
"financial.statements.totalFees": "Fees",
|
|
5468
|
+
"financial.statements.net": "Net",
|
|
5469
|
+
"financial.statements.transfer": "Transfer",
|
|
5470
|
+
"financial.statements.reconciled": "Reconciled with the bank statement",
|
|
5471
|
+
"financial.statements.imported": "Imported from the bank \xB7 not reconciled",
|
|
5472
|
+
"financial.statements.reconciledWith": "Reconciled with {{ref}}",
|
|
5473
|
+
"financial.statements.newTransfer": "New Transfer",
|
|
5474
|
+
// Transfer dialog
|
|
5475
|
+
"financial.transfer.title": "Transfer Between Accounts",
|
|
5476
|
+
"financial.transfer.from": "From account",
|
|
5477
|
+
"financial.transfer.to": "To account",
|
|
5478
|
+
"financial.transfer.amount": "Amount",
|
|
5479
|
+
"financial.transfer.date": "Date",
|
|
5480
|
+
"financial.transfer.notes": "Notes",
|
|
5481
|
+
"financial.transfer.optionalNotes": "Optional notes...",
|
|
5482
|
+
"financial.transfer.selectAccount": "Select account...",
|
|
5483
|
+
"financial.transfer.submit": "Transfer",
|
|
5484
|
+
"financial.transfer.processing": "Transferring...",
|
|
5485
|
+
"financial.transfer.cancel": "Cancel",
|
|
5486
|
+
"financial.transfer.created": "Transfer recorded",
|
|
5487
|
+
"financial.transfer.failed": "Failed to record transfer",
|
|
5488
|
+
"financial.transfer.sameAccount": "Source and destination must differ",
|
|
5489
|
+
"financial.transfer.invalidAmount": "Amount must be greater than zero"
|
|
4415
5490
|
};
|
|
4416
5491
|
|
|
4417
5492
|
// src/locales/pt-BR.ts
|
|
@@ -4544,6 +5619,21 @@ var ptBR = {
|
|
|
4544
5619
|
"financial.nav.rules": "Regras",
|
|
4545
5620
|
"financial.nav.statements": "Extratos",
|
|
4546
5621
|
"financial.nav.summary": "Resumo",
|
|
5622
|
+
"financial.reconciliation.title": "Concilia\xE7\xE3o",
|
|
5623
|
+
"financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lan\xE7amentos",
|
|
5624
|
+
"financial.reconciliation.pending": "{{count}} para conciliar",
|
|
5625
|
+
"financial.reconciliation.allDone": "Tudo conciliado",
|
|
5626
|
+
"financial.reconciliation.empty": "Nada para conciliar",
|
|
5627
|
+
"financial.reconciliation.emptyHint": "As transa\xE7\xF5es banc\xE1rias importadas aparecem aqui para concilia\xE7\xE3o.",
|
|
5628
|
+
"financial.reconciliation.bankLines": "Linhas do banco",
|
|
5629
|
+
"financial.reconciliation.candidates": "Correspond\xEAncias sugeridas",
|
|
5630
|
+
"financial.reconciliation.selectLine": "Selecione uma linha do banco para ver as sugest\xF5es",
|
|
5631
|
+
"financial.reconciliation.noCandidates": "Nenhuma correspond\xEAncia sugerida para esta linha",
|
|
5632
|
+
"financial.reconciliation.match": "Conciliar",
|
|
5633
|
+
"financial.reconciliation.accept": "Aceitar como novo",
|
|
5634
|
+
"financial.reconciliation.acceptHint": "Sem contrapartida interna \u2014 manter como lan\xE7amento avulso",
|
|
5635
|
+
"financial.reconciliation.matchScore": "{{score}}% compat\xEDvel",
|
|
5636
|
+
"financial.reconciliation.refresh": "Atualizar",
|
|
4547
5637
|
"financial.onboarding.back": "Voltar",
|
|
4548
5638
|
"financial.onboarding.configureHint": "Voc\xEA pode configurar isso em detalhes pelo \xEDcone de Configura\xE7\xF5es dentro do m\xF3dulo Financeiro.",
|
|
4549
5639
|
"financial.onboarding.getStarted": "Come\xE7ar",
|
|
@@ -4608,6 +5698,8 @@ var ptBR = {
|
|
|
4608
5698
|
"financial.settingsPage.title": "Configura\xE7\xF5es Financeiras",
|
|
4609
5699
|
"financial.payment.noMethods": "Nenhuma forma de pagamento configurada.",
|
|
4610
5700
|
"financial.payment.configureMethods": "Configurar formas de pagamento",
|
|
5701
|
+
"financial.payment.noAccounts": "Nenhuma conta compat\xEDvel. Crie uma nas Configura\xE7\xF5es.",
|
|
5702
|
+
"financial.payment.configureAccounts": "Configurar contas",
|
|
4611
5703
|
"financial.statements.account": "Conta",
|
|
4612
5704
|
"financial.statements.from": "De",
|
|
4613
5705
|
"financial.statements.noTransactions": "Sem transa\xE7\xF5es neste per\xEDodo",
|
|
@@ -4743,7 +5835,66 @@ var ptBR = {
|
|
|
4743
5835
|
"financial.cashRegister.close": "Fechar Caixa",
|
|
4744
5836
|
"financial.movements.title": "Movimenta\xE7\xF5es",
|
|
4745
5837
|
"financial.bankAccounts.title": "Contas Banc\xE1rias",
|
|
4746
|
-
"financial.settings.title": "Configura\xE7\xF5es Financeiras"
|
|
5838
|
+
"financial.settings.title": "Configura\xE7\xF5es Financeiras",
|
|
5839
|
+
// Abas de registros nas configurações
|
|
5840
|
+
"registry.payment-methods": "Formas de Pagamento",
|
|
5841
|
+
"registry.payment-methods.description": "Formas de pagamento configuradas com taxas e regras de parcelamento",
|
|
5842
|
+
"registry.bank-accounts": "Contas",
|
|
5843
|
+
"registry.bank-accounts.description": "Contas banc\xE1rias, caixas, cart\xF5es de cr\xE9dito e carteiras digitais",
|
|
5844
|
+
"registry.chart-of-accounts": "Plano de Contas",
|
|
5845
|
+
"registry.chart-of-accounts.description": "Plano de contas hier\xE1rquico para categorizar transa\xE7\xF5es",
|
|
5846
|
+
"registry.cost-centers": "Centros de Custo",
|
|
5847
|
+
"registry.cost-centers.description": "Centros de custo para controle de despesas e or\xE7amentos por departamento",
|
|
5848
|
+
"registry.commission-rules": "Regras de Comiss\xE3o",
|
|
5849
|
+
"registry.commission-rules.description": "Regras de comiss\xE3o para servi\xE7os e produtos",
|
|
5850
|
+
"registry.price-tables": "Tabelas de Pre\xE7o",
|
|
5851
|
+
"registry.price-tables.description": "Tabelas de pre\xE7o de venda e custo",
|
|
5852
|
+
"registry.price-variations": "Varia\xE7\xF5es de Pre\xE7o",
|
|
5853
|
+
"registry.price-variations.description": "Regras de desconto e acr\xE9scimo de pre\xE7os",
|
|
5854
|
+
"registry.payment-method-types": "Tipos de Pagamento",
|
|
5855
|
+
"registry.payment-method-types.description": "Tipos de pagamento do sistema (dinheiro, cart\xE3o, PIX, etc.)",
|
|
5856
|
+
"registry.card-brands": "Bandeiras de Cart\xE3o",
|
|
5857
|
+
"registry.card-brands.description": "Bandeiras de cart\xE3o de cr\xE9dito e d\xE9bito",
|
|
5858
|
+
// Extrato
|
|
5859
|
+
"financial.statements.columnDate": "Data",
|
|
5860
|
+
"financial.statements.columnDescription": "Descri\xE7\xE3o",
|
|
5861
|
+
"financial.statements.columnDebit": "D\xE9bito",
|
|
5862
|
+
"financial.statements.columnCredit": "Cr\xE9dito",
|
|
5863
|
+
"financial.statements.columnFee": "Taxa",
|
|
5864
|
+
"financial.statements.columnBalance": "Saldo",
|
|
5865
|
+
"financial.statements.columnMethod": "Forma",
|
|
5866
|
+
"financial.statements.columnDoc": "Documento",
|
|
5867
|
+
"financial.statements.installmentShort": "Parc. {{n}}",
|
|
5868
|
+
"financial.statements.viewInvoice": "Ver fatura",
|
|
5869
|
+
"financial.person.empty": "Nenhuma atividade financeira para este contato ainda",
|
|
5870
|
+
"financial.statements.opening": "Saldo inicial",
|
|
5871
|
+
"financial.statements.closing": "Saldo final",
|
|
5872
|
+
"financial.statements.currentBalance": "Saldo atual",
|
|
5873
|
+
"financial.statements.totalCredits": "Cr\xE9ditos",
|
|
5874
|
+
"financial.statements.totalDebits": "D\xE9bitos",
|
|
5875
|
+
"financial.statements.totalFees": "Taxas",
|
|
5876
|
+
"financial.statements.net": "L\xEDquido",
|
|
5877
|
+
"financial.statements.transfer": "Transfer\xEAncia",
|
|
5878
|
+
"financial.statements.reconciled": "Conciliado com o extrato banc\xE1rio",
|
|
5879
|
+
"financial.statements.imported": "Importado do banco \xB7 n\xE3o conciliado",
|
|
5880
|
+
"financial.statements.reconciledWith": "Conciliado com {{ref}}",
|
|
5881
|
+
"financial.statements.newTransfer": "Nova Transfer\xEAncia",
|
|
5882
|
+
// Diálogo de transferência
|
|
5883
|
+
"financial.transfer.title": "Transfer\xEAncia entre Contas",
|
|
5884
|
+
"financial.transfer.from": "Conta de origem",
|
|
5885
|
+
"financial.transfer.to": "Conta de destino",
|
|
5886
|
+
"financial.transfer.amount": "Valor",
|
|
5887
|
+
"financial.transfer.date": "Data",
|
|
5888
|
+
"financial.transfer.notes": "Observa\xE7\xF5es",
|
|
5889
|
+
"financial.transfer.optionalNotes": "Observa\xE7\xF5es opcionais...",
|
|
5890
|
+
"financial.transfer.selectAccount": "Selecione a conta...",
|
|
5891
|
+
"financial.transfer.submit": "Transferir",
|
|
5892
|
+
"financial.transfer.processing": "Transferindo...",
|
|
5893
|
+
"financial.transfer.cancel": "Cancelar",
|
|
5894
|
+
"financial.transfer.created": "Transfer\xEAncia registrada",
|
|
5895
|
+
"financial.transfer.failed": "Falha ao registrar transfer\xEAncia",
|
|
5896
|
+
"financial.transfer.sameAccount": "Origem e destino devem ser diferentes",
|
|
5897
|
+
"financial.transfer.invalidAmount": "O valor deve ser maior que zero"
|
|
4747
5898
|
};
|
|
4748
5899
|
|
|
4749
5900
|
// src/locales/index.ts
|
|
@@ -4751,8 +5902,104 @@ var financialLocales = {
|
|
|
4751
5902
|
en,
|
|
4752
5903
|
"pt-BR": ptBR
|
|
4753
5904
|
};
|
|
4754
|
-
|
|
4755
|
-
|
|
5905
|
+
var STATUS_STYLE = {
|
|
5906
|
+
paid: "bg-success/15 text-success",
|
|
5907
|
+
partial: "bg-warning/15 text-warning",
|
|
5908
|
+
overdue: "bg-destructive/15 text-destructive",
|
|
5909
|
+
open: "bg-muted text-muted-foreground",
|
|
5910
|
+
cancelled: "bg-muted text-muted-foreground line-through"
|
|
5911
|
+
};
|
|
5912
|
+
function PersonStatementWidget({ person, provider, config }) {
|
|
5913
|
+
const t2 = core.useTranslation();
|
|
5914
|
+
const currency = config.currency;
|
|
5915
|
+
const [invoices, setInvoices] = React14.useState([]);
|
|
5916
|
+
const [loading, setLoading] = React14.useState(true);
|
|
5917
|
+
React14.useEffect(() => {
|
|
5918
|
+
let alive = true;
|
|
5919
|
+
if (!person?.id) {
|
|
5920
|
+
setLoading(false);
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5923
|
+
setLoading(true);
|
|
5924
|
+
provider.getInvoices({ contactId: person.id, pageSize: 200 }).then((r) => {
|
|
5925
|
+
if (alive) {
|
|
5926
|
+
setInvoices(r.data);
|
|
5927
|
+
setLoading(false);
|
|
5928
|
+
}
|
|
5929
|
+
}).catch(() => {
|
|
5930
|
+
if (alive) setLoading(false);
|
|
5931
|
+
});
|
|
5932
|
+
return () => {
|
|
5933
|
+
alive = false;
|
|
5934
|
+
};
|
|
5935
|
+
}, [person?.id, provider]);
|
|
5936
|
+
const fmt = (n) => saas.formatCurrency(n, currency);
|
|
5937
|
+
const totals = React14.useMemo(() => {
|
|
5938
|
+
const active = invoices.filter((i) => i.status !== "cancelled");
|
|
5939
|
+
const billed = active.reduce((s, i) => s + (i.totalAmount ?? 0), 0);
|
|
5940
|
+
const paid = active.reduce((s, i) => s + (i.paidAmount ?? 0), 0);
|
|
5941
|
+
return { billed, paid, balance: billed - paid };
|
|
5942
|
+
}, [invoices]);
|
|
5943
|
+
const openInvoice = (inv) => {
|
|
5944
|
+
const base = inv.direction === "credit" ? "receivables" : "payables";
|
|
5945
|
+
window.location.hash = `/financial/${base}/detail/${inv.id}`;
|
|
5946
|
+
};
|
|
5947
|
+
const columns = React14.useMemo(() => [
|
|
5948
|
+
{
|
|
5949
|
+
id: "date",
|
|
5950
|
+
header: t2("financial.invoice.columnDate"),
|
|
5951
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground text-xs", children: row.original.invoiceDate })
|
|
5952
|
+
},
|
|
5953
|
+
{
|
|
5954
|
+
id: "description",
|
|
5955
|
+
header: t2("financial.invoice.columnDescription"),
|
|
5956
|
+
cell: ({ row }) => {
|
|
5957
|
+
const inv = row.original;
|
|
5958
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("button", { onClick: () => openInvoice(inv), className: "text-left", children: [
|
|
5959
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-medium text-primary hover:underline", children: inv.itemsSummary || inv.contactName || t2("financial.invoice.invoice") }),
|
|
5960
|
+
inv.fiscalNumber && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "ml-1.5 text-[10px] text-muted-foreground font-mono", children: inv.fiscalNumber })
|
|
5961
|
+
] });
|
|
5962
|
+
}
|
|
5963
|
+
},
|
|
5964
|
+
{
|
|
5965
|
+
id: "amount",
|
|
5966
|
+
header: t2("financial.invoice.columnAmount"),
|
|
5967
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: `text-right block text-xs ${row.original.direction === "credit" ? "text-success" : "text-destructive"}`, children: fmt(row.original.totalAmount) })
|
|
5968
|
+
},
|
|
5969
|
+
{
|
|
5970
|
+
id: "paid",
|
|
5971
|
+
header: t2("financial.invoice.columnPaid"),
|
|
5972
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-xs text-muted-foreground", children: fmt(row.original.paidAmount) })
|
|
5973
|
+
},
|
|
5974
|
+
{
|
|
5975
|
+
id: "balance",
|
|
5976
|
+
header: t2("financial.statements.columnBalance"),
|
|
5977
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-right block text-xs font-medium", children: fmt((row.original.totalAmount ?? 0) - (row.original.paidAmount ?? 0)) })
|
|
5978
|
+
},
|
|
5979
|
+
{
|
|
5980
|
+
id: "status",
|
|
5981
|
+
header: t2("financial.invoice.columnStatus"),
|
|
5982
|
+
cell: ({ row }) => /* @__PURE__ */ jsxRuntime.jsx("span", { className: `inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-medium capitalize ${STATUS_STYLE[row.original.status] ?? STATUS_STYLE.open}`, children: row.original.status })
|
|
5983
|
+
}
|
|
5984
|
+
], [currency, t2]);
|
|
5985
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-3", children: [
|
|
5986
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
|
|
5987
|
+
/* @__PURE__ */ jsxRuntime.jsx(Stat, { label: t2("financial.invoice.summaryTotal"), value: fmt(totals.billed) }),
|
|
5988
|
+
/* @__PURE__ */ jsxRuntime.jsx(Stat, { label: t2("financial.invoice.summaryPaid"), value: fmt(totals.paid), positive: true }),
|
|
5989
|
+
/* @__PURE__ */ jsxRuntime.jsx(Stat, { label: t2("financial.statements.columnBalance"), value: fmt(totals.balance), accent: true })
|
|
5990
|
+
] }),
|
|
5991
|
+
loading ? /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: [], loading: true, skeletonRows: 5, compact: true }) : invoices.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col items-center justify-center py-10 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
5992
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Wallet, { className: "h-7 w-7 text-muted-foreground mb-2" }),
|
|
5993
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.person.empty") })
|
|
5994
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx(ui.DataTable, { columns, data: invoices, compact: true })
|
|
5995
|
+
] });
|
|
5996
|
+
}
|
|
5997
|
+
function Stat({ label, value, accent, positive }) {
|
|
5998
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `rounded-lg border px-3 py-2 ${accent ? "bg-muted/40" : "bg-card"}`, children: [
|
|
5999
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-[10px] font-medium text-muted-foreground uppercase truncate", children: label }),
|
|
6000
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `text-sm font-semibold mt-0.5 ${positive ? "text-success" : "text-foreground"}`, children: value })
|
|
6001
|
+
] });
|
|
6002
|
+
}
|
|
4756
6003
|
function createSafeFinancialProvider() {
|
|
4757
6004
|
return core.createSafeDataProvider(
|
|
4758
6005
|
() => createSupabaseFinancialProvider(),
|
|
@@ -4793,7 +6040,9 @@ function resolveConfig(options) {
|
|
|
4793
6040
|
cashRegisters: options?.modules?.cashRegisters !== false,
|
|
4794
6041
|
statements: options?.modules?.statements !== false,
|
|
4795
6042
|
commissions: options?.modules?.commissions !== false,
|
|
4796
|
-
cards: options?.modules?.cards !== false
|
|
6043
|
+
cards: options?.modules?.cards !== false,
|
|
6044
|
+
// Opt-in: only shown when an app explicitly enables reconciliation (bank connector present).
|
|
6045
|
+
reconciliation: options?.modules?.reconciliation === true
|
|
4797
6046
|
},
|
|
4798
6047
|
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
4799
6048
|
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
@@ -4815,7 +6064,9 @@ function createFinancialPlugin(options) {
|
|
|
4815
6064
|
const provider = options?.dataProvider ?? createSafeFinancialProvider();
|
|
4816
6065
|
const store = createFinancialStore(provider);
|
|
4817
6066
|
const dashboardWidgets = createFinancialDashboardWidgets({ config, provider, store });
|
|
4818
|
-
const
|
|
6067
|
+
const PersonStatementBound = (props) => React14__default.default.createElement(PersonStatementWidget, { person: props.item, provider, config });
|
|
6068
|
+
PersonStatementBound.displayName = "FinancialPersonStatement";
|
|
6069
|
+
const PageComponent = () => React14__default.default.createElement(FinancialPage, { config, provider, store, registries: financialRegistries });
|
|
4819
6070
|
return {
|
|
4820
6071
|
id: "financial",
|
|
4821
6072
|
name: config.labels.pageTitle,
|
|
@@ -4848,7 +6099,15 @@ function createFinancialPlugin(options) {
|
|
|
4848
6099
|
permission: { feature: "financial", action: "read" }
|
|
4849
6100
|
}
|
|
4850
6101
|
],
|
|
4851
|
-
widgets: [
|
|
6102
|
+
widgets: [
|
|
6103
|
+
{
|
|
6104
|
+
id: "financial-person-statement",
|
|
6105
|
+
zone: "person.detail.financial",
|
|
6106
|
+
component: PersonStatementBound,
|
|
6107
|
+
order: 0,
|
|
6108
|
+
permission: { feature: "financial", action: "read" }
|
|
6109
|
+
}
|
|
6110
|
+
],
|
|
4852
6111
|
dashboardWidgets,
|
|
4853
6112
|
aiTools: [
|
|
4854
6113
|
{
|
|
@@ -4919,7 +6178,14 @@ function createFinancialPlugin(options) {
|
|
|
4919
6178
|
label: "Financial",
|
|
4920
6179
|
icon: "DollarSign",
|
|
4921
6180
|
component: (() => {
|
|
4922
|
-
const FinancialSettingsTab = () =>
|
|
6181
|
+
const FinancialSettingsTab = () => React14__default.default.createElement(saas.PluginSettingsPanel, {
|
|
6182
|
+
title: "Financial Settings",
|
|
6183
|
+
subtitle: "Payment methods, accounts, and configurations",
|
|
6184
|
+
generalSettings: React14__default.default.createElement(FinancialGeneralSettings),
|
|
6185
|
+
registries: financialRegistries,
|
|
6186
|
+
hostPluginId: "financial",
|
|
6187
|
+
routeBase: "/settings/financial"
|
|
6188
|
+
});
|
|
4923
6189
|
FinancialSettingsTab.displayName = "FinancialSettingsTab";
|
|
4924
6190
|
return FinancialSettingsTab;
|
|
4925
6191
|
})(),
|