@fayz-ai/plugin-financial 0.1.2 → 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/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.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import React14, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
2
2
|
import { createSafeDataProvider, registerTranslations, useTranslation, getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
|
|
3
|
-
import { toast, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, PageTransition, ModulePage, Button, DashboardCanvas, SubpageHeader, DatePicker,
|
|
4
|
-
import { createPluginContext, dedup, useModuleNavigation, ModuleActionBar, SettingsGroup, ToggleRow,
|
|
3
|
+
import { toast, defineKpiWidget, defineChartWidget, defineCustomWidget, defineTableWidget, DataTable, PageTransition, ModulePage, Button, DashboardCanvas, SubpageHeader, DatePicker, TooltipProvider, Tooltip, TooltipTrigger, TooltipContent, CurrencyInput, useSaveBar, SearchSelect, ListView, KpiCard, ChartWidget, Card, CardHeader, CardTitle, CardContent, TableWidget } from '@fayz-ai/ui';
|
|
4
|
+
import { createPluginContext, PluginSettingsPanel, dedup, useModuleNavigation, ModuleActionBar, SettingsGroup, ToggleRow, formatCurrency, parseViewId, PersonLink } from '@fayz-ai/saas';
|
|
5
5
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
6
|
-
import { DollarSign, Landmark, CreditCard, BookOpen, Check, ChevronRight, Receipt, ChevronDown, Clock, X, Calculator, StickyNote, Play, Square, ChevronUp, MoreVertical, Ban, LayoutList, Plus, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, FileText, User, Calendar, Hash, CalendarDays, ExternalLink, Building2, Search, Tag, Trash2, FileCheck,
|
|
6
|
+
import { Wallet, DollarSign, Landmark, CreditCard, BookOpen, Check, ChevronRight, RefreshCw, ArrowDownCircle, ArrowUpCircle, Sparkles, Link2, ArrowRightLeft, Receipt, ChevronDown, Clock, CheckCheck, X, Calculator, StickyNote, Play, Square, ChevronUp, MoreVertical, Ban, LayoutList, Plus, CircleDashed, CircleAlert, CircleCheckBig, CircleEllipsis, Pencil, FileText, User, Calendar, Hash, CalendarDays, ExternalLink, Building2, Search, Tag, Trash2, FileCheck, QrCode, Banknote, ArrowUpRight, ArrowDownRight, AlertTriangle } from 'lucide-react';
|
|
7
7
|
import { createPortal } from 'react-dom';
|
|
8
8
|
import { createStore as createStore$1 } from 'zustand/vanilla';
|
|
9
9
|
|
|
@@ -249,12 +249,18 @@ var formId = 1;
|
|
|
249
249
|
function fid() {
|
|
250
250
|
return `f${formId++}`;
|
|
251
251
|
}
|
|
252
|
-
function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdate, onRemove, entityLookups }) {
|
|
252
|
+
function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdate, onRemove, entityLookups, showTypeSelector }) {
|
|
253
253
|
const t2 = useTranslation();
|
|
254
254
|
const total = item.quantity * item.unitPrice - (item.discount ?? 0) + (item.surcharge ?? 0);
|
|
255
255
|
const kindLabel = itemTypes.find((tp) => tp.value === item.itemKind)?.label ?? item.itemKind;
|
|
256
256
|
const hasAdjustments = (item.discount ?? 0) > 0 || (item.surcharge ?? 0) > 0;
|
|
257
257
|
const [showAdjustments, setShowAdjustments] = useState(hasAdjustments);
|
|
258
|
+
function handleFieldKeyDown(e) {
|
|
259
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
260
|
+
e.preventDefault();
|
|
261
|
+
onToggle();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
258
264
|
return /* @__PURE__ */ jsxs("div", { className: "border-b last:border-0", children: [
|
|
259
265
|
/* @__PURE__ */ jsxs(
|
|
260
266
|
"div",
|
|
@@ -291,7 +297,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
291
297
|
}
|
|
292
298
|
),
|
|
293
299
|
expanded && /* @__PURE__ */ jsxs("div", { className: "px-4 pb-4 pt-1 ml-8 space-y-3 bg-muted/10", children: [
|
|
294
|
-
/* @__PURE__ */ jsx("div", { className: "flex gap-1.5", children: itemTypes.map((tp) => /* @__PURE__ */ jsx(
|
|
300
|
+
showTypeSelector && /* @__PURE__ */ jsx("div", { className: "flex gap-1.5", children: itemTypes.map((tp) => /* @__PURE__ */ jsx(
|
|
295
301
|
"button",
|
|
296
302
|
{
|
|
297
303
|
onClick: () => onUpdate({ itemKind: tp.value }),
|
|
@@ -340,6 +346,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
340
346
|
type: "text",
|
|
341
347
|
value: item.description,
|
|
342
348
|
onChange: (e) => onUpdate({ description: e.target.value }),
|
|
349
|
+
onKeyDown: handleFieldKeyDown,
|
|
343
350
|
placeholder: t2("financial.invoiceForm.itemDescription"),
|
|
344
351
|
autoFocus: true,
|
|
345
352
|
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"
|
|
@@ -355,11 +362,12 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
355
362
|
min: 1,
|
|
356
363
|
value: item.quantity,
|
|
357
364
|
onChange: (e) => onUpdate({ quantity: Number(e.target.value) || 1 }),
|
|
365
|
+
onKeyDown: handleFieldKeyDown,
|
|
358
366
|
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"
|
|
359
367
|
}
|
|
360
368
|
)
|
|
361
369
|
] }),
|
|
362
|
-
/* @__PURE__ */ jsx("div", { className: "col-span-4", children: /* @__PURE__ */ jsx(
|
|
370
|
+
/* @__PURE__ */ jsx("div", { className: "col-span-4", onKeyDown: handleFieldKeyDown, children: /* @__PURE__ */ jsx(
|
|
363
371
|
CurrencyInput,
|
|
364
372
|
{
|
|
365
373
|
label: t2("financial.invoiceForm.unitPrice"),
|
|
@@ -395,6 +403,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
395
403
|
step: 0.01,
|
|
396
404
|
value: item.discount ?? 0,
|
|
397
405
|
onChange: (e) => onUpdate({ discount: Number(e.target.value) || 0 }),
|
|
406
|
+
onKeyDown: handleFieldKeyDown,
|
|
398
407
|
className: "flex-1 bg-transparent px-2 py-1 text-xs outline-none"
|
|
399
408
|
}
|
|
400
409
|
)
|
|
@@ -412,6 +421,7 @@ function ItemRow({ item, index, itemTypes, currency, expanded, onToggle, onUpdat
|
|
|
412
421
|
step: 0.01,
|
|
413
422
|
value: item.surcharge ?? 0,
|
|
414
423
|
onChange: (e) => onUpdate({ surcharge: Number(e.target.value) || 0 }),
|
|
424
|
+
onKeyDown: handleFieldKeyDown,
|
|
415
425
|
className: "flex-1 bg-transparent px-2 py-1 text-xs outline-none"
|
|
416
426
|
}
|
|
417
427
|
)
|
|
@@ -475,6 +485,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
475
485
|
const config = useFinancialConfig();
|
|
476
486
|
const { currency, itemTypes, locations } = config;
|
|
477
487
|
const provider = useFinancialProvider();
|
|
488
|
+
const hasItemLookups = itemTypes.some((tp) => config.entityLookups[tp.value]);
|
|
489
|
+
const defaultItemKind = itemTypes.find((tp) => tp.value === "other")?.value ?? itemTypes.find((tp) => !config.entityLookups[tp.value])?.value ?? itemTypes[0]?.value ?? "other";
|
|
478
490
|
const createInvoice = useFinancialStore((s) => s.createInvoice);
|
|
479
491
|
const hasLocations = locations.length > 0;
|
|
480
492
|
const isEdit = !!editId;
|
|
@@ -585,6 +597,10 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
585
597
|
setExpandedItemId(newItem._id);
|
|
586
598
|
setAddingItem(false);
|
|
587
599
|
}
|
|
600
|
+
function startAddItem() {
|
|
601
|
+
if (hasItemLookups) setAddingItem(true);
|
|
602
|
+
else addItemOfKind(defaultItemKind);
|
|
603
|
+
}
|
|
588
604
|
function updateItem(id, data) {
|
|
589
605
|
setItems(items.map((it) => it._id === id ? { ...it, ...data } : it));
|
|
590
606
|
}
|
|
@@ -804,7 +820,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
804
820
|
/* @__PURE__ */ jsxs(
|
|
805
821
|
"button",
|
|
806
822
|
{
|
|
807
|
-
onClick:
|
|
823
|
+
onClick: startAddItem,
|
|
808
824
|
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",
|
|
809
825
|
children: [
|
|
810
826
|
/* @__PURE__ */ jsx(Plus, { className: "h-3 w-3" }),
|
|
@@ -826,7 +842,7 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
826
842
|
items.length === 0 && !addingItem ? /* @__PURE__ */ jsxs("div", { className: "py-10 text-center", children: [
|
|
827
843
|
/* @__PURE__ */ jsx(LayoutList, { className: "h-8 w-8 text-muted-foreground/30 mx-auto mb-2" }),
|
|
828
844
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.invoiceForm.noItems") }),
|
|
829
|
-
/* @__PURE__ */ jsx("button", { onClick:
|
|
845
|
+
/* @__PURE__ */ jsx("button", { onClick: startAddItem, className: "text-xs text-primary hover:underline mt-1", children: t2("financial.invoiceForm.addFirstItem") })
|
|
830
846
|
] }) : items.map((item, i) => /* @__PURE__ */ jsx(
|
|
831
847
|
ItemRow,
|
|
832
848
|
{
|
|
@@ -841,7 +857,8 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
841
857
|
removeItem(item._id);
|
|
842
858
|
if (expandedItemId === item._id) setExpandedItemId(null);
|
|
843
859
|
},
|
|
844
|
-
entityLookups: config.entityLookups
|
|
860
|
+
entityLookups: config.entityLookups,
|
|
861
|
+
showTypeSelector: hasItemLookups
|
|
845
862
|
},
|
|
846
863
|
item._id
|
|
847
864
|
))
|
|
@@ -937,6 +954,15 @@ function InvoiceFormView({ direction, editId, onSaved }) {
|
|
|
937
954
|
` })
|
|
938
955
|
] });
|
|
939
956
|
}
|
|
957
|
+
var ACCOUNT_TYPES_FOR_TX = {
|
|
958
|
+
cash: ["cash_register"],
|
|
959
|
+
credit_card: ["credit_card", "bank_account"],
|
|
960
|
+
debit_card: ["bank_account", "cash_register"],
|
|
961
|
+
pix: ["bank_account", "digital_wallet"],
|
|
962
|
+
bank_transfer: ["bank_account"],
|
|
963
|
+
check: ["bank_account"],
|
|
964
|
+
voucher: ["bank_account", "digital_wallet"]
|
|
965
|
+
};
|
|
940
966
|
var TYPE_ICONS = {
|
|
941
967
|
cash: Banknote,
|
|
942
968
|
pix: QrCode,
|
|
@@ -996,9 +1022,19 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
996
1022
|
() => paymentMethods.filter((m) => m.paymentMethodTypeId === selectedTypeId),
|
|
997
1023
|
[paymentMethods, selectedTypeId]
|
|
998
1024
|
);
|
|
999
|
-
const needsBankAccount = txType === "bank_transfer" || txType === "pix" || txType === "check";
|
|
1000
1025
|
const needsCard = txType === "credit_card" || txType === "debit_card";
|
|
1001
1026
|
const needsCardInstallments = txType === "credit_card";
|
|
1027
|
+
const eligibleAccounts = useMemo(() => {
|
|
1028
|
+
const allowed = selectedType?.allowedAccountTypes && selectedType.allowedAccountTypes.length ? selectedType.allowedAccountTypes : ACCOUNT_TYPES_FOR_TX[txType];
|
|
1029
|
+
if (!allowed || !allowed.length) return bankAccounts;
|
|
1030
|
+
const filtered = bankAccounts.filter((a) => allowed.includes(a.accountType));
|
|
1031
|
+
return filtered.length ? filtered : bankAccounts;
|
|
1032
|
+
}, [bankAccounts, selectedType, txType]);
|
|
1033
|
+
useEffect(() => {
|
|
1034
|
+
if (selectedTypeId && !bankAccountId && eligibleAccounts.length) {
|
|
1035
|
+
setBankAccountId(eligibleAccounts[0].id);
|
|
1036
|
+
}
|
|
1037
|
+
}, [selectedTypeId, eligibleAccounts]);
|
|
1002
1038
|
function handleSelectType(typeId) {
|
|
1003
1039
|
setSelectedTypeId(typeId);
|
|
1004
1040
|
setExpandMethodPicker(false);
|
|
@@ -1011,8 +1047,14 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1011
1047
|
setVisible(false);
|
|
1012
1048
|
setTimeout(onClose, 200);
|
|
1013
1049
|
}
|
|
1050
|
+
function handleKeyDown(e) {
|
|
1051
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
1052
|
+
if (e.target.tagName === "TEXTAREA") return;
|
|
1053
|
+
e.preventDefault();
|
|
1054
|
+
if (amount > 0 && selectedTypeId && bankAccountId && !saving) void handlePay();
|
|
1055
|
+
}
|
|
1014
1056
|
async function handlePay() {
|
|
1015
|
-
if (amount <= 0 || !selectedTypeId) return;
|
|
1057
|
+
if (amount <= 0 || !selectedTypeId || !bankAccountId) return;
|
|
1016
1058
|
setSaving(true);
|
|
1017
1059
|
try {
|
|
1018
1060
|
await payMovement({
|
|
@@ -1023,7 +1065,8 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1023
1065
|
paymentMethodTypeId: selectedTypeId || void 0,
|
|
1024
1066
|
bankAccountId: bankAccountId || void 0,
|
|
1025
1067
|
cardBrand: needsCard ? cardBrand || void 0 : void 0,
|
|
1026
|
-
cardInstallments: needsCardInstallments ? cardInstallments : void 0
|
|
1068
|
+
cardInstallments: needsCardInstallments ? cardInstallments : void 0,
|
|
1069
|
+
notes: notes.trim() || void 0
|
|
1027
1070
|
});
|
|
1028
1071
|
setVisible(false);
|
|
1029
1072
|
setTimeout(onPaid, 200);
|
|
@@ -1049,6 +1092,7 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1049
1092
|
transform: visible ? "scale(1) translateY(0)" : "scale(0.95) translateY(8px)"
|
|
1050
1093
|
},
|
|
1051
1094
|
onClick: (e) => e.stopPropagation(),
|
|
1095
|
+
onKeyDown: handleKeyDown,
|
|
1052
1096
|
children: [
|
|
1053
1097
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b sticky top-0 bg-card z-10", children: [
|
|
1054
1098
|
/* @__PURE__ */ jsxs("div", { children: [
|
|
@@ -1158,11 +1202,17 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1158
1202
|
] }, n)) })
|
|
1159
1203
|
] })
|
|
1160
1204
|
] }),
|
|
1161
|
-
|
|
1205
|
+
/* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "100ms" }, children: [
|
|
1162
1206
|
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.payment.bankAccount") }),
|
|
1163
|
-
/* @__PURE__ */ jsxs("
|
|
1207
|
+
eligibleAccounts.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "mt-1 rounded-lg border border-dashed border-input bg-muted/30 px-3 py-2.5 text-center", children: [
|
|
1208
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t2("financial.payment.noAccounts") }),
|
|
1209
|
+
/* @__PURE__ */ jsxs("a", { href: "#/settings/financial/bank-accounts", className: "mt-1 inline-block text-xs font-medium text-primary hover:underline", children: [
|
|
1210
|
+
t2("financial.payment.configureAccounts"),
|
|
1211
|
+
" \u2192"
|
|
1212
|
+
] })
|
|
1213
|
+
] }) : /* @__PURE__ */ 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: [
|
|
1164
1214
|
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.payment.selectAccount") }),
|
|
1165
|
-
|
|
1215
|
+
eligibleAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
1166
1216
|
] })
|
|
1167
1217
|
] }),
|
|
1168
1218
|
/* @__PURE__ */ jsxs("div", { className: "stagger-field", style: { animationDelay: "150ms" }, children: [
|
|
@@ -1188,7 +1238,7 @@ function PaymentModal({ movement, onClose, onPaid }) {
|
|
|
1188
1238
|
variant: "default",
|
|
1189
1239
|
size: "sm",
|
|
1190
1240
|
onClick: handlePay,
|
|
1191
|
-
disabled: amount <= 0 || !selectedTypeId || saving,
|
|
1241
|
+
disabled: amount <= 0 || !selectedTypeId || !bankAccountId || saving,
|
|
1192
1242
|
children: [
|
|
1193
1243
|
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
|
|
1194
1244
|
saving ? t2("financial.payment.processing") : t2("financial.payment.payAmount", { amount: formatCurrency(amount, currency) })
|
|
@@ -1213,10 +1263,11 @@ var STATUS_COLORS = {
|
|
|
1213
1263
|
draft: { bg: "bg-muted text-muted-foreground", icon: CircleDashed, labelKey: "financial.invoice.statusDraft" },
|
|
1214
1264
|
cancelled: { bg: "bg-muted text-muted-foreground", icon: Ban, labelKey: "financial.invoice.statusCancelled" }
|
|
1215
1265
|
};
|
|
1216
|
-
function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
1266
|
+
function InvoiceDetailView({ invoiceId, direction, onBack, onEdit, backLabel }) {
|
|
1217
1267
|
const t2 = useTranslation();
|
|
1218
1268
|
const config = useFinancialConfig();
|
|
1219
1269
|
const { currency } = config;
|
|
1270
|
+
const parentLabel = backLabel ?? (direction === "debit" ? t2("financial.nav.payables") : t2("financial.nav.receivables"));
|
|
1220
1271
|
const provider = useFinancialProvider();
|
|
1221
1272
|
const paymentMethods = useFinancialStore((s) => s.paymentMethods);
|
|
1222
1273
|
const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes);
|
|
@@ -1228,6 +1279,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1228
1279
|
const [invoice, setInvoice] = useState(null);
|
|
1229
1280
|
const [items, setItems] = useState([]);
|
|
1230
1281
|
const [movements, setMovements] = useState([]);
|
|
1282
|
+
const [payments, setPayments] = useState([]);
|
|
1231
1283
|
const [loading, setLoading] = useState(true);
|
|
1232
1284
|
const [payingMovement, setPayingMovement] = useState(null);
|
|
1233
1285
|
const [expandedMovId, setExpandedMovId] = useState(null);
|
|
@@ -1248,6 +1300,9 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1248
1300
|
setMovements(
|
|
1249
1301
|
movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "bill").sort((a, b) => (a.installmentNumber ?? 0) - (b.installmentNumber ?? 0))
|
|
1250
1302
|
);
|
|
1303
|
+
setPayments(
|
|
1304
|
+
movResult.data.filter((m) => m.invoiceId === invoiceId && m.movementKind === "payment").sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? ""))
|
|
1305
|
+
);
|
|
1251
1306
|
setLoading(false);
|
|
1252
1307
|
}
|
|
1253
1308
|
useEffect(() => {
|
|
@@ -1287,7 +1342,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1287
1342
|
}
|
|
1288
1343
|
if (loading) {
|
|
1289
1344
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-5", children: [
|
|
1290
|
-
/* @__PURE__ */ jsx(SubpageHeader, { title: "", onBack, parentLabel
|
|
1345
|
+
/* @__PURE__ */ jsx(SubpageHeader, { title: "", onBack, parentLabel }),
|
|
1291
1346
|
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-card shadow-sm overflow-hidden", children: [
|
|
1292
1347
|
/* @__PURE__ */ jsx("div", { className: "h-1 bg-muted animate-pulse" }),
|
|
1293
1348
|
/* @__PURE__ */ jsxs("div", { className: "p-5 pb-4", children: [
|
|
@@ -1341,7 +1396,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1341
1396
|
}
|
|
1342
1397
|
if (!invoice) {
|
|
1343
1398
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
|
|
1344
|
-
/* @__PURE__ */ jsx(SubpageHeader, { title: t2("financial.invoice.notFound"), onBack, parentLabel
|
|
1399
|
+
/* @__PURE__ */ jsx(SubpageHeader, { title: t2("financial.invoice.notFound"), onBack, parentLabel }),
|
|
1345
1400
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.invoice.notFoundDesc") })
|
|
1346
1401
|
] });
|
|
1347
1402
|
}
|
|
@@ -1388,7 +1443,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1388
1443
|
{
|
|
1389
1444
|
title: invoice.fiscalNumber ? `#${invoice.fiscalNumber}` : invoice.contactName ?? invoice.invoiceDate,
|
|
1390
1445
|
onBack,
|
|
1391
|
-
parentLabel
|
|
1446
|
+
parentLabel,
|
|
1392
1447
|
actions: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
1393
1448
|
invoice.status !== "cancelled" && invoice.status !== "paid" && /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onEdit, children: [
|
|
1394
1449
|
/* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" }),
|
|
@@ -1530,9 +1585,7 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1530
1585
|
const isExpanded = expandedMovId === mov.id;
|
|
1531
1586
|
const movStatus = STATUS_COLORS[mov.status] ?? STATUS_COLORS.pending;
|
|
1532
1587
|
const MovIcon = movStatus.icon;
|
|
1533
|
-
const
|
|
1534
|
-
const methodType = paymentMethodTypes.find((t3) => t3.id === (method?.paymentMethodTypeId ?? mov.paymentMethodTypeId));
|
|
1535
|
-
const account = bankAccounts.find((a) => a.id === mov.bankAccountId);
|
|
1588
|
+
const instPayments = payments.filter((p) => (p.installmentNumber ?? null) === (mov.installmentNumber ?? null));
|
|
1536
1589
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
1537
1590
|
/* @__PURE__ */ jsxs(
|
|
1538
1591
|
"div",
|
|
@@ -1606,43 +1659,53 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1606
1659
|
/* @__PURE__ */ jsx("div", { className: "h-3 w-20 rounded bg-muted/40 animate-pulse" }),
|
|
1607
1660
|
/* @__PURE__ */ jsx("div", { className: "h-3 w-16 rounded bg-muted/30 animate-pulse" }),
|
|
1608
1661
|
/* @__PURE__ */ jsx("div", { className: "h-3 w-24 rounded bg-muted/40 animate-pulse" })
|
|
1609
|
-
] }) : /* @__PURE__ */
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1662
|
+
] }) : instPayments.length === 0 ? /* @__PURE__ */ jsx("div", { className: "text-[11px] text-muted-foreground", children: t2("financial.invoice.noDetails") }) : /* @__PURE__ */ jsx("div", { className: "space-y-1", children: instPayments.map((p) => {
|
|
1663
|
+
const pMethod = paymentMethods.find((m) => m.id === p.paymentMethodId);
|
|
1664
|
+
const pType = paymentMethodTypes.find((tp) => tp.id === (pMethod?.paymentMethodTypeId ?? p.paymentMethodTypeId));
|
|
1665
|
+
const pAccount = bankAccounts.find((a) => a.id === p.bankAccountId);
|
|
1666
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-1 gap-y-0.5 text-[11px] text-muted-foreground", children: [
|
|
1667
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-success", children: formatCurrency(p.paidAmount, currency) }),
|
|
1668
|
+
pType && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1669
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1670
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: pType.name }),
|
|
1671
|
+
pMethod && /* @__PURE__ */ jsxs("span", { children: [
|
|
1672
|
+
"(",
|
|
1673
|
+
pMethod.name,
|
|
1674
|
+
")"
|
|
1675
|
+
] })
|
|
1676
|
+
] }),
|
|
1677
|
+
p.cardBrand && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1678
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1679
|
+
/* @__PURE__ */ jsx(CreditCard, { className: "h-3 w-3 shrink-0" }),
|
|
1680
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium text-foreground", children: p.cardBrand }),
|
|
1681
|
+
p.cardInstallments && /* @__PURE__ */ jsxs("span", { children: [
|
|
1682
|
+
p.cardInstallments,
|
|
1683
|
+
"x"
|
|
1684
|
+
] })
|
|
1685
|
+
] }),
|
|
1686
|
+
pAccount && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1687
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1688
|
+
/* @__PURE__ */ jsx(Building2, { className: "h-3 w-3 shrink-0" }),
|
|
1689
|
+
/* @__PURE__ */ jsx("span", { children: pAccount.name })
|
|
1690
|
+
] }),
|
|
1691
|
+
(p.feeAmount ?? 0) > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1692
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1693
|
+
/* @__PURE__ */ jsxs("span", { className: "text-warning", children: [
|
|
1694
|
+
t2("financial.statements.columnFee"),
|
|
1695
|
+
" ",
|
|
1696
|
+
formatCurrency(p.feeAmount ?? 0, currency)
|
|
1697
|
+
] })
|
|
1698
|
+
] }),
|
|
1699
|
+
p.paymentDate && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1700
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1701
|
+
/* @__PURE__ */ jsx("span", { children: p.paymentDate })
|
|
1702
|
+
] }),
|
|
1703
|
+
p.notes && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1704
|
+
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1705
|
+
/* @__PURE__ */ jsx("span", { className: "italic truncate max-w-[200px]", children: p.notes })
|
|
1625
1706
|
] })
|
|
1626
|
-
] })
|
|
1627
|
-
|
|
1628
|
-
(methodType || mov.cardBrand) && /* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1629
|
-
/* @__PURE__ */ jsx(Building2, { className: "h-3 w-3 shrink-0" }),
|
|
1630
|
-
/* @__PURE__ */ jsx("span", { children: account.name })
|
|
1631
|
-
] }),
|
|
1632
|
-
mov.paidAmount > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1633
|
-
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1634
|
-
/* @__PURE__ */ jsx("span", { className: "font-medium text-success", children: formatCurrency(mov.paidAmount, currency) })
|
|
1635
|
-
] }),
|
|
1636
|
-
mov.paymentDate && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1637
|
-
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1638
|
-
/* @__PURE__ */ jsx("span", { children: mov.paymentDate })
|
|
1639
|
-
] }),
|
|
1640
|
-
mov.notes && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1641
|
-
/* @__PURE__ */ jsx("span", { children: "\xB7" }),
|
|
1642
|
-
/* @__PURE__ */ jsx("span", { className: "italic truncate max-w-[200px]", children: mov.notes })
|
|
1643
|
-
] }),
|
|
1644
|
-
!methodType && !mov.cardBrand && !account && !mov.notes && /* @__PURE__ */ jsx("span", { children: t2("financial.invoice.noDetails") })
|
|
1645
|
-
] }) }) })
|
|
1707
|
+
] }, p.id);
|
|
1708
|
+
}) }) }) })
|
|
1646
1709
|
}
|
|
1647
1710
|
)
|
|
1648
1711
|
] }, mov.id);
|
|
@@ -1706,7 +1769,25 @@ function InvoiceDetailView({ invoiceId, direction, onBack, onEdit }) {
|
|
|
1706
1769
|
` })
|
|
1707
1770
|
] });
|
|
1708
1771
|
}
|
|
1709
|
-
|
|
1772
|
+
|
|
1773
|
+
// src/views/navReferrer.ts
|
|
1774
|
+
var REFERRER_LABEL_KEYS = {
|
|
1775
|
+
summary: "financial.nav.summary",
|
|
1776
|
+
statements: "financial.nav.statements",
|
|
1777
|
+
"cash-registers": "financial.nav.cashRegisters",
|
|
1778
|
+
reconciliation: "financial.nav.reconciliation",
|
|
1779
|
+
commissions: "financial.nav.commissions",
|
|
1780
|
+
cards: "financial.nav.cards",
|
|
1781
|
+
"receivables-list": "financial.nav.receivables",
|
|
1782
|
+
"payables-list": "financial.nav.payables"
|
|
1783
|
+
};
|
|
1784
|
+
function referrerLabel(view, t2) {
|
|
1785
|
+
if (!view) return null;
|
|
1786
|
+
const key = REFERRER_LABEL_KEYS[view];
|
|
1787
|
+
return key ? t2(key) : null;
|
|
1788
|
+
}
|
|
1789
|
+
function PayablesView({ intent, onNavigate, previousView, back }) {
|
|
1790
|
+
const t2 = useTranslation();
|
|
1710
1791
|
if (intent.mode === "new") {
|
|
1711
1792
|
return /* @__PURE__ */ jsx(InvoiceFormView, { direction: "debit", onSaved: (id) => onNavigate(id ? `payables-detail:${id}` : "payables-list") });
|
|
1712
1793
|
}
|
|
@@ -1714,12 +1795,14 @@ function PayablesView({ intent, onNavigate }) {
|
|
|
1714
1795
|
return /* @__PURE__ */ jsx(InvoiceFormView, { direction: "debit", editId: intent.editId, onSaved: () => onNavigate(`payables-detail:${intent.editId}`) });
|
|
1715
1796
|
}
|
|
1716
1797
|
if (intent.mode === "detail" && intent.editId) {
|
|
1798
|
+
const prevLabel = referrerLabel(previousView, t2);
|
|
1717
1799
|
return /* @__PURE__ */ jsx(
|
|
1718
1800
|
InvoiceDetailView,
|
|
1719
1801
|
{
|
|
1720
1802
|
invoiceId: intent.editId,
|
|
1721
1803
|
direction: "debit",
|
|
1722
|
-
|
|
1804
|
+
backLabel: prevLabel ?? void 0,
|
|
1805
|
+
onBack: prevLabel && back ? () => back("payables-list") : () => onNavigate("payables-list"),
|
|
1723
1806
|
onEdit: () => onNavigate(`payables-edit:${intent.editId}`)
|
|
1724
1807
|
}
|
|
1725
1808
|
);
|
|
@@ -1733,7 +1816,8 @@ function PayablesView({ intent, onNavigate }) {
|
|
|
1733
1816
|
}
|
|
1734
1817
|
);
|
|
1735
1818
|
}
|
|
1736
|
-
function ReceivablesView({ intent, onNavigate }) {
|
|
1819
|
+
function ReceivablesView({ intent, onNavigate, previousView, back }) {
|
|
1820
|
+
const t2 = useTranslation();
|
|
1737
1821
|
if (intent.mode === "new") {
|
|
1738
1822
|
return /* @__PURE__ */ jsx(InvoiceFormView, { direction: "credit", onSaved: (id) => onNavigate(id ? `receivables-detail:${id}` : "receivables-list") });
|
|
1739
1823
|
}
|
|
@@ -1741,12 +1825,14 @@ function ReceivablesView({ intent, onNavigate }) {
|
|
|
1741
1825
|
return /* @__PURE__ */ jsx(InvoiceFormView, { direction: "credit", editId: intent.editId, onSaved: () => onNavigate(`receivables-detail:${intent.editId}`) });
|
|
1742
1826
|
}
|
|
1743
1827
|
if (intent.mode === "detail" && intent.editId) {
|
|
1828
|
+
const prevLabel = referrerLabel(previousView, t2);
|
|
1744
1829
|
return /* @__PURE__ */ jsx(
|
|
1745
1830
|
InvoiceDetailView,
|
|
1746
1831
|
{
|
|
1747
1832
|
invoiceId: intent.editId,
|
|
1748
1833
|
direction: "credit",
|
|
1749
|
-
|
|
1834
|
+
backLabel: prevLabel ?? void 0,
|
|
1835
|
+
onBack: prevLabel && back ? () => back("receivables-list") : () => onNavigate("receivables-list"),
|
|
1750
1836
|
onEdit: () => onNavigate(`receivables-edit:${intent.editId}`)
|
|
1751
1837
|
}
|
|
1752
1838
|
);
|
|
@@ -2245,32 +2331,201 @@ function OpenAnotherCollapsible({
|
|
|
2245
2331
|
expanded && /* @__PURE__ */ jsx(CashOpeningPanel, { cashAccounts: availableAccounts, currency, onOpen })
|
|
2246
2332
|
] });
|
|
2247
2333
|
}
|
|
2248
|
-
function
|
|
2334
|
+
function TransferModal({ onClose, onDone, defaultFromId }) {
|
|
2335
|
+
const t2 = useTranslation();
|
|
2336
|
+
const { currency } = useFinancialConfig();
|
|
2337
|
+
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
2338
|
+
const createTransfer = useFinancialStore((s) => s.createTransfer);
|
|
2339
|
+
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
2340
|
+
const [fromAccountId, setFromAccountId] = useState(defaultFromId ?? "");
|
|
2341
|
+
const [toAccountId, setToAccountId] = useState("");
|
|
2342
|
+
const [amount, setAmount] = useState(0);
|
|
2343
|
+
const [date, setDate] = useState((/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2344
|
+
const [notes, setNotes] = useState("");
|
|
2345
|
+
const [saving, setSaving] = useState(false);
|
|
2346
|
+
const [visible, setVisible] = useState(false);
|
|
2347
|
+
useEffect(() => {
|
|
2348
|
+
fetchBankAccounts();
|
|
2349
|
+
requestAnimationFrame(() => setVisible(true));
|
|
2350
|
+
}, []);
|
|
2351
|
+
function handleClose() {
|
|
2352
|
+
setVisible(false);
|
|
2353
|
+
setTimeout(onClose, 200);
|
|
2354
|
+
}
|
|
2355
|
+
const sameAccount = !!fromAccountId && fromAccountId === toAccountId;
|
|
2356
|
+
const canSubmit = !!fromAccountId && !!toAccountId && !sameAccount && amount > 0 && !saving;
|
|
2357
|
+
function handleKeyDown(e) {
|
|
2358
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
2359
|
+
if (e.target.tagName === "TEXTAREA") return;
|
|
2360
|
+
e.preventDefault();
|
|
2361
|
+
if (canSubmit) void handleSubmit();
|
|
2362
|
+
}
|
|
2363
|
+
async function handleSubmit() {
|
|
2364
|
+
if (!canSubmit) return;
|
|
2365
|
+
setSaving(true);
|
|
2366
|
+
try {
|
|
2367
|
+
await createTransfer({ fromAccountId, toAccountId, amount, date, notes: notes || void 0 });
|
|
2368
|
+
setVisible(false);
|
|
2369
|
+
setTimeout(onDone, 200);
|
|
2370
|
+
} catch {
|
|
2371
|
+
} finally {
|
|
2372
|
+
setSaving(false);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
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";
|
|
2376
|
+
return createPortal(
|
|
2377
|
+
/* @__PURE__ */ jsx(
|
|
2378
|
+
"div",
|
|
2379
|
+
{
|
|
2380
|
+
className: "fixed inset-0 z-50 flex items-center justify-center transition-colors duration-200",
|
|
2381
|
+
style: { backgroundColor: visible ? "rgba(0,0,0,0.5)" : "rgba(0,0,0,0)" },
|
|
2382
|
+
onClick: handleClose,
|
|
2383
|
+
children: /* @__PURE__ */ jsxs(
|
|
2384
|
+
"div",
|
|
2385
|
+
{
|
|
2386
|
+
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",
|
|
2387
|
+
style: { opacity: visible ? 1 : 0, transform: visible ? "scale(1) translateY(0)" : "scale(0.95) translateY(8px)" },
|
|
2388
|
+
onClick: (e) => e.stopPropagation(),
|
|
2389
|
+
onKeyDown: handleKeyDown,
|
|
2390
|
+
children: [
|
|
2391
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b", children: [
|
|
2392
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2393
|
+
/* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-4 w-4 text-warning" }),
|
|
2394
|
+
/* @__PURE__ */ jsx("h3", { className: "text-base font-semibold", children: t2("financial.transfer.title") })
|
|
2395
|
+
] }),
|
|
2396
|
+
/* @__PURE__ */ jsx("button", { onClick: handleClose, className: "p-1 text-muted-foreground hover:text-foreground transition-colors", children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" }) })
|
|
2397
|
+
] }),
|
|
2398
|
+
/* @__PURE__ */ jsxs("div", { className: "px-5 py-4 space-y-4", children: [
|
|
2399
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
2400
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2401
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.from") }),
|
|
2402
|
+
/* @__PURE__ */ jsxs("select", { value: fromAccountId, onChange: (e) => setFromAccountId(e.target.value), className: selectClass, children: [
|
|
2403
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.transfer.selectAccount") }),
|
|
2404
|
+
bankAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
2405
|
+
] })
|
|
2406
|
+
] }),
|
|
2407
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2408
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.to") }),
|
|
2409
|
+
/* @__PURE__ */ jsxs("select", { value: toAccountId, onChange: (e) => setToAccountId(e.target.value), className: selectClass, children: [
|
|
2410
|
+
/* @__PURE__ */ jsx("option", { value: "", children: t2("financial.transfer.selectAccount") }),
|
|
2411
|
+
bankAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
2412
|
+
] })
|
|
2413
|
+
] })
|
|
2414
|
+
] }),
|
|
2415
|
+
sameAccount && /* @__PURE__ */ jsx("p", { className: "text-xs text-destructive", children: t2("financial.transfer.sameAccount") }),
|
|
2416
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-3", children: [
|
|
2417
|
+
/* @__PURE__ */ jsx(
|
|
2418
|
+
CurrencyInput,
|
|
2419
|
+
{
|
|
2420
|
+
label: t2("financial.transfer.amount"),
|
|
2421
|
+
value: amount,
|
|
2422
|
+
onChange: setAmount,
|
|
2423
|
+
symbol: currency.symbol,
|
|
2424
|
+
locale: currency.locale,
|
|
2425
|
+
currencyCode: currency.code
|
|
2426
|
+
}
|
|
2427
|
+
),
|
|
2428
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2429
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.date") }),
|
|
2430
|
+
/* @__PURE__ */ jsx(DatePicker, { value: date, onChange: setDate, className: "mt-1" })
|
|
2431
|
+
] })
|
|
2432
|
+
] }),
|
|
2433
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2434
|
+
/* @__PURE__ */ jsx("label", { className: "text-xs font-medium text-muted-foreground", children: t2("financial.transfer.notes") }),
|
|
2435
|
+
/* @__PURE__ */ jsx(
|
|
2436
|
+
"input",
|
|
2437
|
+
{
|
|
2438
|
+
type: "text",
|
|
2439
|
+
value: notes,
|
|
2440
|
+
onChange: (e) => setNotes(e.target.value),
|
|
2441
|
+
placeholder: t2("financial.transfer.optionalNotes"),
|
|
2442
|
+
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"
|
|
2443
|
+
}
|
|
2444
|
+
)
|
|
2445
|
+
] })
|
|
2446
|
+
] }),
|
|
2447
|
+
/* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2 px-5 py-4 border-t", children: [
|
|
2448
|
+
/* @__PURE__ */ jsx(Button, { variant: "outline", size: "sm", onClick: handleClose, children: t2("financial.transfer.cancel") }),
|
|
2449
|
+
/* @__PURE__ */ jsxs(Button, { variant: "default", size: "sm", onClick: handleSubmit, disabled: !canSubmit, children: [
|
|
2450
|
+
/* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5" }),
|
|
2451
|
+
saving ? t2("financial.transfer.processing") : t2("financial.transfer.submit")
|
|
2452
|
+
] })
|
|
2453
|
+
] })
|
|
2454
|
+
]
|
|
2455
|
+
}
|
|
2456
|
+
)
|
|
2457
|
+
}
|
|
2458
|
+
),
|
|
2459
|
+
document.body
|
|
2460
|
+
);
|
|
2461
|
+
}
|
|
2462
|
+
var ALL = "all";
|
|
2463
|
+
function ReconcileMarker({ kind, label }) {
|
|
2464
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 150, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
2465
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx("span", { className: "inline-flex shrink-0 cursor-default", children: kind === "reconciled" ? /* @__PURE__ */ jsx(CheckCheck, { className: "h-3 w-3 text-success/70" }) : /* @__PURE__ */ jsx(Landmark, { className: "h-3 w-3 text-muted-foreground/40" }) }) }),
|
|
2466
|
+
/* @__PURE__ */ jsx(TooltipContent, { className: "text-xs", children: label })
|
|
2467
|
+
] }) });
|
|
2468
|
+
}
|
|
2469
|
+
function ReconciledWithRef({ refLabel, tooltip, onOpen }) {
|
|
2470
|
+
const inner = /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-0.5 text-[10px] text-muted-foreground max-w-[160px]", children: [
|
|
2471
|
+
/* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-2.5 w-2.5 shrink-0" }),
|
|
2472
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: refLabel })
|
|
2473
|
+
] });
|
|
2474
|
+
return /* @__PURE__ */ jsx(TooltipProvider, { delayDuration: 150, children: /* @__PURE__ */ jsxs(Tooltip, { children: [
|
|
2475
|
+
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: onOpen ? /* @__PURE__ */ jsx("button", { onClick: onOpen, className: "hover:text-primary transition-colors", children: inner }) : /* @__PURE__ */ jsx("span", { className: "cursor-default", children: inner }) }),
|
|
2476
|
+
/* @__PURE__ */ jsx(TooltipContent, { className: "text-xs", children: tooltip })
|
|
2477
|
+
] }) });
|
|
2478
|
+
}
|
|
2479
|
+
function StatementsView({ onNavigate }) {
|
|
2249
2480
|
const t2 = useTranslation();
|
|
2250
2481
|
const { currency } = useFinancialConfig();
|
|
2251
2482
|
const bankAccounts = useFinancialStore((s) => s.bankAccounts);
|
|
2252
|
-
const
|
|
2483
|
+
const statement = useFinancialStore((s) => s.statement);
|
|
2253
2484
|
const statementLoading = useFinancialStore((s) => s.statementLoading);
|
|
2485
|
+
const paymentMethods = useFinancialStore((s) => s.paymentMethods);
|
|
2486
|
+
const paymentMethodTypes = useFinancialStore((s) => s.paymentMethodTypes);
|
|
2254
2487
|
const fetchBankAccounts = useFinancialStore((s) => s.fetchBankAccounts);
|
|
2488
|
+
const fetchPaymentMethods = useFinancialStore((s) => s.fetchPaymentMethods);
|
|
2255
2489
|
const fetchStatement = useFinancialStore((s) => s.fetchStatement);
|
|
2256
2490
|
const [accountId, setAccountId] = useState("");
|
|
2257
2491
|
const [dateFrom, setDateFrom] = useState(() => {
|
|
2258
2492
|
const d = /* @__PURE__ */ new Date();
|
|
2259
|
-
d.setMonth(d.getMonth() -
|
|
2493
|
+
d.setMonth(d.getMonth() - 3);
|
|
2260
2494
|
return d.toISOString().slice(0, 10);
|
|
2261
2495
|
});
|
|
2262
2496
|
const [dateTo, setDateTo] = useState(() => (/* @__PURE__ */ new Date()).toISOString().slice(0, 10));
|
|
2497
|
+
const [showTransfer, setShowTransfer] = useState(false);
|
|
2263
2498
|
useEffect(() => {
|
|
2264
2499
|
fetchBankAccounts();
|
|
2500
|
+
fetchPaymentMethods();
|
|
2265
2501
|
}, []);
|
|
2502
|
+
const methodName = useMemo(() => {
|
|
2503
|
+
const byId = new Map(paymentMethods.map((m) => [m.id, m.name]));
|
|
2504
|
+
const typeById = new Map(paymentMethodTypes.map((tp) => [tp.id, tp.name]));
|
|
2505
|
+
return (mov) => {
|
|
2506
|
+
if (mov.paymentMethodId && byId.has(mov.paymentMethodId)) return byId.get(mov.paymentMethodId);
|
|
2507
|
+
if (mov.paymentMethodTypeId && typeById.has(mov.paymentMethodTypeId)) return typeById.get(mov.paymentMethodTypeId);
|
|
2508
|
+
if (mov.cardBrand) return mov.cardBrand;
|
|
2509
|
+
return "\u2014";
|
|
2510
|
+
};
|
|
2511
|
+
}, [paymentMethods, paymentMethodTypes]);
|
|
2512
|
+
function openInvoice(e) {
|
|
2513
|
+
if (!onNavigate || !e.movement.invoiceId) return;
|
|
2514
|
+
const base = e.movement.direction === "credit" ? "receivables" : "payables";
|
|
2515
|
+
onNavigate(`${base}-detail:${e.movement.invoiceId}`);
|
|
2516
|
+
}
|
|
2266
2517
|
useEffect(() => {
|
|
2267
2518
|
if (accountId) {
|
|
2268
|
-
fetchStatement({
|
|
2519
|
+
fetchStatement({
|
|
2520
|
+
bankAccountId: accountId === ALL ? void 0 : accountId,
|
|
2521
|
+
dateRange: { from: dateFrom, to: dateTo }
|
|
2522
|
+
});
|
|
2269
2523
|
}
|
|
2270
2524
|
}, [accountId, dateFrom, dateTo]);
|
|
2271
2525
|
useEffect(() => {
|
|
2272
|
-
if (!accountId
|
|
2273
|
-
}, [
|
|
2526
|
+
if (!accountId) setAccountId(ALL);
|
|
2527
|
+
}, []);
|
|
2528
|
+
const fmt = (n) => formatCurrency(n, currency);
|
|
2274
2529
|
const columns = useMemo(() => [
|
|
2275
2530
|
{
|
|
2276
2531
|
id: "date",
|
|
@@ -2280,29 +2535,112 @@ function StatementsView() {
|
|
|
2280
2535
|
{
|
|
2281
2536
|
id: "description",
|
|
2282
2537
|
header: t2("financial.statements.columnDescription"),
|
|
2283
|
-
cell: ({ row }) =>
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2538
|
+
cell: ({ row }) => {
|
|
2539
|
+
const e = row.original;
|
|
2540
|
+
const isTransfer = e.entryKind !== "movement";
|
|
2541
|
+
const memo = e.movement.notes;
|
|
2542
|
+
const title = e.invoice?.contactName || (isTransfer ? t2("financial.statements.transfer") : memo || "Transaction");
|
|
2543
|
+
const memoLine = memo && memo !== title ? memo : void 0;
|
|
2544
|
+
const doc = e.invoice?.fiscalNumber;
|
|
2545
|
+
const inst = e.movement.installmentNumber;
|
|
2546
|
+
const canOpen = !!onNavigate && !!e.movement.invoiceId;
|
|
2547
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2548
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
|
|
2549
|
+
/* @__PURE__ */ jsx(PersonLink, { personId: e.invoice?.contactId, name: title, size: "sm" }),
|
|
2550
|
+
doc && (canOpen ? /* @__PURE__ */ jsx(
|
|
2551
|
+
"button",
|
|
2552
|
+
{
|
|
2553
|
+
onClick: () => openInvoice(e),
|
|
2554
|
+
className: "text-[10px] text-muted-foreground font-mono hover:text-primary hover:underline",
|
|
2555
|
+
title: t2("financial.statements.viewInvoice"),
|
|
2556
|
+
children: doc
|
|
2557
|
+
}
|
|
2558
|
+
) : /* @__PURE__ */ jsx("span", { className: "text-[10px] text-muted-foreground font-mono", children: doc })),
|
|
2559
|
+
isTransfer && /* @__PURE__ */ 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") }),
|
|
2560
|
+
e.movement.reconciledAt ? /* @__PURE__ */ jsx(ReconcileMarker, { kind: "reconciled", label: t2("financial.statements.reconciled") }) : e.movement.externalSource ? /* @__PURE__ */ jsx(ReconcileMarker, { kind: "imported", label: t2("financial.statements.imported") }) : null,
|
|
2561
|
+
e.reconciledWith && (() => {
|
|
2562
|
+
const rw = e.reconciledWith;
|
|
2563
|
+
const refLabel = rw.fiscalNumber || rw.contactName || rw.description || rw.movementId.slice(0, 8);
|
|
2564
|
+
const base = e.movement.direction === "credit" ? "receivables" : "payables";
|
|
2565
|
+
const onOpen = onNavigate && rw.invoiceId ? () => onNavigate(`${base}-detail:${rw.invoiceId}`) : void 0;
|
|
2566
|
+
return /* @__PURE__ */ jsx(
|
|
2567
|
+
ReconciledWithRef,
|
|
2568
|
+
{
|
|
2569
|
+
refLabel,
|
|
2570
|
+
tooltip: t2("financial.statements.reconciledWith", { ref: refLabel }),
|
|
2571
|
+
onOpen
|
|
2572
|
+
}
|
|
2573
|
+
);
|
|
2574
|
+
})()
|
|
2575
|
+
] }),
|
|
2576
|
+
memoLine && /* @__PURE__ */ jsx("p", { className: "text-[11px] text-foreground/80", children: memoLine }),
|
|
2577
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2578
|
+
/* @__PURE__ */ jsx("span", { className: "capitalize", children: isTransfer && e.counterAccountName ? e.entryKind === "transfer-in" ? `\u2190 ${e.counterAccountName}` : `\u2192 ${e.counterAccountName}` : e.movement.movementKind }),
|
|
2579
|
+
inst ? ` \xB7 ${t2("financial.statements.installmentShort", { n: inst })}` : ""
|
|
2580
|
+
] })
|
|
2581
|
+
] });
|
|
2582
|
+
}
|
|
2583
|
+
},
|
|
2584
|
+
{
|
|
2585
|
+
id: "method",
|
|
2586
|
+
header: t2("financial.statements.columnMethod"),
|
|
2587
|
+
cell: ({ row }) => {
|
|
2588
|
+
const mov = row.original.movement;
|
|
2589
|
+
const name = methodName(mov);
|
|
2590
|
+
return /* @__PURE__ */ jsxs("div", { children: [
|
|
2591
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs", children: name }),
|
|
2592
|
+
mov.cardInstallments && mov.cardInstallments > 1 && /* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2593
|
+
mov.cardInstallments,
|
|
2594
|
+
"x"
|
|
2595
|
+
] })
|
|
2596
|
+
] });
|
|
2597
|
+
}
|
|
2287
2598
|
},
|
|
2288
2599
|
{
|
|
2289
2600
|
id: "debit",
|
|
2290
2601
|
header: t2("financial.statements.columnDebit"),
|
|
2291
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-destructive text-xs", children: row.original.movement.direction === "debit" ?
|
|
2602
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-destructive text-xs", children: row.original.movement.direction === "debit" ? fmt(row.original.net) : "" })
|
|
2292
2603
|
},
|
|
2293
2604
|
{
|
|
2294
2605
|
id: "credit",
|
|
2295
2606
|
header: t2("financial.statements.columnCredit"),
|
|
2296
|
-
cell: ({ row }) =>
|
|
2607
|
+
cell: ({ row }) => {
|
|
2608
|
+
const e = row.original;
|
|
2609
|
+
if (e.movement.direction !== "credit") return /* @__PURE__ */ jsx("span", { className: "block text-right" });
|
|
2610
|
+
return /* @__PURE__ */ jsxs("div", { className: "text-right", children: [
|
|
2611
|
+
/* @__PURE__ */ jsx("span", { className: "block text-success text-xs", children: fmt(e.net) }),
|
|
2612
|
+
e.fee > 0 && /* @__PURE__ */ jsxs("span", { className: "block text-[10px] text-muted-foreground", children: [
|
|
2613
|
+
fmt(e.gross),
|
|
2614
|
+
" \u2212 ",
|
|
2615
|
+
fmt(e.fee)
|
|
2616
|
+
] })
|
|
2617
|
+
] });
|
|
2618
|
+
}
|
|
2619
|
+
},
|
|
2620
|
+
{
|
|
2621
|
+
id: "fee",
|
|
2622
|
+
header: t2("financial.statements.columnFee"),
|
|
2623
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-warning text-xs", children: row.original.fee > 0 ? fmt(row.original.fee) : "" })
|
|
2297
2624
|
},
|
|
2298
2625
|
{
|
|
2299
2626
|
id: "balance",
|
|
2300
2627
|
header: t2("financial.statements.columnBalance"),
|
|
2301
|
-
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block font-medium text-xs", children:
|
|
2628
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block font-medium text-xs", children: fmt(row.original.runningBalance) })
|
|
2302
2629
|
}
|
|
2303
|
-
], [currency, t2]);
|
|
2630
|
+
], [currency, t2, methodName, onNavigate]);
|
|
2631
|
+
const entries = statement?.entries ?? [];
|
|
2304
2632
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2305
|
-
/* @__PURE__ */ jsx(
|
|
2633
|
+
/* @__PURE__ */ jsx(
|
|
2634
|
+
SubpageHeader,
|
|
2635
|
+
{
|
|
2636
|
+
title: t2("financial.statements.title"),
|
|
2637
|
+
subtitle: t2("financial.statements.subtitle"),
|
|
2638
|
+
actions: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => setShowTransfer(true), disabled: bankAccounts.length < 2, children: [
|
|
2639
|
+
/* @__PURE__ */ jsx(ArrowRightLeft, { className: "h-3.5 w-3.5" }),
|
|
2640
|
+
t2("financial.statements.newTransfer")
|
|
2641
|
+
] })
|
|
2642
|
+
}
|
|
2643
|
+
),
|
|
2306
2644
|
/* @__PURE__ */ jsxs("div", { className: "flex flex-col sm:flex-row gap-3 sm:items-end", children: [
|
|
2307
2645
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 max-w-xs", children: [
|
|
2308
2646
|
/* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium text-muted-foreground uppercase", children: t2("financial.statements.account") }),
|
|
@@ -2311,9 +2649,9 @@ function StatementsView() {
|
|
|
2311
2649
|
{
|
|
2312
2650
|
value: accountId,
|
|
2313
2651
|
onChange: (e) => setAccountId(e.target.value),
|
|
2314
|
-
className: "w-full mt-0.5 rounded-input border border-input
|
|
2652
|
+
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",
|
|
2315
2653
|
children: [
|
|
2316
|
-
/* @__PURE__ */ jsx("option", { value:
|
|
2654
|
+
/* @__PURE__ */ jsx("option", { value: ALL, children: t2("financial.summary.allAccounts") }),
|
|
2317
2655
|
bankAccounts.map((a) => /* @__PURE__ */ jsx("option", { value: a.id, children: a.name }, a.id))
|
|
2318
2656
|
]
|
|
2319
2657
|
}
|
|
@@ -2328,10 +2666,31 @@ function StatementsView() {
|
|
|
2328
2666
|
/* @__PURE__ */ jsx(DatePicker, { value: dateTo, onChange: setDateTo, className: "mt-0.5" })
|
|
2329
2667
|
] })
|
|
2330
2668
|
] }),
|
|
2669
|
+
accountId && statement && /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 sm:grid-cols-4 gap-2", children: [
|
|
2670
|
+
/* @__PURE__ */ jsx(SummaryStat, { label: t2("financial.statements.opening"), value: fmt(statement.openingBalance) }),
|
|
2671
|
+
/* @__PURE__ */ jsx(SummaryStat, { label: t2("financial.statements.totalCredits"), value: fmt(statement.totalCredits), positive: true }),
|
|
2672
|
+
/* @__PURE__ */ jsx(SummaryStat, { label: t2("financial.statements.totalDebits"), value: fmt(statement.totalDebits), negative: true }),
|
|
2673
|
+
/* @__PURE__ */ jsx(SummaryStat, { label: t2("financial.statements.closing"), value: fmt(statement.closingBalance), accent: true })
|
|
2674
|
+
] }),
|
|
2331
2675
|
!accountId ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-12 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
2332
2676
|
/* @__PURE__ */ jsx(Receipt, { className: "h-8 w-8 text-muted-foreground mb-2" }),
|
|
2333
2677
|
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.statements.selectToView") })
|
|
2334
|
-
] }) : statementLoading ? /* @__PURE__ */ jsx(DataTable, { columns, data: [], loading: true, skeletonRows:
|
|
2678
|
+
] }) : statementLoading ? /* @__PURE__ */ jsx(DataTable, { columns, data: [], loading: true, skeletonRows: 8, compact: true }) : entries.length === 0 ? /* @__PURE__ */ jsx(DataTable, { columns, data: [], emptyMessage: t2("financial.statements.noTransactions"), compact: true }) : /* @__PURE__ */ jsx(DataTable, { columns, data: entries, compact: true }),
|
|
2679
|
+
showTransfer && /* @__PURE__ */ jsx(
|
|
2680
|
+
TransferModal,
|
|
2681
|
+
{
|
|
2682
|
+
defaultFromId: accountId && accountId !== ALL ? accountId : void 0,
|
|
2683
|
+
onClose: () => setShowTransfer(false),
|
|
2684
|
+
onDone: () => setShowTransfer(false)
|
|
2685
|
+
}
|
|
2686
|
+
)
|
|
2687
|
+
] });
|
|
2688
|
+
}
|
|
2689
|
+
function SummaryStat({ label, value, accent, positive, negative }) {
|
|
2690
|
+
const valueClass = positive ? "text-success" : negative ? "text-destructive" : accent ? "text-foreground" : "text-foreground";
|
|
2691
|
+
return /* @__PURE__ */ jsxs("div", { className: `rounded-lg border px-3 py-2 ${accent ? "bg-muted/40" : "bg-card"}`, children: [
|
|
2692
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-muted-foreground uppercase truncate", children: label }),
|
|
2693
|
+
/* @__PURE__ */ jsx("p", { className: `text-sm font-semibold mt-0.5 ${valueClass}`, children: value })
|
|
2335
2694
|
] });
|
|
2336
2695
|
}
|
|
2337
2696
|
function CommissionsView() {
|
|
@@ -2397,6 +2756,151 @@ function CardsView() {
|
|
|
2397
2756
|
] }, brand))
|
|
2398
2757
|
] });
|
|
2399
2758
|
}
|
|
2759
|
+
function ReconciliationView() {
|
|
2760
|
+
const t2 = useTranslation();
|
|
2761
|
+
const { currency } = useFinancialConfig();
|
|
2762
|
+
const provider = useFinancialProvider();
|
|
2763
|
+
const fmt = (n) => formatCurrency(n, currency);
|
|
2764
|
+
const [lines, setLines] = useState([]);
|
|
2765
|
+
const [loading, setLoading] = useState(true);
|
|
2766
|
+
const [selectedId, setSelectedId] = useState(null);
|
|
2767
|
+
const [candidates, setCandidates] = useState([]);
|
|
2768
|
+
const [candidatesLoading, setCandidatesLoading] = useState(false);
|
|
2769
|
+
const [busy, setBusy] = useState(false);
|
|
2770
|
+
const supported = typeof provider.getUnreconciled === "function";
|
|
2771
|
+
const load = useCallback(async () => {
|
|
2772
|
+
if (!provider.getUnreconciled) return;
|
|
2773
|
+
setLoading(true);
|
|
2774
|
+
try {
|
|
2775
|
+
const data = await provider.getUnreconciled();
|
|
2776
|
+
setLines(data);
|
|
2777
|
+
setSelectedId((prev) => prev && data.some((l) => l.id === prev) ? prev : null);
|
|
2778
|
+
} finally {
|
|
2779
|
+
setLoading(false);
|
|
2780
|
+
}
|
|
2781
|
+
}, [provider]);
|
|
2782
|
+
useEffect(() => {
|
|
2783
|
+
void load();
|
|
2784
|
+
}, [load]);
|
|
2785
|
+
useEffect(() => {
|
|
2786
|
+
if (!selectedId || !provider.suggestReconciliation) {
|
|
2787
|
+
setCandidates([]);
|
|
2788
|
+
return;
|
|
2789
|
+
}
|
|
2790
|
+
let cancelled = false;
|
|
2791
|
+
setCandidatesLoading(true);
|
|
2792
|
+
provider.suggestReconciliation(selectedId).then((c) => {
|
|
2793
|
+
if (!cancelled) setCandidates(c);
|
|
2794
|
+
}).finally(() => {
|
|
2795
|
+
if (!cancelled) setCandidatesLoading(false);
|
|
2796
|
+
});
|
|
2797
|
+
return () => {
|
|
2798
|
+
cancelled = true;
|
|
2799
|
+
};
|
|
2800
|
+
}, [selectedId, provider]);
|
|
2801
|
+
async function match(bankMovementId, matchedMovementId) {
|
|
2802
|
+
if (!provider.reconcileMovement) return;
|
|
2803
|
+
setBusy(true);
|
|
2804
|
+
try {
|
|
2805
|
+
await provider.reconcileMovement({ bankMovementId, matchedMovementId });
|
|
2806
|
+
toast.success(t2("financial.reconciliation.match"));
|
|
2807
|
+
await load();
|
|
2808
|
+
} catch {
|
|
2809
|
+
toast.error(t2("common.error"));
|
|
2810
|
+
} finally {
|
|
2811
|
+
setBusy(false);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
if (!supported) {
|
|
2815
|
+
return /* @__PURE__ */ jsxs("div", { className: "py-16 text-center", children: [
|
|
2816
|
+
/* @__PURE__ */ jsx(Landmark, { className: "h-8 w-8 text-muted-foreground/30 mx-auto mb-2" }),
|
|
2817
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.reconciliation.empty") })
|
|
2818
|
+
] });
|
|
2819
|
+
}
|
|
2820
|
+
const selected = lines.find((l) => l.id === selectedId) ?? null;
|
|
2821
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
2822
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
2823
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
2824
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold tracking-tight", children: t2("financial.reconciliation.title") }),
|
|
2825
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.reconciliation.subtitle") })
|
|
2826
|
+
] }),
|
|
2827
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
2828
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: lines.length > 0 ? t2("financial.reconciliation.pending", { count: lines.length }) : t2("financial.reconciliation.allDone") }),
|
|
2829
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: () => void load(), disabled: loading || busy, children: [
|
|
2830
|
+
/* @__PURE__ */ jsx(RefreshCw, { className: `h-3.5 w-3.5 ${loading ? "animate-spin" : ""}` }),
|
|
2831
|
+
t2("financial.reconciliation.refresh")
|
|
2832
|
+
] })
|
|
2833
|
+
] })
|
|
2834
|
+
] }),
|
|
2835
|
+
lines.length === 0 && !loading ? /* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card shadow-sm py-16 text-center", children: [
|
|
2836
|
+
/* @__PURE__ */ jsx(Check, { className: "h-8 w-8 text-success/40 mx-auto mb-2" }),
|
|
2837
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm font-medium", children: t2("financial.reconciliation.empty") }),
|
|
2838
|
+
/* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground mt-1", children: t2("financial.reconciliation.emptyHint") })
|
|
2839
|
+
] }) : /* @__PURE__ */ jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [
|
|
2840
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card shadow-sm overflow-hidden", children: [
|
|
2841
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-b", children: [
|
|
2842
|
+
/* @__PURE__ */ jsx(Landmark, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2843
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.reconciliation.bankLines") })
|
|
2844
|
+
] }),
|
|
2845
|
+
/* @__PURE__ */ jsx("div", { className: "divide-y max-h-[60vh] overflow-auto", children: lines.map((line) => {
|
|
2846
|
+
const isCredit = line.direction === "credit";
|
|
2847
|
+
return /* @__PURE__ */ jsxs(
|
|
2848
|
+
"button",
|
|
2849
|
+
{
|
|
2850
|
+
onClick: () => setSelectedId(line.id),
|
|
2851
|
+
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" : ""}`,
|
|
2852
|
+
children: [
|
|
2853
|
+
isCredit ? /* @__PURE__ */ jsx(ArrowDownCircle, { className: "h-4 w-4 text-success shrink-0" }) : /* @__PURE__ */ jsx(ArrowUpCircle, { className: "h-4 w-4 text-destructive shrink-0" }),
|
|
2854
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2855
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm truncate", children: line.notes || line.externalId }),
|
|
2856
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2857
|
+
line.paymentDate ?? line.dueDate,
|
|
2858
|
+
line.externalSource && /* @__PURE__ */ jsxs("span", { className: "ml-1 uppercase tracking-wide", children: [
|
|
2859
|
+
"\xB7 ",
|
|
2860
|
+
line.externalSource
|
|
2861
|
+
] })
|
|
2862
|
+
] })
|
|
2863
|
+
] }),
|
|
2864
|
+
/* @__PURE__ */ jsx("span", { className: `text-sm font-semibold shrink-0 ${isCredit ? "text-success" : "text-destructive"}`, children: fmt(line.amount) })
|
|
2865
|
+
]
|
|
2866
|
+
},
|
|
2867
|
+
line.id
|
|
2868
|
+
);
|
|
2869
|
+
}) })
|
|
2870
|
+
] }),
|
|
2871
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-lg border bg-card shadow-sm overflow-hidden", children: [
|
|
2872
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 border-b", children: [
|
|
2873
|
+
/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4 text-muted-foreground" }),
|
|
2874
|
+
/* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t2("financial.reconciliation.candidates") })
|
|
2875
|
+
] }),
|
|
2876
|
+
!selected ? /* @__PURE__ */ jsx("p", { className: "px-4 py-16 text-center text-sm text-muted-foreground", children: t2("financial.reconciliation.selectLine") }) : /* @__PURE__ */ jsx("div", { className: "p-3 space-y-2", children: candidatesLoading ? /* @__PURE__ */ jsx("div", { className: "py-10 text-center text-xs text-muted-foreground", children: "\u2026" }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2877
|
+
candidates.map((c) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3 rounded-md border p-2.5", children: [
|
|
2878
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
2879
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm truncate", children: c.invoice?.contactName || c.movement.notes || c.invoice?.fiscalNumber || t2("financial.reconciliation.match") }),
|
|
2880
|
+
/* @__PURE__ */ jsxs("p", { className: "text-[10px] text-muted-foreground", children: [
|
|
2881
|
+
c.movement.dueDate,
|
|
2882
|
+
/* @__PURE__ */ 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) }) })
|
|
2883
|
+
] })
|
|
2884
|
+
] }),
|
|
2885
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold shrink-0", children: fmt(c.movement.amount) }),
|
|
2886
|
+
/* @__PURE__ */ jsxs(Button, { size: "sm", disabled: busy, onClick: () => match(selected.id, c.movement.id), children: [
|
|
2887
|
+
/* @__PURE__ */ jsx(Link2, { className: "h-3.5 w-3.5" }),
|
|
2888
|
+
t2("financial.reconciliation.match")
|
|
2889
|
+
] })
|
|
2890
|
+
] }, c.movement.id)),
|
|
2891
|
+
candidates.length === 0 && /* @__PURE__ */ jsx("p", { className: "py-6 text-center text-xs text-muted-foreground", children: t2("financial.reconciliation.noCandidates") }),
|
|
2892
|
+
/* @__PURE__ */ jsxs("div", { className: "pt-2 mt-1 border-t", children: [
|
|
2893
|
+
/* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className: "w-full", disabled: busy, onClick: () => match(selected.id), children: [
|
|
2894
|
+
/* @__PURE__ */ jsx(Check, { className: "h-3.5 w-3.5" }),
|
|
2895
|
+
t2("financial.reconciliation.accept")
|
|
2896
|
+
] }),
|
|
2897
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] text-muted-foreground text-center mt-1", children: t2("financial.reconciliation.acceptHint") })
|
|
2898
|
+
] })
|
|
2899
|
+
] }) })
|
|
2900
|
+
] })
|
|
2901
|
+
] })
|
|
2902
|
+
] });
|
|
2903
|
+
}
|
|
2400
2904
|
function FinancialGeneralSettings() {
|
|
2401
2905
|
const t2 = useTranslation();
|
|
2402
2906
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
@@ -2521,13 +3025,20 @@ function buildNav(config, activeView, navigate, t2) {
|
|
|
2521
3025
|
onClick: () => navigate("cash-registers")
|
|
2522
3026
|
});
|
|
2523
3027
|
}
|
|
2524
|
-
|
|
3028
|
+
items.push({
|
|
3029
|
+
id: "statements",
|
|
3030
|
+
label: t2("financial.nav.statements"),
|
|
3031
|
+
icon: "Receipt",
|
|
3032
|
+
active: activeView === "statements",
|
|
3033
|
+
onClick: () => navigate("statements")
|
|
3034
|
+
});
|
|
3035
|
+
if (config.modules.reconciliation) {
|
|
2525
3036
|
items.push({
|
|
2526
|
-
id: "
|
|
2527
|
-
label: t2("financial.nav.
|
|
2528
|
-
icon: "
|
|
2529
|
-
active: activeView === "
|
|
2530
|
-
onClick: () => navigate("
|
|
3037
|
+
id: "reconciliation",
|
|
3038
|
+
label: t2("financial.nav.reconciliation"),
|
|
3039
|
+
icon: "ArrowLeftRight",
|
|
3040
|
+
active: activeView === "reconciliation",
|
|
3041
|
+
onClick: () => navigate("reconciliation")
|
|
2531
3042
|
});
|
|
2532
3043
|
}
|
|
2533
3044
|
if (config.modules.commissions) {
|
|
@@ -2555,7 +3066,7 @@ function buildNav(config, activeView, navigate, t2) {
|
|
|
2555
3066
|
return items;
|
|
2556
3067
|
}
|
|
2557
3068
|
function FinancialPage({ config, provider, store, registries }) {
|
|
2558
|
-
const { view, direction, navigate } = useModuleNavigation("/financial", {
|
|
3069
|
+
const { view, direction, navigate, previousView, back } = useModuleNavigation("/financial", {
|
|
2559
3070
|
summary: 0,
|
|
2560
3071
|
"payables-list": 0,
|
|
2561
3072
|
"payables-new": 1,
|
|
@@ -2563,6 +3074,7 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
2563
3074
|
"receivables-new": 1,
|
|
2564
3075
|
"cash-registers": 0,
|
|
2565
3076
|
statements: 0,
|
|
3077
|
+
reconciliation: 0,
|
|
2566
3078
|
commissions: 0,
|
|
2567
3079
|
cards: 0,
|
|
2568
3080
|
settings: 1
|
|
@@ -2620,24 +3132,30 @@ function FinancialPage({ config, provider, store, registries }) {
|
|
|
2620
3132
|
return /* @__PURE__ */ jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(FinancialOnboarding, { onComplete: handleOnboardingComplete }) });
|
|
2621
3133
|
}
|
|
2622
3134
|
if (isSettings && registries && registries.length > 0) {
|
|
2623
|
-
return /* @__PURE__ */ jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(PageTransition, { transitionKey: "settings", direction, children: /* @__PURE__ */
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
3135
|
+
return /* @__PURE__ */ jsx(FinancialContextProvider, { config, provider, store, children: /* @__PURE__ */ jsx(PageTransition, { transitionKey: "settings", direction, children: /* @__PURE__ */ jsx("div", { style: { padding: "24px" }, children: /* @__PURE__ */ jsx(
|
|
3136
|
+
PluginSettingsPanel,
|
|
3137
|
+
{
|
|
3138
|
+
title: t2("financial.settingsPage.title"),
|
|
3139
|
+
subtitle: t2("financial.settingsPage.subtitle"),
|
|
3140
|
+
generalSettings: /* @__PURE__ */ jsx(FinancialGeneralSettings, {}),
|
|
3141
|
+
registries,
|
|
3142
|
+
hostPluginId: "financial",
|
|
3143
|
+
routeBase: "/settings/financial"
|
|
3144
|
+
}
|
|
3145
|
+
) }) }) });
|
|
2630
3146
|
}
|
|
2631
3147
|
function renderView() {
|
|
2632
3148
|
switch (intent.view) {
|
|
2633
3149
|
case "payables":
|
|
2634
|
-
return /* @__PURE__ */ jsx(PayablesView, { intent, onNavigate: navigate });
|
|
3150
|
+
return /* @__PURE__ */ jsx(PayablesView, { intent, onNavigate: navigate, previousView, back });
|
|
2635
3151
|
case "receivables":
|
|
2636
|
-
return /* @__PURE__ */ jsx(ReceivablesView, { intent, onNavigate: navigate });
|
|
3152
|
+
return /* @__PURE__ */ jsx(ReceivablesView, { intent, onNavigate: navigate, previousView, back });
|
|
2637
3153
|
case "cash-registers":
|
|
2638
3154
|
return /* @__PURE__ */ jsx(CashRegistersView, {});
|
|
2639
3155
|
case "statements":
|
|
2640
|
-
return /* @__PURE__ */ jsx(StatementsView, {});
|
|
3156
|
+
return /* @__PURE__ */ jsx(StatementsView, { onNavigate: navigate });
|
|
3157
|
+
case "reconciliation":
|
|
3158
|
+
return /* @__PURE__ */ jsx(ReconciliationView, {});
|
|
2641
3159
|
case "commissions":
|
|
2642
3160
|
return /* @__PURE__ */ jsx(CommissionsView, {});
|
|
2643
3161
|
case "cards":
|
|
@@ -2841,6 +3359,9 @@ function now() {
|
|
|
2841
3359
|
function today() {
|
|
2842
3360
|
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2843
3361
|
}
|
|
3362
|
+
function round2(n) {
|
|
3363
|
+
return Math.round(n * 100) / 100;
|
|
3364
|
+
}
|
|
2844
3365
|
function matchesDateRange(dateStr, range) {
|
|
2845
3366
|
if (!range || !dateStr) return true;
|
|
2846
3367
|
return dateStr >= range.from && dateStr <= range.to;
|
|
@@ -3044,37 +3565,60 @@ function createMockFinancialProvider(options) {
|
|
|
3044
3565
|
return paginate(results, query.page, query.pageSize);
|
|
3045
3566
|
},
|
|
3046
3567
|
async payMovement(input) {
|
|
3047
|
-
const
|
|
3048
|
-
if (!
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3568
|
+
const bill = store.movements.find((m) => m.id === input.movementId);
|
|
3569
|
+
if (!bill) throw new Error(`Movement ${input.movementId} not found`);
|
|
3570
|
+
const method = input.paymentMethodId ? store.paymentMethods.find((pm) => pm.id === input.paymentMethodId) : void 0;
|
|
3571
|
+
const feePct = bill.direction === "credit" ? method?.discountValue ?? 0 : 0;
|
|
3572
|
+
const thisFee = round2(input.amount * (feePct / 100));
|
|
3573
|
+
const payment = {
|
|
3574
|
+
id: uid(),
|
|
3575
|
+
invoiceId: bill.invoiceId,
|
|
3576
|
+
direction: bill.direction,
|
|
3577
|
+
movementKind: "payment",
|
|
3578
|
+
amount: input.amount,
|
|
3579
|
+
paidAmount: input.amount,
|
|
3580
|
+
feeAmount: thisFee,
|
|
3581
|
+
status: "paid",
|
|
3582
|
+
dueDate: bill.dueDate,
|
|
3583
|
+
paymentDate: input.paymentDate,
|
|
3584
|
+
installmentNumber: bill.installmentNumber,
|
|
3585
|
+
paymentMethodId: input.paymentMethodId,
|
|
3586
|
+
paymentMethodTypeId: input.paymentMethodTypeId,
|
|
3587
|
+
bankAccountId: input.bankAccountId,
|
|
3588
|
+
cashSessionId: input.cashSessionId,
|
|
3589
|
+
cardBrand: input.cardBrand,
|
|
3590
|
+
cardInstallments: input.cardInstallments,
|
|
3591
|
+
notes: input.notes,
|
|
3592
|
+
tenantId,
|
|
3593
|
+
createdAt: now(),
|
|
3594
|
+
updatedAt: now()
|
|
3595
|
+
};
|
|
3596
|
+
store.movements.push(payment);
|
|
3597
|
+
bill.paidAmount = round2(bill.paidAmount + input.amount);
|
|
3598
|
+
bill.status = bill.paidAmount >= bill.amount ? "paid" : "partial";
|
|
3599
|
+
bill.paymentDate = void 0;
|
|
3600
|
+
bill.bankAccountId = void 0;
|
|
3601
|
+
bill.updatedAt = now();
|
|
3602
|
+
if (input.bankAccountId) {
|
|
3603
|
+
const account = store.bankAccounts.find((a) => a.id === input.bankAccountId);
|
|
3060
3604
|
if (account) {
|
|
3061
|
-
if (
|
|
3062
|
-
account.currentBalance
|
|
3605
|
+
if (bill.direction === "credit") {
|
|
3606
|
+
account.currentBalance = round2(account.currentBalance + (input.amount - thisFee));
|
|
3063
3607
|
} else {
|
|
3064
|
-
account.currentBalance
|
|
3608
|
+
account.currentBalance = round2(account.currentBalance - input.amount);
|
|
3065
3609
|
}
|
|
3066
3610
|
account.updatedAt = now();
|
|
3067
3611
|
}
|
|
3068
3612
|
}
|
|
3069
|
-
if (
|
|
3070
|
-
const invoice = store.invoices.find((i) => i.id ===
|
|
3613
|
+
if (bill.invoiceId) {
|
|
3614
|
+
const invoice = store.invoices.find((i) => i.id === bill.invoiceId);
|
|
3071
3615
|
if (invoice) {
|
|
3072
|
-
const
|
|
3073
|
-
invoice.paidAmount =
|
|
3616
|
+
const bills = store.movements.filter((m) => m.invoiceId === invoice.id && m.movementKind === "bill");
|
|
3617
|
+
invoice.paidAmount = bills.reduce((sum, m) => sum + m.paidAmount, 0);
|
|
3074
3618
|
recalcInvoiceStatus(invoice);
|
|
3075
3619
|
}
|
|
3076
3620
|
}
|
|
3077
|
-
return
|
|
3621
|
+
return payment;
|
|
3078
3622
|
},
|
|
3079
3623
|
async cancelMovement(id) {
|
|
3080
3624
|
const movement = store.movements.find((m) => m.id === id);
|
|
@@ -3082,6 +3626,54 @@ function createMockFinancialProvider(options) {
|
|
|
3082
3626
|
movement.status = "cancelled";
|
|
3083
3627
|
movement.updatedAt = now();
|
|
3084
3628
|
},
|
|
3629
|
+
// --- Transfers (between accounts) ---
|
|
3630
|
+
async createTransfer(input) {
|
|
3631
|
+
if (!input.fromAccountId || !input.toAccountId || input.fromAccountId === input.toAccountId) {
|
|
3632
|
+
throw new Error("Source and destination accounts must differ");
|
|
3633
|
+
}
|
|
3634
|
+
if (!(input.amount > 0)) throw new Error("Amount must be greater than zero");
|
|
3635
|
+
const transferId = uid();
|
|
3636
|
+
const debitMovementId = uid();
|
|
3637
|
+
const creditMovementId = uid();
|
|
3638
|
+
const base = {
|
|
3639
|
+
movementKind: "transfer",
|
|
3640
|
+
amount: input.amount,
|
|
3641
|
+
paidAmount: input.amount,
|
|
3642
|
+
feeAmount: 0,
|
|
3643
|
+
status: "paid",
|
|
3644
|
+
dueDate: input.date,
|
|
3645
|
+
paymentDate: input.date,
|
|
3646
|
+
notes: input.notes,
|
|
3647
|
+
tenantId,
|
|
3648
|
+
createdAt: now(),
|
|
3649
|
+
updatedAt: now()
|
|
3650
|
+
};
|
|
3651
|
+
store.movements.push({
|
|
3652
|
+
...base,
|
|
3653
|
+
id: debitMovementId,
|
|
3654
|
+
direction: "debit",
|
|
3655
|
+
bankAccountId: input.fromAccountId,
|
|
3656
|
+
metadata: { transferId, transferRole: "out", counterAccountId: input.toAccountId }
|
|
3657
|
+
});
|
|
3658
|
+
store.movements.push({
|
|
3659
|
+
...base,
|
|
3660
|
+
id: creditMovementId,
|
|
3661
|
+
direction: "credit",
|
|
3662
|
+
bankAccountId: input.toAccountId,
|
|
3663
|
+
metadata: { transferId, transferRole: "in", counterAccountId: input.fromAccountId }
|
|
3664
|
+
});
|
|
3665
|
+
const from = store.bankAccounts.find((a) => a.id === input.fromAccountId);
|
|
3666
|
+
const to = store.bankAccounts.find((a) => a.id === input.toAccountId);
|
|
3667
|
+
if (from) {
|
|
3668
|
+
from.currentBalance = round2(from.currentBalance - input.amount);
|
|
3669
|
+
from.updatedAt = now();
|
|
3670
|
+
}
|
|
3671
|
+
if (to) {
|
|
3672
|
+
to.currentBalance = round2(to.currentBalance + input.amount);
|
|
3673
|
+
to.updatedAt = now();
|
|
3674
|
+
}
|
|
3675
|
+
return { transferId, debitMovementId, creditMovementId };
|
|
3676
|
+
},
|
|
3085
3677
|
// --- Bank Accounts ---
|
|
3086
3678
|
async getBankAccounts() {
|
|
3087
3679
|
return store.bankAccounts.filter((a) => a.isActive);
|
|
@@ -3186,25 +3778,76 @@ function createMockFinancialProvider(options) {
|
|
|
3186
3778
|
if (dateRange) results = results.filter((t2) => matchesDateRange(t2.expectedDate, dateRange));
|
|
3187
3779
|
return results.sort((a, b) => a.expectedDate.localeCompare(b.expectedDate));
|
|
3188
3780
|
},
|
|
3189
|
-
// --- Statements ---
|
|
3781
|
+
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
3190
3782
|
async getStatement(query) {
|
|
3191
|
-
const
|
|
3192
|
-
|
|
3193
|
-
const
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3783
|
+
const todayStr = today();
|
|
3784
|
+
const isRealized = (m) => m.status !== "cancelled" && !!m.paymentDate;
|
|
3785
|
+
const consolidated = !query.bankAccountId;
|
|
3786
|
+
const scopeAccounts = consolidated ? store.bankAccounts.filter((a) => a.isActive) : store.bankAccounts.filter((a) => a.id === query.bankAccountId);
|
|
3787
|
+
const accountIds = new Set(scopeAccounts.map((a) => a.id));
|
|
3788
|
+
const accountNameById = new Map(store.bankAccounts.map((a) => [a.id, a.name]));
|
|
3789
|
+
const initialSum = scopeAccounts.reduce((s, a) => s + a.initialBalance, 0);
|
|
3790
|
+
if (!consolidated && accountIds.size === 0) {
|
|
3791
|
+
return { entries: [], openingBalance: 0, closingBalance: 0, totalCredits: 0, totalDebits: 0, totalFees: 0, net: 0, accountId: query.bankAccountId };
|
|
3792
|
+
}
|
|
3793
|
+
const netOf = (m) => m.direction === "credit" ? m.paidAmount - (m.feeAmount ?? 0) : -m.paidAmount;
|
|
3794
|
+
const inScope = store.movements.filter((m) => isRealized(m) && (consolidated || m.bankAccountId && accountIds.has(m.bankAccountId)));
|
|
3795
|
+
const openingBalance = round2(initialSum + inScope.filter((m) => (m.paymentDate ?? "") < query.dateRange.from).reduce((s, m) => s + netOf(m), 0));
|
|
3796
|
+
const rangeMovs = inScope.filter((m) => matchesDateRange(m.paymentDate, query.dateRange)).sort((a, b) => (a.paymentDate ?? "").localeCompare(b.paymentDate ?? "") || a.createdAt.localeCompare(b.createdAt));
|
|
3797
|
+
let balance = openingBalance;
|
|
3798
|
+
let totalCredits = 0, totalDebits = 0, totalFees = 0;
|
|
3799
|
+
const entries = rangeMovs.map((movement) => {
|
|
3800
|
+
const gross = movement.paidAmount;
|
|
3801
|
+
const isCredit = movement.direction === "credit";
|
|
3802
|
+
const fee = isCredit ? movement.feeAmount ?? 0 : 0;
|
|
3803
|
+
const net = isCredit ? round2(gross - fee) : gross;
|
|
3804
|
+
const isTransfer = movement.movementKind === "transfer";
|
|
3805
|
+
const meta = movement.metadata ?? {};
|
|
3806
|
+
const entryKind = isTransfer ? meta.transferRole === "in" ? "transfer-in" : "transfer-out" : "movement";
|
|
3807
|
+
const counterAccountId = isTransfer ? meta.counterAccountId : void 0;
|
|
3808
|
+
balance = round2(isCredit ? balance + net : balance - net);
|
|
3809
|
+
if (!(consolidated && isTransfer)) {
|
|
3810
|
+
if (isCredit) totalCredits = round2(totalCredits + net);
|
|
3811
|
+
else totalDebits = round2(totalDebits + net);
|
|
3812
|
+
totalFees = round2(totalFees + fee);
|
|
3203
3813
|
}
|
|
3204
|
-
|
|
3205
|
-
|
|
3814
|
+
return {
|
|
3815
|
+
movement,
|
|
3816
|
+
invoice: movement.invoiceId ? store.invoices.find((i) => i.id === movement.invoiceId) : void 0,
|
|
3817
|
+
runningBalance: balance,
|
|
3818
|
+
gross,
|
|
3819
|
+
fee,
|
|
3820
|
+
net,
|
|
3821
|
+
entryKind,
|
|
3822
|
+
counterAccountId,
|
|
3823
|
+
counterAccountName: counterAccountId ? accountNameById.get(counterAccountId) : void 0
|
|
3824
|
+
};
|
|
3825
|
+
});
|
|
3826
|
+
for (const e of entries) {
|
|
3827
|
+
const mid = e.movement.matchedMovementId;
|
|
3828
|
+
const mm = mid ? store.movements.find((m) => m.id === mid) : void 0;
|
|
3829
|
+
if (!mm) continue;
|
|
3830
|
+
const inv = mm.invoiceId ? store.invoices.find((i) => i.id === mm.invoiceId) : void 0;
|
|
3831
|
+
e.reconciledWith = {
|
|
3832
|
+
movementId: mm.id,
|
|
3833
|
+
description: mm.notes ?? void 0,
|
|
3834
|
+
invoiceId: mm.invoiceId,
|
|
3835
|
+
fiscalNumber: inv?.fiscalNumber,
|
|
3836
|
+
contactName: inv?.contactName
|
|
3837
|
+
};
|
|
3206
3838
|
}
|
|
3207
|
-
|
|
3839
|
+
const accountCurrentBalance = round2(initialSum + inScope.filter((m) => (m.paymentDate ?? "") <= todayStr).reduce((s, m) => s + netOf(m), 0));
|
|
3840
|
+
return {
|
|
3841
|
+
entries,
|
|
3842
|
+
openingBalance,
|
|
3843
|
+
closingBalance: balance,
|
|
3844
|
+
totalCredits,
|
|
3845
|
+
totalDebits,
|
|
3846
|
+
totalFees,
|
|
3847
|
+
net: round2(totalCredits - totalDebits),
|
|
3848
|
+
accountId: query.bankAccountId,
|
|
3849
|
+
accountCurrentBalance
|
|
3850
|
+
};
|
|
3208
3851
|
},
|
|
3209
3852
|
// --- Summary ---
|
|
3210
3853
|
async getSummary(dateRange) {
|
|
@@ -3219,7 +3862,7 @@ function createMockFinancialProvider(options) {
|
|
|
3219
3862
|
const monthStart = todayStr.slice(0, 7) + "-01";
|
|
3220
3863
|
const monthEnd = todayStr;
|
|
3221
3864
|
const monthMovements = store.movements.filter(
|
|
3222
|
-
(m) => m.
|
|
3865
|
+
(m) => m.movementKind === "payment" && matchesDateRange(m.paymentDate, dateRange ?? { from: monthStart, to: monthEnd })
|
|
3223
3866
|
);
|
|
3224
3867
|
const overdueReceivable = pendingReceivable.filter((m) => m.dueDate < todayStr);
|
|
3225
3868
|
const overduePayable = pendingPayable.filter((m) => m.dueDate < todayStr);
|
|
@@ -3234,6 +3877,90 @@ function createMockFinancialProvider(options) {
|
|
|
3234
3877
|
overduePayableCount: overduePayable.length,
|
|
3235
3878
|
overduePayableAmount: overduePayable.reduce((s, m) => s + (m.amount - m.paidAmount), 0)
|
|
3236
3879
|
};
|
|
3880
|
+
},
|
|
3881
|
+
// --- Reconciliation (conciliação) ---
|
|
3882
|
+
async importBankTransactions(input) {
|
|
3883
|
+
let imported = 0, duplicates = 0;
|
|
3884
|
+
for (const line of input.lines) {
|
|
3885
|
+
const dup = store.movements.some((m) => m.externalSource === line.externalSource && m.externalId === line.externalId);
|
|
3886
|
+
if (dup) {
|
|
3887
|
+
duplicates++;
|
|
3888
|
+
continue;
|
|
3889
|
+
}
|
|
3890
|
+
store.movements.push({
|
|
3891
|
+
id: uid(),
|
|
3892
|
+
direction: line.direction,
|
|
3893
|
+
movementKind: "payment",
|
|
3894
|
+
amount: line.amount,
|
|
3895
|
+
paidAmount: line.amount,
|
|
3896
|
+
status: "paid",
|
|
3897
|
+
dueDate: line.date,
|
|
3898
|
+
paymentDate: line.date,
|
|
3899
|
+
bankAccountId: line.bankAccountId ?? input.bankAccountId,
|
|
3900
|
+
notes: line.description,
|
|
3901
|
+
externalId: line.externalId,
|
|
3902
|
+
externalSource: line.externalSource,
|
|
3903
|
+
tenantId,
|
|
3904
|
+
createdAt: now(),
|
|
3905
|
+
updatedAt: now()
|
|
3906
|
+
});
|
|
3907
|
+
imported++;
|
|
3908
|
+
}
|
|
3909
|
+
return { imported, duplicates };
|
|
3910
|
+
},
|
|
3911
|
+
async getUnreconciled(query) {
|
|
3912
|
+
let results = store.movements.filter((m) => m.externalSource && !m.reconciledAt);
|
|
3913
|
+
if (query?.bankAccountId) results = results.filter((m) => m.bankAccountId === query.bankAccountId);
|
|
3914
|
+
if (query?.dateRange) results = results.filter((m) => matchesDateRange(m.paymentDate, query.dateRange));
|
|
3915
|
+
return results.sort((a, b) => (b.paymentDate ?? "").localeCompare(a.paymentDate ?? ""));
|
|
3916
|
+
},
|
|
3917
|
+
async suggestReconciliation(bankMovementId) {
|
|
3918
|
+
const line = store.movements.find((m) => m.id === bankMovementId);
|
|
3919
|
+
if (!line) return [];
|
|
3920
|
+
const amount = line.amount;
|
|
3921
|
+
const tol = Math.max(0.01, round2(amount * 0.02));
|
|
3922
|
+
const lineDate = line.paymentDate ?? line.dueDate;
|
|
3923
|
+
return store.movements.filter((m) => !m.externalSource && !m.reconciledAt && m.direction === line.direction && m.id !== bankMovementId && Math.abs(m.amount - amount) <= tol).map((m) => {
|
|
3924
|
+
const amtScore = amount > 0 ? Math.max(0, 1 - Math.abs(m.amount - amount) / amount) : 0;
|
|
3925
|
+
const refDate = m.dueDate ?? m.paymentDate;
|
|
3926
|
+
const days = lineDate && refDate ? Math.abs((new Date(lineDate).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
|
|
3927
|
+
const dateScore = Math.max(0, 1 - days / 30);
|
|
3928
|
+
return {
|
|
3929
|
+
movement: m,
|
|
3930
|
+
invoice: m.invoiceId ? store.invoices.find((i) => i.id === m.invoiceId) : void 0,
|
|
3931
|
+
score: round2(0.7 * amtScore + 0.3 * dateScore)
|
|
3932
|
+
};
|
|
3933
|
+
}).sort((a, b) => b.score - a.score).slice(0, 25);
|
|
3934
|
+
},
|
|
3935
|
+
async reconcileMovement(input) {
|
|
3936
|
+
const line = store.movements.find((m) => m.id === input.bankMovementId);
|
|
3937
|
+
if (line) {
|
|
3938
|
+
line.reconciledAt = now();
|
|
3939
|
+
line.matchedMovementId = input.matchedMovementId;
|
|
3940
|
+
line.updatedAt = now();
|
|
3941
|
+
}
|
|
3942
|
+
if (input.matchedMovementId) {
|
|
3943
|
+
const internal = store.movements.find((m) => m.id === input.matchedMovementId);
|
|
3944
|
+
if (internal) {
|
|
3945
|
+
internal.reconciledAt = now();
|
|
3946
|
+
internal.updatedAt = now();
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
},
|
|
3950
|
+
async unreconcileMovement(bankMovementId) {
|
|
3951
|
+
const line = store.movements.find((m) => m.id === bankMovementId);
|
|
3952
|
+
if (!line) return;
|
|
3953
|
+
const matchedId = line.matchedMovementId;
|
|
3954
|
+
line.reconciledAt = void 0;
|
|
3955
|
+
line.matchedMovementId = void 0;
|
|
3956
|
+
line.updatedAt = now();
|
|
3957
|
+
if (matchedId) {
|
|
3958
|
+
const internal = store.movements.find((m) => m.id === matchedId);
|
|
3959
|
+
if (internal) {
|
|
3960
|
+
internal.reconciledAt = void 0;
|
|
3961
|
+
internal.updatedAt = now();
|
|
3962
|
+
}
|
|
3963
|
+
}
|
|
3237
3964
|
}
|
|
3238
3965
|
};
|
|
3239
3966
|
return provider;
|
|
@@ -3313,6 +4040,21 @@ async function fetchInvoiceBalances(pub, ids) {
|
|
|
3313
4040
|
}
|
|
3314
4041
|
return map;
|
|
3315
4042
|
}
|
|
4043
|
+
function round22(n) {
|
|
4044
|
+
return Math.round(n * 100) / 100;
|
|
4045
|
+
}
|
|
4046
|
+
async function getMethodFeePct(pub, paymentMethodId) {
|
|
4047
|
+
if (!paymentMethodId) return 0;
|
|
4048
|
+
const { data } = await pub.from("payment_methods").select("discount_value").eq("id", paymentMethodId).single();
|
|
4049
|
+
return Number(data?.discount_value) || 0;
|
|
4050
|
+
}
|
|
4051
|
+
function genId() {
|
|
4052
|
+
try {
|
|
4053
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
|
|
4054
|
+
} catch {
|
|
4055
|
+
}
|
|
4056
|
+
return "tr-" + Date.now().toString(36) + "-" + Math.random().toString(36).slice(2, 10);
|
|
4057
|
+
}
|
|
3316
4058
|
function createSupabaseFinancialProvider() {
|
|
3317
4059
|
const provider = {
|
|
3318
4060
|
// --- Invoices (orders at stage=invoiced/paid/partial/overdue/cancelled with direction) ---
|
|
@@ -3513,16 +4255,38 @@ function createSupabaseFinancialProvider() {
|
|
|
3513
4255
|
const { core, pub } = getClients();
|
|
3514
4256
|
const { data: mov } = await pub.from("financial_movements").select("*").eq("id", input.movementId).single();
|
|
3515
4257
|
if (!mov) throw new Error("Movement not found");
|
|
3516
|
-
const
|
|
3517
|
-
const
|
|
3518
|
-
const
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
4258
|
+
const tenantId = mov.tenant_id ?? getTenantId();
|
|
4259
|
+
const feePct = mov.direction === "credit" ? await getMethodFeePct(pub, input.paymentMethodId) : 0;
|
|
4260
|
+
const thisFee = round22(input.amount * (feePct / 100));
|
|
4261
|
+
const paymentRow = {
|
|
4262
|
+
tenant_id: tenantId,
|
|
4263
|
+
invoice_id: mov.invoice_id,
|
|
4264
|
+
direction: mov.direction,
|
|
4265
|
+
movement_kind: "payment",
|
|
4266
|
+
amount: input.amount,
|
|
4267
|
+
paid_amount: input.amount,
|
|
4268
|
+
fee_amount: thisFee,
|
|
4269
|
+
status: "paid",
|
|
4270
|
+
due_date: mov.due_date,
|
|
4271
|
+
payment_date: input.paymentDate,
|
|
4272
|
+
installment_number: mov.installment_number,
|
|
4273
|
+
payment_method_id: input.paymentMethodId ?? null,
|
|
4274
|
+
payment_method_type_id: input.paymentMethodTypeId ?? null,
|
|
4275
|
+
bank_account_id: input.bankAccountId ?? null,
|
|
4276
|
+
cash_session_id: input.cashSessionId ?? null,
|
|
4277
|
+
card_brand: input.cardBrand ?? null,
|
|
4278
|
+
card_installments: input.cardInstallments ?? null,
|
|
4279
|
+
notes: input.notes ?? null
|
|
4280
|
+
};
|
|
4281
|
+
let { data, error } = await pub.from("financial_movements").insert(paymentRow).select().single();
|
|
4282
|
+
if (error && /fee_amount/i.test(error.message ?? "")) {
|
|
4283
|
+
const { fee_amount, ...rest } = paymentRow;
|
|
4284
|
+
({ data, error } = await pub.from("financial_movements").insert(rest).select().single());
|
|
4285
|
+
}
|
|
4286
|
+
if (error) throw error;
|
|
4287
|
+
const newPaid = round22((mov.paid_amount ?? 0) + input.amount);
|
|
4288
|
+
const billStatus = newPaid >= mov.amount ? "paid" : "partial";
|
|
4289
|
+
await pub.from("financial_movements").update({ paid_amount: newPaid, status: billStatus, payment_date: null, bank_account_id: null }).eq("id", mov.id);
|
|
3526
4290
|
if (mov.invoice_id) {
|
|
3527
4291
|
const { data: allMovs } = await pub.from("financial_movements").select("status, amount, paid_amount").eq("invoice_id", mov.invoice_id).eq("movement_kind", "bill");
|
|
3528
4292
|
const movs = allMovs ?? [];
|
|
@@ -3543,6 +4307,51 @@ function createSupabaseFinancialProvider() {
|
|
|
3543
4307
|
const { pub } = getClients();
|
|
3544
4308
|
await pub.from("financial_movements").update({ status: "cancelled" }).eq("id", id);
|
|
3545
4309
|
},
|
|
4310
|
+
// --- Transfers (between accounts) ---
|
|
4311
|
+
async createTransfer(input) {
|
|
4312
|
+
const { pub } = getClients();
|
|
4313
|
+
if (!input.fromAccountId || !input.toAccountId || input.fromAccountId === input.toAccountId) {
|
|
4314
|
+
throw new Error("Source and destination accounts must differ");
|
|
4315
|
+
}
|
|
4316
|
+
if (!(input.amount > 0)) throw new Error("Amount must be greater than zero");
|
|
4317
|
+
const tenantId = getTenantId();
|
|
4318
|
+
const transferId = genId();
|
|
4319
|
+
const base = {
|
|
4320
|
+
tenant_id: tenantId,
|
|
4321
|
+
movement_kind: "transfer",
|
|
4322
|
+
amount: input.amount,
|
|
4323
|
+
paid_amount: input.amount,
|
|
4324
|
+
fee_amount: 0,
|
|
4325
|
+
status: "paid",
|
|
4326
|
+
due_date: input.date,
|
|
4327
|
+
payment_date: input.date,
|
|
4328
|
+
notes: input.notes ?? null
|
|
4329
|
+
};
|
|
4330
|
+
const rowsToInsert = [
|
|
4331
|
+
{
|
|
4332
|
+
...base,
|
|
4333
|
+
direction: "debit",
|
|
4334
|
+
bank_account_id: input.fromAccountId,
|
|
4335
|
+
metadata: { transferId, transferRole: "out", counterAccountId: input.toAccountId }
|
|
4336
|
+
},
|
|
4337
|
+
{
|
|
4338
|
+
...base,
|
|
4339
|
+
direction: "credit",
|
|
4340
|
+
bank_account_id: input.toAccountId,
|
|
4341
|
+
metadata: { transferId, transferRole: "in", counterAccountId: input.fromAccountId }
|
|
4342
|
+
}
|
|
4343
|
+
];
|
|
4344
|
+
let { data, error } = await pub.from("financial_movements").insert(rowsToInsert).select("id, direction");
|
|
4345
|
+
if (error && /fee_amount/i.test(error.message ?? "")) {
|
|
4346
|
+
const stripped = rowsToInsert.map(({ fee_amount, ...rest }) => rest);
|
|
4347
|
+
({ data, error } = await pub.from("financial_movements").insert(stripped).select("id, direction"));
|
|
4348
|
+
}
|
|
4349
|
+
if (error) throw error;
|
|
4350
|
+
const rows = data ?? [];
|
|
4351
|
+
const debitMovementId = rows.find((r) => r.direction === "debit")?.id ?? "";
|
|
4352
|
+
const creditMovementId = rows.find((r) => r.direction === "credit")?.id ?? "";
|
|
4353
|
+
return { transferId, debitMovementId, creditMovementId };
|
|
4354
|
+
},
|
|
3546
4355
|
// --- Bank Accounts ---
|
|
3547
4356
|
async getBankAccounts() {
|
|
3548
4357
|
const { pub } = getClients();
|
|
@@ -3640,18 +4449,102 @@ function createSupabaseFinancialProvider() {
|
|
|
3640
4449
|
getClients();
|
|
3641
4450
|
return [];
|
|
3642
4451
|
},
|
|
3643
|
-
// --- Statements ---
|
|
4452
|
+
// --- Statements (ERP extract: opening balance + realized cash + per-row net/fee) ---
|
|
3644
4453
|
async getStatement(query) {
|
|
3645
|
-
const { pub } = getClients();
|
|
3646
|
-
const
|
|
3647
|
-
const { data:
|
|
3648
|
-
|
|
3649
|
-
|
|
3650
|
-
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
4454
|
+
const { core, pub } = getClients();
|
|
4455
|
+
const today2 = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4456
|
+
const { data: acctRows } = await pub.from("bank_accounts").select("id, name, initial_balance, is_active");
|
|
4457
|
+
const allAccounts = acctRows ?? [];
|
|
4458
|
+
const consolidated = !query.bankAccountId;
|
|
4459
|
+
const scopeAccounts = consolidated ? allAccounts.filter((a) => a.is_active) : allAccounts.filter((a) => a.id === query.bankAccountId);
|
|
4460
|
+
const accountIds = scopeAccounts.map((a) => a.id);
|
|
4461
|
+
const accountNameById = new Map(allAccounts.map((a) => [a.id, a.name]));
|
|
4462
|
+
const initialSum = scopeAccounts.reduce((s, a) => s + (Number(a.initial_balance) || 0), 0);
|
|
4463
|
+
if (!consolidated && accountIds.length === 0) {
|
|
4464
|
+
return { entries: [], openingBalance: 0, closingBalance: 0, totalCredits: 0, totalDebits: 0, totalFees: 0, net: 0, accountId: query.bankAccountId };
|
|
4465
|
+
}
|
|
4466
|
+
const scopeFilter = (qb) => consolidated ? qb : qb.in("bank_account_id", accountIds);
|
|
4467
|
+
const netOf = (r) => r.direction === "credit" ? (Number(r.paid_amount) || 0) - (Number(r.fee_amount) || 0) : -(Number(r.paid_amount) || 0);
|
|
4468
|
+
const { data: priorRows } = await scopeFilter(pub.from("financial_movements").select("*").neq("status", "cancelled").lt("payment_date", query.dateRange.from));
|
|
4469
|
+
const priorNet = (priorRows ?? []).reduce((s, r) => s + netOf(r), 0);
|
|
4470
|
+
const openingBalance = round22(initialSum + priorNet);
|
|
4471
|
+
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"));
|
|
4472
|
+
const invoiceIds = [...new Set((movs ?? []).map((r) => r.invoice_id).filter(Boolean))];
|
|
4473
|
+
const invoiceById = /* @__PURE__ */ new Map();
|
|
4474
|
+
if (invoiceIds.length) {
|
|
4475
|
+
const { data: orders } = await core.from("orders").select("id, party_id, reference_number, metadata").in("id", invoiceIds);
|
|
4476
|
+
for (const o of orders ?? []) {
|
|
4477
|
+
invoiceById.set(o.id, { id: o.id, contactId: o.party_id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number });
|
|
4478
|
+
}
|
|
4479
|
+
}
|
|
4480
|
+
let balance = openingBalance;
|
|
4481
|
+
let totalCredits = 0, totalDebits = 0, totalFees = 0;
|
|
4482
|
+
const entries = (movs ?? []).map((r) => {
|
|
4483
|
+
const movement = snakeToCamel(r);
|
|
4484
|
+
const gross = Number(r.paid_amount) || 0;
|
|
4485
|
+
const isCredit = r.direction === "credit";
|
|
4486
|
+
const fee = isCredit ? Number(r.fee_amount) || 0 : 0;
|
|
4487
|
+
const net = isCredit ? round22(gross - fee) : gross;
|
|
4488
|
+
const meta = r.metadata ?? {};
|
|
4489
|
+
const isTransfer = r.movement_kind === "transfer";
|
|
4490
|
+
const entryKind = isTransfer ? meta.transferRole === "in" ? "transfer-in" : "transfer-out" : "movement";
|
|
4491
|
+
const counterAccountId = isTransfer ? meta.counterAccountId : void 0;
|
|
4492
|
+
if (isCredit) balance = round22(balance + net);
|
|
4493
|
+
else balance = round22(balance - net);
|
|
4494
|
+
if (!(consolidated && isTransfer)) {
|
|
4495
|
+
if (isCredit) totalCredits = round22(totalCredits + net);
|
|
4496
|
+
else totalDebits = round22(totalDebits + net);
|
|
4497
|
+
totalFees = round22(totalFees + fee);
|
|
4498
|
+
}
|
|
4499
|
+
return {
|
|
4500
|
+
movement,
|
|
4501
|
+
invoice: r.invoice_id ? invoiceById.get(r.invoice_id) : void 0,
|
|
4502
|
+
runningBalance: balance,
|
|
4503
|
+
gross,
|
|
4504
|
+
fee,
|
|
4505
|
+
net,
|
|
4506
|
+
entryKind,
|
|
4507
|
+
counterAccountId,
|
|
4508
|
+
counterAccountName: counterAccountId ? accountNameById.get(counterAccountId) : void 0
|
|
4509
|
+
};
|
|
3654
4510
|
});
|
|
4511
|
+
const matchedIds = [...new Set(entries.map((e) => e.movement.matchedMovementId).filter(Boolean))];
|
|
4512
|
+
if (matchedIds.length) {
|
|
4513
|
+
const { data: matched } = await pub.from("financial_movements").select("id, invoice_id, notes").in("id", matchedIds);
|
|
4514
|
+
const matchedById = new Map((matched ?? []).map((m) => [m.id, m]));
|
|
4515
|
+
const matchedInvoiceIds = [...new Set((matched ?? []).map((m) => m.invoice_id).filter(Boolean))];
|
|
4516
|
+
const invById = /* @__PURE__ */ new Map();
|
|
4517
|
+
if (matchedInvoiceIds.length) {
|
|
4518
|
+
const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", matchedInvoiceIds);
|
|
4519
|
+
for (const o of orders ?? []) invById.set(o.id, o);
|
|
4520
|
+
}
|
|
4521
|
+
for (const e of entries) {
|
|
4522
|
+
const mid = e.movement.matchedMovementId;
|
|
4523
|
+
const mm = mid ? matchedById.get(mid) : void 0;
|
|
4524
|
+
if (!mm) continue;
|
|
4525
|
+
const inv = mm.invoice_id ? invById.get(mm.invoice_id) : void 0;
|
|
4526
|
+
e.reconciledWith = {
|
|
4527
|
+
movementId: mm.id,
|
|
4528
|
+
description: mm.notes ?? void 0,
|
|
4529
|
+
invoiceId: mm.invoice_id ?? void 0,
|
|
4530
|
+
fiscalNumber: inv?.reference_number ?? void 0,
|
|
4531
|
+
contactName: inv?.metadata?.contactName ?? void 0
|
|
4532
|
+
};
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
const { data: todayRows } = await scopeFilter(pub.from("financial_movements").select("*").neq("status", "cancelled").lte("payment_date", today2));
|
|
4536
|
+
const accountCurrentBalance = round22(initialSum + (todayRows ?? []).reduce((s, r) => s + netOf(r), 0));
|
|
4537
|
+
return {
|
|
4538
|
+
entries,
|
|
4539
|
+
openingBalance,
|
|
4540
|
+
closingBalance: balance,
|
|
4541
|
+
totalCredits,
|
|
4542
|
+
totalDebits,
|
|
4543
|
+
totalFees,
|
|
4544
|
+
net: round22(totalCredits - totalDebits),
|
|
4545
|
+
accountId: query.bankAccountId,
|
|
4546
|
+
accountCurrentBalance
|
|
4547
|
+
};
|
|
3655
4548
|
},
|
|
3656
4549
|
// --- Summary ---
|
|
3657
4550
|
async getSummary(dateRange) {
|
|
@@ -3668,7 +4561,7 @@ function createSupabaseFinancialProvider() {
|
|
|
3668
4561
|
const overdueCredit = unpaidCredit.filter((m) => m.status === "overdue" || m.due_date < today2);
|
|
3669
4562
|
const overdueDebit = unpaidDebit.filter((m) => m.status === "overdue" || m.due_date < today2);
|
|
3670
4563
|
const monthStart = today2.slice(0, 7) + "-01";
|
|
3671
|
-
const monthPaid = allMovs.filter((m) => m.
|
|
4564
|
+
const monthPaid = allMovs.filter((m) => m.movement_kind === "payment" && m.payment_date >= monthStart);
|
|
3672
4565
|
return {
|
|
3673
4566
|
totalBalance,
|
|
3674
4567
|
totalReceivable: unpaidCredit.reduce((s, m) => s + m.amount - m.paid_amount, 0),
|
|
@@ -3680,6 +4573,93 @@ function createSupabaseFinancialProvider() {
|
|
|
3680
4573
|
overduePayableCount: overdueDebit.length,
|
|
3681
4574
|
overduePayableAmount: overdueDebit.reduce((s, m) => s + m.amount - m.paid_amount, 0)
|
|
3682
4575
|
};
|
|
4576
|
+
},
|
|
4577
|
+
// --- Reconciliation (conciliação) ---
|
|
4578
|
+
// Imported bank-statement lines live in financial_movements tagged with
|
|
4579
|
+
// (external_source, external_id), status='paid'. Reconciling links a line
|
|
4580
|
+
// to the internal movement it settles (or accepts it standalone).
|
|
4581
|
+
async importBankTransactions(input) {
|
|
4582
|
+
const { pub } = getClients();
|
|
4583
|
+
const tenantId = getTenantId();
|
|
4584
|
+
let imported = 0, duplicates = 0;
|
|
4585
|
+
for (const line of input.lines) {
|
|
4586
|
+
const { error } = await pub.from("financial_movements").insert({
|
|
4587
|
+
tenant_id: tenantId,
|
|
4588
|
+
direction: line.direction,
|
|
4589
|
+
movement_kind: "payment",
|
|
4590
|
+
amount: line.amount,
|
|
4591
|
+
paid_amount: line.amount,
|
|
4592
|
+
status: "paid",
|
|
4593
|
+
due_date: line.date,
|
|
4594
|
+
payment_date: line.date,
|
|
4595
|
+
bank_account_id: line.bankAccountId ?? input.bankAccountId ?? null,
|
|
4596
|
+
notes: line.description,
|
|
4597
|
+
external_id: line.externalId,
|
|
4598
|
+
external_source: line.externalSource
|
|
4599
|
+
});
|
|
4600
|
+
if (error) {
|
|
4601
|
+
if (error.code === "23505") duplicates++;
|
|
4602
|
+
else throw error;
|
|
4603
|
+
} else imported++;
|
|
4604
|
+
}
|
|
4605
|
+
return { imported, duplicates };
|
|
4606
|
+
},
|
|
4607
|
+
async getUnreconciled(query) {
|
|
4608
|
+
const { pub } = getClients();
|
|
4609
|
+
let qb = pub.from("financial_movements").select("*").not("external_source", "is", null).is("reconciled_at", null);
|
|
4610
|
+
if (query?.bankAccountId) qb = qb.eq("bank_account_id", query.bankAccountId);
|
|
4611
|
+
if (query?.dateRange) qb = qb.gte("payment_date", query.dateRange.from).lte("payment_date", query.dateRange.to);
|
|
4612
|
+
const { data } = await qb.order("payment_date", { ascending: false });
|
|
4613
|
+
return (data ?? []).map((r) => snakeToCamel(r));
|
|
4614
|
+
},
|
|
4615
|
+
async suggestReconciliation(bankMovementId) {
|
|
4616
|
+
const { core, pub } = getClients();
|
|
4617
|
+
const { data: line } = await pub.from("financial_movements").select("*").eq("id", bankMovementId).single();
|
|
4618
|
+
if (!line) return [];
|
|
4619
|
+
const amount = Number(line.amount) || 0;
|
|
4620
|
+
const lineDate = line.payment_date ?? line.due_date;
|
|
4621
|
+
const tol = Math.max(0.01, round22(amount * 0.02));
|
|
4622
|
+
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);
|
|
4623
|
+
const candidates = (data ?? []).map((r) => {
|
|
4624
|
+
const amtDiff = Math.abs((Number(r.amount) || 0) - amount);
|
|
4625
|
+
const amtScore = amount > 0 ? Math.max(0, 1 - amtDiff / amount) : 0;
|
|
4626
|
+
const refDate = r.due_date ?? r.payment_date;
|
|
4627
|
+
const days = lineDate && refDate ? Math.abs((new Date(lineDate).getTime() - new Date(refDate).getTime()) / 864e5) : 30;
|
|
4628
|
+
const dateScore = Math.max(0, 1 - days / 30);
|
|
4629
|
+
return {
|
|
4630
|
+
movement: snakeToCamel(r),
|
|
4631
|
+
score: round22(0.7 * amtScore + 0.3 * dateScore)
|
|
4632
|
+
};
|
|
4633
|
+
}).sort((a, b) => b.score - a.score);
|
|
4634
|
+
const invoiceIds = [...new Set(candidates.map((c) => c.movement.invoiceId).filter(Boolean))];
|
|
4635
|
+
if (invoiceIds.length) {
|
|
4636
|
+
const { data: orders } = await core.from("orders").select("id, reference_number, metadata").in("id", invoiceIds);
|
|
4637
|
+
const byId = new Map((orders ?? []).map((o) => [o.id, o]));
|
|
4638
|
+
for (const c of candidates) {
|
|
4639
|
+
const o = c.movement.invoiceId ? byId.get(c.movement.invoiceId) : void 0;
|
|
4640
|
+
if (o) c.invoice = { id: o.id, contactName: o.metadata?.contactName, fiscalNumber: o.reference_number };
|
|
4641
|
+
}
|
|
4642
|
+
}
|
|
4643
|
+
return candidates;
|
|
4644
|
+
},
|
|
4645
|
+
async reconcileMovement(input) {
|
|
4646
|
+
const { pub } = getClients();
|
|
4647
|
+
const now2 = (/* @__PURE__ */ new Date()).toISOString();
|
|
4648
|
+
await pub.from("financial_movements").update({
|
|
4649
|
+
reconciled_at: now2,
|
|
4650
|
+
matched_movement_id: input.matchedMovementId ?? null
|
|
4651
|
+
}).eq("id", input.bankMovementId);
|
|
4652
|
+
if (input.matchedMovementId) {
|
|
4653
|
+
await pub.from("financial_movements").update({ reconciled_at: now2 }).eq("id", input.matchedMovementId);
|
|
4654
|
+
}
|
|
4655
|
+
},
|
|
4656
|
+
async unreconcileMovement(bankMovementId) {
|
|
4657
|
+
const { pub } = getClients();
|
|
4658
|
+
const { data: line } = await pub.from("financial_movements").select("matched_movement_id").eq("id", bankMovementId).single();
|
|
4659
|
+
await pub.from("financial_movements").update({ reconciled_at: null, matched_movement_id: null }).eq("id", bankMovementId);
|
|
4660
|
+
if (line?.matched_movement_id) {
|
|
4661
|
+
await pub.from("financial_movements").update({ reconciled_at: null }).eq("id", line.matched_movement_id);
|
|
4662
|
+
}
|
|
3683
4663
|
}
|
|
3684
4664
|
};
|
|
3685
4665
|
return provider;
|
|
@@ -3701,8 +4681,9 @@ function createFinancialStore(provider) {
|
|
|
3701
4681
|
paymentMethodTypes: [],
|
|
3702
4682
|
summary: null,
|
|
3703
4683
|
summaryLoading: false,
|
|
3704
|
-
|
|
4684
|
+
statement: null,
|
|
3705
4685
|
statementLoading: false,
|
|
4686
|
+
statementQuery: null,
|
|
3706
4687
|
cardTransactions: [],
|
|
3707
4688
|
cardsLoading: false,
|
|
3708
4689
|
async fetchSummary(dateRange) {
|
|
@@ -3744,9 +4725,9 @@ function createFinancialStore(provider) {
|
|
|
3744
4725
|
},
|
|
3745
4726
|
async fetchStatement(query) {
|
|
3746
4727
|
return dedup("fin:statement:" + JSON.stringify(query), async () => {
|
|
3747
|
-
set({ statementLoading: true });
|
|
3748
|
-
const
|
|
3749
|
-
set({
|
|
4728
|
+
set({ statementLoading: true, statementQuery: query });
|
|
4729
|
+
const statement = await provider.getStatement(query);
|
|
4730
|
+
set({ statement, statementLoading: false });
|
|
3750
4731
|
});
|
|
3751
4732
|
},
|
|
3752
4733
|
async fetchCardTransactions(dateRange) {
|
|
@@ -3782,6 +4763,22 @@ function createFinancialStore(provider) {
|
|
|
3782
4763
|
throw err;
|
|
3783
4764
|
}
|
|
3784
4765
|
},
|
|
4766
|
+
async createTransfer(input) {
|
|
4767
|
+
try {
|
|
4768
|
+
await provider.createTransfer(input);
|
|
4769
|
+
const query = get().statementQuery;
|
|
4770
|
+
const [bankAccounts, summary, statement] = await Promise.all([
|
|
4771
|
+
provider.getBankAccounts(),
|
|
4772
|
+
provider.getSummary(),
|
|
4773
|
+
query ? provider.getStatement(query) : Promise.resolve(get().statement)
|
|
4774
|
+
]);
|
|
4775
|
+
set({ bankAccounts, summary, statement });
|
|
4776
|
+
toast.success("Transfer recorded");
|
|
4777
|
+
} catch (err) {
|
|
4778
|
+
toast.error("Failed to record transfer", { description: err?.message });
|
|
4779
|
+
throw err;
|
|
4780
|
+
}
|
|
4781
|
+
},
|
|
3785
4782
|
async cancelInvoice(invoiceId) {
|
|
3786
4783
|
try {
|
|
3787
4784
|
await provider.cancelInvoice(invoiceId);
|
|
@@ -3843,7 +4840,9 @@ var paymentMethodEntity = {
|
|
|
3843
4840
|
defaultSort: "name",
|
|
3844
4841
|
fields: [
|
|
3845
4842
|
{ key: "name", label: "Name", type: "text", required: true, showInTable: true },
|
|
3846
|
-
|
|
4843
|
+
// FK to the seeded payment_method_types catalog — load real ids at runtime
|
|
4844
|
+
// (a static enum select would write the string into the uuid column).
|
|
4845
|
+
{ key: "paymentMethodTypeId", label: "Type", type: "relation", relation: { table: "payment_method_types", labelField: "name" }, showInTable: true, required: true },
|
|
3847
4846
|
{ key: "discountValue", label: "Discount %", type: "number", showInTable: true },
|
|
3848
4847
|
{ key: "interestValue", label: "Interest %", type: "number", showInTable: true },
|
|
3849
4848
|
{ key: "minInstallments", label: "Min Installments", type: "number", defaultValue: 1 },
|
|
@@ -4206,6 +5205,21 @@ var en = {
|
|
|
4206
5205
|
"financial.nav.rules": "Rules",
|
|
4207
5206
|
"financial.nav.statements": "Statements",
|
|
4208
5207
|
"financial.nav.summary": "Summary",
|
|
5208
|
+
"financial.reconciliation.title": "Reconciliation",
|
|
5209
|
+
"financial.reconciliation.subtitle": "Match imported bank lines to your movements",
|
|
5210
|
+
"financial.reconciliation.pending": "{{count}} to reconcile",
|
|
5211
|
+
"financial.reconciliation.allDone": "All caught up",
|
|
5212
|
+
"financial.reconciliation.empty": "Nothing to reconcile",
|
|
5213
|
+
"financial.reconciliation.emptyHint": "Imported bank transactions will appear here for matching.",
|
|
5214
|
+
"financial.reconciliation.bankLines": "Bank lines",
|
|
5215
|
+
"financial.reconciliation.candidates": "Suggested matches",
|
|
5216
|
+
"financial.reconciliation.selectLine": "Select a bank line to see suggested matches",
|
|
5217
|
+
"financial.reconciliation.noCandidates": "No suggested matches for this line",
|
|
5218
|
+
"financial.reconciliation.match": "Match",
|
|
5219
|
+
"financial.reconciliation.accept": "Accept as new",
|
|
5220
|
+
"financial.reconciliation.acceptHint": "No internal counterpart \u2014 keep as a standalone entry",
|
|
5221
|
+
"financial.reconciliation.matchScore": "{{score}}% match",
|
|
5222
|
+
"financial.reconciliation.refresh": "Refresh",
|
|
4209
5223
|
"financial.onboarding.back": "Back",
|
|
4210
5224
|
"financial.onboarding.configureHint": "You can configure this in detail from the Settings gear icon inside the Financial module.",
|
|
4211
5225
|
"financial.onboarding.getStarted": "Get Started",
|
|
@@ -4270,6 +5284,8 @@ var en = {
|
|
|
4270
5284
|
"financial.settingsPage.title": "Financial Settings",
|
|
4271
5285
|
"financial.payment.noMethods": "No payment methods configured.",
|
|
4272
5286
|
"financial.payment.configureMethods": "Set up payment methods",
|
|
5287
|
+
"financial.payment.noAccounts": "No compatible account. Create one in Settings.",
|
|
5288
|
+
"financial.payment.configureAccounts": "Set up accounts",
|
|
4273
5289
|
"financial.statements.account": "Account",
|
|
4274
5290
|
"financial.statements.from": "From",
|
|
4275
5291
|
"financial.statements.noTransactions": "No transactions in this period",
|
|
@@ -4405,7 +5421,66 @@ var en = {
|
|
|
4405
5421
|
"financial.cashRegister.close": "Close Register",
|
|
4406
5422
|
"financial.movements.title": "Movements",
|
|
4407
5423
|
"financial.bankAccounts.title": "Bank Accounts",
|
|
4408
|
-
"financial.settings.title": "Financial Settings"
|
|
5424
|
+
"financial.settings.title": "Financial Settings",
|
|
5425
|
+
// Settings registry tabs
|
|
5426
|
+
"registry.payment-methods": "Payment Methods",
|
|
5427
|
+
"registry.payment-methods.description": "Configured payment methods with fees and installment rules",
|
|
5428
|
+
"registry.bank-accounts": "Accounts",
|
|
5429
|
+
"registry.bank-accounts.description": "Bank accounts, cash registers, credit cards, and digital wallets",
|
|
5430
|
+
"registry.chart-of-accounts": "Chart of Accounts",
|
|
5431
|
+
"registry.chart-of-accounts.description": "Hierarchical chart of accounts for categorizing transactions",
|
|
5432
|
+
"registry.cost-centers": "Cost Centers",
|
|
5433
|
+
"registry.cost-centers.description": "Cost centers for expense tracking and departmental budgets",
|
|
5434
|
+
"registry.commission-rules": "Commission Rules",
|
|
5435
|
+
"registry.commission-rules.description": "Commission rules for service and product revenue sharing",
|
|
5436
|
+
"registry.price-tables": "Price Tables",
|
|
5437
|
+
"registry.price-tables.description": "Price tables for sale and cost pricing",
|
|
5438
|
+
"registry.price-variations": "Price Variations",
|
|
5439
|
+
"registry.price-variations.description": "Discount and surcharge rules for pricing",
|
|
5440
|
+
"registry.payment-method-types": "Payment Types",
|
|
5441
|
+
"registry.payment-method-types.description": "System payment types (cash, card, PIX, etc.)",
|
|
5442
|
+
"registry.card-brands": "Card Brands",
|
|
5443
|
+
"registry.card-brands.description": "Credit and debit card brands",
|
|
5444
|
+
// Statements / extract
|
|
5445
|
+
"financial.statements.columnDate": "Date",
|
|
5446
|
+
"financial.statements.columnDescription": "Description",
|
|
5447
|
+
"financial.statements.columnDebit": "Debit",
|
|
5448
|
+
"financial.statements.columnCredit": "Credit",
|
|
5449
|
+
"financial.statements.columnFee": "Fee",
|
|
5450
|
+
"financial.statements.columnBalance": "Balance",
|
|
5451
|
+
"financial.statements.columnMethod": "Method",
|
|
5452
|
+
"financial.statements.columnDoc": "Document",
|
|
5453
|
+
"financial.statements.installmentShort": "Inst. {{n}}",
|
|
5454
|
+
"financial.statements.viewInvoice": "View invoice",
|
|
5455
|
+
"financial.person.empty": "No financial activity for this contact yet",
|
|
5456
|
+
"financial.statements.opening": "Opening balance",
|
|
5457
|
+
"financial.statements.closing": "Closing balance",
|
|
5458
|
+
"financial.statements.currentBalance": "Current balance",
|
|
5459
|
+
"financial.statements.totalCredits": "Credits",
|
|
5460
|
+
"financial.statements.totalDebits": "Debits",
|
|
5461
|
+
"financial.statements.totalFees": "Fees",
|
|
5462
|
+
"financial.statements.net": "Net",
|
|
5463
|
+
"financial.statements.transfer": "Transfer",
|
|
5464
|
+
"financial.statements.reconciled": "Reconciled with the bank statement",
|
|
5465
|
+
"financial.statements.imported": "Imported from the bank \xB7 not reconciled",
|
|
5466
|
+
"financial.statements.reconciledWith": "Reconciled with {{ref}}",
|
|
5467
|
+
"financial.statements.newTransfer": "New Transfer",
|
|
5468
|
+
// Transfer dialog
|
|
5469
|
+
"financial.transfer.title": "Transfer Between Accounts",
|
|
5470
|
+
"financial.transfer.from": "From account",
|
|
5471
|
+
"financial.transfer.to": "To account",
|
|
5472
|
+
"financial.transfer.amount": "Amount",
|
|
5473
|
+
"financial.transfer.date": "Date",
|
|
5474
|
+
"financial.transfer.notes": "Notes",
|
|
5475
|
+
"financial.transfer.optionalNotes": "Optional notes...",
|
|
5476
|
+
"financial.transfer.selectAccount": "Select account...",
|
|
5477
|
+
"financial.transfer.submit": "Transfer",
|
|
5478
|
+
"financial.transfer.processing": "Transferring...",
|
|
5479
|
+
"financial.transfer.cancel": "Cancel",
|
|
5480
|
+
"financial.transfer.created": "Transfer recorded",
|
|
5481
|
+
"financial.transfer.failed": "Failed to record transfer",
|
|
5482
|
+
"financial.transfer.sameAccount": "Source and destination must differ",
|
|
5483
|
+
"financial.transfer.invalidAmount": "Amount must be greater than zero"
|
|
4409
5484
|
};
|
|
4410
5485
|
|
|
4411
5486
|
// src/locales/pt-BR.ts
|
|
@@ -4538,6 +5613,21 @@ var ptBR = {
|
|
|
4538
5613
|
"financial.nav.rules": "Regras",
|
|
4539
5614
|
"financial.nav.statements": "Extratos",
|
|
4540
5615
|
"financial.nav.summary": "Resumo",
|
|
5616
|
+
"financial.reconciliation.title": "Concilia\xE7\xE3o",
|
|
5617
|
+
"financial.reconciliation.subtitle": "Concilie as linhas importadas do banco com seus lan\xE7amentos",
|
|
5618
|
+
"financial.reconciliation.pending": "{{count}} para conciliar",
|
|
5619
|
+
"financial.reconciliation.allDone": "Tudo conciliado",
|
|
5620
|
+
"financial.reconciliation.empty": "Nada para conciliar",
|
|
5621
|
+
"financial.reconciliation.emptyHint": "As transa\xE7\xF5es banc\xE1rias importadas aparecem aqui para concilia\xE7\xE3o.",
|
|
5622
|
+
"financial.reconciliation.bankLines": "Linhas do banco",
|
|
5623
|
+
"financial.reconciliation.candidates": "Correspond\xEAncias sugeridas",
|
|
5624
|
+
"financial.reconciliation.selectLine": "Selecione uma linha do banco para ver as sugest\xF5es",
|
|
5625
|
+
"financial.reconciliation.noCandidates": "Nenhuma correspond\xEAncia sugerida para esta linha",
|
|
5626
|
+
"financial.reconciliation.match": "Conciliar",
|
|
5627
|
+
"financial.reconciliation.accept": "Aceitar como novo",
|
|
5628
|
+
"financial.reconciliation.acceptHint": "Sem contrapartida interna \u2014 manter como lan\xE7amento avulso",
|
|
5629
|
+
"financial.reconciliation.matchScore": "{{score}}% compat\xEDvel",
|
|
5630
|
+
"financial.reconciliation.refresh": "Atualizar",
|
|
4541
5631
|
"financial.onboarding.back": "Voltar",
|
|
4542
5632
|
"financial.onboarding.configureHint": "Voc\xEA pode configurar isso em detalhes pelo \xEDcone de Configura\xE7\xF5es dentro do m\xF3dulo Financeiro.",
|
|
4543
5633
|
"financial.onboarding.getStarted": "Come\xE7ar",
|
|
@@ -4602,6 +5692,8 @@ var ptBR = {
|
|
|
4602
5692
|
"financial.settingsPage.title": "Configura\xE7\xF5es Financeiras",
|
|
4603
5693
|
"financial.payment.noMethods": "Nenhuma forma de pagamento configurada.",
|
|
4604
5694
|
"financial.payment.configureMethods": "Configurar formas de pagamento",
|
|
5695
|
+
"financial.payment.noAccounts": "Nenhuma conta compat\xEDvel. Crie uma nas Configura\xE7\xF5es.",
|
|
5696
|
+
"financial.payment.configureAccounts": "Configurar contas",
|
|
4605
5697
|
"financial.statements.account": "Conta",
|
|
4606
5698
|
"financial.statements.from": "De",
|
|
4607
5699
|
"financial.statements.noTransactions": "Sem transa\xE7\xF5es neste per\xEDodo",
|
|
@@ -4737,7 +5829,66 @@ var ptBR = {
|
|
|
4737
5829
|
"financial.cashRegister.close": "Fechar Caixa",
|
|
4738
5830
|
"financial.movements.title": "Movimenta\xE7\xF5es",
|
|
4739
5831
|
"financial.bankAccounts.title": "Contas Banc\xE1rias",
|
|
4740
|
-
"financial.settings.title": "Configura\xE7\xF5es Financeiras"
|
|
5832
|
+
"financial.settings.title": "Configura\xE7\xF5es Financeiras",
|
|
5833
|
+
// Abas de registros nas configurações
|
|
5834
|
+
"registry.payment-methods": "Formas de Pagamento",
|
|
5835
|
+
"registry.payment-methods.description": "Formas de pagamento configuradas com taxas e regras de parcelamento",
|
|
5836
|
+
"registry.bank-accounts": "Contas",
|
|
5837
|
+
"registry.bank-accounts.description": "Contas banc\xE1rias, caixas, cart\xF5es de cr\xE9dito e carteiras digitais",
|
|
5838
|
+
"registry.chart-of-accounts": "Plano de Contas",
|
|
5839
|
+
"registry.chart-of-accounts.description": "Plano de contas hier\xE1rquico para categorizar transa\xE7\xF5es",
|
|
5840
|
+
"registry.cost-centers": "Centros de Custo",
|
|
5841
|
+
"registry.cost-centers.description": "Centros de custo para controle de despesas e or\xE7amentos por departamento",
|
|
5842
|
+
"registry.commission-rules": "Regras de Comiss\xE3o",
|
|
5843
|
+
"registry.commission-rules.description": "Regras de comiss\xE3o para servi\xE7os e produtos",
|
|
5844
|
+
"registry.price-tables": "Tabelas de Pre\xE7o",
|
|
5845
|
+
"registry.price-tables.description": "Tabelas de pre\xE7o de venda e custo",
|
|
5846
|
+
"registry.price-variations": "Varia\xE7\xF5es de Pre\xE7o",
|
|
5847
|
+
"registry.price-variations.description": "Regras de desconto e acr\xE9scimo de pre\xE7os",
|
|
5848
|
+
"registry.payment-method-types": "Tipos de Pagamento",
|
|
5849
|
+
"registry.payment-method-types.description": "Tipos de pagamento do sistema (dinheiro, cart\xE3o, PIX, etc.)",
|
|
5850
|
+
"registry.card-brands": "Bandeiras de Cart\xE3o",
|
|
5851
|
+
"registry.card-brands.description": "Bandeiras de cart\xE3o de cr\xE9dito e d\xE9bito",
|
|
5852
|
+
// Extrato
|
|
5853
|
+
"financial.statements.columnDate": "Data",
|
|
5854
|
+
"financial.statements.columnDescription": "Descri\xE7\xE3o",
|
|
5855
|
+
"financial.statements.columnDebit": "D\xE9bito",
|
|
5856
|
+
"financial.statements.columnCredit": "Cr\xE9dito",
|
|
5857
|
+
"financial.statements.columnFee": "Taxa",
|
|
5858
|
+
"financial.statements.columnBalance": "Saldo",
|
|
5859
|
+
"financial.statements.columnMethod": "Forma",
|
|
5860
|
+
"financial.statements.columnDoc": "Documento",
|
|
5861
|
+
"financial.statements.installmentShort": "Parc. {{n}}",
|
|
5862
|
+
"financial.statements.viewInvoice": "Ver fatura",
|
|
5863
|
+
"financial.person.empty": "Nenhuma atividade financeira para este contato ainda",
|
|
5864
|
+
"financial.statements.opening": "Saldo inicial",
|
|
5865
|
+
"financial.statements.closing": "Saldo final",
|
|
5866
|
+
"financial.statements.currentBalance": "Saldo atual",
|
|
5867
|
+
"financial.statements.totalCredits": "Cr\xE9ditos",
|
|
5868
|
+
"financial.statements.totalDebits": "D\xE9bitos",
|
|
5869
|
+
"financial.statements.totalFees": "Taxas",
|
|
5870
|
+
"financial.statements.net": "L\xEDquido",
|
|
5871
|
+
"financial.statements.transfer": "Transfer\xEAncia",
|
|
5872
|
+
"financial.statements.reconciled": "Conciliado com o extrato banc\xE1rio",
|
|
5873
|
+
"financial.statements.imported": "Importado do banco \xB7 n\xE3o conciliado",
|
|
5874
|
+
"financial.statements.reconciledWith": "Conciliado com {{ref}}",
|
|
5875
|
+
"financial.statements.newTransfer": "Nova Transfer\xEAncia",
|
|
5876
|
+
// Diálogo de transferência
|
|
5877
|
+
"financial.transfer.title": "Transfer\xEAncia entre Contas",
|
|
5878
|
+
"financial.transfer.from": "Conta de origem",
|
|
5879
|
+
"financial.transfer.to": "Conta de destino",
|
|
5880
|
+
"financial.transfer.amount": "Valor",
|
|
5881
|
+
"financial.transfer.date": "Data",
|
|
5882
|
+
"financial.transfer.notes": "Observa\xE7\xF5es",
|
|
5883
|
+
"financial.transfer.optionalNotes": "Observa\xE7\xF5es opcionais...",
|
|
5884
|
+
"financial.transfer.selectAccount": "Selecione a conta...",
|
|
5885
|
+
"financial.transfer.submit": "Transferir",
|
|
5886
|
+
"financial.transfer.processing": "Transferindo...",
|
|
5887
|
+
"financial.transfer.cancel": "Cancelar",
|
|
5888
|
+
"financial.transfer.created": "Transfer\xEAncia registrada",
|
|
5889
|
+
"financial.transfer.failed": "Falha ao registrar transfer\xEAncia",
|
|
5890
|
+
"financial.transfer.sameAccount": "Origem e destino devem ser diferentes",
|
|
5891
|
+
"financial.transfer.invalidAmount": "O valor deve ser maior que zero"
|
|
4741
5892
|
};
|
|
4742
5893
|
|
|
4743
5894
|
// src/locales/index.ts
|
|
@@ -4745,8 +5896,104 @@ var financialLocales = {
|
|
|
4745
5896
|
en,
|
|
4746
5897
|
"pt-BR": ptBR
|
|
4747
5898
|
};
|
|
4748
|
-
|
|
4749
|
-
|
|
5899
|
+
var STATUS_STYLE = {
|
|
5900
|
+
paid: "bg-success/15 text-success",
|
|
5901
|
+
partial: "bg-warning/15 text-warning",
|
|
5902
|
+
overdue: "bg-destructive/15 text-destructive",
|
|
5903
|
+
open: "bg-muted text-muted-foreground",
|
|
5904
|
+
cancelled: "bg-muted text-muted-foreground line-through"
|
|
5905
|
+
};
|
|
5906
|
+
function PersonStatementWidget({ person, provider, config }) {
|
|
5907
|
+
const t2 = useTranslation();
|
|
5908
|
+
const currency = config.currency;
|
|
5909
|
+
const [invoices, setInvoices] = useState([]);
|
|
5910
|
+
const [loading, setLoading] = useState(true);
|
|
5911
|
+
useEffect(() => {
|
|
5912
|
+
let alive = true;
|
|
5913
|
+
if (!person?.id) {
|
|
5914
|
+
setLoading(false);
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5917
|
+
setLoading(true);
|
|
5918
|
+
provider.getInvoices({ contactId: person.id, pageSize: 200 }).then((r) => {
|
|
5919
|
+
if (alive) {
|
|
5920
|
+
setInvoices(r.data);
|
|
5921
|
+
setLoading(false);
|
|
5922
|
+
}
|
|
5923
|
+
}).catch(() => {
|
|
5924
|
+
if (alive) setLoading(false);
|
|
5925
|
+
});
|
|
5926
|
+
return () => {
|
|
5927
|
+
alive = false;
|
|
5928
|
+
};
|
|
5929
|
+
}, [person?.id, provider]);
|
|
5930
|
+
const fmt = (n) => formatCurrency(n, currency);
|
|
5931
|
+
const totals = useMemo(() => {
|
|
5932
|
+
const active = invoices.filter((i) => i.status !== "cancelled");
|
|
5933
|
+
const billed = active.reduce((s, i) => s + (i.totalAmount ?? 0), 0);
|
|
5934
|
+
const paid = active.reduce((s, i) => s + (i.paidAmount ?? 0), 0);
|
|
5935
|
+
return { billed, paid, balance: billed - paid };
|
|
5936
|
+
}, [invoices]);
|
|
5937
|
+
const openInvoice = (inv) => {
|
|
5938
|
+
const base = inv.direction === "credit" ? "receivables" : "payables";
|
|
5939
|
+
window.location.hash = `/financial/${base}/detail/${inv.id}`;
|
|
5940
|
+
};
|
|
5941
|
+
const columns = useMemo(() => [
|
|
5942
|
+
{
|
|
5943
|
+
id: "date",
|
|
5944
|
+
header: t2("financial.invoice.columnDate"),
|
|
5945
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground text-xs", children: row.original.invoiceDate })
|
|
5946
|
+
},
|
|
5947
|
+
{
|
|
5948
|
+
id: "description",
|
|
5949
|
+
header: t2("financial.invoice.columnDescription"),
|
|
5950
|
+
cell: ({ row }) => {
|
|
5951
|
+
const inv = row.original;
|
|
5952
|
+
return /* @__PURE__ */ jsxs("button", { onClick: () => openInvoice(inv), className: "text-left", children: [
|
|
5953
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-primary hover:underline", children: inv.itemsSummary || inv.contactName || t2("financial.invoice.invoice") }),
|
|
5954
|
+
inv.fiscalNumber && /* @__PURE__ */ jsx("span", { className: "ml-1.5 text-[10px] text-muted-foreground font-mono", children: inv.fiscalNumber })
|
|
5955
|
+
] });
|
|
5956
|
+
}
|
|
5957
|
+
},
|
|
5958
|
+
{
|
|
5959
|
+
id: "amount",
|
|
5960
|
+
header: t2("financial.invoice.columnAmount"),
|
|
5961
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: `text-right block text-xs ${row.original.direction === "credit" ? "text-success" : "text-destructive"}`, children: fmt(row.original.totalAmount) })
|
|
5962
|
+
},
|
|
5963
|
+
{
|
|
5964
|
+
id: "paid",
|
|
5965
|
+
header: t2("financial.invoice.columnPaid"),
|
|
5966
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-xs text-muted-foreground", children: fmt(row.original.paidAmount) })
|
|
5967
|
+
},
|
|
5968
|
+
{
|
|
5969
|
+
id: "balance",
|
|
5970
|
+
header: t2("financial.statements.columnBalance"),
|
|
5971
|
+
cell: ({ row }) => /* @__PURE__ */ jsx("span", { className: "text-right block text-xs font-medium", children: fmt((row.original.totalAmount ?? 0) - (row.original.paidAmount ?? 0)) })
|
|
5972
|
+
},
|
|
5973
|
+
{
|
|
5974
|
+
id: "status",
|
|
5975
|
+
header: t2("financial.invoice.columnStatus"),
|
|
5976
|
+
cell: ({ row }) => /* @__PURE__ */ 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 })
|
|
5977
|
+
}
|
|
5978
|
+
], [currency, t2]);
|
|
5979
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
|
|
5980
|
+
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-3 gap-2", children: [
|
|
5981
|
+
/* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryTotal"), value: fmt(totals.billed) }),
|
|
5982
|
+
/* @__PURE__ */ jsx(Stat, { label: t2("financial.invoice.summaryPaid"), value: fmt(totals.paid), positive: true }),
|
|
5983
|
+
/* @__PURE__ */ jsx(Stat, { label: t2("financial.statements.columnBalance"), value: fmt(totals.balance), accent: true })
|
|
5984
|
+
] }),
|
|
5985
|
+
loading ? /* @__PURE__ */ jsx(DataTable, { columns, data: [], loading: true, skeletonRows: 5, compact: true }) : invoices.length === 0 ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-10 text-center rounded-lg border-2 border-dashed border-muted", children: [
|
|
5986
|
+
/* @__PURE__ */ jsx(Wallet, { className: "h-7 w-7 text-muted-foreground mb-2" }),
|
|
5987
|
+
/* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: t2("financial.person.empty") })
|
|
5988
|
+
] }) : /* @__PURE__ */ jsx(DataTable, { columns, data: invoices, compact: true })
|
|
5989
|
+
] });
|
|
5990
|
+
}
|
|
5991
|
+
function Stat({ label, value, accent, positive }) {
|
|
5992
|
+
return /* @__PURE__ */ jsxs("div", { className: `rounded-lg border px-3 py-2 ${accent ? "bg-muted/40" : "bg-card"}`, children: [
|
|
5993
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] font-medium text-muted-foreground uppercase truncate", children: label }),
|
|
5994
|
+
/* @__PURE__ */ jsx("p", { className: `text-sm font-semibold mt-0.5 ${positive ? "text-success" : "text-foreground"}`, children: value })
|
|
5995
|
+
] });
|
|
5996
|
+
}
|
|
4750
5997
|
function createSafeFinancialProvider() {
|
|
4751
5998
|
return createSafeDataProvider(
|
|
4752
5999
|
() => createSupabaseFinancialProvider(),
|
|
@@ -4787,7 +6034,9 @@ function resolveConfig(options) {
|
|
|
4787
6034
|
cashRegisters: options?.modules?.cashRegisters !== false,
|
|
4788
6035
|
statements: options?.modules?.statements !== false,
|
|
4789
6036
|
commissions: options?.modules?.commissions !== false,
|
|
4790
|
-
cards: options?.modules?.cards !== false
|
|
6037
|
+
cards: options?.modules?.cards !== false,
|
|
6038
|
+
// Opt-in: only shown when an app explicitly enables reconciliation (bank connector present).
|
|
6039
|
+
reconciliation: options?.modules?.reconciliation === true
|
|
4791
6040
|
},
|
|
4792
6041
|
labels: { ...DEFAULT_LABELS, ...options?.labels },
|
|
4793
6042
|
currency: { ...DEFAULT_CURRENCY, ...options?.currency },
|
|
@@ -4809,7 +6058,9 @@ function createFinancialPlugin(options) {
|
|
|
4809
6058
|
const provider = options?.dataProvider ?? createSafeFinancialProvider();
|
|
4810
6059
|
const store = createFinancialStore(provider);
|
|
4811
6060
|
const dashboardWidgets = createFinancialDashboardWidgets({ config, provider, store });
|
|
4812
|
-
const
|
|
6061
|
+
const PersonStatementBound = (props) => React14.createElement(PersonStatementWidget, { person: props.item, provider, config });
|
|
6062
|
+
PersonStatementBound.displayName = "FinancialPersonStatement";
|
|
6063
|
+
const PageComponent = () => React14.createElement(FinancialPage, { config, provider, store, registries: financialRegistries });
|
|
4813
6064
|
return {
|
|
4814
6065
|
id: "financial",
|
|
4815
6066
|
name: config.labels.pageTitle,
|
|
@@ -4842,7 +6093,15 @@ function createFinancialPlugin(options) {
|
|
|
4842
6093
|
permission: { feature: "financial", action: "read" }
|
|
4843
6094
|
}
|
|
4844
6095
|
],
|
|
4845
|
-
widgets: [
|
|
6096
|
+
widgets: [
|
|
6097
|
+
{
|
|
6098
|
+
id: "financial-person-statement",
|
|
6099
|
+
zone: "person.detail.financial",
|
|
6100
|
+
component: PersonStatementBound,
|
|
6101
|
+
order: 0,
|
|
6102
|
+
permission: { feature: "financial", action: "read" }
|
|
6103
|
+
}
|
|
6104
|
+
],
|
|
4846
6105
|
dashboardWidgets,
|
|
4847
6106
|
aiTools: [
|
|
4848
6107
|
{
|
|
@@ -4913,7 +6172,14 @@ function createFinancialPlugin(options) {
|
|
|
4913
6172
|
label: "Financial",
|
|
4914
6173
|
icon: "DollarSign",
|
|
4915
6174
|
component: (() => {
|
|
4916
|
-
const FinancialSettingsTab = () =>
|
|
6175
|
+
const FinancialSettingsTab = () => React14.createElement(PluginSettingsPanel, {
|
|
6176
|
+
title: "Financial Settings",
|
|
6177
|
+
subtitle: "Payment methods, accounts, and configurations",
|
|
6178
|
+
generalSettings: React14.createElement(FinancialGeneralSettings),
|
|
6179
|
+
registries: financialRegistries,
|
|
6180
|
+
hostPluginId: "financial",
|
|
6181
|
+
routeBase: "/settings/financial"
|
|
6182
|
+
});
|
|
4917
6183
|
FinancialSettingsTab.displayName = "FinancialSettingsTab";
|
|
4918
6184
|
return FinancialSettingsTab;
|
|
4919
6185
|
})(),
|